View Javadoc
1   package fr.ifremer.tutti.ui.swing.util.table;
2   
3   /*
4    * #%L
5    * Tutti :: UI
6    * %%
7    * Copyright (C) 2012 - 2014 Ifremer
8    * %%
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU General Public License as
11   * published by the Free Software Foundation, either version 3 of the 
12   * License, or (at your option) any later version.
13   * 
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Public License for more details.
18   * 
19   * You should have received a copy of the GNU General Public 
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/gpl-3.0.html>.
22   * #L%
23   */
24  
25  import com.google.common.base.Preconditions;
26  import com.google.common.collect.Sets;
27  import fr.ifremer.tutti.ui.swing.content.operation.catches.species.edit.SampleCategoryComponent.SampleCategoryEditor;
28  import fr.ifremer.tutti.ui.swing.util.AbstractTuttiBeanUIModel;
29  import fr.ifremer.tutti.ui.swing.util.AbstractTuttiUIHandler;
30  import fr.ifremer.tutti.ui.swing.util.TuttiBeanMonitor;
31  import fr.ifremer.tutti.ui.swing.util.TuttiUI;
32  import fr.ifremer.tutti.ui.swing.util.computable.ComputableDataTableCell.TuttiComputedOrNotDataTableCellEditor;
33  import jaxx.runtime.SwingUtil;
34  import jaxx.runtime.swing.editor.cell.NumberCellEditor;
35  import org.apache.commons.collections4.CollectionUtils;
36  import org.apache.commons.lang3.ArrayUtils;
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  import org.jdesktop.swingx.JXTable;
40  import org.nuiton.jaxx.application.swing.table.AbstractApplicationTableModel;
41  import org.nuiton.jaxx.application.swing.table.MoveToNextEditableCellAction;
42  import org.nuiton.jaxx.application.swing.table.MoveToNextEditableRowAction;
43  import org.nuiton.jaxx.application.swing.table.MoveToPreviousEditableCellAction;
44  import org.nuiton.jaxx.application.swing.table.MoveToPreviousEditableRowAction;
45  
46  import javax.swing.AbstractAction;
47  import javax.swing.JTable;
48  import javax.swing.ListSelectionModel;
49  import javax.swing.event.ListSelectionEvent;
50  import javax.swing.event.ListSelectionListener;
51  import javax.swing.table.TableCellEditor;
52  import javax.swing.table.TableColumn;
53  import javax.swing.table.TableColumnModel;
54  import java.awt.event.KeyAdapter;
55  import java.awt.event.KeyEvent;
56  import java.beans.PropertyChangeEvent;
57  import java.beans.PropertyChangeListener;
58  import java.util.Enumeration;
59  import java.util.HashMap;
60  import java.util.List;
61  import java.util.Map;
62  import java.util.Set;
63  
64  /**
65   * @param <R> type of a row
66   * @param <M> type of the ui model
67   * @author Tony Chemit - chemit@codelutin.com
68   * @since 0.2
69   */
70  public abstract class AbstractTuttiTableUIHandler<R extends AbstractTuttiBeanUIModel, M extends AbstractTuttiTableUIModel<?, R, M>, UI extends TuttiUI<M, ?>> extends AbstractTuttiUIHandler<M, UI> {
71  
72      /** Logger. */
73      private static final Log log =
74              LogFactory.getLog(AbstractTuttiTableUIHandler.class);
75  
76      /**
77       * @return the table model handled byt the main table.
78       * @since 0.2
79       */
80      public abstract AbstractApplicationTableModel<R> getTableModel();
81  
82      /**
83       * @return the main table of the ui.
84       * @since 0.2
85       */
86      public abstract JXTable getTable();
87  
88      /**
89       * Validates the given row.
90       *
91       * @param row row to validate
92       * @return {@code true} if row is valid, {@code false} otherwise.
93       * @since 0.2
94       */
95      protected abstract boolean isRowValid(R row);
96  
97      /**
98       * Invoke each time the {@link AbstractTuttiBeanUIModel#modify} state on
99       * the current selected row changed.
100      *
101      * @param rowIndex     row index of the modified row
102      * @param row          modified row
103      * @param propertyName name of the modified property of the row
104      * @param oldValue     old value of the modified property
105      * @param newValue     new value of the modified property
106      * @since 0.3
107      */
108     protected void onRowModified(int rowIndex,
109                                  R row,
110                                  String propertyName,
111                                  Object oldValue,
112                                  Object newValue) {
113         getModel().setModify(true);
114     }
115 
116     /**
117      * Given the row monitor and his monitored row, try to save it if required.
118      *
119      * Coming in this method, we are sure that row is not null.
120      *
121      * @param rowMonitor the row monitor (see {@link #rowMonitor})
122      * @param row        the row to save if necessary
123      * @since 0.3
124      */
125     protected abstract void saveSelectedRowIfRequired(TuttiBeanMonitor<R> rowMonitor, R row);
126 
127     /**
128      * Monitor the selected row (save it only if something has changed).
129      *
130      * @since 0.2
131      */
132     private final TuttiBeanMonitor<R> rowMonitor;
133 
134     protected AbstractTuttiTableUIHandler(String... properties) {
135 
136         rowMonitor = new TuttiBeanMonitor<>(properties);
137 
138         // listen when bean is changed
139         rowMonitor.addPropertyChangeListener(TuttiBeanMonitor.PROPERTY_BEAN, new PropertyChangeListener() {
140 
141             final Set<String> propertiesToSkip =
142                     Sets.newHashSet(getRowPropertiesToIgnore());
143 
144             final PropertyChangeListener l = new PropertyChangeListener() {
145                 @Override
146                 public void propertyChange(PropertyChangeEvent evt) {
147                     String propertyName = evt.getPropertyName();
148 
149                     R row = (R) evt.getSource();
150 
151                     Object oldValue = evt.getOldValue();
152                     Object newValue = evt.getNewValue();
153 
154                     int rowIndex = getTableModel().getRowIndex(row);
155 
156                     if (AbstractTuttiBeanUIModel.PROPERTY_VALID.equals(propertyName)) {
157                         onRowValidStateChanged(rowIndex, row,
158                                                (Boolean) oldValue,
159                                                (Boolean) newValue);
160                     } else if (AbstractTuttiBeanUIModel.PROPERTY_MODIFY.equals(propertyName)) {
161                         onRowModifyStateChanged(rowIndex, row,
162                                                 (Boolean) oldValue,
163                                                 (Boolean) newValue);
164                     } else if (!propertiesToSkip.contains(propertyName)) {
165 
166                         if (log.isDebugEnabled()) {
167                             log.debug("row [" + rowIndex + "] property " +
168                                               propertyName + " changed from " + oldValue +
169                                               " to " + newValue);
170                         }
171                         onRowModified(rowIndex, row,
172                                       propertyName,
173                                       oldValue,
174                                       newValue);
175                     }
176                 }
177             };
178 
179             @Override
180             public void propertyChange(PropertyChangeEvent evt) {
181                 R oldValue = (R) evt.getOldValue();
182                 R newValue = (R) evt.getNewValue();
183                 if (log.isDebugEnabled()) {
184                     log.debug("Monitor row changed from " +
185                                       oldValue + " to " + newValue);
186                 }
187                 if (oldValue != null) {
188                     oldValue.removePropertyChangeListener(l);
189                 }
190                 if (newValue != null) {
191                     newValue.addPropertyChangeListener(l);
192                 }
193             }
194         });
195     }
196 
197     //------------------------------------------------------------------------//
198     //-- Internal methods (row methods)                                     --//
199     //------------------------------------------------------------------------//
200 
201     protected String[] getRowPropertiesToIgnore() {
202         return ArrayUtils.EMPTY_STRING_ARRAY;
203     }
204 
205     protected void onModelRowsChanged(List<R> rows) {
206         if (log.isDebugEnabled()) {
207             log.debug("Will set " + (rows == null ? 0 : rows.size()) +
208                               " rows on model.");
209         }
210         if (CollectionUtils.isNotEmpty(rows)) {
211 
212             // compute valid state for each row
213             for (R row : rows) {
214                 recomputeRowValidState(row);
215             }
216         }
217         getTableModel().setRows(rows);
218     }
219 
220     protected void onRowModifyStateChanged(int rowIndex,
221                                            R row,
222                                            Boolean oldValue,
223                                            Boolean newValue) {
224         if (log.isDebugEnabled()) {
225             log.debug("row [" + rowIndex + "] modify state changed from " +
226                               oldValue + " to " + newValue);
227         }
228     }
229 
230     protected void onRowValidStateChanged(int rowIndex,
231                                           R row,
232                                           Boolean oldValue,
233                                           Boolean newValue) {
234 
235         if (log.isDebugEnabled()) {
236             log.debug("row [" + rowIndex + "] valid state changed from " +
237                               oldValue + " to " + newValue);
238         }
239 
240         if (rowIndex > -1) {
241             getTableModel().fireTableRowsUpdated(rowIndex, rowIndex);
242         }
243     }
244 
245     protected void onAfterSelectedRowChanged(int oldRowIndex,
246                                              R oldRow,
247                                              int newRowIndex,
248                                              R newRow) {
249         if (log.isDebugEnabled()) {
250             log.debug("Selected row changed from [" + oldRowIndex + "] to [" +
251                               newRowIndex + "]");
252         }
253     }
254 
255     //------------------------------------------------------------------------//
256     //-- Internal methods (init methods)                                    --//
257     //------------------------------------------------------------------------//
258 
259     protected void initTable(JXTable table) {
260 
261         // by default do not authorize to change column orders
262         table.getTableHeader().setReorderingAllowed(false);
263 
264         addHighlighters(table);
265 
266         // when model data change let's propagate it table model
267         getModel().addPropertyChangeListener(AbstractTuttiTableUIModel.PROPERTY_ROWS, evt -> onModelRowsChanged((List<R>) evt.getNewValue()));
268 
269         // always scroll to selected row
270         SwingUtil.scrollToTableSelection(getTable());
271 
272         // always force to uninstall listener
273         uninstallTableSaveOnRowChangedSelectionListener();
274 
275         // save when row chaged and was modified
276         installTableSaveOnRowChangedSelectionListener();
277     }
278 
279     //------------------------------------------------------------------------//
280     //-- Internal methods (listener methods)                                --//
281     //------------------------------------------------------------------------//
282 
283     private ListSelectionListener tableSelectionListener;
284 
285     private Map<JTable, KeyAdapter> keyAdapters = new HashMap<>();
286 
287     protected void installTableSaveOnRowChangedSelectionListener() {
288 
289         Preconditions.checkState(
290                 tableSelectionListener == null,
291                 "There is already a tableSelectionListener registred, " +
292                         "remove it before invoking this method.");
293 
294         // create new listener
295         // save when row chaged and was modified
296 
297         tableSelectionListener = new ListSelectionListener() {
298             /**
299              * Current selected row index.
300              *
301              * @since 0.3
302              */
303             protected int selectedRowIndex;
304 
305             @Override
306             public void valueChanged(ListSelectionEvent e) {
307 
308                 if (log.isDebugEnabled()) {
309                     log.debug("Selection changed: " + e);
310                 }
311                 // need this for the first modification when no selection,
312                 // otherwise monitor is set after the first alter, so won't be
313                 // save directly...
314 
315                 ListSelectionModel source = (ListSelectionModel) e.getSource();
316 
317                 int oldRowIndex = selectedRowIndex;
318                 int newRowIndex = source.getLeadSelectionIndex();
319 
320                 R oldRow = rowMonitor.getBean();
321 
322                 if (oldRow == null || oldRowIndex != newRowIndex) {
323 
324                     R newRow;
325 
326                     if (source.isSelectionEmpty()) {
327 
328                         newRow = null;
329                     } else {
330                         newRow = getTableModel().getEntry(newRowIndex);
331                     }
332 
333                     if (log.isDebugEnabled()) {
334                         log.debug("Will monitor entry: " + newRowIndex);
335                     }
336                     rowMonitor.setBean(newRow);
337 
338                     selectedRowIndex = newRowIndex;
339 
340                     onAfterSelectedRowChanged(oldRowIndex,
341                                               oldRow,
342                                               selectedRowIndex,
343                                               rowMonitor.getBean());
344                 }
345             }
346         };
347 
348         if (log.isDebugEnabled()) {
349             log.debug("Intall " + tableSelectionListener + " on tableModel " + getTableModel());
350         }
351 
352         getTable().getSelectionModel().addListSelectionListener(tableSelectionListener);
353     }
354 
355     protected void uninstallTableSaveOnRowChangedSelectionListener() {
356 
357         if (tableSelectionListener != null) {
358 
359             if (log.isDebugEnabled()) {
360                 log.debug("Desintall " + tableSelectionListener);
361             }
362 
363             // there was a previous selection listener, remove it
364             getTable().getSelectionModel().removeListSelectionListener(tableSelectionListener);
365             tableSelectionListener = null;
366         }
367     }
368 
369     protected void installTableKeyListener(TableColumnModel columnModel, final JTable table) {
370         installTableKeyListener(columnModel, table, true);
371     }
372 
373     protected void installTableKeyListener(TableColumnModel columnModel, JTable table, boolean enterToChangeRow) {
374 
375         Preconditions.checkState(
376                 keyAdapters.get(table) == null,
377                 "There is already a tableSelectionListener registred, " +
378                         "remove it before invoking this method.");
379 
380         AbstractApplicationTableModel model = (AbstractApplicationTableModel) table.getModel();
381 
382         MoveToNextEditableCellAction nextCellAction = MoveToNextEditableCellAction.newAction(model, table);
383         MoveToPreviousEditableCellAction previousCellAction = MoveToPreviousEditableCellAction.newAction(model, table);
384         MoveToNextEditableRowAction nextRowAction = MoveToNextEditableRowAction.newAction(model, table);
385         MoveToPreviousEditableRowAction previousRowAction = MoveToPreviousEditableRowAction.newAction(model, table);
386 
387         KeyAdapter keyAdapter = new KeyAdapter() {
388 
389             @Override
390             public void keyPressed(KeyEvent e) {
391                 TableCellEditor editor = table.getCellEditor();
392 
393                 int keyCode = e.getKeyCode();
394                 boolean shiftDown = e.isShiftDown();
395 
396                 if (gotoPreviousCell(keyCode, shiftDown)) {
397 
398                     consumeAction(e, editor, previousCellAction);
399 
400                 } else if (gotoNextCell(keyCode)) {
401                     consumeAction(e, editor, nextCellAction);
402 
403                 } else if (gotoPreviousRow(keyCode, shiftDown)) {
404 
405                     consumeAction(e, editor, previousRowAction);
406 
407                 } else if (gotoNextRow(keyCode)) {
408 
409                     consumeAction(e, editor, nextRowAction);
410 
411                 }
412             }
413 
414             protected void consumeAction(KeyEvent e, TableCellEditor editor, AbstractAction action) {
415                 e.consume();
416                 if (editor != null) {
417                     editor.stopCellEditing();
418                 }
419                 action.actionPerformed(null);
420             }
421 
422             protected boolean gotoPreviousCell(int keyCode, boolean shiftDown) {
423                 return keyCode == KeyEvent.VK_LEFT
424                         || (keyCode == KeyEvent.VK_TAB && shiftDown)
425                         || (!enterToChangeRow && keyCode == KeyEvent.VK_ENTER && shiftDown);
426             }
427 
428             protected boolean gotoNextCell(int keyCode) {
429                 return keyCode == KeyEvent.VK_RIGHT
430                         || keyCode == KeyEvent.VK_TAB
431                         || (!enterToChangeRow && keyCode == KeyEvent.VK_ENTER);
432             }
433 
434             protected boolean gotoPreviousRow(int keyCode, boolean shiftDown) {
435                 return keyCode == KeyEvent.VK_UP
436                         || (enterToChangeRow && keyCode == KeyEvent.VK_ENTER && shiftDown);
437             }
438 
439             protected boolean gotoNextRow(int keyCode) {
440                 return keyCode == KeyEvent.VK_DOWN
441                         || (enterToChangeRow && keyCode == KeyEvent.VK_ENTER);
442             }
443 
444         };
445         keyAdapters.put(table, keyAdapter);
446 
447         if (log.isDebugEnabled()) {
448             log.debug("Intall " + keyAdapter);
449         }
450 
451         table.addKeyListener(keyAdapter);
452 
453         Enumeration<TableColumn> columns = columnModel.getColumns();
454         while (columns.hasMoreElements()) {
455             TableColumn tableColumn = columns.nextElement();
456             TableCellEditor cellEditor = tableColumn.getCellEditor();
457             if (cellEditor instanceof NumberCellEditor) {
458                 NumberCellEditor editor = (NumberCellEditor) cellEditor;
459                 editor.getNumberEditor().getTextField().addKeyListener(keyAdapter);
460 
461             } else if (cellEditor instanceof TuttiComputedOrNotDataTableCellEditor) {
462                 TuttiComputedOrNotDataTableCellEditor editor =
463                         (TuttiComputedOrNotDataTableCellEditor) cellEditor;
464                 editor.getNumberEditor().getTextField().addKeyListener(keyAdapter);
465 
466             } else if (cellEditor instanceof SampleCategoryEditor) {
467                 SampleCategoryEditor editor = (SampleCategoryEditor) cellEditor;
468                 editor.getNumberEditor().getTextField().addKeyListener(keyAdapter);
469             }
470         }
471     }
472 
473     protected void uninstallTableKeyListener(JTable table) {
474 
475         KeyAdapter keyAdapter = keyAdapters.get(table);
476         if (keyAdapter != null) {
477 
478             if (log.isDebugEnabled()) {
479                 log.debug("Desintall " + keyAdapter);
480             }
481 
482             table.removeKeyListener(keyAdapter);
483 
484             TableColumnModel columnModel = table.getColumnModel();
485             Enumeration<TableColumn> columns = columnModel.getColumns();
486             while (columns.hasMoreElements()) {
487                 TableColumn tableColumn = columns.nextElement();
488                 TableCellEditor cellEditor = tableColumn.getCellEditor();
489                 if (cellEditor instanceof NumberCellEditor) {
490                     NumberCellEditor editor = (NumberCellEditor) cellEditor;
491                     editor.getNumberEditor().getTextField().removeKeyListener(keyAdapter);
492                 }
493             }
494             keyAdapters.remove(table);
495         }
496     }
497 
498     protected final void saveSelectedRowIfNeeded() {
499 
500         R row = rowMonitor.getBean();
501 
502         if (row != null) {
503             saveSelectedRowIfRequired(rowMonitor, row);
504         }
505     }
506 
507 
508     protected void cleanrRowMonitor() {
509         rowMonitor.clearModified();
510     }
511 
512     protected final void recomputeRowValidState(R row) {
513 
514         // recompute row valid state
515         boolean valid = isRowValid(row);
516 
517         // apply it to row
518         row.setValid(valid);
519 
520         if (valid) {
521             getModel().removeRowInError(row);
522         } else {
523             getModel().addRowInError(row);
524         }
525     }
526 
527 }