javajtablejlistglazedlists

Why does my ListSelectionListener see multiple events when I delete multiple items from a list using removeAll ( and e.getValueIsAdjusting == false)?


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

Solution

  • 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.