How This Book Is Organized

Java > Core SWING advanced programming > 7. TABLE EDITING > Tabbing Between Editable Cells

 

Tabbing Between Editable Cells

Now that the currency table has the ability to be edited and updated, there is one more usability feature you might want to add to it. Suppose you wanted the user to be able to quickly update several of the exchange rates in the body of the table. As the table currently stands, the user would have to click with the mouse in each cell to be changed, type the new value, and then click in the next cell and so on. Alternatively, the user could use the arrow keys to move between the cells whose values need to be changed. While this would work, it would be more convenient if it were possible to use the tab key to jump around between the editable cells, skipping over those which are not editable. This would be faster than using the mouse and much better than using the cursor keys because it avoids the need to manually move the focus over cells that cannot be edited. The table does not directly support the use of the tab key in this way, but with a little work this feature can be implemented.

Designing the Edit Traversal Mechanism

Let's first look at how you might want this to work. In terms of the currency table, the old and new exchange rate values are both directly editable. Typically, you might want the user to update the old rate and then the new rate for one currency, followed by the old rate for the next and so on. The user would start by editing the previous exchange rate. Pressing the tab key should then cause that edit to be complete, and the new value written to the table model, and then the cell holding the new currency value should automatically get the focus and install its editor. The next tab key press should take the focus to the old exchange rate for the next currency, and so on.

This description highlights the fact that there are two separate (but related) parts to this problem:

  1. Determining which cells should be involved in the tab order for automatic editing.

  2. Arranging for the tab key to be detected and cause the termination of the current edit, followed by starting the next one.

Let's tackle these issues separately. Assume first that the mechanism to change the editing cell has been implemented and the tab key is detected. Where should the edit focus be moved? In the currency table data model used in the last example, there are three editable columns in each data row:

  • The old exchange rate column.

  • The new exchange rate column.

  • The column containing the update button.

The simplest approach would be to define the tabbing order to be left to right across each row, moving between cells for which the data model isCellEditable method returns true. This would be simple to implement given the row and column number of the cell being edited, the next one would be found by repeatedly incrementing the column number and calling the JTable isCellEditable with the same row and the new column number until an editable cell was found.

Core Note

As ever, when dealing with table column indices, you need to be careful because the user can reorder the table columns, or because there may be columns in the TableModel that are not displayed. In general, the column indices used by view-related operations are not the same as those in the model. In this case, to determine whether the cell to be edited is editable, we need to use the column number, which is view-based and call the TableModel isCellEditable method, which uses model-based column indices. Fortunately, JTable has its own isCellEditable method that maps between the two sets of column indices. Alternatively, you can use the JTable convertColumnlndexToModel and convertColumnIndexToView methods to perform these mappings. You'll see how these methods are used when we look at the implementation of this example.



If the end of the row is reached, the algorithm would continue by incrementing the row number and resetting the column number to 0. This simple approach would suffice for many tables, but it is not always good enough.

In many cases, you would want to be able to limit the editable cells reachable using the tab key as an accelerator. In the case of the table in the last example, the tab order generated by this simple algorithm would be from the old exchange rate to the new one and then to the update button. The problem with this is that you want the tab key to move the focus to each cell and start its editor. As you know, starting the editor in the table's update button column actually causes the content of that row to be updated in the database (if there is one) without further intervention from the user. This might not always be appropriate. To give yourself the chance to avoid this side effect, you need some way to specify which of the editable columns should be included in the automatic tabbing mechanism.

Now let's move on to the second issue how to detect the tab key during an edit and arrange for it to move to the next editable cell. For the table to be in edit mode, the user must first start an edit by double-clicking or directly typing a new value in an editable cell. Once the table is in this state, you need to be able to catch the TAB key and react to it in the proper way. This implies that you need to get key events from the editing component. However, as you already know, by default the input focus during editing is directed to the table not to the editing component, so the TAB key would actually be handled by the JTable, not by the editor. To be notified of a TAB key event, then, it is necessary to register a KeyListener on the JTable. Even this is not sufficient, however, because it is possible for custom editors to grab the focus for themselves, so it is necessary to register a KeyListener on the editor component as well. The intent of this design is that the KeyListener would grab TAB keys and do whatever is necessary to stop the current edit and start editing in the next editable column. There is, however, one more step that needs to be taken.

