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