swingx-master/swingx-core/src/test/java/org/jdesktop/swingx/JXTableIssues.java
/*
 * $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;
    }
    

}
