swingx-master/swingx-core/src/test/java/org/jdesktop/swingx/JXTreeTableIssues.java
/*
 * $Id$
 * 
 * Copyright 2004 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.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import java.util.Vector;
import java.util.logging.Logger;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.event.TableModelEvent;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;

import org.jdesktop.swingx.JXTreeTable.TreeTableModelAdapter;
import org.jdesktop.swingx.JXTreeTableUnitTest.JXTreeTableA;
import org.jdesktop.swingx.action.AbstractActionExt;
import org.jdesktop.swingx.decorator.Highlighter;
import org.jdesktop.swingx.decorator.HighlighterFactory;
import org.jdesktop.swingx.hyperlink.AbstractHyperlinkAction;
import org.jdesktop.swingx.renderer.CellContext;
import org.jdesktop.swingx.renderer.CheckBoxProvider;
import org.jdesktop.swingx.renderer.ComponentProvider;
import org.jdesktop.swingx.renderer.DefaultTableRenderer;
import org.jdesktop.swingx.renderer.DefaultTreeRenderer;
import org.jdesktop.swingx.renderer.HyperlinkProvider;
import org.jdesktop.swingx.renderer.LabelProvider;
import org.jdesktop.swingx.renderer.RendererVisualCheck.TextAreaProvider;
import org.jdesktop.swingx.renderer.StringValue;
import org.jdesktop.swingx.renderer.StringValues;
import org.jdesktop.swingx.renderer.WrappingIconPanel;
import org.jdesktop.swingx.renderer.WrappingProvider;
import org.jdesktop.swingx.table.ColumnFactory;
import org.jdesktop.swingx.table.TableColumnExt;
import org.jdesktop.swingx.test.ActionMapTreeTableModel;
import org.jdesktop.swingx.test.ComponentTreeTableModel;
import org.jdesktop.swingx.test.TreeTableUtils;
import org.jdesktop.swingx.treetable.AbstractMutableTreeTableNode;
import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode;
import org.jdesktop.swingx.treetable.DefaultTreeTableModel;
import org.jdesktop.swingx.treetable.FileSystemModel;
import org.jdesktop.swingx.treetable.MutableTreeTableNode;
import org.jdesktop.swingx.treetable.TreeTableModel;
import org.jdesktop.swingx.treetable.TreeTableNode;
import org.jdesktop.test.TableModelReport;
import org.junit.Test;

import static org.junit.Assert.*;

/**
 * Test to exposed known issues of <code>JXTreeTable</code>. <p>
 * 
 * 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.<p>
 * 
 * Once the issues are fixed and the corresponding methods are passing, they
 * should be moved over to the XXTest. 
 * 
 * @author Jeanette Winzenburg
 */
public class JXTreeTableIssues extends InteractiveTestCase {
    private static final Logger LOG = Logger.getLogger(JXTreeTableIssues.class
            .getName());
    public static void main(String[] args) {
        setSystemLF(true);
        JXTreeTableIssues test = new JXTreeTableIssues();
        try {
//            test.runInteractiveTests();
//            test.runInteractiveTests(".*Combo.*");
//            test.runInteractiveTests(".*Text.*");
//            test.runInteractiveTests(".*TreeExpand.*");
//            test.runInteractiveTests("interactive.*EditWith.*");
//            test.runInteractiveTests("interactive.*Clip.*");
            test.runInteractive("Prototype");
//          test.runInteractiveTests("interactive.*CustomColor.*");
              
        } catch (Exception e) {
            System.err.println("exception when executing interactive tests:");
            e.printStackTrace();
        }
    }

    /**
     * Issue #1509: configure column width based on prototype not working at all 
     * for hierarchical column.
     * 
     */
    public void interactivePrototype() {
        ColumnFactory factory = new ColumnFactory() {

            @Override
            protected int calcPrototypeWidth(JXTable table,
                    TableColumnExt columnExt) {
                if (isHierarchicalPrototype(table, columnExt))  {
                    return calcHierarchicalPrototypeWidth((JXTreeTable) table, columnExt);
                }
                return super.calcPrototypeWidth(table, columnExt);
            }

            protected boolean isHierarchicalPrototype(JXTable table,
                    TableColumnExt columnExt) {
                return (table instanceof JXTreeTable) 
                        && ((JXTreeTable) table).getTreeTableModel().getHierarchicalColumn() == columnExt.getModelIndex()
                        && columnExt.getPrototypeValue() != null;
            }

            TreeCellRenderer dummy = new DefaultTreeCellRenderer();
            protected int calcHierarchicalPrototypeWidth(JXTreeTable table,
                    TableColumnExt columnExt) {
                JXTree renderer = (JXTree) getCellRenderer(table, columnExt);
                // commented lines would be the obvious step down into the "real" sizing
                // requirements, but giving reasonable result due to internal black magic
//                TreeCellRenderer treeRenderer = renderer.getCellRenderer();
//                Component comp = treeRenderer.getTreeCellRendererComponent(renderer, 
//                        columnExt.getPrototypeValue(), false, false, false, -1, false);
                // instead, measure the dummy
                Component comp = dummy.getTreeCellRendererComponent(renderer, 
                        columnExt.getPrototypeValue(), false, false, false, -1, false);
                
                return Math.max(renderer.getPreferredSize().width, comp.getPreferredSize().width);
            }
            
        };
        JXTreeTable table = new JXTreeTable();
        table.setColumnFactory(factory);
        table.setTreeTableModel(new FileSystemModel());
        table.getColumnExt(0).setPrototypeValue("long longer longest still not enough to really see some effect of the prototype if availabel");
        // Issue #1510: prototype value handling broken in underlying JXTable
        // need to manually force the config
        table.getColumnFactory().configureColumnWidths(table, table.getColumnExt(0));
        showWithScrollingInFrame(table, "Issue #1509: prototype on hierarchical");
    }
    
