A TabKeyActionHandler

We can write a single action handler for the tab, shift-tab, right arrow, and left arrow keys. The difference is whether the selection steps forward or backward (delta of +1 or -1), and whether the selection wraps to another row (wrap true or false). We will treat each of the cases where the selection moves off the edge of ether of the tables as a special case.

  1 
  2 package com.vizitsolutions.identitytable;
  3 
  4 import javax.swing.AbstractAction;
  5 import java.awt.event.ActionEvent;
  6 import javax.swing.JTable;
  7 import javax.swing.ListSelectionModel;
  8 import java.awt.Rectangle;
  9 
 10 
 11 /**
 12  * <p>$Id: $</p>
 13  *
 14  * Make tab, shift-tab, and the left and right arrows behave reasonably in the
 15  * IdentityTable, which is a composite of two separate JTables. Shift the focus
 16  * to and from the composited table to make it appear that it is all a single
 17  * table.
 18  *
 19  * @author  Alex Kluge
 20  * @version $Revision: $, $Date: $
 21  */
 22 public class TabKeyActionHandler extends AbstractAction
 23 {
 24     /** 
 25      * The number of cells to shift. This is either +1 for a tab or right arrow,
 26      * or -1 for a shift tab or left arrow.
 27      */
 28     int                delta;
 29     
 30     /**
 31      * The column selection model for the main table. This is used to
 32      * set the appropriate column as selected when the keyboard is used to
 33      * navigate to another cell. The selected cell is the intersection of the
 34      * selected column and the selected row.
 35      * 
 36      * @see rowSelectionModel
 37      */
 38     ListSelectionModel mainColumnSelectionModel;
 39     
 40     /**
 41      * The main table, contains the table model columns other than the
 42      * row header columns.
 43      */
 44     JTable             mainTable;
 45     
 46     /**
 47      * The column selection model for the row header table. This is used to
 48      * set the appropriate column as selected when the keyboard is used to
 49      * navigate to another cell. The selected cell is the intersection of the
 50      * selected column and the selected row.
 51      * 
 52      * @see rowSelectionModel
 53      */
 54     ListSelectionModel rowHeaderColumnSelectionModel;
 55     
 56     /**
 57      * This table is the row header for the table.
 58      */
 59     JTable             rowHeaderTable;
 60     
 61     /**
 62      * The selected row. The row selection model, and hence the selected row,
 63      * spans both of the component tables, so the same row is always selected
 64      * in both tables.
 65      * 
 66      * @see mainColumnSelectionModel, rowHeaderColumnSelectionModel
 67      */
 68     ListSelectionModel rowSelectionModel;
 69     
 70     /**
 71      * Describes the action when the focus is shifted from the end of a row. If
 72      * wrap is true, continue on to the next or previous row. If false, then do
 73      * nothing.
 74      */
 75     boolean            wrap;
 76 
 77     /**
 78      * Construct a TabKeyActionHandler. The TabKeyActionHandler handles the tab
 79      * as well as the left and right arrow keys. The selected cell is shifted
 80      * forward or backward as given by the value of delta (+1, -1).
 81      * 
 82      * @param rowHeaderTable    A JTable containing the row header columns for the
 83      *                          identity table.
 84      * @param mainTable         A JTable containing the remainder of the columns.
 85      * @param rowSelectionModel
 86      * @param rowHeaderColumnSelectionModel
 87      * @param mainColumnSelectionModel
 88      * @param delta
 89      * @param wrap
 90      */
 91     public TabKeyActionHandler(JTable             rowHeaderTable,           JTable             mainTable,
 92                                ListSelectionModel rowSelectionModel,        ListSelectionModel headerColumnSelectionModel,
 93                                ListSelectionModel mainColumnSelectionModel, int                delta,
 94                                boolean            wrap)
 95     {
 96         this.rowHeaderTable             = rowHeaderTable;
 97         this.mainTable                  = mainTable;
 98         this.rowSelectionModel          = rowSelectionModel;
 99         this.rowHeaderColumnSelectionModel = headerColumnSelectionModel;
100         this.mainColumnSelectionModel   = mainColumnSelectionModel;
101         this.delta                      = delta;
102         this.wrap                       = wrap;
103     }
104     
105     /**
106      * Handle the key event by shifting the selected cell forward or backward
107      * one column.
108      * 
109      * @param event
110      */
111     public void actionPerformed(ActionEvent event)
112     {
113         JTable currentTable = (JTable)event.getSource();
114         
115         // If the table is being edited and we can't stop it, return immediately.
116         if (currentTable.isEditing() && !currentTable.getCellEditor().stopCellEditing())
117         {
118             return;
119         }
120         
121         adjustColumn(currentTable);
122     }
123     
124     /**
125      * Adjust the selected table cell forward or backward one column.
126      * 
127      * @param currentTable
128      */
129     public void adjustColumn(JTable currentTable)
130     {
131         // This handles the case where no cell is selected. Just as with the
132         // standard JTable, the selection is shifted onto the JTable.
133         int rowIndex = rowSelectionModel.getLeadSelectionIndex();
134         if (rowIndex < 0)
135         {
136             rowIndex = 0;
137         }
138         
139         int columnIndex;
140         
141         // We have to know which table is the currently selected table before we
142         // can adjust the selected cell. Deal with the rowHeaderTable case
143         // first.
144         if (currentTable == rowHeaderTable)
145         {
146             // 
147             columnIndex = rowHeaderColumnSelectionModel.getLeadSelectionIndex();
148             // If no column is selected
149             if (columnIndex < 0)
150             {
151                 // shift to the first column
152                 columnIndex = 0;
153             }
154             
155             // Move the cell forward or backward as per the (+1, -1) value of
156             // delta.
157             columnIndex += delta;
158             
159             // If we have walked off the edge of the row header table.
160             if (columnIndex > rowHeaderTable.getColumnCount()-1)
161             {
162                 // Stepped from the row header onto the left edge (0 column) of the main table.
163                 Rectangle currentCell = mainTable.getCellRect(rowIndex, 0, true);
164                 // Make sure that the cell is visible. This may scroll the main table.
165                 mainTable.scrollRectToVisible(currentCell);
166                 // Clear the selection in the row header.
167                 rowHeaderColumnSelectionModel.clearSelection();
168                 // Shift the focus from the row header table to the main table.
169                 mainTable.requestFocusInWindow();
170                 // We know that the selected column is the first main column.
171                 mainColumnSelectionModel.setSelectionInterval(0, 0);
172                 // And we are on the same row as previously.
173                 rowSelectionModel.setSelectionInterval(rowIndex, rowIndex);
174                 return;
175             }
176             else if (columnIndex < 0)
177             {
178                 // We just walked off the left edge of the row. For example,
179                 // shift-tab in the first column, if we don't wrap to the next
180                 // column, we're done.
181                 if (!wrap)
182                 {
183                     return;
184                 }
185                 // We will want the last column in the previous row.
186                 rowIndex -=1;
187                 columnIndex = mainTable.getColumnCount()-1;
188                 
189                 // If we just stepped off the top of the table too
190                 if (rowIndex < 0)
191                 {
192                     // Wrap around to the bottom, just as the stock JTable.
193                     rowIndex = mainTable.getRowCount()-1;
194                 }
195                 
196                 // Wrap from the left edge the header column, to the right edge of the previous
197                 // column in the main table. Scroll to the appropriate column in the main table.
198                 Rectangle currentCell = mainTable.getCellRect(rowIndex, columnIndex, true);
199                 mainTable.scrollRectToVisible(currentCell);
200                 rowHeaderColumnSelectionModel.clearSelection();
201                 mainTable.requestFocusInWindow();
202                 mainColumnSelectionModel.setSelectionInterval(columnIndex, columnIndex);
203                 rowSelectionModel.setSelectionInterval(rowIndex, rowIndex);
204                 return;
205             }
206             else
207             {
208                 // Still within the row header, so select, and scroll to the appropriate column.
209                 Rectangle currentCell = rowHeaderTable.getCellRect(rowIndex, columnIndex, true);
210                 rowHeaderTable.scrollRectToVisible(currentCell);
211                 rowHeaderColumnSelectionModel.setSelectionInterval(columnIndex, columnIndex);
212                 rowSelectionModel.setSelectionInterval(rowIndex, rowIndex);
213                 return;
214             }
215         }
216         else
217         {
218             // A cell in the main table is the currently selected cell.
219             columnIndex = mainColumnSelectionModel.getLeadSelectionIndex();
220             if (columnIndex < 0)
221             {
222                 columnIndex = 0;
223             }
224             
225             columnIndex += delta;
226             
227             // If we have walked off the right edge of the main table.
228             if (columnIndex > mainTable.getColumnCount()-1)
229             {
230                 // If we don't wrap to the next column, we're done.
231                 if (!wrap)
232                 {
233                     return;
234                 }
235                 
236                 // Wrap to the first column of the next row.
237                 rowIndex += 1;
238                 columnIndex = 0;
239                 
240                 // And if we walked off the bottom of the table,
241                 if (rowIndex > mainTable.getRowCount()-1)
242                 {
243                     // wrap to the top.
244                     rowIndex = 0;
245                 }
246                 
247                 // We have walked of the right edge of the main table. Step around to the zero column of
248                 // the next row of the row header. Scroll to the selected row header cell, and to the
249                 // leftmost (0) column of the main table.
250                 Rectangle currentCell = rowHeaderTable.getCellRect(rowIndex, columnIndex, true);
251                 rowHeaderTable.scrollRectToVisible(currentCell);
252                 currentCell = mainTable.getCellRect(0, 0, true);
253                 mainTable.scrollRectToVisible(currentCell);
254                 mainColumnSelectionModel.clearSelection();
255                 rowHeaderTable.requestFocusInWindow();
256                 rowHeaderColumnSelectionModel.setSelectionInterval(columnIndex, columnIndex);
257                 rowSelectionModel.setSelectionInterval(rowIndex, rowIndex);
258                 return;
259             }
260             else if (columnIndex < 0)
261             {
262                 // We just stepped from the left edge of the main table to the right edge of the header table.
263                 // Select and scroll to the rightmost (highest value) column in the row header.
264                 Rectangle currentCell = rowHeaderTable.getCellRect(rowIndex, columnIndex, true);
265                 rowHeaderTable.scrollRectToVisible(currentCell);
266                 mainColumnSelectionModel.clearSelection();
267                 rowHeaderTable.requestFocusInWindow();
268                 // This is the highest (rightmost) column in the header.
269                 columnIndex = rowHeaderTable.getColumnCount()-1;
270                 rowHeaderColumnSelectionModel.setSelectionInterval(columnIndex, columnIndex);
271                 rowSelectionModel.setSelectionInterval(rowIndex, rowIndex);
272                 return;
273             }
274             else
275             {
276                 // Step to the next cell in the main table.
277                 Rectangle currentCell = mainTable.getCellRect(rowIndex, columnIndex, true);
278                 mainTable.scrollRectToVisible(currentCell);
279                 mainColumnSelectionModel.setSelectionInterval(columnIndex, columnIndex);
280                 rowSelectionModel.setSelectionInterval(rowIndex, rowIndex);
281                 return;
282             }
283         }
284     }
285 }
286 
287 

Notes