I'm using GlazedLists' EventList
and EventTableModel
in the below example. I'm not sure it it makes a difference. I have a table that I'm watching for selection changes. When I delete multiple items, the ListSelectionListener
sees multiple events and, when inside the handler, the selected indexes reported by the table match the state of the model before the deletion occurred even though the model has already been changed.
When I run the below example, there are 7 items added to a list. If I select the the last 2 items, the following output ends up on the console:
Selected row count: 2
Item list size: 7
Selected index: 5
Selected index: 6
That's what I'd expect, but when I delete those 2 items, I get the following output:
Selected row count: 1
Item list size: 5
Selected index: 5
Selected row count: 0
Item list size: 5
Since I'm deleting items in a contiguous block using removeAll
on the list, I assume that's one event, but the ListSelectionListener
seems to be getting notified as if it's two separate events. If I delete 4 items, the listener sees 4 events.
The table and model are getting out of sync, but I'm not sure why. If items are deleted from the end of the list, the selected indexes reported by the table can be larger than the underlying list size. Basically, the indexes returned from JTable.getSelectedRows
aren't reliable until the last selection event caused by the call to removeAll
on the underlying model.
How can I get notification about selection changes after the list selection has stabilized and the JTable
is going to report the correct selected indexes?
import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.gui.AdvancedTableFormat;
import ca.odell.glazedlists.impl.sort.ComparableComparator;
import ca.odell.glazedlists.swing.EventTableModel;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import static javax.swing.WindowConstants.EXIT_ON_CLOSE;
public class MultiDeleteMain {
// The number of items that should be added to the model.
@SuppressWarnings("FieldCanBeLocal")
private final int itemCount = 7;
private EventList<Item> itemList;
private JTable itemTable;
public static void main(String[] args) {
new MultiDeleteMain();
}
public MultiDeleteMain() {
SwingUtilities.invokeLater(new Runnable() {
@SuppressWarnings("ConstantConditions")
@Override
public void run() {
// The delete function needs access to the list and table, so
// they are stored as instance variables.
itemList = createItemList();
itemTable = createItemTable(itemList);
addListSelectionListenerToItemTable(itemTable);
JPanel mainPanel = new JPanel(new BorderLayout());
mainPanel.add(createDeleteButton(), BorderLayout.NORTH);
mainPanel.add(new JScrollPane(itemTable), BorderLayout.CENTER);
JFrame mainFrame = new JFrame("Multi-deletion in list test.");
mainFrame.setContentPane(mainPanel);
mainFrame.pack();
mainFrame.setSize(300, mainFrame.getHeight());
mainFrame.setLocationRelativeTo(null);
mainFrame.setDefaultCloseOperation(EXIT_ON_CLOSE);
mainFrame.setVisible(true);
}
});
}
private EventList<Item> createItemList() {
EventList<Item> itemList = new BasicEventList<>();
for (int i = 0; i < itemCount; i++) {
itemList.add(new Item("Item " + i));
}
return itemList;
}
@SuppressWarnings("ConstantConditions")
private JTable createItemTable(EventList<Item> itemList) {
JTable itemTable = new JTable(new EventTableModel<>(itemList, new EventTableModelFormat()));
itemTable.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
return itemTable;
}
private void addListSelectionListenerToItemTable(final JTable itemTable) {
ListSelectionListener listener = new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
if(!e.getValueIsAdjusting()) {
System.out.println("Selected row count: " + itemTable.getSelectedRowCount());
System.out.println("Item list size: " + itemList.size());
for(Integer index : itemTable.getSelectedRows()) {
System.out.println("Selected index: " + index);
}
System.out.println();
}
}
};
itemTable.getSelectionModel().addListSelectionListener(listener);
}
private JButton createDeleteButton() {
JButton deleteButton = new JButton("Delete");
deleteButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
deleteSelectedItems();
}
});
return deleteButton;
}
@SuppressWarnings("ConstantConditions")
private void deleteSelectedItems() {
List<Item> itemsToDelete = new ArrayList<>();
for (Integer rowIndex : itemTable.getSelectedRows()) {
int convertedIndex = itemTable.convertRowIndexToModel(rowIndex);
itemsToDelete.add(itemList.get(convertedIndex));
}
itemList.removeAll(itemsToDelete);
itemTable.revalidate();
itemTable.repaint();
}
// Enum for managing table columns
private static enum Columns {
NAME("Name", String.class, new ComparableComparator());
private final String name;
private final Class type;
private final Comparator comparator;
private Columns(String name, Class type, Comparator comparator) {
this.name = name;
this.type = type;
this.comparator = comparator;
}
}
// Each table holds a list of items.
private static class Item {
private final String name;
private Item(String name) {
this.name = name;
}
}
// Table format for use with the EventTableModel
private static class EventTableModelFormat implements AdvancedTableFormat<Item> {
@Override
public int getColumnCount() {
return 1;
}
@Override
public String getColumnName(int i) {
return Columns.values()[i].name;
}
@Override
public Object getColumnValue(Item item, int i) {
return item.name;
}
@Override
public Class getColumnClass(int column) {
return Columns.values()[column].type;
}
@Override
public Comparator getColumnComparator(int column) {
System.out.println("Asked for comparator.");
return Columns.values()[column].comparator;
}
}
}
We would have to know how the EventList and EventTableModel look like. My bet is that the EventTableModel doesn't have any guarantee that deleteAll method will pass only consecutive elements. So the only option is to remove them one by one, firing a separate event for each of them.