TAB key presses are special because they are used to move the keyboard focus between components. In fact, Swing components filter out TAB key presses at a very low level and pass them to the Swing focus manager, where they are consumed. Because of this, our KeyListener would never actually be notified of TAB keys. Also, pressing TAB would actually move focus away from the table, which is not the desired effect. To avoid both problems, it is necessary to disable the Swing focus manager when editing a cell in a table that supports movement between cells using the TAB key. You'll see exactly how this is done shortly.

Implementing a Table with TAB Edit Traversal

Now it's time to look at the actual implementation. As noted earlier, you need to be able to configure which columns of the table will be editable and react to the TAB key. Because this information needs to be stored in the table object, the simplest way to do this is to subclass JTable and provide a method that allows the programmer to specify the list of tabbable columns. The implementation shown in Listing 7-9 creates a subclass of JTable called TabEditTable that contains all the logic for handling TAB traversal between editable cells.

Listing 7-9 A Table with TAB Edit Traversal
 package AdvancedSwing.Chapter7; import javax.swing.*; import javax.swing.FocusManager; import javax.swing.table.*; import javax.swing.event.*; import java.awt.*; import java.awt.event.*; import java.util.*; import AdvancedSwing.Chapter6.TableUtilities; public class TabEditTable extends JTable {    public TabEditTable() {       super();    }    public TabEditTable(TableModel dm) {       super(dm);    }    public TabEditTable(TableModel dm,                        TableColumnModel cm) {       super(dm, cm);    }    public TabEditTable(TableModel dm,                        TableColumnModel cm,                        ListSelectionModel sm){       super(dm, cm, sm);    }    public TabEditTable(int numRows, int numColumns) {       super(numRows, numColumns);    }    public TabEditTable(final Vector rowData, final Vector                                                  columnNames) {       super(rowData, columnNames);    }     public TabEditTable(final Object[][] rowData, final                                         Object[] columnNames) {       super(rowData, columnNames);    }    // Set the columns that contain tabbable editors    public void setEditingColumns(int[] columns) {       editingColumns = columns;       convertEditableColumnsToView();    }    public int[] getEditingColumns() {       return editingColumns;    }    // Overrides of JTable methods    public boolean editCellAt(int row, int column,                                         EventObject evt) {       if (super.editCellAt(row, column, evt) == false) {          return false;       }       if (viewEditingColumns != null) {          // Note: column is specified in terms          // of the column model          int length = viewEditingColumns.length;          for (int i = 0; i < length; i++) {             if (column == viewEditingColumns[i]) {                Component comp = getEditorComponent();                comp.addKeyListener(tabKeyListener);                this.addKeyListener(tabKeyListener);                focusManager =FocusManager.getCurrentManager();                FocusManager.disableSwingFocusManager();                inTabbingEditor = true;                comp.requestFocus();                break;             }          }       }       return true;    }    public void editingStopped(ChangeEvent evt) {       if (inTabbingEditor == true) {          Component comp = getEditorComponent();          comp.removeKeyListener(tabKeyListener);          this.removeKeyListener(tabKeyListener);          FocusManager.setCurrentManager(focusManager);          inTabbingEditor = false;       }       super.editingStopped(evt);    }    protected void convertEditableColumnsToView() {       // Convert the editable columns to view column numbers       if (editingColumns == null) {          viewEditingColumns = null;          return;       }       // Create a set of editable columns in terms of view       // column numbers in ascending order. Note that not all       // editable columns in the data model need be visible.       int length = editingColumns.length;       viewEditingColumns = new int[length];       int nextSlot = 0;       for (int i = 0; i < length; i++)          { int viewIndex =                   convertColumnIndexToView(editingColumns[i]);          if (viewIndex != -1) {             viewEditingColumns[nextSlot++] = viewIndex;          }       }       // Now create an array of the right length       // to hold the view indices       if (nextSlot < length) {          int[] tempArray = new int[nextSlot];          System.arraycopy(viewEditingColumns, 0,                           tempArray, 0, nextSlot);          viewEditingColumns = tempArray;       }       // Finally, sort the view columns into order        TableUtilities.sort(viewEditingColumns);    }    protected void moveToNextEditor(int row, int column,                                              boolean forward) {       // Column is specified in terms of the column model       if (viewEditingColumns != null) {          int length = viewEditingColumns.length;          // Move left-to-right or right-to-left          // across the table          for (int i = 0; i < length; i++) {             if (viewEditingColumns[i] == column) {                // Select the next column to edit                if (forward == true) {                   if (++i == length) {                      // Reached end of row - wrap                      i = 0;                      row++;                      if (row == getRowCount()) {                         // End of table - wrap                         row = 0;                      }                   }                } else {                   if (--i < 0) {                      i = length - 1;                      row--;                      if (row < 0) {                         row = getRowCount() - 1;                      }                   }                }                final int newRow = row;                final int newColumn = viewEditingColumns[i];                // Start editing at new location                SwingUtilities.invokeLater(new Runnable() {                   public void run() {                      editCellAt(newRow, newColumn);                      ListSelectionModel rowSel =                           getSelectionModel();                      ListSelectionModel columnSel =                           getColumnModel().getSelectionModel();                      rowSel.setSelectionlnterval(                           newRow, newRow);                      columnSel.setSelectionInterval(                           newColumn, newColumn);                   }                });                break;             }          }       }    }    // Catch changes to the table column model    public void columnAdded(TableColumnModelEvent e) {       super.columnAdded(e);       convertEditableColumnsToView();    }    public void columnRemoved(TableColumnModelEvent e) {       super.columnRemoved(e);       convertEditableColumnsToView();    }    public void columnMoved(TableColumnModelEvent e) {       super.columnMoved(e);       convertEditableColumnsToView();    }    public class TabKeyListener extends KeyAdapter {       public void keyPressed(KeyEvent evt) {          if (evt.getKeyCode() == KeyEvent.VK_TAB) {             if (inTabbingEditor == true) {                TableCellEditor editor = getCellEditor();                int editRow = getEditingRow();                int editColumn = getEditingColumn();                if (editor != null) {                   boolean stopped = editor.stopCellEditing();                   if (stopped == true) {                      boolean forward = (evt.isShiftDown() ==                                                         false);                      moveToNextEditor(editRow, editColumn,                                                       forward);                   }                }             }          }       }    }    protected boolean inTabbingEditor;    protected FocusManager focusManager;    protected int [] editingColumns;        // Model columns    protected int [] viewEditingColumns;    // View columns    protected TabKeyListener tabKeyListener =                                           new TabKeyListener(); } 

Although there seems to be quite a lot of code, it is easy to see how this table works by breaking the implementation into pieces that mirror the design aspects that were covered earlier. Notice that this class has a set of constructors that mirros those of JTable, so that the programmer can create a TabEditTable in exactly the same way as creating an ordinary table. Having created the table, the only extra step needed is to specify which table columns participate in the tab traversal mechanism by calling the setEditingColumns method. The other public methods of this class are actually part of the logic that controls the tab traversal; they don't need to be directly called by the programmer. In most cases, then, the programmer can convert a table without tab support by simply replacing the construction of JTable by construction of a TabEditTable and then invoking setEditingColumns.

Specifying the Table Columns

The setEditingColumns method is supplied with an array of integers that specifies the column numbers of the columns that participate in tab traversal editing. Relating this to the currency table in which the two exchange rate columns should support tab traversal, you might invoke this method as follows:

 int[] tabColumns = { 1, 2 }; tbl.setEditingColumns (tabColumns); 

However, this code hides a subtlety that we've mentioned already in this chapter in different contexts. Are the column numbers specified in terms of the column order in the data model or as indices in the tables TableColumnModel? Initially, columns 1 and 2 in the data model might actually be displayed in columns 1 and 2 of the table, but this needn't always be the case and, even if it is, the user could reorder the table columns at any time. From the programmers point of view, the most sensible option is to specify the column indices in terms of the columns in the data model, because these indices are invariant the exchange rates are always in columns 1 and 2 of the data model. This is, in fact, how the setEditingColumns model method in Listing 7-9 is implemented. As you can see, it stores the column list in an instance variable called editingColumns, which can be retrieved at any time using the getEditingColumns method.

The problem with saving data column model indices is that the table methods that deal with editing and use a column number (such as editCellAt) all work in terms of the TableColumnModel indices. As you'll see, this means that when the tab key is being used to move the edit focus around the table you'll have the TableColumnModel index to work from, not the data model index. For this reason, it is necessary to map from data model indices to TableColumnModel indices, which can be done using the JTable convertColumnIndexToView method.

The most natural way for the tabbing operation to work is for the tab key to move the editing focus across each row from left to right and to wrap back to the leftmost participating column at the end of the row. If tab is used with shift, this order would be reversed. When the decision is being made as to which cell to edit next, it would be useful to have an array of column indices in TableColumnModel order. Then, to move the edit focus to the right, you would just move to the next element in the array from the one currently in use and to move to the left you move back by one entry. Thus, when setEditingColumns is called, the array of column numbers is converted to an array of TableColumnModel indices and stored in the instance variable viewEditingColumns for use when the TAB key is being processed. The conversion is performed by using the convertEditableColumnsToView method.

The convertEditableColumnsToView method is fairly simple. It first creates a new array of the same size as the one passed to setEditingColumns and walks down the array of data model indices converting each to the corresponding TableColumnModel index and storing it in the next slot in the new array. Because not all the data model columns need be included in the table column model for a particular table, it is possible that one or more of the columns designated as participating in tabbing might not have a corresponding TableColumnModel index. In this case, convertColumnIndexToView returns -1 and that column is ignored. As a result, the final array may be shorter than the one initially passed to setEditingColumns and, if it is, it is truncated by copying it to a new array of the appropriate size. Finally, the new array is sorted into ascending order, so that the leftmost participating column comes first, followed by the next one to its right and so on. This makes it possible to see the tabbing order in terms of the indices of the table columns as they are displayed by reading the array from the start to the end.

Core Note

The sort method used by convertEditableColumnsToView is a simple-minded sort whose implementation is not interesting and which is not shown here. If you are using Java 2, you could instead use the sort method of the java.lang.Arrays class, which would almost certainly be more efficient.



When setEditingColumns returns, then, the viewEditingColumns variable contains the tabbing columns in sorted order. However, this is not quite the end of the story. Suppose the user reorders the table columns. If this happens, the mapping created by setEditingColumns might become incorrect.

For example, in the initial configuration of the currency table, the data model column indices map directly to the TableColumnModel indices, so the viewEditingColumns array would initially contain the values {1, 2}. Now, if the leftmost exchange rate column were dragged to the left of the table, its TableColumnModel index would become 0 instead of 1 and it would be necessary to change viewEditingColumns to {0, 2} instead. Furthermore, if the rightmost exchange rate column were now dragged to the left to occupy position 0, the correct value of viewEditingColumns would now be {0, 1} and the tab order between the two exchange rate columns would be reversed in terms of their column headings, but would still be left to right across the table. To recompute the array values it is necessary to gain control whenever the order of columns in the TableColumnModel changes.

To be notified of changes to the TableColumnModel, it is necessary to implement a TableColumnModelListener. Fortunately, JTable already implements this interface and registers itself as a TableColumnModelListenerof its own TableColumnModel. To receive notification of column order changes, TabEditTable simply overrides the following TableColumnModelListener methods of JTable:

  • columnAdded

  • columnRemoved

  • columnMoved

Each of these methods simply calls the JTable implementation and then invokes convertEditableColumnsToView to convert the data column model indices (stored in editingColumns) to the TableColumnModel indices that are correct for the new column order.

Changing the Editing Cell

It remains to actually implement the code that will capture the tab key and move the edit focus elsewhere. As noted above, it is necessary to gain control when editing is started and add a listener to receive the tab key. Because editing is always begun by calling the JTable editCellAt method, the natural thing to do is to override this method in TabEditTable. If you look at the implementation of editCellAt in Listing 7-9, you'll see that it first checks whether the edit is actually going to start. If so, it then verifies that the cell about to be edited is in the list of columns for which tabbing should be active. Note that, because editCellAt receives a column number expressed in terms of the TableColumnModel ordering, this check is made by looking at the viewEditingColumns array, not the original array of data model indices supplied to setEditingColumns. If the cell is in this list, the following steps are taken:

Step 1.

The editor component installed by the superclass editCellAt method is obtained using getEditorComponent.

Step 2.

A KeyListener is added to this component and to the table.

Step 3.

The Swing focus manager is disabled so that tab keys are not lost.

Step 4.

A flag is set indicating that the current editor should have special actions when the tab key is detected.

Step 5.

The input focus is passed to the editing component.

The last step attempts to ensure that keys are passed directly to the editing component, but it is not sufficient because the table sometimes grabs the focus back. Hence, it is still necessary to install a KeyListener on the table as well.

As each key is passed to the editor component, the KeyListener implemented by the inner class TabKeyListener is notified. The keyPressed method of this class checks whether the key is a tab key and if it is, the current edit is stopped by invoking the editor's stopCellEditing method. If this works, the editor has been removed and the edit focus is moved to the next eligible cell by calling the TabEditTable moveToNextEditor method.

The moveToNextEditor method is given the row and column number of the cell last edited and a flag indicating whether it should move forward or backward in the tabbing order; this flag is set to indicate backward movement if the shift key was pressed together with tab. This method can be overridden to provide any kind of tabbing behavior that you want. The default implementation provides the natural left-to-right or right-to-left tabbing order described earlier in this section. It does this by using the editing column number, which is a TableColumnModel index, to locate its current place in the viewEditingColumns array. If it is moving forward, it selects the column in the next entry of the array; otherwise, it selects the previous entry. If this would involve moving off the end of the array in either direction, the row number is incremented or decremented as necessary and the next column is taken as the one in the first or last entry of the viewEditingColumns array. Either way, the last step is to call editCellAt to start an edit in the new cell. However, this is not done directly instead, the operation is deferred using the Swingutilities invokeLater method, which allows key processing to complete before the new edit starts. Notice also that the new cell being edited is made the currently selected cell by manipulating the table's row and column selection models.

You can see how this works in practice using the following command:

 java AdvancedSwing.Chapter7.TabbableCurrencyTable 

First, start an edit by double-clicking somewhere in the column headed Yesterday, make a change, and then press tab. When you do this, you'll find that the new value is written to the table and the cell in the Today column to its right starts editing. As you continue to press tab, you'll find that you can access all the exchange rate values in turn, but not any of the other cells. Also, you'll find that you can use shift and tab to traverse the table in the other direction and that the operation wraps when you reach the top or bottom of the table.

To verify that this mechanism is resilient to column order changes, try dragging the currency rate columns to various locations within the table and then start an edit and press the tab key. You'll find that tabbing still works, that it still allows you to access only the exchange rate columns, and that tab continues to move you from left to right and top to bottom, even if you swap the order of the two editable columns.

 

 



Core Swing
Core Swing: Advanced Programming
ISBN: 0130832928
EAN: 2147483647
Year: 1999
Pages: 55
Authors: Kim Topley

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net