JDK-4820833 : JTable: Selection anchor (and lead) not updated when inserting or deleting rows.
  • Type: Bug
  • Component: client-libs
  • Sub-Component: javax.swing
  • Affected Version: 1.3.1_06
  • Priority: P4
  • Status: Closed
  • Resolution: Cannot Reproduce
  • OS: windows_xp
  • CPU: x86
  • Submitted: 2003-02-19
  • Updated: 2004-09-22
  • Resolved: 2004-09-22
Related Reports
Relates :  
Relates :  
Description

Name: jk109818			Date: 02/19/2003


FULL PRODUCT VERSION :
java version "1.4.1"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.1-b21)
Java HotSpot(TM) Client VM (build 1.4.1-b21, mixed mode)

FULL OPERATING SYSTEM VERSION :
Microsoft Windows 2000 [Version 5.00.2195]

ADDITIONAL OPERATING SYSTEMS :
It probably occurs on all Operating Systems.  In other words,
this bug is not OS specific.

A DESCRIPTION OF THE PROBLEM :
Using JTable, if rows are added or deleted, the selection
mechanism (the DefaultListSelectionModel) does not update
its anchor and lead properties.  Thus the focus rectangle will
draw in an incorrect cell as the anchor determines which
cell gets the focus.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1.Run the example provided in the source code below
2. Click in cell at row 2 and column 0 (with "Angela" in it)
3. Click the "Add Rows" button.  This adds two rows between
row 0 and row 1.
4. Notice that the focus rectangle stays in row 2 column 0
where "Kathy" now resides rather than in row 4 column 0
where "Angela" now is.
5. Notice also that the new rows (marked by Allison & Kathy)
are not selected because the row after the insert (i.e.
Mark) was unselected at the time.

6. If you repeat seps 1-5 but this time select Mark instead
of Angela, you will see that the inserted rows (marked by
Allison, Kathy, and Mark) are selected after the insert
(because the row after the insert (i.e. Mark) was selected
prior to the insert).

Note that similar problems happen if you delete rows instead
of adding them.

EXPECTED VERSUS ACTUAL BEHAVIOR :
I expected the focus rectangle to stay on the same cell after
an insert rather than "jumping" to an unexepected cell.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableColumn;
import java.awt.*;
import java.awt.event.*;
import java.util.Vector;

public class SimpleTableDemo extends JPanel implements ActionListener
{

protected JTable        table = null;
protected MyTableModel  model = null;

public SimpleTableDemo()
{
    // Set a simple BoxLayout manager
    setLayout(new BoxLayout(this, BoxLayout.X_AXIS));

    model = new MyTableModel();
    table = new JTable(model);
    table.setPreferredScrollableViewportSize(new Dimension(600, 150));

    table.setIntercellSpacing(new Dimension(0, 0));
    DefaultTableCellRenderer defaultRenderer = new DefaultTableCellRenderer();
    defaultRenderer.setHorizontalAlignment(SwingConstants.RIGHT);
    TableColumn column = table.getColumnModel().getColumn(2);
    column.setCellRenderer(defaultRenderer);

    // Create the scroll pane and add the table to it.
    JScrollPane scrollPane = new JScrollPane(table);

    // Add the scroll pane to the panel
    add(scrollPane);

    // Create and add an AddRows button for the table
    JButton buttonAddRows = new JButton("Add Rows");
    buttonAddRows.addActionListener(this);
    add(buttonAddRows);
}

public static void main(String[] args)
{
    JFrame frame = new JFrame("SimpleTableDemo");
    frame.addWindowListener(new WindowAdapter() {
        public void windowClosing(WindowEvent e)
        {
            System.exit(0);
        }
    });

    SimpleTableDemo panel = new SimpleTableDemo();
    frame.getContentPane().add(panel);
    frame.pack();
    frame.setVisible(true);
}

public void actionPerformed(ActionEvent e)
{
    model.addRows();
    table.requestFocus();
}

public class MyTableModel extends AbstractTableModel
{
    protected Object[][] data = {
        {"Mary", "Campione",
         "Snowboarding", new Integer(5), new Boolean(false)},
        {"Alison", "Huml",
         "Rowing", new Integer(3), new Boolean(true)},
        {"Kathy", "Walrath",
         "Chasing toddlers", new Integer(2), new Boolean(false)},
        {"Mark", "Andrews",
         "Speed reading", new Integer(20), new Boolean(true)},
        {"Angela", "Lih",
         "Teaching high school", new Integer(4), new Boolean(false)},
        {"Peter", "Puck",
         "Hockey", new Integer(7), new Boolean(false)},
        {"Martha", "Durant",
         "Travelling", new Integer(5), new Boolean(true)}
    };

    protected String[] columnNames = {"First Name",
                                      "Last Name",
                                      "Sport",
                                      "# of Years",
                                      "Vegetarian"};

    protected Vector v = null;

    final int first  = 1;
    final int second = 2;

    public MyTableModel()
    {
        v = new Vector();
        for (int i = 0; i < data.length; i++) {
            if (i < first || i > second) {
                v.add(data[i]);
            }
        }
    }

    public void addRows()
    {
        v = new Vector();
        for (int i = 0; i < data.length; i++) {
                v.add(data[i]);
        }
        fireTableRowsInserted(first, second);
    }

    public int getRowCount()
    {
        return(v.size());
    }

    public int getColumnCount()
    {
        return(data[0].length);
    }

    public String getColumnName(int columnIndex)
    {
        return(columnNames[columnIndex]);
    }

    public Class getColumnClass(int columnIndex)
    {
        Class c = String.class;
        if (columnIndex == 3) {
            c = Integer.class;
        }
        else if (columnIndex == 4) {
            c = Boolean.class;
        }
        return(c);
    }

    public boolean isCellEditable(int rowIndex, int columnIndex)
    {
        return(true);
    }

    public Object getValueAt(int rowIndex, int columnIndex)
    {
        Object[] o = (Object [])v.get(rowIndex);
        return(o[columnIndex]);
    }
}

}

---------- END SOURCE ----------

CUSTOMER WORKAROUND :
Extend the DefaultListSelectionModel class.  Override
insertIndexInterval() and removeIndexInterval() to first
call the super's method, adjust the anchor and lead
appropriately, and set them on the list selection model.
Note that it would have been really helpful here if the
anchorIndex and leadIndex variables were protected instead
of private because setting these values thru the API causes
selection events to be fired.  Thus this workaround is not a
perfect solution.

Note also that also would have been very useful to have an
insert method like:

public void insertIndexInterval(int index, int length,
boolean before, boolean state)

where the user could have control over what state (selected
or not selected) the values in the insert interval will get.
 Currently all the rows in the insert interval get set to
the value of the row following the insert interval.
(Review ID: 181001) 
======================================================================

Comments
EVALUATION The problem of lead and anchor not being updated when inserting or deleting rows has been fixed already in 1.5.0 (see 4303294 and 4803311). As for the current behavior of newly inserted data becoming selected when inserted at an index with a currently selected row, that behavior will be difficult to change without breaking backward compatibility. It's easy to unselect these new rows programmatically. ###@###.### 2004-09-22
22-09-2004