    /**
     * Issue #1379-swingx: support access to underlying TreeTableModel in TreeTableModelAdapter.
     * 
     * PENDING JW:
     * The bind isn't really fool-proof tight: its possible for subclasses to bind an
     * arbitrary tree to a TreeTableModelAdapter.
     * Dont want to change, though, as client code might have mis-used ... 
     */
    @Test(expected = IllegalStateException.class)
    public void testTreeTableAdapterBind() {
        JXTreeTableA table = new JXTreeTableA(createActionTreeModel());
        TreeTableModelAdapter model = table.createAdapter(new JTree()); 
        model.bind(table);
    }
   

    /**
     * Custom renderer colors of Swingx DefaultTreeRenderer not respected.
     * (same in J/X/Tree). 
     * 
     * A bit surprising - probably due to the half-hearted support (backward
     * compatibility) of per-provider colors: they are set by the glueing
     * renderer to the provider's default visuals. Which is useless if the
     * provider is a wrapping provider - the wrappee's default visuals are unchanged.
     * 
     * PENDING JW: think about complete removal. Client code should move over
     * completely to highlighter/renderer separation anyway.
     * 
     * 
     */
    public void interactiveXRendererCustomColor() {
        JXTreeTable treeTable = new JXTreeTable(new FileSystemModel());
        treeTable.addHighlighter(HighlighterFactory.createSimpleStriping());
        DefaultTreeRenderer swingx = new DefaultTreeRenderer();
        // in a treetable this has no effect: treetable.applyRenderer 
        // internally resets them to the same colors as tree itself
        // (configured by the table's highlighters
        swingx.setBackground(Color.YELLOW);
        treeTable.setTreeCellRenderer(swingx);
        JTree tree = new JXTree(treeTable.getTreeTableModel());
        DefaultTreeRenderer other = new DefaultTreeRenderer();
        other.setBackground(Color.YELLOW);
//        other.setBackgroundSelectionColor(Color.RED);
        tree.setCellRenderer(other);
        JXFrame frame = wrapWithScrollingInFrame(treeTable, tree, "swingx renderers - highlight complete cell");
        frame.setVisible(true);  
    }

    /**
     * Custom renderer colors of core DefaultTreeCellRenderer not respected.
     * This is intentional: treeTable's highlighters must rule, so the
     * renderer colors are used to force the treecellrenderer to use the 
     * correct values.
     */
    public void interactiveCoreRendererCustomColor() {
        JXTreeTable treeTable = new JXTreeTable(new FileSystemModel());
        treeTable.addHighlighter(HighlighterFactory.createSimpleStriping());
        DefaultTreeCellRenderer legacy = createBackgroundTreeRenderer();
        // in a treetable this has no effect: treetable.applyRenderer 
        // internally resets them to the same colors as tree itself
        // (configured by the table's highlighters
        legacy.setBackgroundNonSelectionColor(Color.YELLOW);
        legacy.setBackgroundSelectionColor(Color.RED);
        treeTable.setTreeCellRenderer(legacy);
        JTree tree = new JXTree(treeTable.getTreeTableModel());
        DefaultTreeCellRenderer other = createBackgroundTreeRenderer();
        other.setBackgroundNonSelectionColor(Color.YELLOW);
        other.setBackgroundSelectionColor(Color.RED);
        tree.setCellRenderer(other);
        JXFrame frame = wrapWithScrollingInFrame(treeTable, tree, "legacy renderers - highlight complete cell");
        frame.setVisible(true);  
    }

