JDK-6791934 : JTable displays incorrectly using TableRowSorter.setSortsOnUpdates(true)
  • Type: Bug
  • Component: client-libs
  • Sub-Component: javax.swing
  • Affected Version: 6u10
  • Priority: P4
  • Status: Open
  • Resolution: Unresolved
  • OS: windows_xp
  • CPU: x86
  • Submitted: 2009-01-09
  • Updated: 2017-05-23
Description
FULL PRODUCT VERSION :
java version "1.6.0_10"
Java(TM) SE Runtime Environment (build 1.6.0_10-b33)
Java HotSpot(TM) Client VM (build 11.0-b15, mixed mode, sharing)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows XP [Version 5.1.2600]

A DESCRIPTION OF THE PROBLEM :
Using the standard TableRowSorter to implement simple row filtering, if you specify sorter.setSortsOnUpdates(true), the JTable doesn't repaint properly when receiving updates. The paint problem is exposed using Metal, Windows and (3rd party) Alloy look and feels and so I assume is the result of inconsistent data being provided by the JTable for the view

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
The program attached below is a very simple application which creates a 1-column table of various things. It has a JComboBox that changes the state of the TableRowSorter's RowFilter. This filters the table according to whether the entries can fly.

The toggle button at the bottom modifies one of the entries in the table to enable/disable it to fly and fires an update from the table model.

The entry which changes is #3 in the model - it changes from "BATFINK: without Cape" to "BATFINK: with Cape" with presses of the toggle button.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
I would expect that the behaviour would be that if you were filtering on "Flightless" things, and the button were used to enable one of the items to fly, it would appear in the JTable. Conversely, it would disappear in the table if the button was toggled again.


ACTUAL -
The JTable does not draw the model properly.

If you are filtering on "Flightless" and add a Cape to BATFINK, the row should disappear completely. It does not - the same number of rows are visible expect that now one row's value (below the removed row) is duplicated and visible in two rows. If the example is changed such that BATFINK is the last item in the table, when it should remove itself from the view, it does not. The last row in the table still appears except with no contents and only one cell!

Alternatively, if you are filtering on "Can fly" and add a cape to BATFINK, the row should become visible. It does not; nothing happens at all.



REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableRowSorter;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.*;

/**
 * @author Chris Marshall
 */
public class TableRowSorterBug {

    public static void main(String[] args) {
        JFrame f = new JFrame();
        JPanel p = new JPanel(new BorderLayout());

        JComboBox cb = new JComboBox(new Object[] {"--", "Can fly", "Flightless"} );
        cb.setSelectedItem("--");

        final TestTableModel dm = new TestTableModel();
        final TableRowSorter<TestTableModel> s = new TableRowSorter<TestTableModel>(dm);
        s.setSortsOnUpdates(true);
        s.setRowFilter(new FlightFilter(cb));


        cb.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                s.sort();
            }
        });

        final JToggleButton switchCape = new JToggleButton("Add Cape");
        switchCape.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                dm.cape = !dm.cape;
                switchCape.setText(dm.cape ? "Remove Cape" : "Add Cape");
                dm.fireTableRowsUpdated(3, 3); //SWAP 3 and 7 in the model and change this to 7, 7
//                s.sort(); //When this line is uncommented, the code works as expected
            }
        });

        JTable t = new JTable(dm);
        t.setRowSorter(s);
        p.add(cb, BorderLayout.NORTH);
        p.add(new JScrollPane(t), BorderLayout.CENTER);
        p.add(switchCape, BorderLayout.SOUTH);
        f.setContentPane(p);
        f.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        f.pack();
        f.setVisible(true);

    }

    private static class TestTableModel extends AbstractTableModel {
        private boolean cape;

        public int getRowCount() {
            return 8;
        }

        public int getColumnCount() {
            return 2;
        }

        public Object getValueAt(int rowIndex, int columnIndex) {
          if (columnIndex == 0) {
            switch (rowIndex) {
                case 0 :
                    return "RACCOON";
                case 1 :
                    return "PENGUIN";
                case 2 :
                    return "PIGLET";
                case 3 : //SWAP with 7 to get weird table painting
                    return "BATFINK: " + (cape ? "with Cape" : "Capeless");
                case 4 :
                    return "WOLF";
                case 5 :
                    return "JUGGERNAUT";
                case 6 :
                    return "RAVEN";
                case 7 :
                    return "OWL";
                default:
                    throw new IllegalArgumentException();
            }
          }
          else {
            return rowIndex;
          }
        }
    }

    private static class FlightFilter extends RowFilter<TestTableModel, Integer> {
        private final JComboBox cb;

        public FlightFilter(JComboBox cb) {
            this.cb = cb;
        }

        @Override
        public boolean include(Entry<? extends TestTableModel, ? extends Integer> entry) {
            if ("--".equals(cb.getSelectedItem())) {
                return true;
            }
            Object value = entry.getValue(0);

            if ("RAVEN".equals(value) || "OWL".equals(value) || "BATFINK: with Cape".equals(value)) {
                return "Can fly".equals(cb.getSelectedItem());
            }
            return !"Can fly".equals(cb.getSelectedItem());
        }
    }
}
---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
The workaround (commented out in the example above) is to force a "rowSorter.sort()" on the row sorter when the model updates

Comments
EVALUATION I'll have a deeper look at this problem
13-01-2009