/*
* $Id$
*
* Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
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.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.CellEditor;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableModel;
import org.jdesktop.swingx.action.BoundAction;
import org.jdesktop.swingx.decorator.AbstractHighlighter;
import org.jdesktop.swingx.decorator.ColorHighlighter;
import org.jdesktop.swingx.decorator.ComponentAdapter;
import org.jdesktop.swingx.decorator.HighlightPredicate;
import org.jdesktop.swingx.decorator.HighlighterFactory;
import org.jdesktop.swingx.table.TableColumnExt;
import org.jdesktop.test.AncientSwingTeam;
import org.jdesktop.test.CellEditorReport;
import org.jdesktop.test.PropertyChangeReport;
import org.jdesktop.test.SerializableSupport;
/**
* Test to exposed known issues of <code>JXTable</code>.
*
* Ideally, there would be at least one failing test method per open
* Issue in the issue tracker. Plus additional failing test methods for
* not fully specified or not yet decided upon features/behaviour.
*
* @author Jeanette Winzenburg
*/
public class JXTableIssues extends InteractiveTestCase {
private static final Logger LOG = Logger.getLogger(JXTableIssues.class
.getName());
public static void main(String args[]) {
JXTableIssues test = new JXTableIssues();
setSystemLF(true);
try {
// test.runInteractiveTests();
test.runInteractiveTests("interactive.*Scroll.*");
// test.runInteractiveTests("interactive.*Render.*");
// test.runInteractiveTests("interactive.*ExtendOnRemoveAdd.*");
// test.runInteractiveTests("interactive.*Extend.*");
// test.runInteractiveTests("interactive.*Repaint.*");
} catch (Exception e) {
System.err.println("exception when executing interactive tests:");
e.printStackTrace();
}
}
/**
* Quick check for a forum report:
* getValueAt called on init for each cell (even the invisible).
*
* Looks okay: getValueAt called for visible cells only.
* @throws Exception
*
*/
public void testGetValueOnInit() throws Exception {
// This test will not work in a headless configuration.
if (GraphicsEnvironment.isHeadless()) {
LOG.fine("cannot run test - headless environment");
return;
}
final List<Integer> set = new ArrayList<Integer>();
final JXTable table = new JXTable() {
@Override
public Object getValueAt(int row, int column) {
set.add(row);
return super.getValueAt(row, column);
}
};
showWithScrollingInFrame(table, "");
table.setModel(new DefaultTableModel(100, 5));
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
// failing - one row off?
assertEquals(table.getColumnCount() * table.getVisibleRowCount(), set.size());
}
});
}
/**
* Issue #4614616: editor lookup broken for interface types.
* With editors (vs. renderers), the solution is not obvious -
* interfaces can't be instantiated. As a consequence, the
* GenericEditor can't cope (returns null as component which
* it must not but that's another issue).
*
*/
public void testNPEEditorForInterface() {
DefaultTableModel model = new DefaultTableModel(10, 2) {
@Override
public Class<?> getColumnClass(int columnIndex) {
return Comparable.class;
}
};
JXTable table = new JXTable(model);
table.prepareEditor(table.getCellEditor(0, 0), 0, 0);
}
/**
* Not defined: what should happen if the edited column is hidden?
* For sure, editing must be terminated - but canceled or stopped?
*
* Here we test if the table is not editing after editable property
* of the currently edited column is changed to false.
*/
public void testTableNotEditingOnColumnVisibleChange() {
JXTable table = new JXTable(10, 2);
TableColumnExt columnExt = table.getColumnExt(0);
table.editCellAt(0, 0);
// sanity
assertTrue(table.isEditing());
assertEquals(0, table.getEditingColumn());
columnExt.setVisible(false);
assertFalse("table must have terminated edit",table.isEditing());
fail("forcing a fail - cancel editing is a side-effect of removal notification");
}
/**
* Issue #359-swing: find suitable rowHeight.
*
* Text selection in textfield has row of metrics.getHeight.
* Suitable rowHeight should should take border into account:
* for a textfield that's the metrics height plus 2.
*
* PENDING: this passes locally, fails on server
*/
public void testRowHeightFontMetrics() {
JXTable table = new JXTable(10, 2);
TableCellEditor editor = table.getCellEditor(1, 1);
Component comp = table.prepareEditor(editor, 1, 1);
assertEquals(comp.getPreferredSize().height, table.getRowHeight());
}
/**
* a quick sanity test: reporting okay?.
* (doesn't belong here, should test the tools
* somewhere else)
*
*/
public void testCellEditorFired() {
JXTable table = new JXTable(10, 2);
table.editCellAt(0, 0);
CellEditorReport report = new CellEditorReport();
TableCellEditor editor = table.getCellEditor();
editor.addCellEditorListener(report);
editor.cancelCellEditing();
assertEquals("total count must be equals to canceled",
report.getCanceledEventCount(), report.getEventCount());
assertEquals("editor must have fired canceled", 1, report.getCanceledEventCount());
assertEquals("editor must not have fired stopped", 0, report.getStoppedEventCount());
report.clear();
assertEquals("canceled cleared", 0, report.getCanceledEventCount());
assertEquals("total cleared", 0, report.getStoppedEventCount());
// same cell, same editor
table.editCellAt(0, 0);
editor.stopCellEditing();
assertEquals("total count must be equals to stopped",
report.getStoppedEventCount(), report.getEventCount());
assertEquals("editor must not have fired canceled", 0, report.getCanceledEventCount());
// JW: surprising... it really fires twice?
assertEquals("editor must have fired stopped", 1, report.getStoppedEventCount());
}
/**
* Issue #349-swingx: table not serializable
*
* Part of the problem is in TableRolloverController.
*
*/
public void testSerializationRollover() {
JXTable table = new JXTable();
SerializableSupport.serialize(table);
}
/**
* Issue #349-swingx: table not serializable
*
* Part of it seems to be in BoundAction.
*
*/
public void testSerializationRolloverFalse() {
JXTable table = new JXTable();
table.setRolloverEnabled(false);
ActionMap actionMap = table.getActionMap();
Object[] keys = actionMap.keys();
for (int i = 0; i < keys.length; i++) {
if (actionMap.get(keys[i]) instanceof BoundAction) {
actionMap.remove(keys[i]);
}
}
SerializableSupport.serialize(table);
}
/**
* Issue??-swingx: turn off scrollbar doesn't work if the
* table was initially in autoResizeOff mode.
*
* Problem with state management.
*
*/
public void testHorizontalScrollEnabled() {
JXTable table = new JXTable();
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
assertEquals("horizontalScroll must be on", true, table.isHorizontalScrollEnabled());
table.setHorizontalScrollEnabled(false);
assertEquals("horizontalScroll must be off", false, table.isHorizontalScrollEnabled());
}
/**
* we have a slight inconsistency in event values: setting the
* client property to null means "false" but the event fired
* has the newValue null.
*
* The way out is to _not_ set the client prop manually, always go
* through the property setter.
*/
public void testClientPropertyNull() {
JXTable table = new JXTable();
// sanity assert: setting client property set's property
PropertyChangeReport report = new PropertyChangeReport();
table.addPropertyChangeListener(report);
table.putClientProperty("terminateEditOnFocusLost", null);
assertFalse(table.isTerminateEditOnFocusLost());
assertEquals(1, report.getEventCount());
assertEquals(1, report.getEventCount("terminateEditOnFocusLost"));
assertEquals(false, report.getLastNewValue("terminateEditOnFocusLost"));
}
//----------------- interactive
/**
* Issue: scrollpane not sized to fit visibleRowCount exactly
*
* PENDING JW: report!
*/
public void interactiveVisibleRowCount() {
JXTable table = new JXTable(new AncientSwingTeam());
table.setTableHeader(null);
table.setVisibleRowCount(table.getRowCount());
table.setHighlighters(HighlighterFactory.createAlternateStriping(2));
showWithScrollingInFrame(table, "visible rowCount jxtable - must fit");
assertEquals(table.getRowCount() * table.getRowHeight(), table.getPreferredScrollableViewportSize().height);
}
public void interactiveVisibleRowCountCore() {
JTable table = new JTable(new AncientSwingTeam()) {
/**
* @inherited <p>
*/
@Override
public Dimension getPreferredScrollableViewportSize() {
Dimension dim = super.getPreferredScrollableViewportSize();
dim.height = getRowHeight() * getRowCount();
return dim;
}
};
table.setTableHeader(null);
showWithScrollingInFrame(table, "core ");
}
/**
* Use-case: null always at bottom, independent of sort order.
* Unsupported by core default. Can SwingX? No ...
*/
public void interactiveSortIgnoreNull() {
TableModel model = new DefaultTableModel(10, 3);
for (int i = 0; i < 3; i++) {
model.setValueAt(i+1, i, 0);
}
JTable table = new JTable();
table.setAutoCreateRowSorter(true);
table.setModel(model);
JXTable xtable = new JXTable(model);
showWithScrollingInFrame(xtable, table, "JXTable <-> JTable: ignore null in sort");
}
/**
* After moving to Mustang, this might be vieweda as a bug ;-)
* Was: Unconditional repaint on cell update (through the default
* identify filter).
* Is: only the updated cell is repainted, unrelated (from model perspective)
* cells (like others in row) are not repainted.
*/
public void interactiveRepaintOnUpdateSingleCell() {
JXTable table = new JXTable(10, 5);
// highlight complete row if first cell starts with a
HighlightPredicate predicate = new HighlightPredicate() {
public boolean isHighlighted(Component renderer,
ComponentAdapter adapter) {
return adapter.getString(0).startsWith("a");
}
};
ColorHighlighter highlighter = new ColorHighlighter(predicate, Color.MAGENTA, null, Color.MAGENTA, null);
table.addHighlighter(highlighter);
JXTable other = new JXTable(table.getModel());
other.addHighlighter(highlighter);
JXFrame frame = wrapWithScrollingInFrame(table, other, "repaint on update in first");
addMessage(frame, "edit first cell in left table (start with/out a)");
show(frame);
}
/**
* Issue #610-swingx: Cancel editing via Escape doesn't fire editingCanceled.
*
* Reported against ComboBoxCellEditor in the autoComplete package, but actually
* a JTable _never_ fires a editingCanceled for any editor. Reason is that the
* cancel Action registered in BasisTableUI incorrectly calls table.removeEditor
* instead of getCelleditor.cancelEditing.
*
* Quick hack around that: JXTable registers its own cancel action.
*
* Still open: esc when popup is open will only close the popup, not cancel the
* edit (which requires a second esc).
*
*/
public void interactiveEditingCanceledOnEscape() {
final JTextField field = new JTextField();
JXTable xTable = new JXTable(10, 3);
CellEditor editor = xTable.getDefaultEditor(Object.class);
CellEditorListener l = new CellEditorListener() {
public void editingCanceled(ChangeEvent e) {
field.setText("canceled");
}
public void editingStopped(ChangeEvent e) {
field.setText("stopped");
}};
editor.addCellEditorListener(l);
JTable table = new JTable(xTable.getModel());
CellEditor coreEditor = table.getDefaultEditor(Object.class);
coreEditor.addCellEditorListener(l);
JXFrame frame = wrapWithScrollingInFrame(xTable, table, "#610-swingx: escape doesn't fire editing canceled");
frame.add(field, BorderLayout.SOUTH);
frame.setVisible(true);
}
/**
* Match highlighter fails to display correctly if column-based highlighter alters background
* color.
*/
public void interactiveColumnHighlightingWithSearch() {
JXTable table = new JXTable(new AncientSwingTeam());
table.getColumnExt("Favorite Color").setHighlighters(new AbstractHighlighter() {
@Override
protected Component doHighlight(Component renderer, ComponentAdapter adapter) {
Color color = (Color) adapter.getValue();
if (renderer instanceof JComponent) {
((JComponent) renderer).setBorder(BorderFactory.createLineBorder(color));
}
return renderer;
}
});
table.getColumnExt(0).addHighlighter(
new ColorHighlighter(new HighlightPredicate() {
public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
return adapter.getValue().toString().contains("e");
}
}, Color.GREEN, null));
JFrame frame = wrapWithScrollingInFrame(table, "Column Highlighter with Search Test");
table.putClientProperty(JXTable.MATCH_HIGHLIGHTER, true);
//should highlight Jeff with Yellow
table.getSearchable().search("e", 3);
frame.setVisible(true);
}
//-------------------- adapted jesse wilson: #223
/**
* Enhancement: modifying (= filtering by resetting the content) should keep
* selection
*
*/
public void testModifyTableContentAndSelection() {
CompareTableBehaviour compare = new CompareTableBehaviour(new Object[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" });
compare.table.getSelectionModel().setSelectionInterval(2, 5);
Object[] selectedObjects = new Object[] { "C", "D", "E", "F" };
assertSelection(compare.tableModel, compare.table.getSelectionModel(), selectedObjects);
compare.tableModel.setContents(new Object[] { "B", "C", "D", "F", "G", "H" });
Object[] selectedObjectsAfterModify = (new Object[] { "C", "D", "F" });
assertSelection(compare.tableModel, compare.table.getSelectionModel(), selectedObjectsAfterModify);
}
/**
* Enhancement: modifying (= filtering by resetting the content) should keep
* selection
*/
public void testModifyXTableContentAndSelection() {
CompareTableBehaviour compare = new CompareTableBehaviour(new Object[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" });
compare.xTable.getSelectionModel().setSelectionInterval(2, 5);
Object[] selectedObjects = new Object[] { "C", "D", "E", "F" };
assertSelection(compare.tableModel, compare.xTable.getSelectionModel(), selectedObjects);
compare.tableModel.setContents(new Object[] { "B", "C", "D", "F", "G", "H" });
Object[] selectedObjectsAfterModify = (new Object[] { "C", "D", "F" });
assertSelection(compare.tableModel, compare.xTable.getSelectionModel(), selectedObjectsAfterModify);
}
private void assertSelection(TableModel tableModel, ListSelectionModel selectionModel, Object[] expected) {
List<Object> selected = new ArrayList<Object>();
for(int r = 0; r < tableModel.getRowCount(); r++) {
if(selectionModel.isSelectedIndex(r)) selected.add(tableModel.getValueAt(r, 0));
}
List<?> expectedList = Arrays.asList(expected);
assertEquals("selected Objects must be as expected", expectedList, selected);
}
public static class CompareTableBehaviour {
public ReallySimpleTableModel tableModel;
public JTable table;
public JXTable xTable;
public CompareTableBehaviour(Object[] model) {
tableModel = new ReallySimpleTableModel();
tableModel.setContents(model);
table = new JTable(tableModel);
xTable = new JXTable(tableModel);
table.getColumnModel().getColumn(0).setHeaderValue("JTable");
xTable.getColumnModel().getColumn(0).setHeaderValue("JXTable");
}
};
/**
* A one column table model where all the data is in an Object[] array.
*/
static class ReallySimpleTableModel extends AbstractTableModel {
private List<Object> contents = new ArrayList<Object>();
public void setContents(List<Object> contents) {
this.contents.clear();
this.contents.addAll(contents);
fireTableDataChanged();
}
public void setContents(Object[] contents) {
setContents(Arrays.asList(contents));
}
public void removeRow(int row) {
contents.remove(row);
fireTableRowsDeleted(row, row);
}
public int getRowCount() {
return contents.size();
}
public int getColumnCount() {
return 1;
}
public Object getValueAt(int row, int column) {
if(column != 0) throw new IllegalArgumentException();
return contents.get(row);
}
}
//--------------------
/**
* returns a tableModel with count rows filled with
* ascending integers in first column
* starting from startRow.
* @param startRow the value of the first row
* @param rowCount the number of rows
* @return
*/
@SuppressWarnings("unused")
private DefaultTableModel createAscendingModel(int startRow, final int rowCount,
final int columnCount, boolean fillLast) {
DefaultTableModel model = new DefaultTableModel(rowCount, columnCount) {
@Override
public Class<?> getColumnClass(int column) {
Object value = rowCount > 0 ? getValueAt(0, column) : null;
return value != null ? value.getClass() : super.getColumnClass(column);
}
};
int filledColumn = fillLast ? columnCount - 1 : 0;
for (int i = 0; i < model.getRowCount(); i++) {
model.setValueAt(new Integer(startRow++), i, filledColumn);
}
return model;
}
@SuppressWarnings("unused")
private DefaultTableModel createAscendingModel(int startRow, int count) {
DefaultTableModel model = new DefaultTableModel(count, 5);
for (int i = 0; i < model.getRowCount(); i++) {
model.setValueAt(new Integer(startRow++), i, 0);
}
return model;
}
}