View Javadoc
1   package com.jsql.view.swing.table;
2   
3   import com.jsql.util.LogLevelUtil;
4   import org.apache.logging.log4j.LogManager;
5   import org.apache.logging.log4j.Logger;
6   
7   import javax.swing.*;
8   import javax.swing.event.TableModelEvent;
9   import javax.swing.event.TableModelListener;
10  import javax.swing.table.TableCellRenderer;
11  import javax.swing.table.TableColumn;
12  import javax.swing.table.TableColumnModel;
13  import javax.swing.table.TableModel;
14  import java.awt.*;
15  import java.awt.event.ActionEvent;
16  import java.beans.PropertyChangeEvent;
17  import java.beans.PropertyChangeListener;
18  import java.util.HashMap;
19  import java.util.Map;
20  
21  /**
22   * Class to manage the widths of columns in a table.
23   *
24   * Various properties control how the width of the column is calculated.
25   * Another property controls whether column width calculation should be dynamic.
26   * Finally, various Actions will be added to the table to allow the user
27   * to customize the functionality.
28   *
29   * This class was designed to be used with tables that use an auto resize mode
30   * of AUTO_RESIZE_OFF. With all other modes you are constrained as the width
31   * of the columns must fit inside the table. So if you increase one column, one
32   * or more of the other columns must decrease. Because of this the resize mode
33   * of RESIZE_ALL_COLUMNS will work the best.
34   */
35  public class AdjusterTableColumn implements PropertyChangeListener, TableModelListener {
36  
37      private static final Logger LOGGER = LogManager.getRootLogger();
38  
39      private final JTable tableAdjust;
40      private final int spacing;
41      private boolean isColumnHeaderIncluded;
42      private boolean isColumnDataIncluded;
43      private boolean isOnlyAdjustLarger;
44      private boolean isDynamicAdjustment;
45      private final Map<TableColumn, Integer> columnSizes = new HashMap<>();
46  
47      /**
48       * Specify the table and use default spacing
49       */
50      public AdjusterTableColumn(JTable table) {
51          this(table, 6);
52      }
53  
54      /**
55       * Specify the table and spacing
56       */
57      public AdjusterTableColumn(JTable tableAdjust, int spacing) {
58          this.tableAdjust = tableAdjust;
59          this.spacing = spacing;
60          this.setColumnHeaderIncluded(true);
61          this.setColumnDataIncluded(true);
62          this.setOnlyAdjustLarger(true);
63          this.setDynamicAdjustment(false);
64          this.installActions();
65      }
66  
67      /**
68       * Adjust the widths of all the columns in the table
69       */
70      public void adjustColumns() {
71          TableColumnModel tcm = this.tableAdjust.getColumnModel();
72          for (var i = 0 ; i < tcm.getColumnCount() ; i++) {
73              this.adjustColumn(i);
74          }
75      }
76  
77      /**
78       * Adjust the width of the specified column in the table
79       */
80      public void adjustColumn(final int column) {
81          var tableColumn = this.tableAdjust.getColumnModel().getColumn(column);
82          if (!tableColumn.getResizable()) {
83              return;
84          }
85  
86          int columnHeaderWidth = this.getColumnHeaderWidth(column);
87          int columnDataWidth   = this.getColumnDataWidth(column);
88          int preferredWidth    = Math.max(columnHeaderWidth, columnDataWidth);
89          this.updateTableColumn(column, preferredWidth);
90      }
91  
92      /**
93       * Calculated the width based on the column name
94       */
95      private int getColumnHeaderWidth(int column) {
96          if (!this.isColumnHeaderIncluded) {
97              return 0;
98          }
99  
100         var tableColumn = this.tableAdjust.getColumnModel().getColumn(column);
101         Object value = tableColumn.getHeaderValue();
102         TableCellRenderer renderer = tableColumn.getHeaderRenderer();
103         if (renderer == null) {
104             renderer = this.tableAdjust.getTableHeader().getDefaultRenderer();
105         }
106 
107         var c = renderer.getTableCellRendererComponent(this.tableAdjust, value, false, false, -1, column);
108         return c.getPreferredSize().width;
109     }
110 
111     /**
112      * Calculate the width based on the widest cell renderer for the
113      * given column.
114      */
115     private int getColumnDataWidth(int column) {
116         if (!this.isColumnDataIncluded) {
117             return 0;
118         }
119 
120         var preferredWidth = 0;
121         int maxWidth = this.tableAdjust.getColumnModel().getColumn(column).getMaxWidth();
122 
123         for (var row = 0 ; row < this.tableAdjust.getRowCount() ; row++) {
124             preferredWidth = Math.max(preferredWidth, this.getCellDataWidth(row, column));
125             // We've exceeded the maximum width, no need to check other rows
126             if (preferredWidth >= maxWidth) {
127                 break;
128             }
129         }
130         return preferredWidth;
131     }
132 
133     /**
134      * Get the preferred width for the specified cell
135      */
136     private int getCellDataWidth(int row, int column) {
137         // Invoke the renderer for the cell to calculate the preferred width
138         TableCellRenderer cellRenderer = this.tableAdjust.getCellRenderer(row, column);
139         Component c = this.tableAdjust.prepareRenderer(cellRenderer, row, column);
140         if (c != null) {  // Fix #96130: NullPointerException on c
141             return c.getPreferredSize().width + this.tableAdjust.getIntercellSpacing().width;
142         } else {
143             LOGGER.log(LogLevelUtil.CONSOLE_JAVA, "Unexpected missing cell, data width set to 0");
144             return 100;
145         }
146     }
147 
148     /**
149      * Update the TableColumn with the newly calculated width
150      */
151     private void updateTableColumn(int column, int width) {
152         final var tableColumn = this.tableAdjust.getColumnModel().getColumn(column);
153         if (!tableColumn.getResizable()) {
154             return;
155         }
156 
157         int calculatedWidth = width;
158         calculatedWidth += this.spacing;
159         // Don't shrink the column width
160         if (this.isOnlyAdjustLarger) {
161             calculatedWidth = Math.max(calculatedWidth, tableColumn.getPreferredWidth());
162         }
163 
164         this.columnSizes.put(tableColumn, tableColumn.getWidth());
165         this.tableAdjust.getTableHeader().setResizingColumn(tableColumn);
166         tableColumn.setWidth(calculatedWidth);
167     }
168 
169     /**
170      * Restore the widths of the columns in the table to its previous width
171      */
172     public void restoreColumns() {
173         TableColumnModel tableColumnModel = this.tableAdjust.getColumnModel();
174         for (var i = 0 ; i < tableColumnModel.getColumnCount() ; i++) {
175             this.restoreColumn(i);
176         }
177     }
178 
179     /**
180      * Restore the width of the specified column to its previous width
181      */
182     private void restoreColumn(int column) {
183         var tableColumn = this.tableAdjust.getColumnModel().getColumn(column);
184         Integer width = this.columnSizes.get(tableColumn);
185         if (width != null) {
186             this.tableAdjust.getTableHeader().setResizingColumn(tableColumn);
187             tableColumn.setWidth(width);
188         }
189     }
190 
191     /**
192      * Indicates whether to include the header in the width calculation
193      */
194     public void setColumnHeaderIncluded(boolean isColumnHeaderIncluded) {
195         this.isColumnHeaderIncluded = isColumnHeaderIncluded;
196     }
197 
198     /**
199      * Indicates whether to include the model data in the width calculation
200      */
201     public void setColumnDataIncluded(boolean isColumnDataIncluded) {
202         this.isColumnDataIncluded = isColumnDataIncluded;
203     }
204 
205     /**
206      * Indicates whether columns can only be increased in size
207      */
208     public void setOnlyAdjustLarger(boolean isOnlyAdjustLarger) {
209         this.isOnlyAdjustLarger = isOnlyAdjustLarger;
210     }
211 
212     /**
213      * Indicate whether changes to the model should cause the width to be
214      * dynamically recalculated.
215      */
216     public void setDynamicAdjustment(boolean isDynamicAdjustment) {
217         // May need to add or remove the TableModelListener when changed
218         if (this.isDynamicAdjustment != isDynamicAdjustment) {
219             if (isDynamicAdjustment) {
220                 this.tableAdjust.addPropertyChangeListener(this);
221                 this.tableAdjust.getModel().addTableModelListener(this);
222             } else {
223                 this.tableAdjust.removePropertyChangeListener(this);
224                 this.tableAdjust.getModel().removeTableModelListener(this);
225             }
226         }
227         this.isDynamicAdjustment = isDynamicAdjustment;
228     }
229     
230     /**
231      * Implement the PropertyChangeListener
232      */
233     @Override
234     public void propertyChange(PropertyChangeEvent e) {
235         // When the TableModel changes we need to update the listeners
236         // and column widths
237         if ("model".equals(e.getPropertyName())) {
238             TableModel model = (TableModel) e.getOldValue();
239             model.removeTableModelListener(this);
240 
241             model = (TableModel) e.getNewValue();
242             model.addTableModelListener(this);
243             this.adjustColumns();
244         }
245     }
246     
247     /**
248      * Implement the TableModelListener
249      */
250     @Override
251     public void tableChanged(TableModelEvent e) {
252         if (!this.isColumnDataIncluded) {
253             return;
254         }
255 
256         // A cell has been updated
257         if (e.getType() == TableModelEvent.UPDATE) {
258             int column = this.tableAdjust.convertColumnIndexToView(e.getColumn());
259 
260             // Only need to worry about an increase in width for this cell
261             if (this.isOnlyAdjustLarger) {
262                 int row = e.getFirstRow();
263                 var tableColumn = this.tableAdjust.getColumnModel().getColumn(column);
264                 if (tableColumn.getResizable()) {
265                     int width = this.getCellDataWidth(row, column);
266                     this.updateTableColumn(column, width);
267                 }
268             } else {
269                 this.adjustColumn(column);  // Could be an increase of decrease so check all rows
270             }
271         } else {
272             this.adjustColumns();  // The update affected more than one column so adjust all columns
273         }
274     }
275 
276     /**
277      * Install Actions to give user control of certain functionality.
278      */
279     private void installActions() {
280         this.installColumnAction(true,  true,  "adjustColumn",   "control ADD");
281         this.installColumnAction(false, true,  "adjustColumns",  "control shift ADD");
282         this.installColumnAction(true,  false, "restoreColumn",  "control SUBTRACT");
283         this.installColumnAction(false, false, "restoreColumns", "control shift SUBTRACT");
284 
285         this.installToggleAction(true,  false, "toggleDynamic",  "control MULTIPLY");
286         this.installToggleAction(false, true,  "toggleLarger",   "control DIVIDE");
287     }
288 
289     /**
290      * Update the input and action maps with a new ColumnAction
291      */
292     private void installColumnAction(boolean isSelectedColumn, boolean isAdjust, String key, String keyStroke) {
293         Action action = new ColumnAction(isSelectedColumn, isAdjust);
294         var ks = KeyStroke.getKeyStroke(keyStroke);
295         
296         this.tableAdjust.getInputMap().put(ks, key);
297         this.tableAdjust.getActionMap().put(key, action);
298     }
299 
300     /**
301      * Update the input and action maps with new ToggleAction
302      */
303     private void installToggleAction(boolean isToggleDynamic, boolean isToggleLarger, String key, String keyStroke) {
304         Action action = new ToggleAction(isToggleDynamic, isToggleLarger);
305         var ks = KeyStroke.getKeyStroke(keyStroke);
306         
307         this.tableAdjust.getInputMap().put(ks, key);
308         this.tableAdjust.getActionMap().put(key, action);
309     }
310 
311     /**
312      * Action to adjust or restore the width of a single column or all columns
313      */
314     class ColumnAction extends AbstractAction {
315         
316         private final boolean isSelectedColumn;
317         private final boolean isAdjust;
318 
319         public ColumnAction(boolean isSelectedColumn, boolean isAdjust) {
320             this.isSelectedColumn = isSelectedColumn;
321             this.isAdjust = isAdjust;
322         }
323 
324         @Override
325         public void actionPerformed(ActionEvent e) {
326             // Handle selected column(s) width change actions
327             if (this.isSelectedColumn) {
328                 int[] columns = AdjusterTableColumn.this.tableAdjust.getSelectedColumns();
329 
330                 for (int column: columns) {
331                     if (this.isAdjust) {
332                         AdjusterTableColumn.this.adjustColumn(column);
333                     } else {
334                         AdjusterTableColumn.this.restoreColumn(column);
335                     }
336                 }
337             } else {
338                 if (this.isAdjust) {
339                     AdjusterTableColumn.this.adjustColumns();
340                 } else {
341                     AdjusterTableColumn.this.restoreColumns();
342                 }
343             }
344         }
345     }
346 
347     /**
348      * Toggle properties of the TableColumnAdjuster so the user can
349      * customize the functionality to their preferences
350      */
351     class ToggleAction extends AbstractAction {
352         
353         private final boolean isToggleDynamic;
354         private final boolean isToggleLarger;
355 
356         public ToggleAction(boolean isToggleDynamic, boolean isToggleLarger) {
357             this.isToggleDynamic = isToggleDynamic;
358             this.isToggleLarger = isToggleLarger;
359         }
360 
361         @Override
362         public void actionPerformed(ActionEvent e) {
363             if (this.isToggleDynamic) {
364                 AdjusterTableColumn.this.setDynamicAdjustment(!AdjusterTableColumn.this.isDynamicAdjustment);
365             } else if (this.isToggleLarger) {
366                 AdjusterTableColumn.this.setOnlyAdjustLarger(!AdjusterTableColumn.this.isOnlyAdjustLarger);
367             }
368         }
369     }
370 }