    private DefaultTreeCellRenderer createBackgroundTreeRenderer() {
        DefaultTreeCellRenderer legacy = new DefaultTreeCellRenderer() {

            @Override
            public Component getTreeCellRendererComponent(JTree tree,
                    Object value, boolean sel, boolean expanded, boolean leaf,
                    int row, boolean hasFocus) {
                Component comp = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf,
                        row, hasFocus);
                if (sel) {
                    comp.setBackground(getBackgroundSelectionColor());
                } else {
                    comp.setBackground(getBackgroundNonSelectionColor());
                }
                return comp;
            }
            
            
        };
        return legacy;
    }


    
    /**
     * Issue #493-swingx: incorrect table events fired.
     * Issue #592-swingx: (no structureChanged table events) is a special
     *   case of the former.
     * 
     * Here: add support to prevent a structureChanged even when setting
     * the root. May be required if the columns are stable and the
     * model lazily loaded. Quick hack would be to add a clientProperty?
     * 
     * @throws InvocationTargetException 
     * @throws InterruptedException 
     */
    public void testTableEventOnSetRootNoStructureChange() throws InterruptedException, InvocationTargetException {
        TreeTableModel model = createCustomTreeTableModelFromDefault();
        final JXTreeTable table = new JXTreeTable(model);
        table.setRootVisible(true);
        table.expandAll();
        final TableModelReport report = new TableModelReport();
        table.getModel().addTableModelListener(report);
        ((DefaultTreeTableModel) model).setRoot(new DefaultMutableTreeTableNode("other"));  
        SwingUtilities.invokeAndWait(new Runnable() {
            @Override
            public void run() {
                assertEquals("tableModel must have fired", 1, report.getEventCount());
                assertTrue("event type must be dataChanged " + TableModelReport.printEvent(report.getLastEvent()), 
                        report.isDataChanged(report.getLastEvent()));
            }
        });        
        
    }



    /**
     * Issue #576-swingx: sluggish scrolling (?).
     * Here - use default model
     */
    public void interactiveScrollAlternateHighlightDefaultModel() {
        final JXTable table = new JXTable(0, 6);
        DefaultMutableTreeTableNode root = new DefaultMutableTreeTableNode("root");
        for (int i = 0; i < 5000; i++) {
            root.insert(new DefaultMutableTreeTableNode(i), i);
        }
        final JXTreeTable treeTable = new JXTreeTable(new DefaultTreeTableModel(root));
        treeTable.expandAll();
        table.setModel(treeTable.getModel());
        final Highlighter hl =
            HighlighterFactory.createAlternateStriping(UIManager.getColor("Panel.background"),
            Color.WHITE);
        treeTable.setHighlighters(hl);
        table.setHighlighters(hl);
        final JXFrame frame = wrapWithScrollingInFrame(treeTable, table, "sluggish scrolling");
        Action toggleHighlighter = new AbstractActionExt("toggle highlighter") {

            @Override
            public void actionPerformed(ActionEvent e) {
                if (treeTable.getHighlighters().length == 0) {
                    treeTable.addHighlighter(hl);
                    table.addHighlighter(hl);
                } else {
                    treeTable.removeHighlighter(hl);
                    table.removeHighlighter(hl);
                }
                
            }
            
        };
        Action scroll = new AbstractActionExt("start scroll") {

            @Override
            public void actionPerformed(ActionEvent e) {
                for (int i = 0; i < table.getRowCount(); i++) {
                    table.scrollRowToVisible(i);
                    treeTable.scrollRowToVisible(i);
                }
                
            }
            
        };
        addAction(frame, toggleHighlighter);
        addAction(frame, scroll);
        frame.setVisible(true);
    }
    

    /**
     * Issue #576-swingx: sluggish scrolling (?)
     * 
     * Here: use FileSystemModel
     */
    public void interactiveScrollAlternateHighlight() {
        final JXTable table = new JXTable(0, 6);
        final JXTreeTable treeTable = new JXTreeTable(new FileSystemModel());
        final Highlighter hl =
            HighlighterFactory.createAlternateStriping(UIManager.getColor("Panel.background"),
            Color.WHITE);
        treeTable.setHighlighters(hl);
        table.setHighlighters(hl);
        final JXFrame frame = wrapWithScrollingInFrame(treeTable, table, "sluggish scrolling");
        Action expand = new AbstractActionExt("start expand") {
            @Override
            public void actionPerformed(ActionEvent e) {
                for (int i = 0; i < 5000; i++) {
                    treeTable.expandRow(i);
                }
                table.setModel(treeTable.getModel());
            }
            
        };
        Action toggleHighlighter = new AbstractActionExt("toggle highlighter") {

            @Override
            public void actionPerformed(ActionEvent e) {
                if (treeTable.getHighlighters().length == 0) {
                    treeTable.addHighlighter(hl);
                    table.addHighlighter(hl);
                } else {
                    treeTable.removeHighlighter(hl);
                    table.removeHighlighter(hl);
                }
                
            }
            
        };
        Action scroll = new AbstractActionExt("start scroll") {

            @Override
            public void actionPerformed(ActionEvent e) {
                for (int i = 0; i < table.getRowCount(); i++) {
                    table.scrollRowToVisible(i);
                    treeTable.scrollRowToVisible(i);
                }
                
            }
            
        };
        addAction(frame, expand);
        addAction(frame, toggleHighlighter);
        addAction(frame, scroll);
        frame.setVisible(true);
    }
    

    /**
     * Issue #493-swingx: JXTreeTable.TreeTableModelAdapter: Inconsistency
     * firing update.
     * 
     * Test update events after updating table.
     * 
     * from tiberiu@dev.java.net
     * 
     * @throws InvocationTargetException 
     * @throws InterruptedException 
     */
    public void testTableEventUpdateOnTreeTableSetValueForRoot() throws InterruptedException, InvocationTargetException {
        TreeTableModel model = createCustomTreeTableModelFromDefault();
        final JXTreeTable table = new JXTreeTable(model);
        table.setRootVisible(true);
        table.expandAll();
        final int row = 0;
        // sanity
        assertEquals("JTree", table.getValueAt(row, 0).toString());
        assertTrue("root must be editable", table.getModel().isCellEditable(0, 0));
        final TableModelReport report = new TableModelReport();
        table.getModel().addTableModelListener(report);
        // doesn't fire or isn't detectable? 
        // Problem was: model was not-editable.
        table.setValueAt("games", row, 0);
        SwingUtilities.invokeAndWait(new Runnable() {
            @Override
            public void run() {
                assertEquals("tableModel must have fired", 1, report.getEventCount());
                assertEquals("the event type must be update " + TableModelReport.printEvent(report.getLastEvent())
                        , 1, report.getUpdateEventCount());
                TableModelEvent event = report.getLastUpdateEvent();
                assertEquals("the updated row ", row, event.getFirstRow());
            }
        });        
    }

    // -------------- interactive tests


    /**
     * Issue #493-swingx: JXTreeTable.TreeTableModelAdapter: Inconsistency
     * firing update on a recursive delete on a parent node.
     * 
     * By recursive delete on a parent node it is understood that first we 
     * remove its children and then the parent node. After each child removed
     * we are making an update over the parent. During this update the problem 
     * occurs: the index row for the parent is -1 and hence it is made an update
     * over the row -1 (the header) and as it can be seen the preffered widths
     * of column header are not respected anymore and are restored to the default
     * preferences (all equal).
     * 
     * from tiberiu@dev.java.net
     */
    public void interactiveTreeTableModelAdapterDeleteUpdate() {
        final DefaultTreeTableModel customTreeTableModel = (DefaultTreeTableModel) 
            createCustomTreeTableModelFromDefault();
        final JXTreeTable table = new JXTreeTable(customTreeTableModel);
        table.setRootVisible(true);
        table.expandAll();
        table.getColumn("A").setPreferredWidth(100);
        table.getColumn("A").setMinWidth(100);
        table.getColumn("A").setMaxWidth(100);
        JXTree xtree = new JXTree(customTreeTableModel);
        xtree.setRootVisible(true);
        xtree.expandAll();
        final JXFrame frame = wrapWithScrollingInFrame(table, xtree,
                "JXTreeTable.TreeTableModelAdapter: Inconsistency firing update on recursive delete");
        final MutableTreeTableNode deletedNode = (MutableTreeTableNode) table.getPathForRow(6).getLastPathComponent();
        MutableTreeTableNode child1 = (MutableTreeTableNode) table.getPathForRow(6+1).getLastPathComponent();
        MutableTreeTableNode child2 = (MutableTreeTableNode) table.getPathForRow(6+2).getLastPathComponent();
        MutableTreeTableNode child3 = (MutableTreeTableNode) table.getPathForRow(6+3).getLastPathComponent();
        MutableTreeTableNode child4 = (MutableTreeTableNode) table.getPathForRow(6+4).getLastPathComponent();
        
        final MutableTreeTableNode[] children = {child1, child2, child3, child4 };
        final String[] values = {"v1", "v2", "v3", "v4"};
        final ActionListener l = new ActionListener() {
            int count = 0;
            @Override
            public void actionPerformed(ActionEvent e) {
                if (count > values.length) return;
                if (count == values.length) {
                    customTreeTableModel.removeNodeFromParent(deletedNode);
                    count++;
                } else {
                    // one in each run
                  removeChild(customTreeTableModel, deletedNode, children, values);
                  count++;
                  // all in one
//                    for (int i = 0; i < values.length; i++) {
//                        removeChild(customTreeTableModel, deletedNode, children, values);
//                        count++;
//                    }     
                }
            }
            /**
             * @param customTreeTableModel
             * @param deletedNode
             * @param children
             * @param values
             */
            private void removeChild(final DefaultTreeTableModel customTreeTableModel, final MutableTreeTableNode deletedNode, final MutableTreeTableNode[] children, final String[] values) {
                customTreeTableModel.removeNodeFromParent(children[count]);
                customTreeTableModel.setValueAt(values[count], deletedNode, 0);
            }
            
        };
        Action changeValue = new AbstractAction("delete node sports recursively") {
            Timer timer;
            @Override
            public void actionPerformed(ActionEvent e) {
                if (timer == null) {
                    timer = new Timer(10, l);
                    timer.start();
                } else {
                    timer.stop();
                    setEnabled(false);
                }
                
            }
        };
        addAction(frame, changeValue);
        frame.setVisible(true);
    }


    
    /**
	 * Issue #493-swingx: JXTreeTable.TreeTableModelAdapter: Inconsistency
	 * firing update. Use the second child of root - first is accidentally okay.
	 * 
	 * from tiberiu@dev.java.net
	 * 
	 * TODO DefaultMutableTreeTableNodes do not allow value changes, so this
	 * test will never work
	 */
    public void interactiveTreeTableModelAdapterUpdate() {
        TreeTableModel customTreeTableModel = createCustomTreeTableModelFromDefault();

        final JXTreeTable table = new JXTreeTable(customTreeTableModel);
        table.setRootVisible(true);
        table.expandAll();
        table.setLargeModel(true);
        JXTree xtree = new JXTree(customTreeTableModel);
        xtree.setRootVisible(true);
        xtree.expandAll();
        final JXFrame frame = wrapWithScrollingInFrame(table, xtree,
                "JXTreeTable.TreeTableModelAdapter: Inconsistency firing update");
        Action changeValue = new AbstractAction("change sports to games") {
            @Override
            public void actionPerformed(ActionEvent e) {
                String newValue = "games";
                table.getTreeTableModel().setValueAt(newValue,
                        table.getPathForRow(6).getLastPathComponent(), 0);
            }
        };
        addAction(frame, changeValue);
        Action changeRoot = new AbstractAction("change root") {
            @Override
            public void actionPerformed(ActionEvent e) {
                DefaultMutableTreeTableNode newRoot = new DefaultMutableTreeTableNode("new Root");
                ((DefaultTreeTableModel) table.getTreeTableModel()).setRoot(newRoot);
            }
        };
        addAction(frame, changeRoot);
        frame.pack();
        frame.setVisible(true);
    }

    /**
     * Issue #493-swingx: JXTreeTable.TreeTableModelAdapter: Inconsistency
     * firing delete.
     * 
     * from tiberiu@dev.java.net
     */
    public void interactiveTreeTableModelAdapterDelete() {
        final TreeTableModel customTreeTableModel = createCustomTreeTableModelFromDefault();
        final JXTreeTable table = new JXTreeTable(customTreeTableModel);
        table.setRootVisible(true);
        table.expandAll();
        JXTree xtree = new JXTree(customTreeTableModel);
        xtree.setRootVisible(true);
        xtree.expandAll();
        final JXFrame frame = wrapWithScrollingInFrame(table, xtree,
                "JXTreeTable.TreeTableModelAdapter: Inconsistency firing update");
        Action changeValue = new AbstractAction("delete first child of sports") {
            @Override
            public void actionPerformed(ActionEvent e) {
                MutableTreeTableNode firstChild = (MutableTreeTableNode) table.getPathForRow(6 +1).getLastPathComponent();
                ((DefaultTreeTableModel) customTreeTableModel).removeNodeFromParent(firstChild);
            }
        };
        addAction(frame, changeValue);
        frame.setVisible(true);
    }

    /**
     * Issue #493-swingx: JXTreeTable.TreeTableModelAdapter: Inconsistency
     * firing delete.
     * 
     * from tiberiu@dev.java.net
     */
    public void interactiveTreeTableModelAdapterMutateSelected() {
        final TreeTableModel customTreeTableModel = createCustomTreeTableModelFromDefault();
        final JXTreeTable table = new JXTreeTable(customTreeTableModel);
        table.setRootVisible(true);
        table.expandAll();
        JXTree xtree = new JXTree(customTreeTableModel);
        xtree.setRootVisible(true);
        xtree.expandAll();
        final JXFrame frame = wrapWithScrollingInFrame(table, xtree,
                "JXTreeTable.TreeTableModelAdapter: Inconsistency firing delete expanded folder");
        Action changeValue = new AbstractAction("delete selected node") {
            @Override
            public void actionPerformed(ActionEvent e) {
                int row = table.getSelectedRow();
                if (row < 0) return;
                MutableTreeTableNode firstChild = (MutableTreeTableNode) table.getPathForRow(row).getLastPathComponent();
                ((DefaultTreeTableModel) customTreeTableModel).removeNodeFromParent(firstChild);
            }
        };
        addAction(frame, changeValue);
        Action changeValue1 = new AbstractAction("insert as first child of selected node") {
            @Override
            public void actionPerformed(ActionEvent e) {
                int row = table.getSelectedRow();
                if (row < 0) return;
                
                MutableTreeTableNode firstChild = (MutableTreeTableNode) table.getPathForRow(row).getLastPathComponent();
                MutableTreeTableNode newChild = new DefaultMutableTreeTableNode("inserted");
                ((DefaultTreeTableModel) customTreeTableModel)
                  .insertNodeInto(newChild, firstChild, 0);
            }
        };
        addAction(frame, changeValue1);
        frame.pack();
        frame.setVisible(true);
    }

    /**
     * Issue #493-swingx: JXTreeTable.TreeTableModelAdapter: Inconsistency
     * firing delete.
     * 
     * from tiberiu@dev.java.net
     */
    public void interactiveTreeTableModelAdapterMutateSelectedDiscontinous() {
        final TreeTableModel customTreeTableModel = createCustomTreeTableModelFromDefault();
        final JXTreeTable table = new JXTreeTable(customTreeTableModel);
        table.setRootVisible(true);
        table.expandAll();
        JXTree xtree = new JXTree(customTreeTableModel);
        xtree.setRootVisible(true);
        xtree.expandAll();
        final JXFrame frame = wrapWithScrollingInFrame(table, xtree,
                "JXTreeTable.TreeTableModelAdapter: Inconsistency firing delete expanded folder");
        Action changeValue = new AbstractAction("delete selected node + sibling") {
            @Override
            public void actionPerformed(ActionEvent e) {
                int row = table.getSelectedRow();
                if (row < 0) return;
                MutableTreeTableNode firstChild = (MutableTreeTableNode) table.getPathForRow(row).getLastPathComponent();
                MutableTreeTableNode parent = (MutableTreeTableNode) firstChild.getParent();
                MutableTreeTableNode secondNextSibling = null;
                int firstIndex = parent.getIndex(firstChild);
                if (firstIndex + 2 < parent.getChildCount()) {
                    secondNextSibling = (MutableTreeTableNode) parent.getChildAt(firstIndex + 2);
                }
                if (secondNextSibling != null) {
                	((DefaultTreeTableModel) customTreeTableModel).removeNodeFromParent(secondNextSibling);
                }
                
                ((DefaultTreeTableModel) customTreeTableModel).removeNodeFromParent(firstChild);
            }
        };
        addAction(frame, changeValue);
        Action changeValue1 = new AbstractAction("insert as first child of selected node") {
            @Override
            public void actionPerformed(ActionEvent e) {
                int row = table.getSelectedRow();
                if (row < 0) return;
                
                MutableTreeTableNode firstChild = (MutableTreeTableNode) table.getPathForRow(row).getLastPathComponent();
                MutableTreeTableNode newChild = new DefaultMutableTreeTableNode("inserted");
                ((DefaultTreeTableModel) customTreeTableModel)
                  .insertNodeInto(newChild, firstChild, 0);
            }
        };
        addAction(frame, changeValue1);
        frame.pack();
        frame.setVisible(true);
    }
    /**
     * Creates and returns a custom model from JXTree default model. The model
     * is of type DefaultTreeModel, allowing for easy insert/remove.
     * 
     * @return
     */
    private TreeTableModel createCustomTreeTableModelFromDefault() {
        JXTree tree = new JXTree();
        DefaultTreeModel treeModel = (DefaultTreeModel) tree.getModel();
        TreeTableModel customTreeTableModel = TreeTableUtils
                .convertDefaultTreeModel(treeModel);

        return customTreeTableModel;
    }

    /**
     * A TreeTableModel inheriting from DefaultTreeModel (to ease
     * insert/delete).
     */
    public static class CustomTreeTableModel extends DefaultTreeTableModel {

        /**
         * @param root
         */
        public CustomTreeTableModel(TreeTableNode root) {
            super(root);
        }

        @Override
        public int getColumnCount() {
            return 1;
        }

        @Override
        public String getColumnName(int column) {
            return "User Object";
        }

        @Override
        public Object getValueAt(Object node, int column) {
            return ((DefaultMutableTreeNode) node).getUserObject();
        }

        @Override
        public boolean isCellEditable(Object node, int column) {
            return true;
        }

        @Override
        public void setValueAt(Object value, Object node, int column) {
            ((MutableTreeTableNode) node).setUserObject(value);
            modelSupport.firePathChanged(new TreePath(getPathToRoot((TreeTableNode) node)));
        }

    }

    /**
     * Issue #??-swingx: hyperlink in JXTreeTable hierarchical column not
     * active.
     * 
     */
    public void interactiveTreeTableLinkRendererSimpleText() {
        AbstractHyperlinkAction<Object> simpleAction = new AbstractHyperlinkAction<Object>(null) {

            @Override
            public void actionPerformed(ActionEvent e) {
                LOG.info("hit: " + getTarget());
                
            }
            
        };
        JXTreeTable tree = new JXTreeTable(new FileSystemModel());
        HyperlinkProvider provider =  new HyperlinkProvider(simpleAction);
        tree.getColumn(2).setCellRenderer(new DefaultTableRenderer(provider));
        tree.setTreeCellRenderer(new DefaultTreeRenderer( //provider));
                new WrappingProvider(provider)));
//        tree.setCellRenderer(new LinkRenderer(simpleAction));
        tree.setHighlighters(HighlighterFactory.createSimpleStriping());
        JFrame frame = wrapWithScrollingInFrame(tree, "table and simple links");
        frame.setVisible(true);
    }

    /**
     * Issue ??-swingx: hyperlink/rollover in hierarchical column.
     *
     */
    public void testTreeRendererInitialRollover() {
        JXTreeTable tree = new JXTreeTable(new FileSystemModel());
        assertEquals(tree.isRolloverEnabled(), ((JXTree) tree.getCellRenderer(0, 0)).isRolloverEnabled());
    }

    /**
     * Issue ??-swingx: hyperlink/rollover in hierarchical column.
     *
     */
    public void testTreeRendererModifiedRollover() {
        JXTreeTable tree = new JXTreeTable(new FileSystemModel());
        tree.setRolloverEnabled(!tree.isRolloverEnabled());
        assertEquals(tree.isRolloverEnabled(), ((JXTree) tree.getCellRenderer(0, 0)).isRolloverEnabled());
    }

    /**
     * example how to use a custom component as
     * renderer in tree column of TreeTable.
     *
     */
    public void interactiveTreeTableCustomRenderer() {
        JXTreeTable tree = new JXTreeTable(new FileSystemModel());
        
        StringValue sv = new StringValue( ){

            @Override
            public String getString(Object value) {
                return "..." + StringValues.TO_STRING.getString(value);
            }
            
        };
        ComponentProvider<?> provider = new CheckBoxProvider(sv);
//            /**
//             * custom tooltip: show row. Note: the context is that 
//             * of the rendering tree. No way to get at table state?
//             */
//            @Override
//            protected void configureState(CellContext context) {
//                super.configureState(context);
//                rendererComponent.setToolTipText("Row: " + context.getRow());
//            }
//            
//        };
        tree.setTreeCellRenderer(new DefaultTreeRenderer(provider));
        tree.setHighlighters(HighlighterFactory.createSimpleStriping());
        JFrame frame = wrapWithScrollingInFrame(tree, "treetable and custom renderer");
        frame.setVisible(true);
    }

    /**
     * Quick example to use a TextArea in the hierarchical column
     * of a treeTable. Not really working .. the wrap is not reliable?.
     *
     */
    public void interactiveTextAreaTreeTable() {
        TreeTableModel model = createTreeTableModelWithLongNode();
        JXTreeTable treeTable = new JXTreeTable(model);
        treeTable.setVisibleRowCount(5);
        treeTable.setRowHeight(50);
        treeTable.getColumnExt(0).setPreferredWidth(200);
        TreeCellRenderer renderer = new DefaultTreeRenderer(
                new WrappingProvider(new TextAreaProvider()));
        treeTable.setTreeCellRenderer(renderer);
        showWithScrollingInFrame(treeTable, "TreeTable with text wrapping");
    }
    
    /**
     * @return
     */
    private TreeTableModel createTreeTableModelWithLongNode() {
        MutableTreeTableNode root = createLongNode("some really, maybe really really long text -  "
                + "wrappit .... where needed ");
        root.insert(createLongNode("another really, maybe really really long text -  "
                + "with nothing but junk. wrappit .... where needed"), 0);
        root.insert(createLongNode("another really, maybe really really long text -  "
                + "with nothing but junk. wrappit .... where needed"), 0);
        MutableTreeTableNode node = createLongNode("some really, maybe really really long text -  "
                + "wrappit .... where needed ");
        node.insert(createLongNode("another really, maybe really really long text -  "
                + "with nothing but junk. wrappit .... where needed"), 0);
        root.insert(node, 0);
        root.insert(createLongNode("another really, maybe really really long text -  "
                + "with nothing but junk. wrappit .... where needed"), 0);
        Vector<String> ids = new Vector<String>();
        ids.add("long text");
        ids.add("dummy");
        return new DefaultTreeTableModel(root, ids);
    }

    /**
     * @param string
     * @return
     */
    private MutableTreeTableNode createLongNode(final String string) {
        AbstractMutableTreeTableNode node = new AbstractMutableTreeTableNode() {
            Object rnd = Math.random();
            @Override
            public int getColumnCount() {
                return 2;
            }

            @Override
            public Object getValueAt(int column) {
                if (column == 0) {
                    return string;
                }
                return rnd;
            }
            
        };
        node.setUserObject(string);
        return node;
    }


    /**
     * Experiments to try and understand clipping issues: occasionally, the text
     * in the tree column is clipped even if there is enough space available.
     * 
     * To visualize the rendering component's size we use a WrappingIconProvider
     * which sets a red border.
     * 
     * Calling packxx might pose a problem: for non-large models the node size
     * is cached. In this case, packing before replacing the renderer will lead
     * to incorrect sizes which are hard to invalidate (no way except faking a
     * structural tree event? temporaryly set large model and back, plus repaint
     * works). <p>
     * 
     * both pack-moments seem okay (without ellipses, always filling the box),
     * probably due to fixing swingx-1232 (by forcing the xtree to fire a 
     * change notification on setRenderer)
     */
    public void interactiveTreeTableClipIssueWrappingProvider() {
        final JXTreeTable treeTable = new JXTreeTable(createActionTreeModel());
        treeTable.setHorizontalScrollEnabled(true);
        treeTable.setColumnControlVisible(true);
        // BEWARE: do not pack before setting the renderer
        treeTable.packColumn(0, -1);

        StringValue format = new StringValue() {

            @Override
            public String getString(Object value) {
                if (value instanceof Action) {
                    return ((Action) value).getValue(Action.NAME) + "xx";
                }
                return StringValues.TO_STRING.getString(value);
            }

        };
        ComponentProvider<?> tableProvider = new LabelProvider(format);
        WrappingProvider wrappingProvider = new WrappingProvider(tableProvider) {
            Border redBorder = BorderFactory.createLineBorder(Color.RED);

            @Override
            public WrappingIconPanel getRendererComponent(CellContext context) {
                super.getRendererComponent(context);
                rendererComponent.setBorder(redBorder);
                return rendererComponent;
            }

        };
        DefaultTreeRenderer treeCellRenderer = new DefaultTreeRenderer(
                wrappingProvider);
        treeTable.setTreeCellRenderer(treeCellRenderer);
        treeTable.setHighlighters(HighlighterFactory.createSimpleStriping());
        // at this point a pack is okay, caching will get the correct values
        // treeTable.packColumn(0, -1);

        final JXTree tree = new JXTree(treeTable.getTreeTableModel());
        tree.setCellRenderer(treeCellRenderer);
        tree.setScrollsOnExpand(false);
        JXFrame frame = wrapWithScrollingInFrame(treeTable, tree,
                "treetable and tree with wrapping provider");
        // revalidate doesn't help
        Action revalidate = new AbstractActionExt("revalidate") {

            @Override
            public void actionPerformed(ActionEvent e) {
                treeTable.revalidate();
                tree.revalidate();
                treeTable.repaint();
            }
        };

        // hack around incorrect cached node sizes
        Action large = new AbstractActionExt("large-circle") {

            @Override
            public void actionPerformed(ActionEvent e) {
                treeTable.setLargeModel(true);
                treeTable.setLargeModel(false);
                treeTable.repaint();
            }
        };
        addAction(frame, revalidate);
        addAction(frame, large);
        show(frame);
    }

    /**
     * Experiments to try and understand clipping issues: occasionally, the text
     * in the tree column is clipped even if there is enough space available.
     * 
     * Here we don't change any renderers.
     */
    public void interactiveTreeTableClipIssueDefaultRenderer() {
        final JXTreeTable treeTable = new JXTreeTable(createActionTreeModel());
        treeTable.setHorizontalScrollEnabled(true);
        treeTable.setRootVisible(true);
        treeTable.collapseAll();
        treeTable.packColumn(0, -1);
        
        final JTree tree = new JTree(treeTable.getTreeTableModel());
        tree.collapseRow(0);
        JXFrame frame = wrapWithScrollingInFrame(treeTable, 
                tree, "JXTreeTable vs. core JTree: default renderer");
        Action revalidate = new AbstractActionExt("revalidate") {

            @Override
            public void actionPerformed(ActionEvent e) {
                treeTable.revalidate();
                tree.revalidate();
                
            } };
        addAction(frame, revalidate);
        frame.setVisible(true);
    }

    /**
     * Experiments to try and understand clipping issues: occasionally, the text
     * in the tree column is clipped even if there is enough space available.
     * Here we set a custom (unchanged default) treeCellRenderer which 
     * removes ellipses altogether.
     * 
     */
    public void interactiveTreeTableClipIssueCustomDefaultRenderer() {
        TreeCellRenderer renderer = new DefaultTreeCellRenderer() {

            @Override
            public Component getTreeCellRendererComponent(JTree tree,
                    Object value, boolean sel, boolean expanded, boolean leaf,
                    int row, boolean hasFocus) {
                return super.getTreeCellRendererComponent(tree, value, sel,
                        expanded, leaf, row, hasFocus);
            }

        };
        final JXTreeTable treeTable = new JXTreeTable(createActionTreeModel());
        treeTable.setTreeCellRenderer(renderer);
        treeTable.setHorizontalScrollEnabled(true);
        treeTable.setRootVisible(true);
        treeTable.collapseAll();
        treeTable.packColumn(0, -1);

        final JTree tree = new JTree(treeTable.getTreeTableModel());
        tree.setCellRenderer(renderer);
        tree.collapseRow(0);
        JXFrame frame = wrapWithScrollingInFrame(treeTable, tree,
                "JXTreeTable vs. JTree: custom default renderer without clip");
        Action revalidate = new AbstractActionExt("revalidate") {

            @Override
            public void actionPerformed(ActionEvent e) {
                treeTable.revalidate();
                tree.revalidate();

            }
        };
        addAction(frame, revalidate);
        frame.setVisible(true);
    }

    /**
     * Dirty example how to configure a custom renderer to use
     * treeTableModel.getValueAt(...) for showing.
     * 
     */
    public void interactiveTreeTableGetValueRenderer() {
        JXTreeTable tree = new JXTreeTable(new ComponentTreeTableModel(new JXFrame()));
        ComponentProvider<?> provider = new CheckBoxProvider(StringValues.TO_STRING) {
            
            @Override
            protected String getValueAsString(CellContext context) {
                // this is dirty because the design idea was to keep the renderer 
                // unaware of the context type
                TreeTableModel model = (TreeTableModel) ((JXTree) context.getComponent()).getModel();
                // beware: currently works only if the node is not a DefaultMutableTreeNode
                // otherwise the WrappingProvider tries to be smart and replaces the node
                // by the userObject before passing on to the wrappee! 
                Object nodeValue = model.getValueAt(context.getValue(), 0);
                return formatter.getString(nodeValue);            
              }

          
        };
        tree.setTreeCellRenderer(new DefaultTreeRenderer(provider));
        tree.expandAll();
        tree.setHighlighters(HighlighterFactory.createSimpleStriping());
        JFrame frame = wrapWithScrollingInFrame(tree, "treeTable and getValueAt renderer");
        frame.setVisible(true);
    }


