View Javadoc
1   package fr.ifremer.quadrige3.ui.swing.table;
2   
3   /*-
4    * #%L
5    * Quadrige3 Core :: Quadrige3 UI Common
6    * $Id:$
7    * $HeadURL:$
8    * %%
9    * Copyright (C) 2017 Ifremer
10   * %%
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU Affero General Public License as published by
13   * the Free Software Foundation, either version 3 of the License, or
14   * (at your option) any later version.
15   *
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   *
21   * You should have received a copy of the GNU Affero General Public License
22   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23   * #L%
24   */
25  
26  import com.google.common.collect.Lists;
27  import com.google.common.collect.Sets;
28  import fr.ifremer.quadrige3.core.dao.technical.AlphanumericComparator;
29  import fr.ifremer.quadrige3.core.dao.technical.Assert;
30  import fr.ifremer.quadrige3.core.dao.technical.decorator.Decorator;
31  import fr.ifremer.quadrige3.core.dao.technical.decorator.DecoratorComparator;
32  import fr.ifremer.quadrige3.core.exception.QuadrigeTechnicalException;
33  import fr.ifremer.quadrige3.core.service.ClientServiceLocator;
34  import fr.ifremer.quadrige3.core.service.decorator.DecoratorService;
35  import fr.ifremer.quadrige3.ui.swing.AbstractUIHandler;
36  import fr.ifremer.quadrige3.ui.swing.ApplicationUI;
37  import fr.ifremer.quadrige3.ui.swing.ApplicationUIUtil;
38  import fr.ifremer.quadrige3.ui.swing.component.bean.ExtendedComboBox;
39  import fr.ifremer.quadrige3.ui.swing.component.coordinate.CoordinateEditor;
40  import fr.ifremer.quadrige3.ui.swing.model.BeanMonitor;
41  import fr.ifremer.quadrige3.ui.swing.table.action.*;
42  import fr.ifremer.quadrige3.ui.swing.table.comment.CommentCellEditor;
43  import fr.ifremer.quadrige3.ui.swing.table.comment.CommentCellRenderer;
44  import fr.ifremer.quadrige3.ui.swing.table.editor.*;
45  import fr.ifremer.quadrige3.ui.swing.table.renderer.DateCellRenderer;
46  import fr.ifremer.quadrige3.ui.swing.table.renderer.ExtendedDecoratorCellRenderer;
47  import fr.ifremer.quadrige3.ui.swing.table.renderer.LocalDateCellRenderer;
48  import fr.ifremer.quadrige3.ui.swing.table.renderer.NumberCellRenderer;
49  import jaxx.runtime.SwingUtil;
50  import jaxx.runtime.swing.editor.bean.BeanFilterableComboBox;
51  import jaxx.runtime.swing.editor.bean.BeanUIUtil;
52  import org.apache.commons.collections4.CollectionUtils;
53  import org.apache.commons.collections4.comparators.ComparableComparator;
54  import org.apache.commons.lang3.ArrayUtils;
55  import org.apache.commons.lang3.StringUtils;
56  import org.apache.commons.logging.Log;
57  import org.apache.commons.logging.LogFactory;
58  import org.jdesktop.swingx.JXTable;
59  import org.jdesktop.swingx.autocomplete.ComboBoxCellEditor;
60  import org.jdesktop.swingx.autocomplete.ObjectToStringConverter;
61  import org.jdesktop.swingx.renderer.DefaultTableRenderer;
62  import org.jdesktop.swingx.renderer.StringValue;
63  import org.jdesktop.swingx.table.TableColumnExt;
64  import org.springframework.beans.BeanUtils;
65  
66  import javax.swing.*;
67  import javax.swing.event.*;
68  import javax.swing.table.*;
69  import java.awt.Component;
70  import java.awt.Container;
71  import java.awt.Dimension;
72  import java.awt.Point;
73  import java.awt.event.MouseEvent;
74  import java.beans.IndexedPropertyChangeEvent;
75  import java.beans.PropertyChangeEvent;
76  import java.beans.PropertyChangeListener;
77  import java.math.BigDecimal;
78  import java.text.DecimalFormat;
79  import java.util.*;
80  import java.util.function.Predicate;
81  
82  import static org.nuiton.i18n.I18n.t;
83  
84  /**
85   * <p>Abstract AbstractTableUIHandler class.</p>
86   *
87   * @param <R>  type of a row
88   * @param <M>  type of the ui model
89   * @param <UI> type of the ui
90   * @author Ludovic Pecquot <ludovic.pecquot@e-is.pro>
91   */
92  public abstract class AbstractTableUIHandler<R extends AbstractRowUIModel<?, ?>, M extends AbstractTableUIModel<?, R, M>, UI extends ApplicationUI<M, ?>>
93      extends AbstractUIHandler<M, UI> {
94  
95      /**
96       * Constant <code>DEFAULT_ROW_HEIGHT=24</code>
97       */
98      public static final int DEFAULT_ROW_HEIGHT = 24;
99      /**
100      * Minimun width of a column
101      */
102     public static final int DEFAULT_MIN_COLUMN_WIDTH = 80;
103     /**
104      * Logger.
105      */
106     private static final Log LOG = LogFactory.getLog(AbstractTableUIHandler.class);
107     /**
108      * Monitor the selected row (save it only if something has changed).
109      *
110      * @since 0.2
111      */
112     private final BeanMonitor<R> rowMonitor;
113     //------------------------------------------------------------------------//
114     //-- Internal methods (listener methods)                                --//
115     //------------------------------------------------------------------------//
116     private ListSelectionListener tableSelectionListener;
117     private ListSelectionListener tableScrollSelectionListener;
118     private TableColumnModelListener columnModelListener;
119     private RowSorterListener rowSorterListener;
120 
121     private boolean initializing = false;
122     private boolean initialized = false;
123     private FixedSwingTable fixedTable;
124     private List<TableColumnExt> pendingFixedColumns = new ArrayList<>();
125 
126     /**
127      * <p>Constructor for AbstractTableUIHandler.</p>
128      *
129      * @param properties a {@link String} object.
130      */
131     @SuppressWarnings("unchecked")
132     protected AbstractTableUIHandler(String... properties) {
133 
134         rowMonitor = new BeanMonitor<>(properties);
135 
136         // listen when bean is changed
137         rowMonitor.addPropertyChangeListener(BeanMonitor.PROPERTY_BEAN, new PropertyChangeListener() {
138 
139             final Set<String> propertiesToSkip = Sets.newHashSet(getRowPropertiesToIgnore());
140 
141             final PropertyChangeListener l = evt -> {
142                 String propertyName = evt.getPropertyName();
143 
144                 R row = (R) evt.getSource();
145 
146                 Object oldValue = evt.getOldValue();
147                 Object newValue = evt.getNewValue();
148 
149                 int rowIndex = getTableModel().getRowIndex(row);
150 
151                 if (AbstractRowUIModel.PROPERTY_VALID.equals(propertyName)) {
152                     onRowValidStateChanged(rowIndex, row,
153                         (Boolean) oldValue,
154                         (Boolean) newValue);
155                 } else if (AbstractRowUIModel.PROPERTY_MODIFY.equals(propertyName)) {
156                     onRowModifyStateChanged(rowIndex, row,
157                         (Boolean) oldValue,
158                         (Boolean) newValue);
159                 } else if (AbstractRowUIModel.PROPERTY_SELECTED.equals(propertyName)) {
160                     onRowSelectedStateChanged(rowIndex, row,
161                         (Boolean) oldValue,
162                         (Boolean) newValue);
163                 } else if (!propertiesToSkip.contains(propertyName)) {
164                     if (LOG.isDebugEnabled()) {
165                         LOG.debug(
166                             "row [" + rowIndex + "] property " + propertyName + " changed from " + oldValue + " to " + newValue);
167                     }
168 
169                     // don't fire event if old and value both null
170                     if (oldValue != null || newValue != null) {
171                         onRowModified(rowIndex, row, propertyName,
172                             evt instanceof IndexedPropertyChangeEvent ? ((IndexedPropertyChangeEvent) evt).getIndex() : null,
173                             oldValue, newValue);
174                     }
175                 }
176             };
177 
178             @Override
179             public void propertyChange(PropertyChangeEvent evt) {
180                 R oldValue = (R) evt.getOldValue();
181                 R newValue = (R) evt.getNewValue();
182                 if (LOG.isDebugEnabled()) {
183                     LOG.debug("Monitor row changed from " + oldValue + " to " + newValue);
184                 }
185                 if (oldValue != null) {
186                     oldValue.removePropertyChangeListener(l);
187                 }
188                 if (newValue != null) {
189                     newValue.addPropertyChangeListener(l);
190                 }
191             }
192         });
193     }
194 
195     /**
196      * <p>getTableModel.</p>
197      *
198      * @return the table model handled by the main table.
199      * @since 0.2
200      */
201     public abstract AbstractTableModel<R> getTableModel();
202 
203     /**
204      * <p>getTable.</p>
205      *
206      * @return the main table of the ui.
207      * @since 0.2
208      */
209     public abstract SwingTable getTable();
210 
211     /**
212      * get the fixed table
213      *
214      * @return the fixed table
215      */
216     public FixedSwingTable getFixedTable() {
217         return fixedTable;
218     }
219 
220     //------------------------------------------------------------------------//
221     //-- Initialization methods                                             --//
222     //------------------------------------------------------------------------//
223 
224     /**
225      * <p>initTable.</p>
226      *
227      * @param table a {@link SwingTable} object.
228      */
229     protected void initTable(SwingTable table) {
230         initTable(table, false);
231     }
232 
233     /**
234      * <p>initTable.</p>
235      *
236      * @param table                a {@link SwingTable} object.
237      * @param forceSingleSelection a boolean.
238      */
239     protected void initTable(SwingTable table, boolean forceSingleSelection) {
240         initTable(table, forceSingleSelection, false);
241     }
242 
243     /**
244      * <p>initTable.</p>
245      *
246      * @param table             a {@link SwingTable} object.
247      * @param singleSelection   a boolean.
248      * @param checkBoxSelection a boolean.
249      */
250     @SuppressWarnings("unchecked")
251     protected void initTable(SwingTable table, boolean singleSelection, boolean checkBoxSelection) {
252         Assert.isTrue(!(singleSelection && checkBoxSelection), "Could not have both 'singleSelection' and 'checkBoxSelection' to 'true'");
253 
254         initializing = true;
255         initialized = false;
256 
257         if (table.getTableHeader() == null) {
258             // change table header
259             table.setTableHeader(new SwingTableHeader(getTable().getColumnModel()));
260             table.getTableHeader().setTable(table);
261             table.getTableHeader().updateUI();
262         }
263 
264         // affect table model to table UI model
265         getModel().setTableModel(getTableModel());
266         // also affect table UI model to table model
267         getTableModel().setTableUIModel(getModel());
268 
269         // size properties
270         table.setAutoResizeMode(JXTable.AUTO_RESIZE_OFF);
271         // set a larger row height
272         table.setRowHeight(DEFAULT_ROW_HEIGHT);
273         table.setHorizontalScrollEnabled(true);
274 
275         // by default authorize to change column orders
276         table.getTableHeader().setReorderingAllowed(true);
277 
278         initHighlighters(table);
279 
280         // when model data change let's propagate it to table model
281         getModel().addPropertyChangeListener(AbstractTableUIModel.PROPERTY_ROWS, evt -> onModelRowsChanged((List<R>) evt.getNewValue()));
282         // first assignment of model data to table model
283         getTableModel().setRows(getModel().getRows());
284 
285         // when new rows are added
286         getModel().addPropertyChangeListener(AbstractTableUIModel.PROPERTY_ROWS_ADDED, evt -> onRowsAdded((List<R>) evt.getNewValue()));
287 
288         // before add the column, reset row sorter to prevent exceptions
289         uninstallSortController();
290 
291         if (singleSelection) {
292             // only one selected row at a time
293             table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
294 
295         } else {
296 
297             // selection mode
298             if (checkBoxSelection) {
299 
300                 // only one selected row at a time
301                 table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
302 
303                 // add selection check box
304                 addFixedColumn(CheckTableColumn.class);
305 
306             } else {
307 
308                 // multiple rows selection
309                 getTable().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
310             }
311         }
312 
313         // Add pending fixed columns
314         if (!pendingFixedColumns.isEmpty()) {
315 
316             initFixedTable();
317 
318             pendingFixedColumns.forEach(pendingFixedColumn -> {
319 
320                 // force hide the hidden column and retrieve the real column (=delegate)
321                 if (pendingFixedColumn instanceof HiddenColumn) {
322                     HiddenColumn hiddenColumn = (HiddenColumn) pendingFixedColumn;
323                     hiddenColumn.setVisible(false);
324                     pendingFixedColumn = hiddenColumn.getDelegate();
325                 }
326 
327                 // add it to fixed table
328                 getFixedTable().addColumn(pendingFixedColumn);
329 
330                 // add identifier if missing
331                 if (pendingFixedColumn.getIdentifier() instanceof ColumnIdentifier) {
332                     ColumnIdentifier<R> identifier = (ColumnIdentifier<R>) pendingFixedColumn.getIdentifier();
333                     if (getTableModel().getColumnIndex(identifier.getPropertyName()) == -1) {
334                         getTableModel().getIdentifiers().add(pendingFixedColumn.getModelIndex(), identifier);
335                     }
336                 }
337 
338             });
339 
340             // Clear at the end
341             pendingFixedColumns.clear();
342         }
343 
344         // Install sort controller
345         installSortController();
346 
347         // always force to uninstall listener
348         uninstallTableSelectionListener();
349 
350         // save when row changed and was modified
351         installTableSelectionListener();
352 
353         // Add line numbers action (Mantis #47376)
354         addRowNumbersAction();
355 
356         // add custom column control button
357         table.setColumnControl(new ColumnControlButton(table));
358         table.setColumnControlVisible(true);
359 
360         // Add default String editor
361         table.setDefaultEditor(String.class, new StringCellEditor());
362 
363         // Override the TAB and MAJ+TAB keys to perform specific actions
364         overrideTabKeyActions(table, false);
365 
366         initializing = false;
367         initialized = true;
368     }
369 
370     private void initHighlighters(SwingTable table) {
371 
372         // disable JXTable hack
373         table.putClientProperty(JXTable.USE_DTCR_COLORMEMORY_HACK, Boolean.FALSE);
374 
375         // add highlighters (nothing by default, application should override it)
376         addHighlighters(table);
377 
378     }
379 
380     /**
381      * Override keys actions on table
382      *
383      * @param table                        the table
384      * @param forceStopEditingBeforeAction false by default
385      */
386     protected void overrideTabKeyActions(SwingTable table, boolean forceStopEditingBeforeAction) {
387 
388         overrideKeyAction(table, "pressed TAB", AbstractCellSelectionAction.Direction.NEXT_CELL, forceStopEditingBeforeAction);
389         overrideKeyAction(table, "shift pressed TAB", AbstractCellSelectionAction.Direction.PREVIOUS_CELL, forceStopEditingBeforeAction);
390         overrideKeyAction(table, "pressed RIGHT", AbstractCellSelectionAction.Direction.NEXT_CELL, forceStopEditingBeforeAction);
391         overrideKeyAction(table, "pressed LEFT", AbstractCellSelectionAction.Direction.PREVIOUS_CELL, forceStopEditingBeforeAction);
392         overrideKeyAction(table, "pressed DOWN", AbstractCellSelectionAction.Direction.NEXT_ROW, forceStopEditingBeforeAction);
393         overrideKeyAction(table, "pressed ENTER", AbstractCellSelectionAction.Direction.NEXT_ROW, forceStopEditingBeforeAction);
394 
395     }
396 
397     private void overrideKeyAction(SwingTable table, String key, AbstractCellSelectionAction.Direction direction, boolean forceStopEditingBeforeAction) {
398 
399         String actionName = "actionFor" + StringUtils.deleteWhitespace(StringUtils.capitalize(key));
400 
401         // create new action
402         AbstractCellSelectionAction action = null;
403         switch (direction) {
404 
405             case NEXT_CELL:
406                 action = new NextCellSelectionAction(actionName, this, forceStopEditingBeforeAction);
407                 break;
408             case PREVIOUS_CELL:
409                 action = new PreviousCellSelectionAction(actionName, this, forceStopEditingBeforeAction);
410                 break;
411             case NEXT_ROW:
412                 action = new NextRowSelectionAction(actionName, this, forceStopEditingBeforeAction);
413                 break;
414             case PREVIOUS_ROW:
415                 break;
416         }
417 
418         if (action == null) {
419             return;
420         }
421 
422         // override the input map entry
423         table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(key), actionName);
424 
425         // affect the new action
426         table.getActionMap().put(actionName, action);
427 
428     }
429 
430     /**
431      * <p>installTableSelectionListener.</p>
432      */
433     protected void installTableSelectionListener() {
434 
435         Assert.state(
436             tableSelectionListener == null,
437             "There is already a tableSelectionListener registered, remove it before invoking this method.");
438 
439         // create new listener
440         tableSelectionListener = new ListSelectionListener() {
441 
442             int lastMinIndex = -1;
443             int lastMaxIndex = -1;
444 
445             @Override
446             public void valueChanged(ListSelectionEvent e) {
447 
448                 ListSelectionModel source = (ListSelectionModel) e.getSource();
449 
450                 if (LOG.isDebugEnabled()) {
451                     LOG.debug("Selection changed: " + e);
452                 }
453 
454                 // need this for the first modification when no selection,
455                 // otherwise monitor is set after the first alter, so won't be
456                 // save directly...
457 
458                 // get selection interval
459                 int minIndex = source.getMinSelectionIndex();
460                 int maxIndex = source.getMaxSelectionIndex();
461 
462                 if (source.isSelectionEmpty()) {
463                     minIndex = -1;
464                     maxIndex = -1;
465                 }
466                 if (minIndex == lastMinIndex && maxIndex == lastMaxIndex) {
467                     return;
468                 }
469                 lastMinIndex = minIndex;
470                 lastMaxIndex = maxIndex;
471 
472                 // Stop editing if multiple row selected
473                 if (minIndex != maxIndex) {
474                     getTable().editingStopped(null);
475                 }
476 
477                 // Deactivate rowMonitor to avoid PROPERTY_SELECTED event to fire useless
478                 // also prevent recalculateRowSelection to execute too often
479                 rowMonitor.setBean(null);
480 
481                 // re-affect selected property when selection checkbox is NOT used
482                 if (!hasCheckTableColumn()) {
483                     // reset selected property
484                     getModel().getSelectedRows().forEach(row -> row.setSelected(false));
485 
486                     // set new selected property
487                     for (int index = minIndex; index <= maxIndex; index++) {
488                         if (source.isSelectedIndex(index)) {
489                             int modelIndex = getTable().convertRowIndexToModel(index);
490                             R row = getTableModel().getEntry(modelIndex);
491                             row.setSelected(true);
492                         }
493                     }
494                 }
495 
496                 R newRow;
497 
498                 // Add control to avoid ArrayIndexOfBounds when calling convertRowIndexToModel (Mantis #0027276)
499                 if (source.isSelectionEmpty() || source.getLeadSelectionIndex() < 0 || source.getLeadSelectionIndex() >= getTable().getRowCount()) {
500                     newRow = null;
501                 } else {
502                     int rowIndex = source.getLeadSelectionIndex();
503                     int modelIndex = getTable().convertRowIndexToModel(rowIndex);
504                     newRow = getTableModel().getEntry(modelIndex);
505                     if (LOG.isDebugEnabled()) {
506                         LOG.debug("Will monitor entry: " + rowIndex);
507                     }
508                 }
509 
510                 rowMonitor.setBean(newRow);
511                 getModel().setSingleSelectedRow(newRow);
512 
513                 // select row in table (for example if column order change
514                 if (newRow != null && source.getMinSelectionIndex() == source.getMaxSelectionIndex()) {
515                     selectRow(newRow);
516                 }
517 
518                 // recalculate row selection with new selection
519                 recalculateRowSelection(newRow);
520 
521             }
522         };
523 
524         if (LOG.isDebugEnabled()) {
525             LOG.debug("Install " + tableSelectionListener + " on tableModel " + getTableModel());
526         }
527 
528         getTable().getSelectionModel().addListSelectionListener(tableSelectionListener);
529     }
530 
531     /**
532      * <p>uninstallTableSelectionListener.</p>
533      */
534     protected void uninstallTableSelectionListener() {
535 
536         if (tableSelectionListener != null) {
537 
538             if (LOG.isDebugEnabled()) {
539                 LOG.debug("Uninstall " + tableSelectionListener);
540             }
541 
542             // there was a previous selection listener, remove it
543             getTable().getSelectionModel().removeListSelectionListener(tableSelectionListener);
544             tableSelectionListener = null;
545         }
546     }
547 
548     /**
549      * <p>installTableScrollSelectionListener.</p>
550      */
551     protected void installTableScrollSelectionListener() {
552         Assert.state(
553             tableScrollSelectionListener == null,
554             "There is already a tableScrollSelectionListener registered, remove it before invoking this method.");
555 
556         tableScrollSelectionListener = e -> {
557             if (!e.getValueIsAdjusting()) {
558                 int colIndex = getTable().getSelectedColumn();
559                 int rowIndex = getTable().getSelectedRow();
560                 if (rowIndex > -1 && colIndex > -1) {
561                     getTable().scrollCellToVisible(rowIndex, colIndex);
562                 }
563             }
564         };
565 
566         getTable().getSelectionModel().addListSelectionListener(tableScrollSelectionListener);
567         getTable().getColumnModel().getSelectionModel().addListSelectionListener(tableScrollSelectionListener);
568     }
569 
570     /**
571      * <p>uninstallTableScrollSelectionListener.</p>
572      */
573     protected void uninstallTableScrollSelectionListener() {
574         if (tableScrollSelectionListener != null) {
575 
576             if (LOG.isDebugEnabled()) {
577                 LOG.debug("Uninstall " + tableScrollSelectionListener);
578             }
579 
580             // there was a previous selection listener, remove it
581             getTable().getSelectionModel().removeListSelectionListener(tableScrollSelectionListener);
582             getTable().getColumnModel().getSelectionModel().removeListSelectionListener(tableScrollSelectionListener);
583             tableScrollSelectionListener = null;
584         }
585     }
586 
587     // Add listener on table column model to save table state (see Mantis #35641)
588     protected void installSaveTableStateListener() {
589         Assert.state(
590             columnModelListener == null,
591             "There is already a columnModelListener registered, remove it before invoking this method.");
592         Assert.state(
593             rowSorterListener == null,
594             "There is already a rowSorterListener registered, remove it before invoking this method.");
595 
596         columnModelListener = new TableColumnModelListener() {
597             @Override
598             public void columnAdded(TableColumnModelEvent event) {
599                 if (!getModel().isLoading()) {
600                     if (LOG.isDebugEnabled()) LOG.debug(event);
601                     saveTableState();
602                 }
603             }
604 
605             @Override
606             public void columnRemoved(TableColumnModelEvent event) {
607                 if (!getModel().isLoading()) {
608                     if (LOG.isDebugEnabled()) LOG.debug(event);
609                     saveTableState();
610                 }
611             }
612 
613             @Override
614             public void columnMoved(TableColumnModelEvent event) {
615                 if (!getModel().isLoading() && event.getFromIndex() != event.getToIndex()) {
616                     if (LOG.isDebugEnabled()) LOG.debug(event);
617                     saveTableState();
618                 }
619             }
620 
621             @Override
622             public void columnMarginChanged(ChangeEvent event) {
623                 if (!getModel().isLoading()) {
624                     if (LOG.isDebugEnabled()) LOG.debug(event);
625                     saveTableState();
626                 }
627             }
628 
629             @Override
630             public void columnSelectionChanged(ListSelectionEvent event) {
631             }
632         };
633 
634         getTable().getColumnModel().addColumnModelListener(columnModelListener);
635 
636         // Add listener on row sorter
637         if (getTable().getRowSorter() != null) {
638             rowSorterListener = event -> {
639                 if (!getModel().isLoading() && event.getType().equals(RowSorterEvent.Type.SORT_ORDER_CHANGED)) {
640                     if (LOG.isDebugEnabled()) LOG.debug(event);
641                     saveTableState();
642                 }
643             };
644 
645             getTable().getRowSorter().addRowSorterListener(rowSorterListener);
646         }
647     }
648 
649     protected void saveTableState() {
650         getContext().saveComponentInSwingSession(getTable(), getTableModel().getStateContext());
651         if (getFixedTable() != null) {
652             getContext().saveComponentInSwingSession(getFixedTable(), getTableModel().getStateContext());
653         }
654     }
655 
656     protected void restoreTableState() {
657         getContext().restoreComponentFromSwingSession(getTable());
658         if (getFixedTable() != null) {
659             getContext().restoreComponentFromSwingSession(getFixedTable());
660         }
661     }
662 
663     protected void uninstallSaveTableStateListener() {
664         if (columnModelListener != null) {
665             getTable().getColumnModel().removeColumnModelListener(columnModelListener);
666             columnModelListener = null;
667         }
668         if (rowSorterListener != null) {
669             if (getTable().getRowSorter() != null) {
670                 getTable().getRowSorter().removeRowSorterListener(rowSorterListener);
671             }
672             rowSorterListener = null;
673         }
674     }
675 
676     protected void copyParentTableState(SwingTable sourceTable) {
677 
678         getTable().getTableModel().getIdentifiers();
679         getTableModel().getIdentifiers();
680         @SuppressWarnings({"unchecked", "rawtypes"}) List<ColumnIdentifier<R>> targetIdentifiers = new ArrayList<>(getTable().getTableModel().getIdentifiers());
681 
682         Map<Integer, Integer> deferredMove = new HashMap<>();
683         int offset = 0;
684         for (ColumnIdentifier<?> targetIdentifier : targetIdentifiers) {
685             // get source column
686             TableColumnExt sourceColumn = sourceTable.getColumnExt(targetIdentifier);
687             if (sourceColumn == null || sourceColumn instanceof FixedColumn) {
688                 // ignore fixed column
689                 offset++;
690                 continue;
691             }
692             boolean isHidden = sourceColumn instanceof HiddenColumn || !sourceColumn.isVisible();
693 
694             // get target column
695             TableColumnExt targetColumn = getTable().getColumnExt(targetIdentifier);
696             if (targetColumn == null) {
697                 // ignore column but should be impossible
698                 continue;
699             }
700 
701             if (isHidden) {
702                 targetColumn.setVisible(false);
703             } else {
704                 // current parent column position
705                 int position = sourceTable.convertColumnIndexToView(sourceColumn.getModelIndex());
706                 deferredMove.put(position + offset, targetColumn.getModelIndex());
707                 offset = 0;
708             }
709 
710         }
711 
712         // Apply columns moves
713         if (!deferredMove.isEmpty()) {
714             // Process with the target position order instead of model order
715             for (Integer newColumnViewIndex : deferredMove.keySet()) {
716                 int columnViewIndex = getTable().convertColumnIndexToView(deferredMove.get(newColumnViewIndex));
717                 if (columnViewIndex == -1)
718                     continue;
719 
720                 if (newColumnViewIndex >= getTable().getColumnCount()) {
721                     newColumnViewIndex = getTable().getColumnCount() - 1;
722                 }
723 
724                 // Move column to target position
725                 getTable().moveColumn(columnViewIndex, newColumnViewIndex);
726             }
727         }
728     }
729 
730     /**
731      * <p>installSortController.</p>
732      */
733     @SuppressWarnings("unchecked")
734     protected void installSortController() {
735 
736         // uninstall first
737         uninstallSortController();
738 
739         SwingTableSortController controller = new SwingTableSortController(getTableModel());
740         // affect now to let table do initialization
741         getTable().setRowSorter(controller);
742 
743         // create comparator for each column in column model
744         DecoratorService decoratorService = ClientServiceLocator.instance().getDecoratorService();
745         getTable().getColumns(true).stream().map(TableColumnExt.class::cast).forEach(column -> {
746 
747             if (!column.isSortable()) {
748 
749                 controller.setSortable(column.getModelIndex(), false);
750 
751             } else {
752 
753                 ColumnIdentifier identifier = (ColumnIdentifier) column.getIdentifier();
754                 Comparator comparator = null;
755 
756                 // try to find a decorator for this identifier
757                 if (decoratorService != null) {
758                     Decorator decorator = decoratorService.getDecoratorByType(identifier.getPropertyType(), identifier.getDecoratorName());
759                     if (decorator != null) {
760                         // get the decorator comparator if exists
761                         comparator = decorator.getCurrentComparator();
762 
763                         // set this decorator if the comparator dont have it (Mantis #46262)
764                         if (comparator instanceof DecoratorComparator && ((DecoratorComparator) comparator).getDecorator() == null) {
765                             ((DecoratorComparator) comparator).setDecorator(decorator);
766                         }
767                     }
768                 }
769 
770                 // create default comparator from column class
771                 if (comparator == null) {
772                     Class<?> columnClass = identifier.getPropertyType();
773                     if (columnClass == String.class) {
774                         // specific comparator for String
775                         comparator = AlphanumericComparator.instance();
776                     } else if (Comparable.class.isAssignableFrom(columnClass)) {
777                         // default comparator for Comparable objects
778                         comparator = ComparableComparator.comparableComparator();
779                     }
780                 }
781 
782                 // set this comparator to the controller
783                 controller.setComparator(column.getModelIndex(), comparator);
784             }
785         });
786 
787     }
788 
789     /**
790      * <p>uninstallSortController.</p>
791      */
792     protected void uninstallSortController() {
793 
794         // tell the table to not auto create row sorter
795         getTable().setAutoCreateRowSorter(false);
796 
797         // remove actual row sorter
798         getTable().setRowSorter(null);
799 
800         // also for fixed table
801         if (getFixedTable() != null) {
802             getFixedTable().setAutoCreateRowSorter(false);
803             getFixedTable().setRowSorter(null);
804         }
805     }
806 
807     protected SwingTableSortController getSortController() {
808         return (SwingTableSortController) getTable().getRowSorter();
809     }
810 
811     //------------------------------------------------------------------------//
812     //-- Event methods                                                      --//
813     //------------------------------------------------------------------------//
814 
815     /**
816      * <p>onModelRowsChanged.</p>
817      *
818      * @param rows a {@link List} object.
819      */
820     protected void onModelRowsChanged(List<R> rows) {
821         if (LOG.isDebugEnabled()) {
822             LOG.debug("Will set " + (rows == null ? 0 : rows.size())
823                 + " rows on model.");
824         }
825         cleanRowMonitor();
826         getTableModel().setRows(rows);
827     }
828 
829     /**
830      * <p>onRowsAdded.</p>
831      *
832      * @param addedRows a {@link List} object.
833      */
834     protected void onRowsAdded(List<R> addedRows) {
835         if (LOG.isDebugEnabled()) {
836             LOG.debug((addedRows == null ? 0 : addedRows.size())
837                 + " rows added on model.");
838         }
839     }
840 
841     /**
842      * <p>onRowModifyStateChanged.</p>
843      *
844      * @param rowIndex a int.
845      * @param row      a R object.
846      * @param oldValue a {@link Boolean} object.
847      * @param newValue a {@link Boolean} object.
848      */
849     protected void onRowModifyStateChanged(int rowIndex, R row, Boolean oldValue, Boolean newValue) {
850         if (LOG.isDebugEnabled()) {
851             LOG.debug("row [" + rowIndex + "] modify state changed from "
852                 + oldValue + " to " + newValue);
853         }
854     }
855 
856     /**
857      * <p>onRowValidStateChanged.</p>
858      *
859      * @param rowIndex a int.
860      * @param row      a R object.
861      * @param oldValue a {@link Boolean} object.
862      * @param newValue a {@link Boolean} object.
863      */
864     protected void onRowValidStateChanged(int rowIndex, R row, Boolean oldValue, Boolean newValue) {
865         if (LOG.isDebugEnabled()) {
866             LOG.debug("row [" + rowIndex + "] valid state changed from "
867                 + oldValue + " to " + newValue);
868         }
869         if (rowIndex > -1) {
870             getTableModel().fireTableRowsUpdated(rowIndex, rowIndex);
871         }
872     }
873 
874     /**
875      * <p>onRowSelectedStateChanged.</p>
876      *
877      * @param rowIndex a int.
878      * @param row      a R object.
879      * @param oldValue a {@link Boolean} object.
880      * @param newValue a {@link Boolean} object.
881      */
882     protected void onRowSelectedStateChanged(int rowIndex, R row, Boolean oldValue, Boolean newValue) {
883         if (LOG.isDebugEnabled()) {
884             LOG.debug("row [" + rowIndex + "] selected state changed from "
885                 + oldValue + " to " + newValue);
886         }
887         recalculateRowSelection(row);
888     }
889 
890     /**
891      * Invoke each time the modify state on the current selected row changed.
892      *
893      * @param rowIndex      row index of the modified row
894      * @param row           modified row
895      * @param propertyName  name of the modified property of the row
896      * @param propertyIndex index of the modified property of the row
897      * @param oldValue      old value of the modified property
898      * @param newValue      new value of the modified property
899      * @since 0.3
900      */
901     protected void onRowModified(int rowIndex, R row, String propertyName, Integer propertyIndex, Object oldValue, Object newValue) {
902         getModel().setModify(true);
903         getTable().repaint();
904         if (getFixedTable() != null) {
905             getFixedTable().repaint();
906         }
907         recomputeRowValidState(row);
908     }
909 
910     //------------------------------------------------------------------------//
911     //-- Validation methods                                                 --//
912     //------------------------------------------------------------------------//
913 
914     /**
915      * Validates the given row.
916      *
917      * @param row row to validate
918      * @return {@code true} if row is valid, {@code false} otherwise.
919      * @since 1.0.1
920      */
921     protected boolean isRowValid(R row) {
922 
923         // try to validate the mandatory columns
924         getTableModel().getMandatoryIdentifiers().forEach(identifier -> {
925             Object value = identifier.getValue(row);
926             if (value == null || (value instanceof String && StringUtils.isBlank((String) value))) {
927                 row.addInvalidMandatoryProperty(identifier);
928             } else {
929                 row.removeInvalidMandatoryProperty(identifier);
930             }
931         });
932 
933         // a row is valid when no invalid identifier found
934         return row.isMandatoryValid();
935     }
936 
937     /**
938      * <p>recomputeRowValidState.</p>
939      *
940      * @param row a R object.
941      */
942     public final void recomputeRowValidState(R row) {
943 
944         // recompute row valid state
945         boolean valid = isRowValid(row);
946 
947         // apply it to row
948         row.setValid(valid);
949 
950         if (valid) {
951             getModel().removeRowInError(row);
952         } else {
953             getModel().addRowInError(row);
954         }
955 
956     }
957 
958     /**
959      * <p>recomputeRowsValidState.</p>
960      */
961     public void recomputeRowsValidState() {
962         recomputeRowsValidState(true);
963     }
964 
965     /**
966      * <p>recomputeRowsValidState.</p>
967      *
968      * @param validIfNoRow a boolean.
969      */
970     public void recomputeRowsValidState(boolean validIfNoRow) {
971         List<R> rows = getModel().getRows();
972         if (LOG.isDebugEnabled()) {
973             LOG.debug("Will valid " + (rows == null ? 0 : rows.size())
974                 + " rows on model.");
975         }
976 
977         if (CollectionUtils.isNotEmpty(rows)) {
978 
979             // compute valid state for each row
980             Set<R> invalidRows = Sets.newHashSet();
981             rows.forEach(row -> {
982                 // recompute row valid state & apply it to row
983                 row.setValid(isRowValid(row));
984                 // add the row to the correct list
985                 if (!row.isValid()) {
986                     invalidRows.add(row);
987                 }
988             });
989 
990             // apply invalid rows in one line to prevent property change listeners to flicker
991             getModel().setRowsInError(invalidRows);
992         } else {
993             getModel().setValid(validIfNoRow);
994         }
995 
996     }
997 
998     //------------------------------------------------------------------------//
999     //-- Selection methods                                                  --//
1000     //------------------------------------------------------------------------//
1001 
1002     private void recalculateRowSelection(R currentRow) {
1003 
1004         // recalculate selected rows
1005         getModel().populateSelectedRows();
1006 
1007         // redraw header
1008         if (currentRow == null) {
1009             return;
1010         }
1011 
1012         // find CheckTableColumn
1013         findColumnByPredicate(column -> column instanceof CheckTableColumn).ifPresent(column -> {
1014             CheckTableColumn checkTableColumn = (CheckTableColumn) column;
1015             boolean all = true;
1016             Boolean currentSelected = currentRow.isSelected();
1017             for (R row : getTableModel().getRows()) {
1018                 if (!currentSelected.equals(row.isSelected())) {
1019                     all = false;
1020                     break;
1021                 }
1022             }
1023             checkTableColumn.setAllEnabled(all);
1024             checkTableColumn.setAllSelected(!all || currentSelected);
1025 
1026             // repaint
1027             JTableHeader h = checkTableColumn.getTable().getTableHeader();
1028             h.repaint(h.getHeaderRect(checkTableColumn.getTable().convertColumnIndexToView(checkTableColumn.getModelIndex())));
1029         });
1030     }
1031 
1032     /**
1033      * <p>selectAllValidRows.</p>
1034      */
1035     public void selectAllValidRows() {
1036         if (getModel().getRowCount() > 0) {
1037             getModel().getRows().stream().filter(R::isValid).forEach(row -> row.selected = true);
1038             recalculateRowSelection(getModel().getRows().get(0));
1039         }
1040     }
1041 
1042     /**
1043      * <p>selectAllEditableRows.</p>
1044      */
1045     public void selectAllEditableRows() {
1046         if (getModel().getRowCount() > 0) {
1047             getModel().getRows().stream().filter(row -> !row.isCalculated() && row.isEditable()).forEach(row -> row.selected = true);
1048             recalculateRowSelection(getModel().getRows().get(0));
1049         }
1050     }
1051 
1052     /**
1053      * <p>unselectAllRows.</p>
1054      */
1055     public void unselectAllRows() {
1056         getTable().clearSelection();
1057     }
1058 
1059     /**
1060      * Select a row
1061      *
1062      * @param row the row to select
1063      */
1064     public void selectRow(R row) {
1065         int rowViewIndex = getRowViewIndex(row);
1066         if (rowViewIndex == -1) {
1067             return;
1068         }
1069         if (getTable().getSelectionMode() != ListSelectionModel.SINGLE_SELECTION) {
1070             row.setSelected(true);
1071         }
1072         getTable().selectCell(rowViewIndex, null);
1073     }
1074 
1075     /**
1076      * Select a group of row
1077      *
1078      * @param rows rows to select
1079      */
1080     public void selectRows(List<R> rows) {
1081 
1082         rows.forEach(row -> {
1083             int rowViewIndex = getRowViewIndex(row);
1084             if (rowViewIndex != -1) {
1085                 row.setSelected(true);
1086                 getTable().addRowSelectionInterval(rowViewIndex, rowViewIndex);
1087 
1088                 if (rows.indexOf(row) == rows.size() - 1) {
1089                     getTable().scrollCellToVisible(rowViewIndex, 0);
1090                     recalculateRowSelection(row);
1091                 }
1092             }
1093         });
1094 
1095     }
1096 
1097     /**
1098      * Ajoute le focus sur la cellule dans la ligne et a la colonne
1099      *
1100      * @param row La ligne
1101      */
1102     public void setFocusOnCell(final R row) {
1103 
1104         // set current selected
1105         getModel().setSingleSelectedRow(row);
1106         recomputeRowValidState(row);
1107 
1108         SwingUtilities.invokeLater(() -> {
1109 
1110             // get row index
1111             int rowViewIndex = getRowViewIndex(row);
1112             if (rowViewIndex == -1) {
1113                 return;
1114             }
1115             if (!row.isEditable()) {
1116                 getTable().selectCell(rowViewIndex, null);
1117                 return;
1118             }
1119 
1120             // get the first column to edit
1121             final ColumnIdentifier<?> columnIdentifier = getTableModel().getFirstColumnEditing();
1122             if (columnIdentifier != null) {
1123 
1124                 // First, find the column in main table
1125                 SwingTable table = getTable();
1126                 TableColumnExt column = table.getColumnExt(columnIdentifier);
1127                 if (column instanceof HiddenColumn) {
1128                     // find the column in fixed table
1129                     table = getFixedTable();
1130                 }
1131 
1132                 int columnViewIndex = table.convertColumnIndexToView(column.getModelIndex());
1133                 if (columnViewIndex == -1) {
1134                     // select the line only
1135                     table.selectCell(rowViewIndex, null);
1136                     return;
1137                 }
1138 
1139                 int columnCount = table.getColumnCount(false);
1140 
1141                 while (columnViewIndex < columnCount && !table.isCellEditable(rowViewIndex, columnViewIndex)) {
1142                     columnViewIndex++;
1143                 }
1144 
1145                 // abort if column is out out of bounds
1146                 if (columnViewIndex >= columnCount) {
1147                     table.selectCell(rowViewIndex, null);
1148                     return;
1149                 }
1150 
1151                 // Selection de la ligne
1152                 table.selectCell(rowViewIndex, columnViewIndex);
1153 
1154                 // Edition de la cellule recherchee
1155                 table.editCellAt(rowViewIndex, columnViewIndex);
1156             }
1157         });
1158     }
1159 
1160     /**
1161      * <p>selectCell.</p>
1162      *
1163      * @param rowIndex a {@link Integer} object.
1164      * @param colIndex a {@link Integer} object.
1165      * @deprecated use SwingTable.selectCell()
1166      */
1167     @Deprecated
1168     public void selectCell(Integer rowIndex, Integer colIndex) {
1169         getTable().selectCell(rowIndex, colIndex);
1170     }
1171 
1172     /**
1173      * get the row view index of a row
1174      *
1175      * @param row the row
1176      * @return row view index of this row
1177      */
1178     private int getRowViewIndex(R row) {
1179         if (row == null) {
1180             return -1;
1181         }
1182         int rowIndex = getTableModel().getRowIndex(row);
1183         if (rowIndex == -1) {
1184             return -1;
1185         }
1186         return getTable().convertRowIndexToView(rowIndex);
1187     }
1188 
1189     /**
1190      * {@inheritDoc}
1191      * <p>
1192      * Override default Jaxx method because if the table selection mode is MULTIPLE_INTERVAL_SELECTION,
1193      * it will add the selected row in selection model, witch is not acceptable
1194      */
1195     @Override
1196     public void autoSelectRowInTable(MouseEvent e, JPopupMenu popup) {
1197         boolean rightClick = SwingUtilities.isRightMouseButton(e);
1198 
1199         if (e.getSource() instanceof JXTable && (rightClick || SwingUtilities.isLeftMouseButton(e))) {
1200 
1201             // get the coordinates of the mouse click
1202             Point p = e.getPoint();
1203 
1204             JXTable source = (JXTable) e.getSource();
1205 
1206             int[] selectedRows = source.getSelectedRows();
1207             int[] selectedColumns = source.getSelectedColumns();
1208 
1209             // get the row index at this point
1210             int rowIndex = source.rowAtPoint(p);
1211 
1212             // get the column index at this point
1213             int columnIndex = source.columnAtPoint(p);
1214 
1215             if (LOG.isDebugEnabled()) {
1216                 LOG.debug("At point [" + p + "] found Row " + rowIndex + ", Column " + columnIndex);
1217             }
1218 
1219             boolean canContinue = true;
1220 
1221             if (getTable().isEditing()) {
1222 
1223                 // stop editing on main table
1224                 boolean stopEdit = getTable().getCellEditor().stopCellEditing();
1225                 if (!stopEdit) {
1226                     if (LOG.isWarnEnabled()) {
1227                         LOG.warn("Could not stop edit cell on main table...");
1228                     }
1229                     canContinue = false;
1230                 }
1231             }
1232 
1233             if (getFixedTable() != null && getFixedTable().isEditing()) {
1234 
1235                 // stop editing on fixed table
1236                 boolean stopEdit = getFixedTable().getCellEditor().stopCellEditing();
1237                 if (!stopEdit) {
1238                     if (LOG.isWarnEnabled()) {
1239                         LOG.warn("Could not stop edit cell on fixed table...");
1240                     }
1241                     canContinue = false;
1242                 }
1243             }
1244 
1245             if (canContinue) {
1246 
1247                 // select row (could empty selection)
1248                 if (rowIndex == -1) {
1249                     source.clearSelection();
1250                 } else if (!ArrayUtils.contains(selectedRows, rowIndex)) {
1251                     source.setRowSelectionInterval(rowIndex, rowIndex);
1252                 }
1253 
1254                 // select column (could empty selection)
1255                 if (columnIndex == -1) {
1256                     source.clearSelection();
1257                 } else if (!ArrayUtils.contains(selectedColumns, columnIndex)) {
1258                     source.setColumnSelectionInterval(columnIndex, columnIndex);
1259                 }
1260 
1261                 if (rightClick) {
1262 
1263                     // use now model coordinate
1264                     int modelRowIndex = rowIndex == -1 ? -1 : source.convertRowIndexToModel(rowIndex);
1265                     int modelColumnIndex = columnIndex == -1 ? -1 : source.convertColumnIndexToModel(columnIndex);
1266 
1267                     beforeOpenPopup(modelRowIndex, modelColumnIndex);
1268 
1269                     // on right click show popup
1270                     popup.show(source, e.getX(), e.getY());
1271                 }
1272             }
1273         }
1274 
1275     }
1276 
1277     //------------------------------------------------------------------------//
1278     //-- Cell content methods                                               --//
1279     //------------------------------------------------------------------------//
1280 
1281     /**
1282      * Deprecated overridden method to force use of common application classes
1283      */
1284     @Override
1285     @Deprecated
1286     protected <O> TableColumnExt addColumnToModel(
1287         TableColumnModel model,
1288         org.nuiton.jaxx.application.swing.table.ColumnIdentifier<O> identifier) {
1289 
1290         return addColumnToModel(model, null, null, identifier);
1291     }
1292 
1293     /**
1294      * Deprecated overridden method to force use of common application classes
1295      */
1296     @Override
1297     @Deprecated
1298     protected <O> TableColumnExt addColumnToModel(
1299         TableColumnModel model,
1300         TableCellEditor editor,
1301         TableCellRenderer renderer,
1302         org.nuiton.jaxx.application.swing.table.ColumnIdentifier<O> identifier) {
1303 
1304         if (!(model instanceof SwingTableColumnModel)) {
1305             LOG.error("Wrong model type : " + model.getClass().getName());
1306             return null;
1307         }
1308 
1309         if (!(identifier instanceof ColumnIdentifier)) {
1310             LOG.error("Wrong identifier type : " + identifier.getClass().getName());
1311             return null;
1312         }
1313 
1314         return addColumn(editor, renderer, (ColumnIdentifier<R>) identifier);
1315     }
1316 
1317     protected TableColumnExt addBooleanColumnToModel(ColumnIdentifier<R> identifier, JTable table) {
1318         return super.addBooleanColumnToModel(getTable().getColumnModel(), identifier, table);
1319     }
1320 
1321     protected TableColumnExt addColumn(ColumnIdentifier<R> identifier) {
1322         return addColumn(null, null, identifier);
1323     }
1324 
1325     protected TableColumnExt addColumn(TableCellEditor editor, TableCellRenderer renderer, ColumnIdentifier<R> identifier) {
1326         return addColumn(editor, renderer, identifier, false);
1327     }
1328 
1329     private TableColumnExt addColumn(TableCellEditor editor, TableCellRenderer renderer, ColumnIdentifier<R> identifier, boolean hidden) {
1330         // add to model
1331         TableColumnExt column = hidden ? createHiddenColumn(getTable()) : createColumn(getTable());
1332 
1333         column.setIdentifier(identifier);
1334         setEditorAndRenderer(column, editor, renderer, identifier);
1335         return column;
1336     }
1337 
1338     public TableColumnExt addFixedColumn(ColumnIdentifier<R> identifier) {
1339         return addFixedColumn(null, null, identifier);
1340     }
1341 
1342     public TableColumnExt addFixedColumn(TableCellEditor editor, TableCellRenderer renderer, ColumnIdentifier<R> identifier) {
1343 
1344         Assert.isTrue(!initialized, "Add a new fixed column can be made before table initialization");
1345 
1346         // Add hidden column in main table
1347         TableColumnExt column = addColumn(editor, renderer, identifier, true);
1348 
1349         // Add it into pending fixed columns, processed in initTable
1350         pendingFixedColumns.add(column);
1351 
1352         return column;
1353     }
1354 
1355     private TableColumnExt createColumn(SwingTable table) {
1356         TableColumnExt column = new TableColumnExt(table.getColumnCount(true));
1357         table.getColumnModel().addColumn(column);
1358         return column;
1359     }
1360 
1361     private TableColumnExt createHiddenColumn(SwingTable table) {
1362         HiddenColumn column = new HiddenColumn(new TableColumnExt(table.getColumnCount(true)));
1363         table.getColumnModel().addColumn(column);
1364         return column;
1365     }
1366 
1367     protected void setEditorAndRenderer(TableColumnExt column, TableCellEditor editor, TableCellRenderer renderer, ColumnIdentifier<R> identifier) {
1368 
1369         // add a default renderer
1370         if (renderer == null) {
1371             Decorator<?> decorator = getDecorator(identifier.getPropertyType(), identifier.getDecoratorName());
1372             if (decorator != null) {
1373                 renderer = newTableCellRender(decorator);
1374             } else {
1375                 renderer = (table, value, isSelected, hasFocus, row, column1) -> {
1376                     TableCellRenderer defaultRenderer = table.getDefaultRenderer(identifier.getPropertyType());
1377                     JComponent component = (JComponent) defaultRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column1);
1378 
1379                     // add tooltip
1380                     if (value != null) {
1381                         component.setToolTipText(value.toString());
1382                     }
1383 
1384                     return component;
1385                 };
1386             }
1387         }
1388 
1389         column.setCellEditor(editor);
1390         column.setCellRenderer(renderer);
1391 
1392         // if the column is mandatory, set specific headerValue and toolTip
1393         if (identifier.isMandatory()) {
1394             column.setHeaderValue(t("quadrige3.table.mandatoryColumn.header", t(identifier.getHeaderI18nKey())));
1395             column.setToolTipText(t("quadrige3.table.mandatoryColumn.tip", t(identifier.getHeaderTipI18nKey())));
1396             // this column is not hideable
1397             column.setHideable(false);
1398         } else {
1399             // set header and tooltip as HTML
1400             column.setHeaderValue(ApplicationUIUtil.getHtmlString(t(identifier.getHeaderI18nKey())));
1401             column.setToolTipText(ApplicationUIUtil.getHtmlString(t(identifier.getHeaderTipI18nKey())));
1402         }
1403         // by default no column is sortable, must specify it
1404         column.setSortable(false);
1405     }
1406 
1407     protected void removeColumns(Collection<? extends TableColumn> columns) {
1408         if (CollectionUtils.isNotEmpty(columns)) {
1409             columns.stream().sorted((o1, o2) -> Integer.compare(o2.getModelIndex(), o1.getModelIndex())).forEach(this::removeColumn);
1410         }
1411     }
1412 
1413     @SuppressWarnings("unchecked")
1414     protected void removeColumn(TableColumn column) {
1415         ColumnIdentifier<R> identifier = (ColumnIdentifier<R>) column.getIdentifier();
1416         removeColumnIdentifier(identifier);
1417         getTable().getColumnModel().removeColumn(column);
1418     }
1419 
1420     /**
1421      * Remove this identifier from table model, shifting model index on remaining columns on the right
1422      *
1423      * @param identifier to remove
1424      */
1425     private void removeColumnIdentifier(ColumnIdentifier<R> identifier) {
1426         int index = getTableModel().getIdentifiers().indexOf(identifier);
1427         if (index == -1)
1428             return;
1429 
1430         for (int c = index + 1; c < getTable().getColumnModel().getColumnCount(true); c++) {
1431             TableColumnExt column = (TableColumnExt) getTable().getColumnModel().getColumns(true).get(c);
1432             Assert.isTrue(column.getModelIndex() > index, "this model index should be > then the removed identifier index");
1433             column.setModelIndex(column.getModelIndex() - 1);
1434         }
1435 
1436         getTableModel().getIdentifiers().remove(identifier);
1437     }
1438 
1439     @SuppressWarnings("unchecked")
1440     public <C extends TableColumnExt> C addFixedColumn(Class<C> columnClass) {
1441 
1442         Assert.isAssignable(FixedColumn.class, columnClass, "Only column class implementing FixedColumn can be add to the fixed table");
1443 
1444         initFixedTable();
1445 
1446         try {
1447             // create column in fixed table model
1448             C column = BeanUtils.instantiateClass(columnClass.getConstructor(SwingTable.class), getFixedTable());
1449             getFixedTable().getColumnModel().addColumn(column);
1450 
1451             // add identifier in main table
1452             if (column.getIdentifier() instanceof ColumnIdentifier) {
1453                 ColumnIdentifier<R> identifier = (ColumnIdentifier<R>) column.getIdentifier();
1454 
1455                 // create hidden column in main table
1456                 TableColumnExt hiddenColumn = createHiddenColumn(getTable());
1457                 hiddenColumn.setIdentifier(identifier);
1458                 // add identifier if not already present
1459                 if (getTableModel().getColumnIndex(identifier.getPropertyName()) == -1) {
1460                     getTableModel().getIdentifiers().add(identifier);
1461                 }
1462                 // add column transposition in fixed table because both columns can have different model indexes
1463                 getFixedTable().addColumnModelTransposition(column.getModelIndex(), hiddenColumn.getModelIndex());
1464 
1465                 // disable sorting on this column because sort controller is owned by the main table
1466                 hiddenColumn.setSortable(false);
1467 
1468                 // finally hide it
1469                 hiddenColumn.setVisible(false);
1470             }
1471 
1472             return column;
1473         } catch (NoSuchMethodException e) {
1474             throw new QuadrigeTechnicalException(e);
1475         }
1476 
1477     }
1478 
1479     @SuppressWarnings("unchecked")
1480     public void removeFixedColumn(TableColumn fixedColumn) {
1481         Assert.notNull(getFixedTable(), "Can not remove fixed column from absent fixed table");
1482         Assert.isInstanceOf(ColumnIdentifier.class, fixedColumn.getIdentifier());
1483 
1484         ColumnIdentifier<R> identifier = (ColumnIdentifier<R>) fixedColumn.getIdentifier();
1485         removeColumnIdentifier(identifier);
1486 
1487         getFixedTable().removeColumnModelTransposition(fixedColumn.getModelIndex());
1488 
1489         // find the hidden column in main table corresponding with the fixed column to remove
1490         findColumnByPredicate(tableColumn -> tableColumn instanceof HiddenColumn
1491             && ((HiddenColumn) tableColumn).getIdentifier().getPropertyName().equals(identifier.getPropertyName()))
1492             .ifPresent(tableColumn -> getTable().removeColumn(tableColumn));
1493 
1494         getFixedTable().removeColumn(fixedColumn);
1495         if (getFixedTable().getColumnCount() == 0)
1496             removeFixedTable();
1497     }
1498 
1499     protected void initFixedTable() {
1500         if (fixedTable != null)
1501             return;
1502 
1503         JScrollPane scrollPane = getParentScrollPane();
1504         fixedTable = new FixedSwingTable(getTable());
1505 
1506         // add highlighters
1507         initHighlighters(fixedTable);
1508 
1509         // Add default String editor
1510         fixedTable.setDefaultEditor(String.class, new StringCellEditor());
1511 
1512         // Override the TAB and MAJ+TAB keys to perform specific actions
1513         overrideTabKeyActions(fixedTable, false);
1514 
1515         // Set position on scroll pane
1516         scrollPane.setRowHeaderView(fixedTable);
1517         scrollPane.setCorner(JScrollPane.UPPER_LEFT_CORNER, fixedTable.getTableHeader());
1518 
1519     }
1520 
1521     protected void removeFixedTable() {
1522         if (fixedTable == null)
1523             return;
1524 
1525         JScrollPane scrollPane = getParentScrollPane();
1526         scrollPane.setRowHeaderView(null);
1527         scrollPane.setCorner(JScrollPane.UPPER_LEFT_CORNER, null);
1528         fixedTable = null;
1529     }
1530 
1531     public boolean hasRowNumberColumn() {
1532         return hasFixedColumn(tableColumn -> tableColumn instanceof RowNumberColumn);
1533     }
1534 
1535     public boolean hasCheckTableColumn() {
1536         return hasFixedColumn(tableColumn -> tableColumn instanceof CheckTableColumn);
1537     }
1538 
1539     public boolean hasFixedColumn(Predicate<TableColumn> predicate) {
1540         return getTable().getColumnModel().hasFixedColumn(predicate)
1541             || (getFixedTable() != null && getFixedTable().getColumnModel().hasFixedColumn(predicate));
1542     }
1543 
1544     public Optional<TableColumn> findColumnByPredicate(Predicate<TableColumn> predicate) {
1545         Optional<TableColumn> column = getTable().getColumns(true).stream().filter(predicate).findFirst();
1546         if (!column.isPresent() && getFixedTable() != null) {
1547             column = getFixedTable().getColumns(true).stream().filter(predicate).findFirst();
1548         }
1549         return column;
1550     }
1551 
1552     protected <B> TableColumnExt addSimpleComboDataColumnToModel(ColumnIdentifier<R> identifier, List<B> data) {
1553         Decorator<?> decorator = getDecorator(identifier.getPropertyType(), identifier.getDecoratorName());
1554 
1555         JComboBox<B> comboBox = new JComboBox<>();
1556         comboBox.setRenderer(this.newListCellRender(decorator));
1557         List<B> dataToList = Lists.newArrayList(data);
1558         if (!dataToList.isEmpty() && dataToList.get(0) != null) {
1559             dataToList.add(0, null);
1560         }
1561         SwingUtil.fillComboBox(comboBox, dataToList, null);
1562         ObjectToStringConverter converter = BeanUIUtil.newDecoratedObjectToStringConverter(decorator);
1563         BeanUIUtil.decorate(comboBox, converter);
1564         return addColumn(new ComboBoxCellEditor(comboBox), this.newTableCellRender(decorator), identifier);
1565     }
1566 
1567     /**
1568      * override method of AbstractApplicationUIHandler. it creates a BeanFilterableComboBox
1569      *
1570      * @param identifier a {@link ColumnIdentifier} object.
1571      * @param data       a {@link List} object.
1572      * @param resettable a boolean.
1573      * @return a column
1574      */
1575     protected <B> TableColumnExt addFilterableComboDataColumnToModel(ColumnIdentifier<R> identifier,
1576                                                                      List<B> data, boolean resettable) {
1577         @SuppressWarnings("unchecked")
1578         Class<B> beanType = (Class<B>) identifier.getPropertyType();
1579         String decoratorName = identifier.getDecoratorName();
1580         FilterableComboBoxCellEditor<B> editor = newFilterableComboBoxCellEditor(data, beanType, decoratorName, resettable);
1581         return addColumn(editor, newTableCellRender(beanType, decoratorName), identifier);
1582     }
1583 
1584     /**
1585      * <p>addExtendedComboDataColumnToModel.</p>
1586      *
1587      * @param <B>        a B object.
1588      * @param identifier a {@link ColumnIdentifier} object.
1589      * @param data       a {@link List} object.
1590      * @param resettable a boolean.
1591      * @return a {@link org.jdesktop.swingx.table.TableColumnExt} object.
1592      */
1593     protected <B> TableColumnExt addExtendedComboDataColumnToModel(ColumnIdentifier<R> identifier,
1594                                                                    List<B> data, boolean resettable) {
1595         @SuppressWarnings("unchecked")
1596         Class<B> beanType = (Class<B>) identifier.getPropertyType();
1597         String decoratorName = identifier.getDecoratorName();
1598         FilterableComboBoxCellEditor<B> editor = newExtendedComboBoxCellEditor(data, beanType, decoratorName, resettable);
1599         return addColumn(editor, newTableCellRender(beanType, decoratorName), identifier);
1600     }
1601 
1602     /**
1603      * <p>newFilterableComboBoxCellEditor.</p>
1604      *
1605      * @param data       a {@link java.util.List} object.
1606      * @param beanType   a {@link java.lang.Class} object.
1607      * @param resettable a boolean.
1608      * @param <B>        a B object.
1609      * @return a {@link FilterableComboBoxCellEditor} object.
1610      */
1611     protected <B> FilterableComboBoxCellEditor<B> newFilterableComboBoxCellEditor(List<B> data, Class<B> beanType,
1612                                                                                   boolean resettable) {
1613         return newFilterableComboBoxCellEditor(data, beanType, null, resettable);
1614     }
1615 
1616     /**
1617      * <p>newFilterableComboBoxCellEditor.</p>
1618      *
1619      * @param data          a {@link java.util.List} object.
1620      * @param beanType      a {@link java.lang.Class} object.
1621      * @param decoratorName a {@link java.lang.String} object.
1622      * @param resettable    a boolean.
1623      * @param <B>           a B object.
1624      * @return a {@link FilterableComboBoxCellEditor} object.
1625      */
1626     protected <B> FilterableComboBoxCellEditor<B> newFilterableComboBoxCellEditor(List<B> data, Class<B> beanType,
1627                                                                                   String decoratorName, boolean resettable) {
1628 
1629         BeanFilterableComboBox<B> comboBox = new BeanFilterableComboBox<>(getUI());
1630         preInitBeanFilterableComboBox(comboBox, beanType, resettable);
1631         initBeanFilterableComboBox(comboBox, data, null, decoratorName);
1632         return new FilterableComboBoxCellEditor<>(comboBox);
1633     }
1634 
1635     /**
1636      * <p>newExtendedComboBoxCellEditor.</p>
1637      *
1638      * @param data       a {@link java.util.List} object.
1639      * @param beanType   a {@link java.lang.Class} object.
1640      * @param resettable a boolean.
1641      * @param <B>        a B object.
1642      * @return a {@link ExtendedComboBoxCellEditor} object.
1643      */
1644     protected <B> ExtendedComboBoxCellEditor<B> newExtendedComboBoxCellEditor(List<B> data, Class<B> beanType, boolean resettable) {
1645         return newExtendedComboBoxCellEditor(data, beanType, null, resettable);
1646     }
1647 
1648     /**
1649      * <p>newExtendedComboBoxCellEditor.</p>
1650      *
1651      * @param data       a {@link java.util.List} object.
1652      * @param identifier a {@link ColumnIdentifier} object.
1653      * @param resettable a boolean.
1654      * @param <B>        a B object.
1655      * @return a {@link ExtendedComboBoxCellEditor} object.
1656      */
1657     @SuppressWarnings("unchecked")
1658     protected <B> ExtendedComboBoxCellEditor<B> newExtendedComboBoxCellEditor(List<B> data, ColumnIdentifier identifier, boolean resettable) {
1659         Class<B> beanType = identifier.getPropertyType();
1660         return newExtendedComboBoxCellEditor(data, beanType, identifier.getDecoratorName(), resettable);
1661     }
1662 
1663     /**
1664      * <p>newExtendedComboBoxCellEditor.</p>
1665      *
1666      * @param data          a {@link java.util.List} object.
1667      * @param beanType      a {@link java.lang.Class} object.
1668      * @param decoratorName a {@link java.lang.String} object.
1669      * @param resettable    a boolean.
1670      * @param <B>           a B object.
1671      * @return a {@link ExtendedComboBoxCellEditor} object.
1672      */
1673     protected <B> ExtendedComboBoxCellEditor<B> newExtendedComboBoxCellEditor(List<B> data, Class<B> beanType,
1674                                                                               String decoratorName, boolean resettable) {
1675 
1676         ExtendedComboBox<B> comboBox = new ExtendedComboBox<>(getUI());
1677         preInitBeanFilterableComboBox(comboBox, beanType, resettable);
1678         initBeanFilterableComboBox(comboBox, data, null, decoratorName);
1679         return new ExtendedComboBoxCellEditor<>(comboBox);
1680     }
1681 
1682     private <B> void preInitBeanFilterableComboBox(BeanFilterableComboBox<B> comboBox, Class<B> beanType, boolean resettable) {
1683         comboBox.setFilterable(true);
1684         comboBox.setShowReset(resettable);
1685         comboBox.setShowDecorator(false);
1686         comboBox.setBeanType(beanType);
1687         comboBox.setAutoFocus(false);
1688     }
1689 
1690     /**
1691      * <p>addCoordinateColumnToModel.</p>
1692      *
1693      * @param coordinateType a {@link CoordinateEditor.CoordinateType} object.
1694      * @param identifier     a {@link ColumnIdentifier} object.
1695      * @return a {@link org.jdesktop.swingx.table.TableColumnExt} object.
1696      * <p>
1697      */
1698     protected TableColumnExt addCoordinateColumnToModel(
1699         CoordinateEditor.CoordinateType coordinateType,
1700         ColumnIdentifier<R> identifier) {
1701 
1702         TableCellEditor editor = new CoordinateCellEditor(coordinateType);
1703         TableCellRenderer renderer = newNumberCellRenderer(6);
1704         return addColumn(editor, renderer, identifier);
1705     }
1706 
1707     protected TableColumnExt addCommentColumn(ColumnIdentifier<R> identifier) {
1708         return addCommentColumn(identifier, true);
1709     }
1710 
1711     protected TableColumnExt addCommentColumn(ColumnIdentifier<R> identifier, boolean editable) {
1712         return addCommentColumn(identifier, null, editable);
1713     }
1714 
1715     protected TableColumnExt addCommentColumn(ColumnIdentifier<R> identifier, String propertyName, boolean editable) {
1716         TableColumnExt column = addColumn(
1717             CommentCellEditor.newEditor(getContext().getMainUI(), propertyName, identifier.getHeaderI18nKey(), editable),
1718             CommentCellRenderer.newRenderer(),
1719             identifier);
1720         column.setSortable(false);
1721         fixColumnWidth(column, 112); // is a correct width for fr_FR
1722         return column;
1723     }
1724 
1725     /**
1726      * <p>addDatePickerColumnToModel.</p>
1727      *
1728      * @param identifier a {@link ColumnIdentifier} object.
1729      * @return a {@link TableColumnExt} object.
1730      */
1731     protected TableColumnExt addDatePickerColumnToModel(ColumnIdentifier<R> identifier, String dateFormat) {
1732         return addDatePickerColumnToModel(identifier, dateFormat, true);
1733     }
1734 
1735     protected TableColumnExt addLocalDatePickerColumnToModel(ColumnIdentifier<R> identifier, String dateFormat) {
1736         return addLocalDatePickerColumnToModel(identifier, dateFormat, true);
1737     }
1738 
1739     /**
1740      * <p>addDatePickerColumnToModel.</p>
1741      *
1742      * @param identifier a {@link ColumnIdentifier} object.
1743      * @param isEditable a boolean.
1744      * @return a {@link TableColumnExt} object.
1745      */
1746     protected TableColumnExt addDatePickerColumnToModel(ColumnIdentifier<R> identifier, String dateFormat, boolean isEditable) {
1747         return addColumn(
1748             isEditable ? newDateCellEditor(dateFormat) : null,
1749             newDateCellRenderer(dateFormat),
1750             identifier);
1751     }
1752 
1753     protected TableColumnExt addLocalDatePickerColumnToModel(ColumnIdentifier<R> identifier, String dateFormat, boolean isEditable) {
1754         return addColumn(
1755             isEditable ? newLocalDateCellEditor(dateFormat) : null,
1756             newLocalDateCellRenderer(dateFormat),
1757             identifier);
1758     }
1759 
1760     /**
1761      * <p>newDateCellEditor2.</p>
1762      *
1763      * @param pattern a {@link String} object.
1764      * @return a {@link TableCellEditor} object.
1765      */
1766     protected TableCellEditor newDateCellEditor(String pattern) {
1767         return new DatePickerCellEditor(pattern);
1768     }
1769 
1770     protected TableCellEditor newLocalDateCellEditor(String pattern) {
1771         return new LocalDatePickerCellEditor(pattern);
1772     }
1773 
1774     /**
1775      * <p>newDateCellRenderer.</p>
1776      *
1777      * @param pattern a {@link String} object.
1778      * @return a {@link TableCellRenderer} object.
1779      */
1780     protected TableCellRenderer newDateCellRenderer(String pattern) {
1781         return new DateCellRenderer(getTable().getDefaultRenderer(Date.class), pattern);
1782     }
1783 
1784     protected TableCellRenderer newLocalDateCellRenderer(String pattern) {
1785         return new LocalDateCellRenderer(getTable().getDefaultRenderer(Date.class), pattern);
1786     }
1787 
1788     /**
1789      * <p>newBigDecimalRenderer.</p>
1790      *
1791      * @return a {@link javax.swing.table.TableCellRenderer} object.
1792      */
1793     public TableCellRenderer newBigDecimalRenderer() {
1794         DefaultTableRenderer defaultRenderer = new DefaultTableRenderer((StringValue) value -> {
1795 
1796             // be sure the value is a BigDecimal
1797             if (value instanceof BigDecimal) {
1798                 BigDecimal bigDecimal = (BigDecimal) value;
1799                 return bigDecimal.toString();
1800             }
1801 
1802             return "";
1803         }, JLabel.RIGHT);
1804         return new NumberCellRenderer(defaultRenderer);
1805     }
1806 
1807     /**
1808      * return a renderer for numbers with correct decimal format
1809      *
1810      * @param nbDecimal number of decimals in string representation
1811      * @return a TableCellRenderer
1812      */
1813     public TableCellRenderer newNumberCellRenderer(final int nbDecimal) {
1814         DefaultTableRenderer defaultRenderer = new DefaultTableRenderer((StringValue) value -> {
1815             if (value == null) {
1816                 return "";
1817             }
1818             // compute min and max number of decimals
1819             int minDecimal = (value instanceof Integer) ? 0 : 1;
1820             int maxDecimal = (value instanceof Integer) ? 0 : nbDecimal;
1821             // get number formatter
1822             DecimalFormat decimalFormat = ApplicationUIUtil.getDecimalFormat(minDecimal, maxDecimal);
1823             return decimalFormat.format(value);
1824         }, JLabel.RIGHT);
1825         return new NumberCellRenderer(defaultRenderer);
1826     }
1827 
1828     /**
1829      * <p>newNumberCellEditor.</p>
1830      *
1831      * @param numberClass   a {@link java.lang.Class} object.
1832      * @param useSign       a boolean.
1833      * @param numberPattern a {@link java.lang.String} object.
1834      * @param minValue      a N object.
1835      * @param maxValue      a N object.
1836      * @param <N>           a N object.
1837      * @return a {@link javax.swing.table.TableCellEditor} object.
1838      */
1839     public <N extends Number> TableCellEditor newNumberCellEditor(Class<N> numberClass, boolean useSign, String numberPattern, N minValue, N maxValue) {
1840         return new NumberCellEditor<>(numberClass, useSign, numberPattern, minValue, maxValue);
1841     }
1842 
1843     /**
1844      * <p>newNumberCellEditor.</p>
1845      *
1846      * @param numberClass   a {@link java.lang.Class} object.
1847      * @param useSign       a boolean.
1848      * @param numberPattern a {@link java.lang.String} object.
1849      * @param <N>           a N object.
1850      * @return a {@link javax.swing.table.TableCellEditor} object.
1851      */
1852     public <N extends Number> TableCellEditor newNumberCellEditor(Class<N> numberClass, boolean useSign, String numberPattern) {
1853         return newNumberCellEditor(numberClass, useSign, numberPattern, null, null);
1854     }
1855 
1856     /**
1857      * <p>newTableCellRender.</p>
1858      *
1859      * @param identifier a {@link ColumnIdentifier} object.
1860      * @return a {@link TableCellRenderer} object.
1861      */
1862     protected TableCellRenderer newTableCellRender(ColumnIdentifier<?> identifier) {
1863         return newTableCellRender(identifier.getPropertyType(), identifier.getDecoratorName());
1864     }
1865 
1866     /**
1867      * {@inheritDoc}
1868      */
1869     @Override
1870     protected <O> TableCellRenderer newTableCellRender(Class<O> type, String name) {
1871         Decorator<O> decorator = getDecorator(type, name);
1872         return newTableCellRender(decorator, null);
1873     }
1874 
1875     protected <O> TableCellRenderer newTableCellRender(Class<O> type, String context, String tooltipContext) {
1876         Decorator<O> decorator = getDecorator(type, context);
1877         Decorator<O> tooltipDecorator = getDecorator(type, tooltipContext);
1878         return newTableCellRender(decorator, tooltipDecorator);
1879     }
1880 
1881     @Override
1882     @Deprecated
1883     protected <O> TableCellRenderer newTableCellRender(org.nuiton.decorator.Decorator<O> decorator) {
1884         if (decorator instanceof Decorator) {
1885             return newTableCellRender((Decorator<O>) decorator);
1886         }
1887         LOG.warn("use of deprecated method : fr.ifremer.quadrige3.ui.swing.common.table.AbstractTableUIHandler.newTableCellRender(org.nuiton.decorator.Decorator<O>)");
1888         return super.newTableCellRender(decorator);
1889     }
1890 
1891     protected <O> TableCellRenderer newTableCellRender(Decorator<O> decorator) {
1892         return newTableCellRender(decorator, null);
1893     }
1894 
1895     protected TableCellRenderer newTableCellRender(Decorator decorator, Decorator tooltipDecorator) {
1896         return new ExtendedDecoratorCellRenderer(decorator, tooltipDecorator);
1897     }
1898 
1899     /**
1900      * {@inheritDoc}
1901      */
1902     @Override
1903     protected <O> TableCellRenderer newTableCellRender(Class<O> type) {
1904         return this.newTableCellRender(type, null);
1905     }
1906 
1907     //------------------------------------------------------------------------//
1908     //-- Other methods                                                      --//
1909     //------------------------------------------------------------------------//
1910 
1911     /**
1912      * <p>cleanRowMonitor.</p>
1913      */
1914     protected void cleanRowMonitor() {
1915         rowMonitor.clearModified();
1916     }
1917 
1918     /**
1919      * <p>stopCellEditing.</p>
1920      */
1921     public void stopCellEditing() {
1922 
1923         if (getTable().isEditing()) {
1924             getTable().getCellEditor().stopCellEditing();
1925         }
1926         if (getFixedTable() != null && getFixedTable().isEditing()) {
1927             getFixedTable().getCellEditor().stopCellEditing();
1928         }
1929     }
1930 
1931     /**
1932      * <p>getRowPropertiesToIgnore.</p>
1933      *
1934      * @return an array of {@link String} objects.
1935      */
1936     protected String[] getRowPropertiesToIgnore() {
1937         return ArrayUtils.EMPTY_STRING_ARRAY;
1938     }
1939 
1940     /**
1941      * <p>fixColumnWidth.</p>
1942      *
1943      * @param column a {@link TableColumn} object.
1944      * @param width  a int.
1945      */
1946     protected void fixColumnWidth(TableColumn column, int width) {
1947         column.setMinWidth(width);
1948         column.setMaxWidth(width);
1949     }
1950 
1951     protected void fixDefaultColumnWidth(TableColumn column) {
1952         fixColumnWidth(column, DEFAULT_MIN_COLUMN_WIDTH);
1953     }
1954 
1955     protected void setDefaultColumnMinWidth(TableColumn column) {
1956         column.setMinWidth(DEFAULT_MIN_COLUMN_WIDTH);
1957     }
1958 
1959     protected void forceColumnVisibleAtLastPosition(ColumnIdentifier identifier, boolean visible) {
1960         forceColumnVisible(identifier, visible);
1961         if (visible) moveColumnAtLastPosition(identifier);
1962     }
1963 
1964     protected void forceColumnVisible(ColumnIdentifier identifier, boolean visible) {
1965         getTable().getColumns(true).stream()
1966             .filter(column -> column.getIdentifier() == identifier && column instanceof TableColumnExt).findFirst()
1967             .ifPresent(column -> {
1968                 TableColumnExt columnExt = (TableColumnExt) column;
1969                 columnExt.setHideable(!visible);
1970                 columnExt.setVisible(visible);
1971             });
1972     }
1973 
1974     /**
1975      * Move column at last position (right)
1976      *
1977      * @param identifier identifier of column to move
1978      */
1979     protected void moveColumnAtLastPosition(ColumnIdentifier identifier) {
1980         TableColumnExt columnExt = getTable().getColumnExt(identifier);
1981         if (columnExt != null) {
1982             getTable().moveColumn(getTable().convertColumnIndexToView(columnExt.getModelIndex()), getTable().getColumnCount() - 1);
1983         }
1984     }
1985 
1986     /**
1987      * Move column after the anchor. Both columns must exists and visible
1988      *
1989      * @param identifier identifier of column to move
1990      * @param anchor     identifier of anchor column
1991      */
1992     protected void moveColumnAfter(ColumnIdentifier identifier, ColumnIdentifier anchor) {
1993         TableColumnExt columnToMove = getTable().getColumnExt(identifier);
1994         TableColumnExt anchorColumn = getTable().getColumnExt(anchor);
1995         if (columnToMove != null && columnToMove.isVisible() && anchorColumn != null && anchorColumn.isVisible()) {
1996             int columnToMoveIndex = getTable().convertColumnIndexToView(columnToMove.getModelIndex());
1997             int anchorColumnIndex = getTable().convertColumnIndexToView(anchorColumn.getModelIndex());
1998             int targetIndex = Math.min(anchorColumnIndex + (columnToMoveIndex > anchorColumnIndex ? 1 : 0), getTable().getColumnCount() - 1);
1999             getTable().moveColumn(columnToMoveIndex, targetIndex);
2000         }
2001     }
2002 
2003     /**
2004      * Add show/hide row number action to the current SwingTable
2005      */
2006     public void addRowNumbersAction() {
2007         if (getAdditionalTableActions().getActions().stream().noneMatch(action -> action instanceof RowNumberColumnAction)) {
2008             RowNumberColumnAction action = new RowNumberColumnAction(this);
2009             action.putValue(AdditionalTableActions.ACTION_TARGET_GROUP, 0);
2010             getAdditionalTableActions().addAction(action);
2011         }
2012     }
2013 
2014     /**
2015      * Get (and create not exists) the additional table actions
2016      *
2017      * @return AdditionalTableActions of the table
2018      */
2019     public AdditionalTableActions getAdditionalTableActions() {
2020         Action additionalActions = getTable().getActionMap().get(AdditionalTableActions.ACTION_NAME);
2021         if (!(additionalActions instanceof AdditionalTableActions)) {
2022             additionalActions = new AdditionalTableActions();
2023             getTable().getActionMap().put(AdditionalTableActions.ACTION_NAME, additionalActions);
2024         }
2025         return (AdditionalTableActions) additionalActions;
2026     }
2027 
2028     /**
2029      * <p>getNextComponentToFocus.</p>
2030      *
2031      * @return a {@link Component} object.
2032      */
2033     public Component getNextComponentToFocus() {
2034         return null;
2035     }
2036 
2037     /**
2038      * <p>getPreviousComponentToFocus.</p>
2039      *
2040      * @return a {@link Component} object.
2041      */
2042     public Component getPreviousComponentToFocus() {
2043         return null;
2044     }
2045 
2046     private JScrollPane getParentScrollPane() {
2047         Container parent = SwingUtilities.getUnwrappedParent(getTable());
2048         JScrollPane scrollPane = null;
2049         if (parent instanceof JViewport) {
2050             JViewport viewport = (JViewport) parent;
2051             Container viewportParent = viewport.getParent();
2052             if (viewportParent instanceof JScrollPane) {
2053                 scrollPane = (JScrollPane) viewportParent;
2054             }
2055         }
2056         Assert.notNull(scrollPane, "the table should be in a parent scroll pane");
2057         return scrollPane;
2058     }
2059 
2060     /**
2061      * Fix table scrollable view if no column, but fixed table has column (Mantis #53717)
2062      */
2063     protected void fixScrollableHeight() {
2064         if (getFixedTable() != null && getFixedTable().getColumnCount() > 0 && getTable().getColumnCount() == 0) {
2065             SwingUtilities.invokeLater(() ->
2066                 getTable().setPreferredSize(
2067                     new Dimension(
2068                         getTable().getPreferredScrollableViewportSize().width,
2069                         getFixedTable().getPreferredSize().height
2070                     )
2071                 )
2072             );
2073         }
2074     }
2075 }