Fully Interactive JTables (aka Mouseover Editing)

May 17th, 2007 | Kevin

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?

6 Responses to “Fully Interactive JTables (aka Mouseover Editing)”

  1. Carl Says:

    That’s some amazing stuff! I was, however, thrown a bit because of the way text fields don’t glow on mouseover the way buttons/checkboxes do, so it wasn’t as obvious I could click to edit. We’ve done some work with forwarding mouse events. It’s not overly complex, but at present we only have a partial solution because I didn’t implement MouseMotionListener, only MouseListener. The result is you don’t get good behavior out of JButtons, as they get confused unless they get mouseEntered and so on.

  2. Kevin Says:

    A mouseover glow for a textfield sounds like a great idea, esp. since it’s difficult/impossible to identify a component as a textfield when it’s inside a grid.

    The event-forwarding solution would undoubtedly be hairy, and you’d have to keep track of which cell the cursor was hovering over in order to send mouseEntered events to that cell (but not to others), etc. And Brien has pointed out to me that cursor propagation (from the renderer to the JTable) could be tricky. Still, it has the possibility of being a perfect solution, rather than a 90% solution like the one described here.

  3. dberansky Says:

    the problem with this approach is you can’t do highlighting while a cell is in editing mode, otherwise, you may interfere with the editing process. How about using the glass pane? I mean tracking the mouse pointer moving over the table, getting a renderer for the cell currenly hovered over, setting its border and then rendering it into the glass pane?

  4. Chris Says:

    I noticed that in the TwoStageTableCellEditor you define a isFullyEngaged() method but in the handler on line 24 you call isInStageTwo(). Is this a typo?

  5. Francis Says:

    Very cool effect. I’ll be sure to implement this in my code when the time come for it (Bookmarking this page right now).

    About the textfield border highlight, I dont think going all the way to the glasspane is that necessary. You could still effectively use the renderer or the cell editor, which ever will be responsible for the painting and attach a mouselistener to the table (done from within the renderer or celleditor code)which will initiate some boolean variable to indicate the presence of the mouse at the desired point in the table. With this you can paint the border within the renderer or celleditor code.

    I use a similar technique for row highlighting in JTable, JList and JTree. I wish I could post code but I just might do that on my blog or site later.

  6. MF Says:

    Can you please post or link to the complete java source code for this “Fully Interactive JTables” example ? The code in the given windows refers to a “FullMouseAdapter” class and some other methods.
    Thanks.

Leave a Reply


Palantir

Bad Behavior has blocked 416 access attempts in the last 7 days.