problems with editable JList
Maybe one of the experts here can help .... Apologies in advance for
the length of this post; I've tried to make it as short as I could,
but oh well.
I'm trying to work up a prototype for a JList that supports some simple
editing operations: add a new element, replace an existing element,
remove a selected element, and do some simple reordering. My code
is working pretty well, *except* that when I try to set the selection
after an "add" or "remove", it doesn't quite work the way I want.
What I want:
After adding a new element, the new element is selected.
After removing an element, the preceding element is selected,
unless the removed element was the first one, in which case I want
the first one to be selected, or the only one, in which case I want
nothing selected.
How I'm trying to accomplish this:
In my extension of JList, I attach a ListDataListener to the list
data model, and in the listener's methods that deal with addition and
removal events I use setSelectedIndex() or clearSelection() to change
the selection to what I want. And it seems to work, based on what's
reported by debug prints -- *EXCEPT* that something [*] subsequently
changes the selection in a way that I don't want:
After an "add", I end up with a selected index of getModel().getSize(),
which of course is not valid.
After a "remove" of the first element, I end up with nothing selected,
even if there are elements remaining.
[*] Based on attempts to find out what's going on by adding a
lot of debug-print code and poring over source code for various
javax.swing classes, I *think* something in the ListDataListener in
plaf.basic.BasicListUI is responsible.
I've stripped down the prototype to what I think is just about the
smallest program that demonstrates the problem (well, okay, plus some
debug-print code), which appears below. Other than not being very short,
it's an SSCCE.
Am I going about this wrong? Have I tripped over a bug? Initial
development was done with Java 1.6.something, but the code (mis)behaves
the same way with 1.7.something.
Help appreciated!! the problem's not a show-stopper for my use case,
but it *is* annoying, and puzzling ....
/*
Attempt at an editable JList, supporting allow adding, removing, and
changing elements.
Works as desired except for (some) attempts to modify programmatically
what element is selected:
"add" should result in new item being selected (but does not).
"remove" should result in preceding element being selected if there is
one (and that works), or element 0 being selected if the removed element
was the one at index 0 (and that does not work).
*/
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
public class TryListMain extends JFrame {
// ---- variables ----
private static boolean DEBUG = true;
private DefaultListModel model;
private TryList list;
private static int counter = 0;
// ---- constructor ----
public TryListMain() {
super();
model = new DefaultListModel();
list = new TryList(model);
setLayout(new BorderLayout());
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
add(new ButtonPanel(), BorderLayout.NORTH);
add(list, BorderLayout.CENTER);
}
// ---- methods ----
public TryList getList() {
return list;
}
public DefaultListModel getModel() {
return model;
}
public static void debugPrint(String msg) {
if (DEBUG) {
if (msg.length() > 0) {
System.out.println("DBG "+msg);
} else {
System.out.println();
}
}
}
public static void debugPrint() {
debugPrint("");
}
private String generateItem() {
++counter;
return "Element "+counter;
}
// ---- main ----
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
UIManager.put("Button.defaultButtonFollowsFocus", true);
TryListMain theFrame = new TryListMain();
System.out.println("data listeners:");
for (ListDataListener ll :
theFrame.getModel().getListDataListeners())
{
System.out.println(ll.getClass().getName());
}
System.out.println();
System.out.println("list selection listeners:");
for (ListSelectionListener ll :
theFrame.getList().getListSelectionListeners())
{
System.out.println(ll.getClass().getName());
}
System.out.println();
theFrame.setSize(new Dimension(400,200));
theFrame.setVisible(true);
}
});
}
// ---- classes ----
private class ButtonPanel extends JPanel {
private Action removeAction;
private Action replaceAction;
private Action newAction;
private Action showAction;
private void setActionsEnabled() {
int si = list.selectedIndex();
removeAction.setEnabled(si >= 0);
replaceAction.setEnabled(si >= 0);
}
public ButtonPanel() {
super(new FlowLayout());
removeAction = new AbstractAction("Remove") {
@Override
public void actionPerformed(ActionEvent e) {
int si = list.selectedIndex();
TryListMain.debugPrint();
TryListMain.debugPrint("== remove item at "+si);
if (si >= 0) {
model.remove(si);
} else {
// FIXME should never happen
System.err.println("no selected item to remove");
}
}
};
replaceAction = new AbstractAction("Replace") {
@Override
public void actionPerformed(ActionEvent e) {
int si = list.selectedIndex();
if (si >= 0) {
String item = generateItem();
TryListMain.debugPrint();
TryListMain.debugPrint("== replace item at "+si);
model.set(si, item);
} else {
// FIXME should never happen
System.err.println("no selected item to replace");
}
}
};
newAction = new AbstractAction("New") {
@Override
public void actionPerformed(ActionEvent e) {
String item = generateItem();
TryListMain.debugPrint();
TryListMain.debugPrint("== add new item");
model.addElement(item);
}
};
showAction = new AbstractAction("Show selected") {
@Override
public void actionPerformed(ActionEvent e) {
int si = list.selectedIndex();
if (si < 0) {
System.out.println("No selected value");
} else {
System.out.println(
String.format("Selected value (%d of %d) '%s'",
si+1, model.getSize(), model.get(si)));
}
}
};
list.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
setActionsEnabled();
}
});
setActionsEnabled();
add(new JButton(removeAction));
add(new JButton(replaceAction));
add(new JButton(newAction));
add(new JButton(showAction));
}
}
}
class TryList extends JList {
// ---- constructor ----
public TryList(DefaultListModel lModel) {
super(lModel);
getSelectionModel().setSelectionMode(
ListSelectionModel.SINGLE_SELECTION);
getModel().addListDataListener(new ListDataListener() {
@Override
public void intervalAdded(ListDataEvent e) {
printEvent("add", e);
setSelectedIndex(e.getIndex1());
ensureIndexIsVisible(e.getIndex1());
printNewSelection("add");
/*
HERE things seem okay -- desired element selected --
but then something changes the selection to
getModel().getSize(), which of course(?) is not valid
*/
TryListMain.debugPrint();
}
@Override
public void intervalRemoved(ListDataEvent e) {
printEvent("remove", e);
if (getModel().getSize() == 0) {
clearSelection();
} else {
int newSelect = Math.max(0, e.getIndex1()-1);
setSelectedIndex(newSelect);
ensureIndexIsVisible(newSelect);
}
printNewSelection("remove");
/*
HERE things seem okay -- desired element selected --
but then if the removed element was the first one
something changes the selection from (0) to ()
*/
TryListMain.debugPrint();
}
@Override
public void contentsChanged(ListDataEvent e) {
printEvent("change", e);
setSelectedIndex(e.getIndex1());
ensureIndexIsVisible(e.getIndex1());
printNewSelection("change");
TryListMain.debugPrint();
}
});
addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
TryListMain.debugPrint(String.format(
"selection changed to %s, source %s",
selectedIndicesToString(),
e.getSource().getClass().getName()));
}
});
}
// ---- methods ----
// return -1 if nothing selected, or out of range
public int selectedIndex() {
int[] si = getSelectedIndices();
if ((si.length == 1) && (si[0] < getModel().getSize())) {
return si[0];
} else {
return -1;
}
}
private void printEvent(String name, ListDataEvent e) {
TryListMain.debugPrint();
TryListMain.debugPrint(
String.format("%s (%s, %d, %d), %d items (selected %s)",
name,
e.getSource().getClass().getName(),
e.getIndex0(), e.getIndex1(),
getModel().getSize(),
selectedIndicesToString()));
}
private void printNewSelection(String name) {
TryListMain.debugPrint(
String.format("after %s, selection %s",
name,
selectedIndicesToString()));
TryListMain.debugPrint();
}
private String selectedIndicesToString() {
StringBuffer sb = new StringBuffer();
for (int i : getSelectedIndices()) {
if (sb.length() > 0) sb.append(' ');
sb.append(i);
}
return "("+sb.toString()+")";
}
}
--
B. L. Massingill
ObDisclaimer: I don't speak for my employers; they return the favor.