//------------- unit tests    
    /**
     * Issue #399-swingx: editing terminated by selecting editing row.
     *
     */
    public void testSelectionKeepsEditingWithExpandsTrue() {
        JXTreeTable treeTable = new JXTreeTable(new FileSystemModel()) {

            @Override
            public boolean isCellEditable(int row, int column) {
                return true;
            }
            
        };
        // sanity: default value of expandsSelectedPath
        assertTrue(treeTable.getExpandsSelectedPaths());
        boolean canEdit = treeTable.editCellAt(1, 2);
        // sanity: editing started
        assertTrue(canEdit);
        // sanity: nothing selected
        assertTrue(treeTable.getSelectionModel().isSelectionEmpty());
        int editingRow = treeTable.getEditingRow();
        treeTable.setRowSelectionInterval(editingRow, editingRow);
        assertEquals("after selection treeTable editing state must be unchanged", canEdit, treeTable.isEditing());
    }
    
    /**
     * Issue #212-jdnc: reuse editor, install only once.
     * 
     */
    public void testReuseEditor() {
        //TODO rework this test, since we no longer use TreeTableModel.class
//        JXTreeTable treeTable = new JXTreeTable(treeTableModel);
//        CellEditor editor = treeTable.getDefaultEditor(TreeTableModel.class);
//        assertTrue(editor instanceof TreeTableCellEditor);
//        treeTable.setTreeTableModel(simpleTreeTableModel);
//        assertSame("hierarchical editor must be unchanged", editor, 
//                treeTable.getDefaultEditor(TreeTableModel.class));
        fail("#212-jdnc - must be revisited after treeTableModel overhaul");
    }

    /**
     * sanity: toggling select/unselect via mouse the lead is
     * always painted, doing unselect via model (clear/remove path) 
     * seems to clear the lead?
     *
     */
    public void testBasicTreeLeadSelection() {
        JXTree tree = new JXTree();
        TreePath path = tree.getPathForRow(0);
        tree.setSelectionPath(path);
        assertEquals(0, tree.getSelectionModel().getLeadSelectionRow());
        assertEquals(path, tree.getLeadSelectionPath());
        tree.removeSelectionPath(path);
        assertNotNull(tree.getLeadSelectionPath());
        assertEquals(0, tree.getSelectionModel().getLeadSelectionRow());
    }

    
    /**
     * Issue #341-swingx: missing synch of lead.  
     * test lead after setting selection via table.
     *
     *  PENDING: this passes locally, fails on server
     */
    public void testLeadSelectionFromTable() {
        JXTreeTable treeTable = prepareTreeTable(false);
        assertEquals(-1, treeTable.getSelectionModel().getLeadSelectionIndex());
        assertEquals(-1, treeTable.getTreeSelectionModel().getLeadSelectionRow());
        treeTable.setRowSelectionInterval(0, 0);
        assertEquals(treeTable.getSelectionModel().getLeadSelectionIndex(), 
                treeTable.getTreeSelectionModel().getLeadSelectionRow());
        fail("lead selection synch passes locally, fails on server");
    }
    
    /**
     * Issue #341-swingx: missing synch of lead.  
     * test lead after setting selection via treeSelection.
     *  PENDING: this passes locally, fails on server
     *
     */
    public void testLeadSelectionFromTree() {
        JXTreeTable treeTable = prepareTreeTable(false);
        assertEquals(-1, treeTable.getSelectionModel().getLeadSelectionIndex());
        assertEquals(-1, treeTable.getTreeSelectionModel().getLeadSelectionRow());
        treeTable.getTreeSelectionModel().setSelectionPath(treeTable.getPathForRow(0));
        assertEquals(treeTable.getSelectionModel().getLeadSelectionIndex(), 
                treeTable.getTreeSelectionModel().getLeadSelectionRow());
        assertEquals(0, treeTable.getTreeSelectionModel().getLeadSelectionRow());
        fail("lead selection synch passes locally, fails on server");
    }


    /**
     * Issue #341-swingx: missing synch of lead.  
     * test lead after remove selection via tree.
     *
     */
    public void testLeadAfterRemoveSelectionFromTree() {
        JXTreeTable treeTable = prepareTreeTable(true);
        treeTable.getTreeSelectionModel().removeSelectionPath(
                treeTable.getTreeSelectionModel().getLeadSelectionPath());
        assertEquals(treeTable.getSelectionModel().getLeadSelectionIndex(), 
                treeTable.getTreeSelectionModel().getLeadSelectionRow());
        
    }
    
    /**
     * Issue #341-swingx: missing synch of lead.  
     * test lead after clear selection via table.
     *
     */
    public void testLeadAfterClearSelectionFromTable() {
        JXTreeTable treeTable = prepareTreeTable(true);
        treeTable.clearSelection();
        assertEquals(treeTable.getSelectionModel().getLeadSelectionIndex(), 
                treeTable.getTreeSelectionModel().getLeadSelectionRow());
        
    }

    /**
     * Issue #341-swingx: missing synch of lead.  
     * test lead after clear selection via table.
     *
     */
    public void testLeadAfterClearSelectionFromTree() {
        JXTreeTable treeTable = prepareTreeTable(true);
        treeTable.getTreeSelectionModel().clearSelection();
        assertEquals(treeTable.getSelectionModel().getLeadSelectionIndex(), 
                treeTable.getTreeSelectionModel().getLeadSelectionRow());
        
    }

    /**
     * creates and configures a treetable for usage in selection tests.
     * 
     * @param selectFirstRow boolean to indicate if the first row should
     *   be selected.
     * @return
     */
    protected JXTreeTable prepareTreeTable(boolean selectFirstRow) {
        JXTreeTable treeTable = new JXTreeTable(new ComponentTreeTableModel(new JXFrame()));
        treeTable.setRootVisible(true);
        // sanity: assert that we have at least two rows to change selection
        assertTrue(treeTable.getRowCount() > 1);
        if (selectFirstRow) {
            treeTable.setRowSelectionInterval(0, 0);
        }
        return treeTable;
    }


    public void testDummy() {
        
    }

    /**
     * @return
     */
    private TreeTableModel createActionTreeModel() {
        JXTable table = new JXTable(10, 10);
        table.setHorizontalScrollEnabled(true);
        return new ActionMapTreeTableModel(table);
    }

}
