/* * $Id$ * * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, * Santa Clara, California 95054, U.S.A. All rights reserved. */ package org.jdesktop.swingx; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.GraphicsEnvironment; import java.awt.KeyboardFocusManager; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyVetoException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Vector; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.Box; import javax.swing.DefaultCellEditor; import javax.swing.DefaultListModel; import javax.swing.DefaultListSelectionModel; import javax.swing.DefaultRowSorter; import javax.swing.Icon; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDesktopPane; import javax.swing.JFrame; import javax.swing.JInternalFrame; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.ListSelectionModel; import javax.swing.RowFilter; import javax.swing.RowSorter; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.event.RowSorterEvent; import javax.swing.event.TableModelEvent; import javax.swing.plaf.ColorUIResource; import javax.swing.plaf.UIResource; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; import org.jdesktop.swingx.JXTableUnitTest.TakeItAllDummy; import org.jdesktop.swingx.JXTableUnitTest.ThrowingDummy; import org.jdesktop.swingx.action.AbstractActionExt; import org.jdesktop.swingx.sort.RowFilters; import org.jdesktop.swingx.test.XTestUtils; import org.jdesktop.test.AncientSwingTeam; import org.jdesktop.test.CellEditorReport; import org.jdesktop.test.ListSelectionReport; import org.jdesktop.test.PropertyChangeReport; import org.jdesktop.test.TestUtils; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import static org.junit.Assert.*; import static org.jdesktop.swingx.JXTableUnitTest.*; /** * @author Jeanette Winzenburg */ @RunWith(JUnit4.class) @SuppressWarnings({ "rawtypes", "unchecked" }) public class JTableIssues extends InteractiveTestCase { /** * */ private static final String ALTERNATE_ROW_COLOR = "Table.alternateRowColor"; private static final Logger LOG = Logger.getLogger(JTableIssues.class .getName()); public static void main(String args[]) throws Exception { setLAF("Nimbus"); JTableIssues test = new JTableIssues(); try { // test.runInteractiveTests(); // test.runInteractiveTests("interactive.*ColumnControl.*"); // test.runInteractiveTests("interactive.*Edit.*"); // test.runInteractiveTests("interactive.*Sort.*"); // test.runInteractiveTests("interactive.*EditOnFocusLost.*"); // test.runInteractive("SortModelSelection"); test.runInteractive("NimbusCheckBox"); } catch (Exception e) { System.err.println("exception when executing interactive tests:"); e.printStackTrace(); } } /** * Quick check: Nimbus default striping doesn't stripe checkbox */ public void interactiveNimbusCheckBox() { TableModel model = new DefaultTableModel(2, 2) { @Override public Class<?> getColumnClass(int columnIndex) { return columnIndex == 0 ? Boolean.class : super.getColumnClass(columnIndex); } }; // JW: sequence matters - first load addon removes the alternate from // normal handling JTable table = new JTable(model); // JXTable xtable = new JXTable(model); showWithScrollingInFrame(table, "core"); } /** * Issue #1536-swingx: AIOOB on restoring selection with filter * */ @Test public void testSelectionWithFilterXTable() { DefaultTableModel model = new DefaultTableModel(0, 1); // a model with 3 elements is the minimum where to demonstrate // the bug int last = 2; for (int i = 0; i <= last; i++) { model.addRow(new Object[]{i}); } JTable table = new JXTable(model); // table.setAutoCreateRowSorter(true); // set selection at the end table.setRowSelectionInterval(last, last); // exclude rows based on identifier final RowFilter filter = new RowFilters.GeneralFilter() { List excludes = Arrays.asList(0); @Override protected boolean include( Entry<? extends Object, ? extends Object> entry, int index) { return !excludes.contains(entry.getIdentifier()); } }; ((DefaultRowSorter) table.getRowSorter()).setRowFilter(filter); // insertRow _before or at_ selected model index, such that // endIndex (in event) > 1 model.insertRow( 2, new Object[]{"x"}); } /** * Issue #1536-swingx: AIOOB on restoring selection with filter * */ @Test public void testSelectionWithFilterTable() { DefaultTableModel model = new DefaultTableModel(0, 1); // a model with 3 elements is the minimum where to demonstrate // the bug int last = 2; for (int i = 0; i <= last; i++) { model.addRow(new Object[]{i}); } JTable table = new JTable(model); table.setAutoCreateRowSorter(true); // set selection at the end table.setRowSelectionInterval(last, last); // exclude rows based on identifier final RowFilter filter = new RowFilters.GeneralFilter() { List excludes = Arrays.asList(0); @Override protected boolean include( Entry<? extends Object, ? extends Object> entry, int index) { return !excludes.contains(entry.getIdentifier()); } }; ((DefaultRowSorter) table.getRowSorter()).setRowFilter(filter); // insertRow _before or at_ selected model index, such that // endIndex (in event) > 1 model.insertRow( 2, new Object[]{"x"}); } /** * Issue #1536-swingx: AIOOB on restoring selection with filter * This is a core issue, sneaked into ListSortUI by c&p * * Analyzed by reporter to incorrect method usage in SortManager * cacheSelection: selectionModel.insert/removeIndexInterval length of range * but gets last index of range. */ public void interactiveSortModelSelection() { final BulkTableModel model = new BulkTableModel(0, 1) { }; for (int i = 0; i < 10; i++) { model.addRow(new Object[]{i}); } final JTable table = new JTable(model); // table.setSortOrderCycle(SortOrder.ASCENDING, SortOrder.DESCENDING, SortOrder.UNSORTED); table.setAutoCreateRowSorter(true); JXFrame frame = wrapWithScrollingInFrame(table, "sort bug"); Action add = new AbstractAction("add") { int count = model.getRowCount(); @Override public void actionPerformed(ActionEvent e) { int selected = table.getSelectedRow(); if (true) {//(selected < 0) { selected = model.getRowCount() - 3; } model.insertRowsAt(selected , new Object[] {count++} , new Object[] {count++} , new Object[] {count++} ); } }; addAction(frame, add); final RowFilter filter = new RowFilters.GeneralFilter() { List excludes = Arrays.asList(4, 5, 6); @Override protected boolean include( Entry<? extends Object, ? extends Object> entry, int index) { return !excludes.contains(entry.getIdentifier()); } }; Action toggleFilter = new AbstractAction("filter") { @Override public void actionPerformed(ActionEvent e) { DefaultRowSorter sorter = (DefaultRowSorter) table.getRowSorter(); sorter.setRowFilter(sorter.getRowFilter() != null ? null : filter ); } }; addAction(frame, toggleFilter); Action unsort = new AbstractAction("unsort") { @Override public void actionPerformed(ActionEvent e) { DefaultRowSorter sorter = (DefaultRowSorter) table.getRowSorter(); sorter.setSortKeys(null); } }; addAction(frame, unsort); show(frame); } public static class BulkTableModel extends DefaultTableModel { public BulkTableModel(int rows, int columns) { super(rows, columns); } @SuppressWarnings({ "unchecked", "rawtypes" }) public void insertRowsAt(int row, Object[]... rows) { List toInsert = new ArrayList(); for (Object[] data : rows) { Vector rowData = convertToVector(data); toInsert.add(rowData); } dataVector.addAll(row, toInsert); // justifyRows(row, row + toInsert.size()); fireTableRowsInserted(row, row + toInsert.size() - 1); } @Override public Class<?> getColumnClass(int columnIndex) { if (columnIndex == 0) { return Integer.class; } return super.getColumnClass(columnIndex); } private void justifyRows(int from, int to) { // Sometimes the DefaultTableModel is subclassed // instead of the AbstractTableModel by mistake. // Set the number of rows for the case when getRowCount // is overridden. dataVector.setSize(getRowCount()); for (int i = from; i < to; i++) { if (dataVector.elementAt(i) == null) { dataVector.setElementAt(new Vector(), i); } ((Vector)dataVector.elementAt(i)).setSize(getColumnCount()); } } } //------- start testing Issue #1535-swingx /** * Sanity: initially valid entry without forcing edit is behaving as expected */ @Test public void testGenericEditorValidValue() { JTable table = new JTable(create1535TableModel()); table.setValueAt(new ThrowingDummy("valid"), 0, throwOnEmpty); assertStoppedEventOnValidValue(table, 0, throwOnEmpty, false); } /** * Test editor firing when empty value is valid */ @Test public void testGenericEditorValidValueAlways() { JTable table = new JTable(create1535TableModel()); assertStoppedEventOnValidValue(table, 0, takeEmpty, false); assertTrue(table.getValueAt(0, takeEmpty) instanceof TakeItAllDummy); } /** * Editing a not-null value with empty text */ @Test public void testGenericEditorEmptyValueInitiallyValid() { JTable table = new JTable(create1535TableModel()); ThrowingDummy validValue = new ThrowingDummy("valid"); table.setValueAt(validValue, 0, throwOnEmpty); assertNoStoppedEventOnEmptyValue(table, 0, throwOnEmpty, true); assertEquals(validValue, table.getValueAt(0, throwOnEmpty)); } /** * Editing a null value with empty text. */ @Test public void testGenericEditorEmptyValue() { JTable table = new JTable(create1535TableModel()); assertNoStoppedEventOnEmptyValue(table, 0, throwOnEmpty, false); assertEquals(null, table.getValueAt(0, throwOnEmpty)); } //------------------- end testing #1535-swingx @Override @Before public void setUp() throws Exception { super.setUp(); } @BeforeClass public static void setUpClass() throws Exception { setLookAndFeel("Nimbus"); } @Test public void testSelectionInsertBefore() { ListSelectionModel model = new DefaultListSelectionModel(); model.setSelectionInterval(1, 1); model.insertIndexInterval(1, 1, true); assertTrue(model.isSelectedIndex(1)); assertTrue(model.isSelectedIndex(2)); } @Test public void testSelectionInsertAfter() { ListSelectionModel model = new DefaultListSelectionModel(); model.setSelectionInterval(1, 1); model.insertIndexInterval(1, 1, false); assertTrue(model.isSelectedIndex(1)); assertTrue(model.isSelectedIndex(2)); } /** * Core issue: editing not terminated on setModel. * * reported in sun forum: * http://forums.sun.com/thread.jspa?threadID=5422547&tstart=15 */ @Test public void testTerminateEditOnStructurChanged() { JTable table = new JTable(10, 2); table.setAutoCreateColumnsFromModel(false); table.editCellAt(0, 0); assertTrue("sanity: editing", table.isEditing()); ((AbstractTableModel) table.getModel()).fireTableStructureChanged(); assertEquals("editing must be terminated on setModel", false, table.isEditing()); } /** * Core issue: editing not terminated on setModel. * * reported in sun forum: * http://forums.sun.com/thread.jspa?threadID=5422547&tstart=15 */ @Test public void testTerminateEditOnSetModel() { JTable table = new JTable(10, 2); table.setAutoCreateColumnsFromModel(false); table.editCellAt(0, 0); assertTrue("sanity: editing", table.isEditing()); table.setModel(new DefaultTableModel(10, 2)); assertEquals("editing must be terminated on setModel", false, table.isEditing()); } //------------ Nimbus public void interactiveNimbusLabelBackground() throws Exception { final JLabel uiLabel = new JLabel("use ColorUIResource"); uiLabel.setOpaque(true); uiLabel.setBackground(new ColorUIResource(Color.WHITE)); final JLabel label = new JLabel("use plain Color"); label.setOpaque(true); label.setBackground(Color.WHITE); JComponent panel = new JPanel(); panel.setOpaque(true); panel.setBackground(Color.RED); panel.add(uiLabel); panel.add(label); JXFrame frame = wrapInFrame(panel, "ColorUIResource - Color"); Action action = new AbstractAction("setWhite") { @Override public void actionPerformed(ActionEvent e) { uiLabel.setBackground(new ColorUIResource(Color.WHITE)); label.setBackground(Color.WHITE); } }; addAction(frame, action); show(frame); } public void interactiveNimbusTableColor() { final JLabel label = new JLabel("background"); label.setName("Table.Background"); printComponentProperties(label); label.setOpaque(true); label.setBackground(UIManager.getColor("Table.background")); label.setBackground(new ColorUIResource(Color.WHITE)); // label.setBackground(Color.WHITE); final JLabel alternate = new JLabel("alternate"); alternate.setName(ALTERNATE_ROW_COLOR); alternate.setOpaque(true); alternate.setBackground(UIManager.getColor(ALTERNATE_ROW_COLOR)); // assertTrue(UIManager.getColor(ALTERNATE_ROW_COLOR) instanceof UIResource); JComponent panel = new JPanel(); panel.add(label); panel.add(alternate); JXFrame frame = wrapInFrame(panel, "normal - alternate"); Action print = new AbstractAction("print") { @Override public void actionPerformed(ActionEvent e) { printComponentProperties(label); printComponentProperties(alternate); } }; addAction(frame, print); show(frame); } private void printComponentProperties(JLabel label) { LOG.info("LAF/back/alternate " + UIManager.getLookAndFeel() + "\n class " + label.getClass() + "\n name " + label.getName() + "\n back " + label.getBackground() + "\n opaque " + label.isOpaque()); } /** * Issue #6594663: gridlines not settable */ public void interactiveNimbusProperties() { final JTable core = new JTable(new AncientSwingTeam()); final JXTable table = new JXTable(core.getModel()); JXFrame frame = wrapWithScrollingInFrame(core, table, "core <--> xtable: Gridlines?"); JLabel label = new JLabel("got something, just a label opaque ..."); label.setOpaque(true); frame.add(label, BorderLayout.SOUTH); Action action = new AbstractAction("toggle grid") { @Override public void actionPerformed(ActionEvent e) { if (core.getRowMargin() == 0) { core.setIntercellSpacing(new Dimension(1, 1)); core.setShowGrid(true); table.setShowGrid(true, true); } else { } } }; addAction(frame, action); final Action updateUI = new AbstractAction("updateUI") { @Override public void actionPerformed(ActionEvent e) { Color color = UIManager.getColor(ALTERNATE_ROW_COLOR); LOG.info("alternate uiresource? " + (color instanceof UIResource) + color); core.updateUI(); table.updateUI(); core.repaint(); table.repaint(); } }; addAction(frame, updateUI); // NimbusDefaults d; Action alternate = new AbstractAction("alternate back") { @Override public void actionPerformed(ActionEvent e) { Color color = UIManager.getColor(ALTERNATE_ROW_COLOR); LOG.info("alternate uiresource? " + (color instanceof UIResource) + color); UIManager.put(ALTERNATE_ROW_COLOR, new ColorUIResource(color != null ? color : Color.RED)); // UIManager.put(ALTERNATE_ROW_COLOR, Color.RED); updateUI.actionPerformed(null); } }; addAction(frame, alternate); show(frame); } /** * Core issues: throws NPE for interface column classes * Problem with Nimbus: installs LAF renderers, but doesn't for duplicate * for Icon? * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6830678 */ public void interactiveNimbusIconCore() { TableModel model = new DefaultTableModel(10, 2) { @Override public Class<?> getColumnClass(int columnIndex) { if (columnIndex == 0) { return Icon.class; } return super.getColumnClass(columnIndex); } }; model.setValueAt(XTestUtils.loadDefaultIcon(), 0, 0); JTable core = new JTable(model); showWithScrollingInFrame(core, "core: NPE with Icon?"); } public void interactiveNimbusIconX() { TableModel model = new DefaultTableModel(10, 2) { @Override public Class<?> getColumnClass(int columnIndex) { if (columnIndex == 0) { return Icon.class; } return super.getColumnClass(columnIndex); } }; model.setValueAt(XTestUtils.loadDefaultIcon(), 0, 0); JXTable table = new JXTable(model); showWithScrollingInFrame(table, "xtable: NPE with Icon?"); } //---------------- core sorting //------------------ testing core @Test public void testSetRowSorterChangeNotification() { JTable table = new JTable(new AncientSwingTeam()); PropertyChangeReport report = new PropertyChangeReport(); table.addPropertyChangeListener(report); table.setRowSorter(new TableRowSorter<TableModel>(table.getModel())); TestUtils.assertPropertyChangeEvent(report, "rowSorter", null, table.getRowSorter()); } /** * core issue: rowSorter replaced on setAutoCreateRowSorter even without change to flag. */ @Test public void testSetAutoCreateRowSorter() { JTable table = new JTable(); assertEquals("sanity: core table autoCreate off initially", false, table.getAutoCreateRowSorter()); assertNull("sanity: core rowSorter is not created", table.getRowSorter()); table.setAutoCreateRowSorter(true); assertNotNull("sanity: core rowSorter is created", table.getRowSorter()); TableModel model = new AncientSwingTeam(); table.setModel(model); RowSorter<?> sorter = table.getRowSorter(); table.setAutoCreateRowSorter(true); assertSame(sorter, table.getRowSorter()); } /** * If autoCreate if off, the control of updating a sorter's model is left * completely to the client. Corner case: if had been autoCreated, then * turned off, then set a new model, the autoCreated still points to the old * model. */ @Test public void testRowSorterModelUpdated() { JTable table = new JTable(); table.setAutoCreateRowSorter(true); table.setAutoCreateRowSorter(false); TableModel old = table.getModel(); table.setModel(new DefaultTableModel()); assertSame("tend to not extpect: rowsorter still old? ", old, table.getRowSorter().getModel()); assertSame("expect rowSorter's model updated?", table.getModel(), table.getRowSorter().getModel()); } /** * If autoCreate if off, the control of updating a sorter's model is left * completely to the client. Corner case: if had been autoCreated, then * turned off, then set a new model, the autoCreated still points to the old * model. */ @Test public void testRowSorterNulled() { JTable table = new JTable(); table.setAutoCreateRowSorter(true); table.setAutoCreateRowSorter(false); assertNull("expect auto-created rowsorter nulled?", table.getRowSorter()); } //----------------------- interactive /** * Issue #1489-swingx: terminateEditOnFocusLost leads to unexpected focus behaviour * in internalFrame. * * This is a core-issue which shows up in SwingX because JxTable has the property * set to true by default, while core has not. * */ public void interactiveInternalFrameTerminateEditOnFocusLost() { JDesktopPane jDesktopPane = new JDesktopPane(); JInternalFrame jInternalFrame = new JInternalFrame(); jDesktopPane.add(jInternalFrame); jInternalFrame.getContentPane().add(createPanel(true)); JXFrame embddingFrame = wrapInFrame(jDesktopPane, ""); try { jInternalFrame.setMaximum(true); } catch (PropertyVetoException ex) { } jInternalFrame.setVisible(true); show(embddingFrame, 400, 400); PropertyChangeListener pcl = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { Object oldValue = evt.getOldValue(); Object newValue = evt.getNewValue(); if (newValue == null || ! JFrame.class.equals(newValue.getClass())) { int i = 1; } System.out.println(evt.getPropertyName() + " from " + (oldValue == null ? null : oldValue.getClass().getCanonicalName() + oldValue.hashCode()) + " to " + (newValue == null ? null : newValue.getClass().getCanonicalName() + newValue.hashCode())); } }; KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener("permanentFocusOwner", pcl); } private static TableCellEditor createComboCellEditor() { return new DefaultCellEditor(new JComboBox( new Object[] {"Value1", "Value2", "Value3"} )); } private static TableModel createTableModel(String prefix) { String[] columns = new String[3]; for (int i = 0; i < 3; i++) { columns[i] = prefix + " " + (i + 1); } return new DefaultTableModel(columns, 3); } private static JPanel createPanel(boolean terminate) { JPanel panel = new JPanel(); JXTable jXTable = new JXTable(createTableModel("JXTable")); JTable jTable = new JTable(createTableModel("JTable")); // jXTable.setTerminateEditOnFocusLost(terminate); // jTable.putClientProperty("terminateEditOnFocusLost", terminate); jXTable.setDefaultEditor(Object.class, createComboCellEditor()); jTable.setDefaultEditor(Object.class, createComboCellEditor()); JScrollPane scrollPane1 = new JScrollPane(); JScrollPane scrollPane2 = new JScrollPane(); scrollPane1.setViewportView(jXTable); scrollPane2.setViewportView(jTable); panel.add(scrollPane1); panel.add(scrollPane2); jXTable.setPreferredScrollableViewportSize(new Dimension(300, 100)); jTable.setPreferredScrollableViewportSize(new Dimension(300, 100)); return panel; } /** * Core issue: terminateEditOnFocusLost weird behaviour if in InternalFrame * see: * http://forums.java.net/jive/thread.jspa?threadID=64281 * * To reproduce: edit first column (combo dropdown), click in textfield, click again * in first column: editing not started again. * * Same in JXTable, but there always: the terminateEditOnFocusLost is true by default. * * The thread is no longer available, reported as #1489 against JXTable. */ public void interactiveTerminateEditInInternalFrame() { JTable table = new JTable(new AncientSwingTeam()); table.putClientProperty("terminateEditOnFocusLost", true); JComboBox box = new JComboBox(new Object[]{"one", "two"}); table.getColumnModel().getColumn(0).setCellEditor(new DefaultCellEditor(box)); JComponent panel = Box.createVerticalBox(); panel.add(new JScrollPane(table)); panel.add(new JTextField("something to focus outside table")); JInternalFrame internal = new JInternalFrame(); internal.setContentPane(panel); JDesktopPane desktop = new JDesktopPane(); desktop.add(internal); JXFrame frame = wrapInFrame(desktop, "terminate editing when in internal frame (combo)"); internal.pack(); internal.setLocation(50, 30); internal.setVisible(true); show(frame, 600, 600); } public void interactiveAutoRowSorter() { // mimic a table coming out of a component factory, // which makes it autoCreate always final JTable table = new JTable(); table.setAutoCreateRowSorter(true); JXFrame frame = wrapWithScrollingInFrame(table, "autoCreateFalse keeps rowSorter"); Action toggle = new AbstractAction("new model") { @Override public void actionPerformed(ActionEvent e) { // mimic client code - special case table.setAutoCreateRowSorter(false); // silently allows error which fails at runtime when clicking header table.setModel(new DefaultTableModel(20, table.getColumnCount() +1 )); } }; addAction(frame, toggle); show(frame); } /** * Core Issue: the calculation of the repaint region after update is completely broken. * Nevertheless, the cell is updated correctly. Seems like someplace, the complete table * is marked as dirty? <p> * * Reason is that always the complete table is repainted if we have individual rowheights. * slight dirt in the update code: even if already painted, the would be dirty-region * for same rowheights is calculated and repainted (repaintManager folds them into one * repaint request, though). */ public void interactiveRepaintIndiRowHeight() { DefaultTableModel model = new DefaultTableModel(20, 3) { /** * Overridden to reach fire rowUpdated (instead of cellUpdated) */ @SuppressWarnings("unchecked") @Override public void setValueAt(Object aValue, int row, int column) { Vector<Object> rowVector = (Vector<Object>)dataVector.elementAt(row); rowVector.setElementAt(aValue, column); fireTableRowsUpdated(row, row); } }; final JTable table = new JTable(model) ; for (int i = 4; i < table.getRowCount(); i++) { table.setRowHeight(i, table.getRowHeight() + i *4); } JXFrame frame = showInFrame(table, "repaint after update"); Action action = new AbstractAction("update focused") { @Override public void actionPerformed(ActionEvent e) { int selected = table.getRowCount() / 2; if (selected < 0) return; table.setValueAt("XX" + table.getValueAt(selected, 0), selected, 0); } }; addAction(frame, action); } /** * Core issue #6539455: table not properly repainted on update (from model). * * Happens if the update is not triggered by an edit in the table itself. If * so, all is well (repaint called for all of the table). If not, repaint is * limited to the cell that has been updated (not even the whole row is * painted) - correct would be to repaint all rows between the old and new * row view index, inclusively. */ public void interactiveSortOnUpdateNotEditing() { final JTable table = new JTable(new AncientSwingTeam()); table.setAutoCreateRowSorter(true); ((TableRowSorter<?>) table.getRowSorter()).setSortsOnUpdates(true); table.getRowSorter().toggleSortOrder(0); JXFrame frame = showWithScrollingInFrame(table, "updates and repaint"); Action edit = new AbstractAction("update first visible") { @Override public void actionPerformed(ActionEvent e) { table.setValueAt("XXX" + table.getValueAt(0, 0), 0, 0); } }; addAction(frame, edit); } /** * Core issue #6539455: table not properly repainted on update (from model). * * Happens if the update is not triggered by an edit in the table itself. If * so, all is well (repaint called for all of the table). If not, repaint is * limited to the cell that has been updated (not even the whole row is * painted) - correct would be to repaint all rows between the old and new * row view index, inclusively. */ public void interactiveSortOnUpdateNotEditingHack() { final JTable table = new JTableRepaintOnUpdate(); table.setModel(new AncientSwingTeam()); table.setAutoCreateRowSorter(true); ((TableRowSorter<?>) table.getRowSorter()).setSortsOnUpdates(true); table.getRowSorter().toggleSortOrder(0); JXFrame frame = showWithScrollingInFrame(table, "updates and repaint (hacked)"); Action edit = new AbstractAction("update first visible") { @Override public void actionPerformed(ActionEvent e) { table.setValueAt("XXX" + table.getValueAt(0, 0), 0, 0); } }; addAction(frame, edit); } public static class JTableRepaintOnUpdate extends JTable { private UpdateHandler beforeSort; @Override public void sorterChanged(RowSorterEvent e) { super.sorterChanged(e); maybeRepaintOnSorterChanged(e); } private void beforeUpdate(TableModelEvent e) { if (!isSorted()) return; beforeSort = new UpdateHandler(e); } /** * */ private void afterUpdate() { beforeSort = null; } /** * */ private void maybeRepaintOnSorterChanged(RowSorterEvent e) { if (beforeSort == null) return; if ((e == null) || (e.getType() != RowSorterEvent.Type.SORTED)) return; UpdateHandler afterSort = new UpdateHandler(beforeSort); if (afterSort.allHidden(beforeSort)) { return; } else if (afterSort.complex(beforeSort)) { repaint(); return; } int firstRow = afterSort.getFirstCombined(beforeSort); int lastRow = afterSort.getLastCombined(beforeSort); Rectangle first = getCellRect(firstRow, 0, false); first.width = getWidth(); Rectangle last = getCellRect(lastRow, 0, false); repaint(first.union(last)); } private class UpdateHandler { private int firstModelRow; private int lastModelRow; private int viewRow; private boolean allHidden; public UpdateHandler(TableModelEvent e) { firstModelRow = e.getFirstRow(); lastModelRow = e.getLastRow(); convert(); } public UpdateHandler(UpdateHandler e) { firstModelRow = e.firstModelRow; lastModelRow = e.lastModelRow; convert(); } public boolean allHidden(UpdateHandler e) { return this.allHidden && e.allHidden; } public boolean complex(UpdateHandler e) { return (firstModelRow != lastModelRow); } public int getFirstCombined(UpdateHandler e) { if (allHidden) return e.viewRow; if (e.allHidden) return viewRow; return Math.min(viewRow, e.viewRow); } public int getLastCombined(UpdateHandler e) { if (allHidden || e.allHidden) return getRowCount() - 1; return Math.max(viewRow, e.viewRow); } /** * @param e */ private void convert() { // multiple updates if (firstModelRow != lastModelRow) { // don't bother too much - calculation not guaranteed to do anything good // just check if the all changed indices are hidden allHidden = true; for (int i = firstModelRow; i <= lastModelRow; i++) { if (convertRowIndexToView(i) >= 0) { allHidden = false; break; } } viewRow = -1; return; } // single update viewRow = convertRowIndexToView(firstModelRow); allHidden = viewRow < 0; } } /** * @return */ private boolean isSorted() { // JW: not good enough - need a way to decide if there are any sortkeys which // constitute a sort or any effective filters return getRowSorter() != null; } @Override public void tableChanged(TableModelEvent e) { if (isUpdate(e)) { beforeUpdate(e); } try { super.tableChanged(e); } finally { afterUpdate(); } } /** * Convenience method to detect dataChanged table event type. * * @param e the event to examine. * @return true if the event is of type dataChanged, false else. */ protected boolean isDataChanged(TableModelEvent e) { if (e == null) return false; return e.getType() == TableModelEvent.UPDATE && e.getFirstRow() == 0 && e.getLastRow() == Integer.MAX_VALUE; } /** * Convenience method to detect update table event type. * * @param e the event to examine. * @return true if the event is of type update and not dataChanged, false else. */ protected boolean isUpdate(TableModelEvent e) { if (isStructureChanged(e)) return false; return e.getType() == TableModelEvent.UPDATE && e.getLastRow() < Integer.MAX_VALUE; } /** * Convenience method to detect a structureChanged table event type. * @param e the event to examine. * @return true if the event is of type structureChanged or null, false else. */ protected boolean isStructureChanged(TableModelEvent e) { return e == null || e.getFirstRow() == TableModelEvent.HEADER_ROW; } } /** * Core Issue ??: must not sort if mouse in resize region */ public void interactiveSortOnResize() { JTable table = new JTable(new AncientSwingTeam()); table.setAutoCreateRowSorter(true); showWithScrollingInFrame(table, "must not sort in resize"); } /** * Core issue #6539455: table not properly repainted on update (from model). * * This setup differs from the examples (assuming we would add a second table, arggh) * above in that the sorter is shared as well as the model. In this case the repaint is * okay, as the second table receives the event from the sorter outside of its * tableChanged, that is ignoreSort is false. */ public void interactiveSharedRowSorter() { TableModel model = new AncientSwingTeam(); final JTable one = new JTable(); one.setDragEnabled(true); one.setAutoCreateRowSorter(true); one.setModel(model); JTable other = new JTable(model); other.setRowSorter(one.getRowSorter()); JXFrame frame = showWithScrollingInFrame(one, other, "shared model and rowsorter"); Action editFirst = new AbstractAction("prefix X on first") { @Override public void actionPerformed(ActionEvent e) { Object old = one.getValueAt(0, 0); one.setValueAt("X" + old, 0, 0); } }; addAction(frame, editFirst); Action toggleSortOnUpdate = new AbstractAction("toggleSortsOnUpdate") { @Override public void actionPerformed(ActionEvent e) { DefaultRowSorter<?, ?> sorter = (DefaultRowSorter<?, ?>) one.getRowSorter(); sorter.setSortsOnUpdates(!sorter.getSortsOnUpdates()); } }; addAction(frame, toggleSortOnUpdate); } //---------------- end core sorting public void testFormatDefaultRenderer() { DefaultTableModel model = new DefaultTableModel(1, 1) { @Override public Class<?> getColumnClass(int columnIndex) { return Date.class; } }; model.setValueAt("definitely not a date", 0, 0); JTable table = new JTable(model); TableCellRenderer renderer = table.getCellRenderer(0, 0); table.prepareRenderer(renderer , 0, 0); } /** * test that all transferFocus methods stop edits and * fire one stopped event. * * Hmm .. unexpected: we get two stopped? */ public void testStopEditingCoreTable() { JTable table = new JTable(10, 2); table.editCellAt(0, 0); // sanity assertTrue(table.isEditing()); CellEditorReport report = new CellEditorReport(); table.getCellEditor().addCellEditorListener(report); // sanity assertFalse(report.hasEvents()); table.getCellEditor().stopCellEditing(); assertFalse("table must not be editing", table.isEditing()); assertEquals("", 1, report.getEventCount()); assertEquals("", 1, report.getStoppedEventCount()); } /** * test that all transferFocus methods stop edits and * fire one stopped event. * * Hmm .. unexpected: we get two stopped? * Here: let the table prepare the editor (but not install) * * in this case the generic.stopCellEditing calls super * twice! */ public void testStopEditingTableGenericPrepared() { JTable table = new JTable(10, 2); TableCellEditor direct = table.getDefaultEditor(Object.class); CellEditorReport report = new CellEditorReport(); direct.addCellEditorListener(report); TableCellEditor editor = table.getCellEditor(0, 0); // sanity: assertSame(direct, editor); assertFalse(report.hasEvents()); table.prepareEditor(editor, 0, 0); // sanity: prepare did not fire .. assertFalse(report.hasEvents()); editor.stopCellEditing(); assertEquals("prepared - must have fired exactly one event", 1, report.getEventCount()); assertEquals("", 1, report.getStoppedEventCount()); } /** * test that all transferFocus methods stop edits and * fire one stopped event. * * Hmm .. unexpected: we get two stopped? * Here: get the table's editor and prepare manually. * this test passes ... what is in the prepare which * fires? * In this case it calls super.stop once only ... * */ public void testStopEditingTableGenericGetComp() { JTable table = new JTable(10, 2); TableCellEditor editor = table.getCellEditor(0, 0); CellEditorReport report = new CellEditorReport(); editor.addCellEditorListener(report); editor.getTableCellEditorComponent(table, "something", false, 0, 0); editor.stopCellEditing(); assertEquals("", 1, report.getEventCount()); assertEquals("", 1, report.getStoppedEventCount()); } /** * test that all transferFocus methods stop edits and * fire one stopped event. * * Core issue: * Table's generic editor must not return a null component. */ public void testTableGenericEditorNullTable() { JTable table = new JTable(10, 2); TableCellEditor editor = table.getCellEditor(0, 0); Component comp = editor.getTableCellEditorComponent( null, "something", false, 0, 0); assertNotNull("editor must not return null component", comp); } /** * test that all transferFocus methods stop edits and * fire one stopped event. * * Hmm .. unexpected: we get two stopped? * * Here: Must not throw NPE if calling stopCellEditing without previous * getXXComponent. */ public void testTableGenericEditorIsolatedNPE() { JTable table = new JTable(10, 2); TableCellEditor editor = table.getCellEditor(0, 0); editor.stopCellEditing(); } /** * test that all transferFocus methods stop edits and * fire one stopped event. * * Hmm .. unexpected: we get two stopped? Test * DefaultCellEditor - okay. */ public void testStopEditingDefaultCellEditor() { TableCellEditor editor = new DefaultCellEditor(new JTextField()); CellEditorReport report = new CellEditorReport(); editor.addCellEditorListener(report); editor.stopCellEditing(); assertEquals("", 1, report.getEventCount()); assertEquals("", 1, report.getStoppedEventCount()); } /** * core issue: JTable cannot cope with null selection background. * */ public void testNullGridColor() { JTable table = new JTable(); // assertNotNull(UIManager.getColor("Table.gridColor")); assertNotNull(table.getGridColor()); assertEquals(UIManager.getColor("Table.gridColor"), table.getGridColor()); table.setGridColor(null); } /** * core issue: JTable cannot cope with null selection background. * */ public void testNullSelectionBackground() { JTable table = new JTable(); assertNotNull(table.getSelectionBackground()); table.setSelectionBackground(null); } /** * core issue: JTable cannot cope with null selection background. * */ public void testNullSelectionForeground() { JTable table = new JTable(); table.setSelectionForeground(null); } /** * Issue #282-swingx: compare disabled appearance of * collection views. * */ public void testDisabledRenderer() { JList list = new JList(new Object[] {"one", "two"}); list.setEnabled(false); // sanity assertFalse(list.isEnabled()); Component comp = list.getCellRenderer().getListCellRendererComponent(list, "some", 0, false, false); assertEquals(list.isEnabled(), comp.isEnabled()); JTable table = new JTable(10, 2); table.setEnabled(false); // sanity assertFalse(table.isEnabled()); comp = table.prepareRenderer(table.getCellRenderer(0, 0), 0, 0); assertEquals(table.isEnabled(), comp.isEnabled()); } /** * Characterization method: table.addColumn and invalid modelIndex. * * Doesn't blow up because DefaultTableModel.getColumnName is lenient, * that is has no precondition on the index. * */ public void testAddColumn() { JTable table = new JTable(0, 0); table.addColumn(new TableColumn(1)); } public void interactiveAutoStartsEdit() { final String autoKey = "JTable.autoStartsEdit"; final JTable table = new JTable(new AncientSwingTeam()); table.putClientProperty(autoKey, Boolean.TRUE); final String autoStartName = "toggle AutoStart "; boolean isAuto = Boolean.TRUE.equals(table.getClientProperty(autoKey)); Action autoStart = new AbstractActionExt(autoStartName + isAuto) { public void actionPerformed(ActionEvent e) { boolean isAuto = Boolean.TRUE.equals(table.getClientProperty(autoKey)); table.putClientProperty(autoKey, isAuto ? Boolean.FALSE : Boolean.TRUE); setName(autoStartName + !isAuto); } }; JXFrame frame = wrapWithScrollingInFrame(table, "autostart-edit behaviour"); addAction(frame, autoStart); show(frame); } /** * Core Issue ??: standalone table header throws NPE on mouse * events. * * Base reason is that the ui assume a not-null table at varying * places in their code. * * Reason is an unsafe implementation of viewIndexForColumn. Unconditionally * queries the table for index conversion. */ public void interactiveNPEStandaloneHeader() { JXTable table = new JXTable(new AncientSwingTeam()); JXTableHeader header = new JXTableHeader(table.getColumnModel()); JXFrame frame = showWithScrollingInFrame(header, "Standalone header: NPE on mouse gestures"); addMessage(frame, "exact place/gesture is LAF dependent. Base error is to assume header.getTable() != null"); } /** * * * */ public void interactiveToolTipOverEmptyCell() { final DefaultTableModel model = new DefaultTableModel(50, 2); model.setValueAt("not empty", 0, 0); final JTable table = new JTable(model) { @Override public String getToolTipText(MouseEvent event) { int column = columnAtPoint(event.getPoint()); if (column == 0) { return "first column"; } return null; } @Override public Point getToolTipLocation(MouseEvent event) { int column = columnAtPoint(event.getPoint()); int row = rowAtPoint(event.getPoint()); Rectangle cellRect = getCellRect(row, column, false); if (!getComponentOrientation().isLeftToRight()) { cellRect.translate(cellRect.width, 0); } // PENDING JW: otherwise we get a small (borders only) tooltip for null // core issue? yeah ... probably // return getValueAt(row, column) == null ? null : cellRect.getLocation(); return cellRect.getLocation(); } }; JXFrame frame = wrapWithScrollingInFrame(table, "Tooltip over empty"); show(frame); } /** * forum: table does not scroll after setRowSelectionInterval? * * */ public void interactiveAutoScroll() { final DefaultTableModel model = new DefaultTableModel(50, 2); final JTable table = new JTable(model); table.setAutoscrolls(true); Action action = new AbstractAction("select last row: scrolling?") { public void actionPerformed(ActionEvent e) { int selected = table.getRowCount() - 1; if (selected >= 0) { table.setRowSelectionInterval(selected, selected); } } }; JXFrame frame = wrapWithScrollingInFrame(table, "insert at selection"); addAction(frame, action); frame.setVisible(true); } /** * Issue #272-swingx: inserted row is selected. * Not a bug: documented behaviour of DefaultListSelectionModel. * */ public void interactiveInsertAboveSelection() { final DefaultTableModel model = new DefaultTableModel(10, 2); final JTable table = new JTable(model); Action action = new AbstractAction("insertRow") { public void actionPerformed(ActionEvent e) { int selected = table.getSelectedRow(); if (selected < 0) return; model.insertRow(selected, new Object[2]); } }; JXFrame frame = wrapWithScrollingInFrame(table, "insert at selection"); addAction(frame, action); frame.setVisible(true); } public void interactiveLeadAnchor() { final JTable table = new JTable(10, 3) { @Override public void tableChanged(TableModelEvent e) { super.tableChanged(e); if (isDataChanged(e) || isStructureChanged(e)) { focusFirstCell(); } } private void focusFirstCell() { if (getColumnCount() > 0) { getColumnModel().getSelectionModel() .removeSelectionInterval(0, 0); } if (getRowCount() > 0) { getSelectionModel().removeSelectionInterval(0, 0); } } private boolean isDataChanged(TableModelEvent e) { return e.getType() == TableModelEvent.UPDATE && e.getFirstRow() == 0 && e.getLastRow() == Integer.MAX_VALUE; } private boolean isStructureChanged(TableModelEvent e) { return e == null || e.getFirstRow() == TableModelEvent.HEADER_ROW; } }; JXFrame frame = wrapWithScrollingInFrame(table, "auto-lead - force in table subclass"); Action toggleAction = new AbstractAction("Toggle TableModel") { public void actionPerformed(ActionEvent e) { if (table.getRowCount() > 0) { table.setModel(new DefaultTableModel()); } else { table.setModel(new DefaultTableModel(10, 3)); } } }; addAction(frame, toggleAction); frame.setVisible(true); } //---------------------- unit tests /** * Issue #4614616: renderer lookup broken for interface types. * */ public void testNPERendererForInterface() { DefaultTableModel model = new DefaultTableModel(10, 2) { @Override public Class<?> getColumnClass(int columnIndex) { return Comparable.class; } }; JTable table = new JTable(model); table.prepareRenderer(table.getCellRenderer(0, 0), 0, 0); } /** * Issue #4614616: editor lookup broken for interface types. * */ public void testNPEEditorForInterface() { DefaultTableModel model = new DefaultTableModel(10, 2) { @Override public Class<?> getColumnClass(int columnIndex) { return Comparable.class; } }; JTable table = new JTable(model); table.prepareEditor(table.getCellEditor(0, 0), 0, 0); } /** * isCellEditable is doc'ed as: if false, setValueAt * will have no effect. * * */ public void testSetValueDoNothing() { JTable table = new JTable(10, 3) { @Override public boolean isCellEditable(int row, int column) { return false; } }; Object value = table.getValueAt(0, 0); // sanity... assertFalse(table.isCellEditable(0, 0)); table.setValueAt("wrong", 0, 0); assertEquals("value must not be changed", value, table.getValueAt(0, 0)); } /** * Issue #272-swingx: inserted row is selected. * Not a bug: documented behaviour of DefaultListSelectionModel. * */ public void testInsertBeforeSelected() { DefaultTableModel model = new DefaultTableModel(10, 2); JTable table = new JTable(model); table.setRowSelectionInterval(3, 3); model.insertRow(3, new Object[2]); int[] selected = table.getSelectedRows(); assertEquals(1, selected.length); } /** * Issue #272-swingx: inserted row is selected. * Not a bug: documented behaviour of DefaultListSelectionModel. */ public void testInsertBeforeSelectedSM() { DefaultListSelectionModel model = new DefaultListSelectionModel(); model.setSelectionInterval(3, 3); model.insertIndexInterval(3, 1, true); int max = model.getMaxSelectionIndex(); int min = model.getMinSelectionIndex(); assertEquals(max, min); } /** * test contract: getColumn(int) throws ArrayIndexOutofBounds with * invalid column index. * * Subtle autoboxing issue: * JTable has convenience method getColumn(Object) to access by * identifier, but doesn't have delegate method to columnModel.getColumn(int) * Clients assuming the existence of a direct delegate no longer get a * compile-time error message in 1.5 due to autoboxing. * Furthermore, the runtime exception is unexpected (IllegalArgument * instead of AIOOB). * */ public void testTableColumnOffRange() { JTable table = new JTable(2, 1); try { table.getColumn(1); fail("accessing invalid column index must throw ArrayIndexOutofBoundExc"); } catch (ArrayIndexOutOfBoundsException e) { // do nothing: contracted runtime exception } catch (Exception e) { fail("unexpected exception: " + e + "\n" + "accessing invalid column index must throw ArrayIndexOutofBoundExc"); } } public void testTableRowAtNegativePoint() { JTable treeTable = new JTable(1, 4); int negativeYRowHeight = - treeTable.getRowHeight(); int negativeYRowHeightPlusOne = negativeYRowHeight + 1; int negativeYMinimal = -1; // just outside of negative row before first row assertEquals("negative y location rowheight " + negativeYRowHeight + " must return row -1", -1, treeTable.rowAtPoint(new Point(-1, negativeYRowHeight))); // just inside of negative row before first row assertEquals("negative y location " + negativeYRowHeightPlusOne +" must return row -1", -1, treeTable.rowAtPoint(new Point(-1, negativeYRowHeightPlusOne))); // just outside of first row assertEquals("minimal negative y location must return row -1", -1, treeTable.rowAtPoint(new Point(-1, negativeYMinimal))); } public void testLeadSelectionAfterStructureChanged() { DefaultTableModel model = new DefaultTableModel(10, 2) { @Override public void fireTableRowsDeleted(int firstRow, int lastRow) { fireTableStructureChanged(); } }; for (int i = 0; i < model.getRowCount(); i++) { model.setValueAt(i, i, 0); } JTable table = new JTable(model); int rowIndex = table.getRowCount() - 1; table.addRowSelectionInterval(rowIndex, rowIndex); model.removeRow(rowIndex); // JW: this was pre-1.5u5 (?), changed (1.5u6?) to return - 1 // assertEquals("", rowIndex, table.getSelectionModel().getAnchorSelectionIndex()); assertEquals("", -1, table.getSelectionModel().getAnchorSelectionIndex()); ListSelectionReport report = new ListSelectionReport(); table.getSelectionModel().addListSelectionListener(report); } /** * as of jdk1.5u6 the lead/anchor is no longer automatically set. * before (last code I saw is jdk1.5u4) - tableChanged would call * checkLeadAnchor after structureChanged. * CheckLeadAnchor did set the lead/anchor * to the first row if count > 0. * * Always: BasicTableUI repaints the lead-cell in focusGained. * * Now: need to explicitly set _both_ anchor and lead to >= 0 * need to set anchor first. Need to do so for both row/column selection model. * @throws InvocationTargetException * @throws InterruptedException * */ public void testInitialLeadAnchor() throws InterruptedException, InvocationTargetException { // This test will not work in a headless configuration. if (GraphicsEnvironment.isHeadless()) { LOG.fine("cannot run testLeadAnchorOnFocusGained - headless environment"); return; } DefaultTableModel model = new DefaultTableModel(10, 2) { @Override public void fireTableRowsDeleted(int firstRow, int lastRow) { fireTableStructureChanged(); } }; for (int i = 0; i < model.getRowCount(); i++) { model.setValueAt(i, i, 0); } final JTable table = new JTable(model); JFrame frame = new JFrame("anchor on focus"); frame.add(new JScrollPane(table)); frame.setVisible(true); // JW: need to explicitly set _both_ anchor and lead to >= 0 // need to set anchor first // table.getSelectionModel().setAnchorSelectionIndex(0); // table.getSelectionModel().setLeadSelectionIndex(0); // table.getColumnModel().getSelectionModel().setAnchorSelectionIndex(0); // table.getColumnModel().getSelectionModel().setLeadSelectionIndex(0); table.requestFocus(); SwingUtilities.invokeAndWait(new Runnable() { public void run() { assertTrue("table is focused ", table.hasFocus()); assertEquals("anchor must be 0", 0, table.getSelectionModel().getAnchorSelectionIndex()); assertEquals("lead must be 0", 0, table.getSelectionModel().getLeadSelectionIndex()); } }); } /** * as of jdk1.5u6 the lead/anchor is no longer automatically set. * before (last code I saw is jdk1.5u4) - tableChanged would call * checkLeadAnchor after structureChanged. CheckLeadAnchor did set the lead/anchor * to the first row if count > 0. * * Always: BasicTableUI repaints the lead-cell in focusGained. * * Now: need to explicitly set _both_ anchor and lead to >= 0 * need to set anchor first. Need to do so for both row/column selection model. * */ public void testLeadAnchorAfterStructureChanged() { final JTable table = new JTable(10, 2); // JW: need to explicitly set _both_ anchor and lead to >= 0 // need to set anchor first table.getSelectionModel().setAnchorSelectionIndex(0); table.getSelectionModel().setLeadSelectionIndex(0); table.getColumnModel().getSelectionModel().setAnchorSelectionIndex(0); table.getColumnModel().getSelectionModel().setLeadSelectionIndex(0); // sanity... assertEquals("anchor must be 0", 0, table.getSelectionModel().getAnchorSelectionIndex()); assertEquals("lead must be 0", 0, table.getSelectionModel().getLeadSelectionIndex()); table.setModel(new DefaultTableModel(20, 3)); // regression: lead/anchor unconditionally reset to -1 assertEquals("anchor must be 0", 0, table.getSelectionModel().getAnchorSelectionIndex()); assertEquals("lead must be 0", 0, table.getSelectionModel().getLeadSelectionIndex()); } //------------- from incubator ... PENDING: cleanup/remove }