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