Blogs / Tech Blog

Fully Interactive JTables (aka Mouseover Editing)

What sucks about JTables? Everything, of course—but that’s a developer’s perspective. To the user, cell editing is rough around the edges: when and where to click, and how many times—it’s never perfectly clear. Cells in a table just don’t provide the mouseover feedback that regular components do. If only a JTable behaved like a bunch of components thrown into a giant GridBag or TableLayout

mouseover-screenshot.png

Mouseover Editing simulates just that. The idea is to attach a MouseListener to the JTable and call editCellAt(row, col) whenever the cursor moves over a new cell. In other words, even though only one cell in a table can be fully interactive (the editing cell) at any given time, as long as we keep moving that cell to stay underneath the user’s cursor, the whole table will appear to be fully interactive. If done correctly, this will appear to the user as though he’s interacting with a bunch of real components (rather than rendered stamps) inside a giant Grid/GridBag/TableLayout.

Most importantly, the user will get mouseover feedback about which cells are editable, and how to edit them. Checkboxes, buttons, and comboboxes (if the L&F supports it) will highlight to indicate press-ability and the cursor will turn to a text caret when hovering over cells that contain textfields. When done correctly, the effect is nearly seamless and very satisfying.

Here’s a webstart demo. Read on for the solution in code.

There are three parts to the Mouseover Editing solution (over and above what’s needed for typical JTable editing).

Part 1: Identical Editors and Renderers

This is important. Typically, a table will use a JLabel to render a cell and a JTextField to edit the cell. When the editor is swapped in to replace the renderer, there’s a slight flicker—the textfield might have a different background color, the text mayb be in a slightly different location, etc. This is all well and good when the user has to click or double-click to engage the editor, because the click can be perceived as granting permission to the program to do something on its own. However, a simple mouseover is a different story. Here, the look, feel, and placement of the editor should exactly match up with the look, feel, and placement of the renderer; otherwise, there will be a flicker trail every time the cursor passes through the JTable.

In order to make editors and renderers look and behave identically, (a) we need them to start out identical and (b) we need to apply the same transformations to them.

The easiest way to make an identical editor and renderer is to produce two objects from a combined EditorRenderer class. Here’s how that might work for a checkbox:

public class CheckBoxEditorRenderer extends AbstractCellEditor implements TableCellRenderer
{
	private JCheckBox checkbox = new JCheckBox();
	public CheckBoxEditorRenderer() {
		super();
		checkbox.setFocusable(false);
	}
	public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
		if (value instanceof Boolean) {
			checkbox.setSelected((Boolean) value);
		}
		return checkbox;
	}
	public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
		if (value instanceof Boolean) {
			checkbox.setSelected((Boolean) value);
		}
		return checkbox;
	}
	public Object getCellEditorValue() {
		return checkbox.isSelected();
	}
}

Notice how CheckBoxEditorRenderer implements both the TableCellRenderer and TableCellEditor interfaces (the latter via AbstractCellEditor).

Editors and renderers must also be subjected to the same transformations. In a subclass of JTable, say one called MouseoverJTable, use the following format for your prepareEditor() and prepareRenderer() methods:

public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
	Component c = super.prepareRenderer(renderer, row, column);
	return prepareEditorRenderer(c, row, column);
	}
	public Component prepareEditor(TableCellEditor editor, int row, int column) {
	Component c = super.prepareEditor(editor, row, column);
	return prepareEditorRenderer(c, row, column);
	}
	private Component prepareEditorRenderer(Component stamp, int row, int column) {
//	 ...  All sorts of shenanigans can go on here.
	}
}

This ensures that both the editor and the renderer will go through the same decoration path.

Part 2: Two-stage editing

What we’ve presented thus far works really well for buttons and checkboxes, which are minimally interactive. With comboboxes and textfields, there’s a problem. Suppose you mouse over a cell with a combobox. First the combobox will become highlighted (if the L&F supports it) to indicate that it’s clickable; this is expected and reasonable behavior. If you click on it, the combobox flyout will appear; this is also expected and reasonable behavior. Now move your mouse just a little bit…. oh no! You accidentally moused over another cell, and the table decided to swap out the editor you were using (the combobox) and swap in a new editor. Goodbye flyout.

To make sure this doesn’t happen, we need editors with two states: temporarily engaged and fully engaged. When the cursor first moves over a cell, we call editCellAt(r, c), and plop the editor into place, temporarily engaged. This means it’s an interactive JComponent that responds to mouse events, but that it’s okay to swap it out and replace it with a new editor if the cursor changes location. Then, if the user starts interacting with the Editor in a way that requires the editor to keep around a bit of state (like the visibility of a flyout for a combobox), we say that the editor is fully engaged, and that the table isn’t allowed to swap it out until the user disengages it.

What this looks like in code:

public interface TwoStageTableCellEditor extends TableCellEditor
{
    public boolean isFullyEngaged();
}
public class ComboBoxEditorRenderer extends AbstractCellEditor implements TwoStageTableCellEditor, TableCellRenderer
{
    private JComboBox combobox;
    ...
    public Object getCellEditorValue() {
        return combobox.getSelectedItem();
    }
    public boolean isFullyEngaged() {
        return combobox.isPopupVisible();
    }
}

Part 3: Mouseover swapping

Here’s where it all comes together. Mouseover swapping is what your (subclass of) JTable does in order to keep track of which cell is being edited, where the cursor is, and whether to swap the editor from one cell to another. This is kind of boring, and only really makes sense in code, so here you go:

// In the constructor...
// listeners for two-stage editing
this.addMouseListener(twoStageEditingListener);
this.addMouseMotionListener(twoStageEditingListener);
// In the body of your JTable subclass...
private final FullMouseAdapter twoStageEditingListener = new FullMouseAdapter() {
    public void mouseMoved(MouseEvent e) {
        possiblySwitchEditors(e);
    }
    public void mouseEntered(MouseEvent e) {
        possiblySwitchEditors(e);
    }
    public void mouseExited(MouseEvent e) {
        possiblySwitchEditors(e);
    }
    public void mouseClicked(MouseEvent e) {
        possiblySwitchEditors(e);
    }
};
private void possiblySwitchEditors(MouseEvent e) {
    Point p = e.getPoint();
    if (p != null) {
        int row = rowAtPoint(p);
        int col = columnAtPoint(p);
        if (row != getEditingRow() || col != getEditingColumn()) {
            if (isEditing()) {
                TableCellEditor editor = getCellEditor();
                if (editor instanceof TwoStageTableCellEditor && !((TwoStageTableCellEditor)editor).isInStageTwo()) {
                    if (!editor.stopCellEditing()) {
                        editor.cancelCellEditing();
                    }
                }
            }

            if (!isEditing()) {
                if (row != -1 && isCellEditable(row, col)) {
                    editCellAt(row, col);
                }
            }
        }
    }
}

Conclusion

This solution works pretty well. There are a couple flaws (e.g. start editing a textfield and then mouseover a button—no feedback, because the text field is fully engaged), but overall it is much better than an unadorned JTable, and not too much of a burden on the developer. Be warned, though: it may wreak havoc on the focus system.

Extra credit: Is it possible to achieve a similar effect by forwarding MouseEvents through to the renderers/stamps?

Other Blogs