Search code examples
javaswingjtabletablemodel

Highlight changes in a JTable after fireTableDataChanged()


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.


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;
        }
    
    }