Question

I am using a JTable whose TableModel is periodically updated through fireTableDataChanged(). These changes are usually pretty small, such as a single row added or modified, however I can't predict where it will happen.

Is there a way to know which rows have been added or modified on a fireTableDataChanged() ? I would like to highlight these rows so the user will know as well.

Was it helpful?

Solution

First off, you must setup your context as appropriate for Swing: the TableModel must have enough knowledge/control about itself to fully comply to its notification contract. That is it must fire row-/cellUpdated or rowsInserted whenever such a change happens.

Then the basic approach to highlight changes (for a certain time) in the JTable is to

  • implement a custom renderer that decorates cells which are in some storage
  • configure the table with the custom renderer
  • listen to changes of the model
  • add the changeEvents (or a custom object with its relevant properties) to the storage that the renderer knows about
  • use timers to remove the change markers after some time

SwingX simplifies (biased me :-) the rendering part by providing Highlighters and HighlightPredicates: the former do custom visual decorations when the latter decides they should be turned on. The above approach would be adjusted to

  • configure the table with highlighters for visual decoration
  • listen to changes in the model
  • add the changed cell to a custom HighlightPredicate and configure the Highlighter with it
  • use timers to remove the change markers after some time

Below is some code, the management of the timers/predicates factored into a class called ChangeDecorator: it keeps one Highlighter for decorating updated cells and one for decorating inserted rows (Note: this is an example, obviously the logic must be extended to cover updated rows :) It's fed by a modelListener with changes and updates the predicates as needed.

JXTable table = new JXTable(model);
final ChangeDecorator controller = new ChangeDecorator();
table.addHighlighter(controller.getChangeHighlighter());
TableModelListener l = new TableModelListener() {

    @Override
    public void tableChanged(TableModelEvent e) {
        if (TableUtilities.isUpdate(e)) {
            Change change = new Change(e.getFirstRow(), e.getColumn());
            controller.addChange(change);
        } else if (TableUtilities.isInsert(e)) {
            Change change = new Change(e.getFirstRow());
            controller.addChange(change);
        }
    }
};
model.addTableModelListener(l);



/**
 * Manages the Highlighters for inserted rows/updated cells.
 */
public static class ChangeDecorator {

    private List<Change> changes;
    private AbstractHighlighter update;
    private AbstractHighlighter insert;
    private Highlighter compound;

    public ChangeDecorator() {
        changes = new ArrayList<>();
    }

    public Highlighter getChangeHighlighter() {
        if (compound == null) {
            update = new ColorHighlighter(new ChangePredicate(changes, true), 
                    Color.YELLOW, null);
            insert = new ColorHighlighter(new ChangePredicate(changes, false), 
                    Color.GREEN, null);
            compound = new CompoundHighlighter(update, insert);
        }
        return compound;
    }

    public void addChange(Change change) {
        startTimer(change, change.isCell ? update : insert);
    }

    private void startTimer(final Change change, final AbstractHighlighter hl) {
        changes.add(change);
        hl.setHighlightPredicate(new ChangePredicate(changes, change.isCell));
        ActionListener l = new ActionListener() {
            boolean done;
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!done) {
                    done = true;
                    return;
                }
                ((Timer) e.getSource()).stop();
                changes.remove(change);
                hl.setHighlightPredicate(new ChangePredicate(changes, change.isCell));
            }

        };
        Timer timer = new Timer(2000, l);
        timer.setInitialDelay(100);
        timer.start();
    }
}

/**
 * A predicate enables highlighting a cell if it
 * contains a change for that cell. 
 */
public static class ChangePredicate implements HighlightPredicate {

    private List<Change> changes;
    private boolean matchCell;
    public ChangePredicate(List<Change> changes, boolean matchCell) {
        this.changes = new ArrayList(changes);
        this.matchCell = matchCell;
    }

    @Override
    public boolean isHighlighted(Component renderer,
            ComponentAdapter adapter) {
        return changes.contains(createChange(adapter));
    }

    private Change createChange(ComponentAdapter adapter) {
        int modelRow = adapter.convertRowIndexToModel(adapter.row);
        if (matchCell) {
            int modelColumn = 
                    adapter.convertColumnIndexToModel(adapter.column);;
                    return new Change(modelRow, modelColumn);
        }
        return new Change(modelRow);
    }

}

/**
 * A crude class encapsulating a cell change. 
 * 
 */
public static class Change {
    int row;
    int column;
    boolean isCell;

    public Change(int row) {
        this(row, -1, false);
    }

    public Change(int row, int col) {
        this(row, col, true);
    }

    private Change(int row, int col, boolean update) {
        this.row = row;
        this.column = col;
        this.isCell = update;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Change)) return false;
        Change other = (Change) obj;
        return row == other.row && column == other.column && isCell == other.isCell;
    }

}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top