/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.openide.explorer.view;

import java.util.logging.Logger;
import org.openide.awt.MouseUtils;
import org.openide.nodes.Node;
import org.openide.nodes.Node.Property;
import org.openide.nodes.NodeListener;
import org.openide.util.ImageUtilities;
import org.openide.util.NbBundle;
import org.openide.explorer.ExplorerManager;
import org.openide.explorer.ExplorerManager.Provider;
import org.openide.explorer.ExplorerUtils;

import java.awt.*;
import java.awt.event.*;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.logging.Level;

import javax.accessibility.AccessibleContext;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.*;
import javax.swing.plaf.metal.MetalScrollBarUI;
import javax.swing.table.*;
import javax.swing.tree.*;
import org.openide.awt.QuickSearch;
import org.openide.explorer.view.TreeView.PopupAdapter;
import org.openide.explorer.view.TreeView.PopupSupport;
import org.openide.explorer.view.TreeView.TreePropertyListener;
import org.openide.nodes.Children;
import org.openide.nodes.FilterNode;
import org.openide.nodes.NodeMemberEvent;
import org.openide.nodes.NodeReorderEvent;
import org.openide.util.Utilities;


/** Explorer view. Allows to view tree of nodes on the left
 * and its properties in table on the right.
 * <p>
 * The main mechanism for setting what properties are displayed is
 * <a href="#setProperties"><code>setProperties (Node.Property[])</code></a>.
 * Pass this method an
 * array of properties.  These will act as a template, and properties of
 * the displayed nodes which share the same <i>name</i> will be used in
 * the columns of the table.
 *
 * You can customize behaviour
 * of property columns using <code>Property.setValue (String parameter,
 * Object value)</code>.  For example,
 * assume you have following array of properties:
 * <br><code>org.openide.nodes.Node.Property[] properties</code><br>
 *
 * if you need second column to be initially invisible in TreeTableView, you
 * should set its custom parameter:
 * <br><code>properties[1].setValue ("InvisibleInTreeTableView", Boolean.TRUE);</code>
 *
 * <TABLE border="1" summary="custom parameter list">
 *     <TR>
 *         <TH> Parameter name
 *         </TH>
 *         <TH> Parameter type
 *         </TH>
 *         <TH> Description
 *         </TH>
 *     </TR>
 *     <TR>
 *         <TD> InvisibleInTreeTableView</TD>
 *         <TD> Boolean </TD>
 *         <TD> This property column should be initially invisible (hidden).</TD>
 *     </TR>
 *     <TR>
 *         <TD> ComparableColumnTTV</TD>
 *         <TD> Boolean </TD>
 *         <TD> This property column should be  used for sorting.</TD>
 *     </TR>
 *     <TR>
 *         <TD> SortingColumnTTV</TD>
 *         <TD> Boolean </TD>
 *         <TD> TreeTableView should be initially sorted by this property column.</TD>
 *     </TR>
 *     <TR>
 *         <TD> DescendingOrderTTV</TD>
 *         <TD> Boolean </TD>
 *         <TD> If this parameter and <code>SortingColumnTTV</code> is set, TreeTableView should
 *              be initially sorted by this property columns in descending order.
 *         </TD>
 *     </TR>
 *     <TR>
 *         <TD> OrderNumberTTV</TD>
 *         <TD> Integer </TD>
 *         <TD> If this parameter is set to <code>N</code>, this property column will be
 *             displayed as Nth column of table. If not set, column will be
 *             displayed in natural order.
 *         </TD>
 *     </TR>
 *     <TR>
 *         <TD> TreeColumnTTV</TD>
 *         <TD> Boolean </TD>
 *         <TD> Identifies special property representing first (tree) column. To allow setting
 *             of <code>SortingColumnTTV, DescendingOrderTTV, ComparableColumnTTV</code> parameters
 *             also for first (tree) column, use this special parameter and add
 *             this property to Node.Property[] array before calling
 *             TreeTableView.setProperties (Node.Property[]).
 *         </TD>
 *     </TR>
 *    <TR>
 *        <TD> ColumnMnemonicCharTTV</TD>
 *        <TD> String </TD>
 *        <TD> When set, this parameter contains the mnemonic character for column's
 *            display name (e.g. in <I>Change Visible Columns</I> dialog window).
 *            If not set, no mnemonic will be displayed.
 *        </TD>
 *    </TR>
 *    <TR>
 *        <TD> ColumnDisplayNameWithMnemonicTTV</TD>
 *        <TD> String </TD>
 *        <TD> When set, this parameter contains column's display name with
 *              '&amp;' as the mnemonic. This parameter should be preferred over
 *              ColumnMnemonicCharTTV.
 *        </TD>
 *    </TR>
 * </TABLE>
 *
 * <p>
 * This class is a <q>view</q>
 * to use it properly you need to add it into a component which implements
 * {@link Provider}. Good examples of that can be found 
 * in {@link ExplorerUtils}. Then just use 
 * {@link Provider#getExplorerManager} call to get the {@link ExplorerManager}
 * and control its state.
 * </p>
 * <p>
 * There can be multiple <q>views</q> under one container implementing {@link Provider}. Select from
 * range of predefined ones or write your own:
 * </p>
 * <ul>
 *      <li>{@link org.openide.explorer.view.BeanTreeView} - shows a tree of nodes</li>
 *      <li>{@link org.openide.explorer.view.ContextTreeView} - shows a tree of nodes without leaf nodes</li>
 *      <li>{@link org.openide.explorer.view.ListView} - shows a list of nodes</li>
 *      <li>{@link org.openide.explorer.view.IconView} - shows a rows of nodes with bigger icons</li>
 *      <li>{@link org.openide.explorer.view.ChoiceView} - creates a combo box based on the explored nodes</li>
 *      <li>{@link org.openide.explorer.view.TreeTableView} - shows tree of nodes together with a set of their {@link Property}</li>
 *      <li>{@link org.openide.explorer.view.MenuView} - can create a {@link JMenu} structure based on structure of {@link Node}s</li>
 * </ul>
 * <p>
 * All of these views use {@link ExplorerManager#find} to walk up the AWT hierarchy and locate the
 * {@link ExplorerManager} to use as a controler. They attach as listeners to
 * it and also call its setter methods to update the shared state based on the
 * user action. Not all views make sence together, but for example
 * {@link org.openide.explorer.view.ContextTreeView} and {@link org.openide.explorer.view.ListView} were designed to complement
 * themselves and behaves like windows explorer. The {@link org.openide.explorer.propertysheet.PropertySheetView}
 * for example should be able to work with any other view.
 * </p>
 *
 * @author  jrojcek
 * @since 1.7
 */
public class TreeTableView extends BeanTreeView {
    // icon of column button
    private static final String COLUMNS_ICON = "org/netbeans/modules/openide/explorer/columns.gif"; // NOI18N

    // icons of ascending/descending order in column header
    private static final String SORT_ASC_ICON = "org/netbeans/modules/openide/explorer/columnsSortedAsc.gif"; // NOI18N
    private static final String SORT_DESC_ICON = "org/netbeans/modules/openide/explorer/columnsSortedDesc.gif"; // NOI18N

    /** The table */
    protected JTable treeTable;
    private NodeTableModel tableModel;

    // Tree scroll support
    private JScrollBar hScrollBar;
    private JScrollPane scrollPane;
    private ScrollListener listener;

    // hiding columns allowed
    private boolean allowHideColumns = false;

    // sorting by column allowed
    private boolean allowSortingByColumn = false;

    // hide horizontal scrollbar
    private boolean hideHScrollBar = false;

    // button in corner of scroll pane
    private JButton colsButton = null;

    // tree model with sorting support
    private SortedNodeTreeModel sortedNodeTreeModel;

    /** Listener on keystroke to invoke default action */
    private ActionListener defaultTreeActionListener;

    // default treetable header renderer
    private TableCellRenderer defaultHeaderRenderer = null;
    private MouseUtils.PopupMouseAdapter tableMouseListener;

    /** Accessible context of this class (implemented by inner class AccessibleTreeTableView). */
    private AccessibleContext accessContext;
    private TreeColumnProperty treeColumnProperty = new TreeColumnProperty();
    private int treeColumnWidth;
    private Component treeTableParent = null;
    private QuickSearch quickSearch;
    private Component searchpanel;

    /** Create TreeTableView with default NodeTableModel
     */
    public TreeTableView() {
        this(new NodeTableModel());
    }

    /** Creates TreeTableView with provided NodeTableModel.
     * @param ntm node table model
     */
    public TreeTableView(NodeTableModel ntm) {
        setLayout(new SearchScrollPaneLayout());
        tableModel = ntm;

        initializeTreeTable();
        setPopupAllowed(true);
        setDefaultActionAllowed(true);

        initializeTreeScrollSupport();

        // add scrollbar and scrollpane into a panel
        JPanel p = new CompoundScrollPane();
        p.setLayout(new BorderLayout());
        scrollPane.setViewportView(treeTable);
        p.add(BorderLayout.CENTER, scrollPane);

        Icon icon = ImageUtilities.image2Icon(ImageUtilities.loadImage(COLUMNS_ICON)); // NOI18N
        colsButton = new javax.swing.JButton(icon);
        // For HiDPI support.
        colsButton.setDisabledIcon(ImageUtilities.createDisabledIcon(icon));
        colsButton.getAccessibleContext().setAccessibleName(NbBundle.getMessage(TreeTableView.class, "ACN_ColumnsSelector")); //NOI18N
        colsButton.getAccessibleContext().setAccessibleDescription(NbBundle.getMessage(TreeTableView.class, "ACD_ColumnsSelector")); //NOI18N
        colsButton.addActionListener(
            new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent evt) {
                    selectVisibleColumns();
                }
            }
        );

        JPanel sbp = new JPanel();
        sbp.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
        sbp.add(hScrollBar);
        p.add(BorderLayout.SOUTH, sbp);

        super.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        super.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);
        setViewportView(p);
        setBorder(BorderFactory.createEmptyBorder()); //NOI18N
        setViewportBorder(BorderFactory.createEmptyBorder()); //NOI18N
    }

    @Override
    public void setRowHeader(JViewport rowHeader) {
        rowHeader.setBorder(BorderFactory.createEmptyBorder());
        super.setRowHeader(rowHeader);
    }

    /* Overriden to allow hide special horizontal scrollbar
     */
    @Override
    public void setHorizontalScrollBarPolicy(int policy) {
        hideHScrollBar = (policy == JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

        if (hideHScrollBar) {
            hScrollBar.setVisible(false);
            ((TreeTable) treeTable).setTreeHScrollingEnabled(false);
        }
    }

    /* Overriden to delegate policy of vertical scrollbar to inner scrollPane
     */
    @Override
    public void setVerticalScrollBarPolicy(int policy) {
        if (scrollPane == null) {
            return;
        }

        allowHideColumns = (policy == JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);

        if (allowHideColumns) {
            scrollPane.setCorner(JScrollPane.UPPER_RIGHT_CORNER, colsButton);
        }

        treeTable.getTableHeader().setReorderingAllowed(allowHideColumns);

        scrollPane.setVerticalScrollBarPolicy(policy);
    }

    @Override
    protected NodeTreeModel createModel() {
        return getSortedNodeTreeModel();
    }

    /** Requests focus for the tree component. Overrides superclass method. */
    @Override
    public void requestFocus() {
        if (treeTable != null) {
            treeTable.requestFocus();
        }
    }

    @Override
    public boolean requestFocusInWindow() {
        boolean res = super.requestFocusInWindow();

        //#44856: pass the focus request to the treetable as well 
        if (null != treeTable) {
            treeTable.requestFocus();
        }

        return res;
    }

    /* Sets sorting ability
     */
    private void setAllowSortingByColumn(boolean allow) {
        if (allow && (allow != allowSortingByColumn)) {
            addMouseListener(
                new MouseAdapter() {
                    @Override
                    public void mouseClicked(MouseEvent evt) {
                        // Check whether it was really a click
                        if (evt.getClickCount() == 0) return ;
                        Component c = evt.getComponent();

                        if (c instanceof JTableHeader) {
                            JTableHeader h = (JTableHeader) c;
                            int index = h.columnAtPoint(evt.getPoint());

                            //issue 38442, column can be -1 if this is the
                            //upper right corner - there's no column there,
                            //so make sure it's an index >=0.
                            if (index >= 0) {
                                clickOnColumnAction(index - 1);
                            }
                        }
                    }
                }
            );
        }

        allowSortingByColumn = allow;
    }

    /* Change sorting after clicking on comparable column header.
     * Cycle through ascending -> descending -> no sort -> (start over)
     */
    private void clickOnColumnAction(int index) {
        if (index == -1) {
            if (treeColumnProperty.isComparable()) {
                if (treeColumnProperty.isSortingColumn()) {
                    if (!treeColumnProperty.isSortOrderDescending()) {
                        setSortingOrder(false);
                    } else {
                        noSorting();
                    }
                } else {
                    int realIndex = tableModel.translateVisibleColumnIndex(index);
                    setSortingColumn(index);
                    setSortingOrder(true);
                }
            }
        } else if (tableModel.isComparableColumn(index)) {
            if (tableModel.isSortingColumnEx(tableModel.translateVisibleColumnIndex(index))) {
                if (!tableModel.isSortOrderDescending()) {
                    setSortingOrder(false);
                } else {
                    noSorting();
                }
            } else {
                int realIndex = tableModel.translateVisibleColumnIndex(index);
                setSortingColumn(realIndex);
                setSortingOrder(true);
            }
        }
    }

    private void selectVisibleColumns() {
        setCurrentWidths();

        String viewName = null;

        if (getParent() != null) {
            viewName = getParent().getName();
        }

        if (
            tableModel.selectVisibleColumns(
                    viewName, treeTable.getColumnName(0), getSortedNodeTreeModel().getRootDescription()
                )
        ) {
            if (tableModel.getSortingColumn() == -1) {
                getSortedNodeTreeModel().setSortedByProperty(null);
            }

            setTreePreferredWidth(treeColumnWidth);

            for (int i = 0; i < tableModel.getColumnCount(); i++) {
                setTableColumnPreferredWidth(tableModel.getArrayIndex(i), tableModel.getVisibleColumnWidth(i));
            }
        }
    }

    private void setCurrentWidths() {
        treeColumnWidth = treeTable.getColumnModel().getColumn(0).getWidth();

        for (int i = 0; i < tableModel.getColumnCount(); i++) {
            int w = treeTable.getColumnModel().getColumn(i + 1).getWidth();
            tableModel.setVisibleColumnWidth(i, w);
        }
    }

    /** Do not initialize tree now. We will do it from our constructor.
     * [dafe] Used probably because this method is called *before* superclass
     * is fully created (constructor finished) which is horrible but I don't
     * have enough knowledge about this code to change it.
     */
    @Override
    void initializeTree() {
    }
    
    private final Object searchConstraints = new Object();

    @Override
    public void add(Component comp, Object constraints) {
        if (constraints == searchConstraints) {
            searchpanel = comp;
            constraints = null;
        }
        super.add(comp, constraints);
    }

    @Override
    public void remove(Component comp) {
        if (comp == searchpanel) {
            searchpanel = null;
        }
        super.remove(comp);
    }
    
    /** Initialize tree and treeTable.
     */
    private void initializeTreeTable() {
        treeModel = createModel();
        TreeTable tt = new TreeTable(treeModel, tableModel);
        treeTable = tt;
        tree = ((TreeTable) treeTable).getTree();
        TableQuickSearchSupport tqss = new TableQuickSearchSupport(tt, tt.getQuickSearchTableFilter(), tt.getQuickSearchSettings());
        quickSearch = QuickSearch.attach(this, searchConstraints, tqss, tqss.createSearchPopupMenu());
        tt.addKeyListener(new KeyListener() {
            @Override
            public void keyTyped(KeyEvent e) {
                quickSearch.processKeyEvent(e);
            }
            @Override
            public void keyPressed(KeyEvent e) {
                quickSearch.processKeyEvent(e);
            }
            @Override
            public void keyReleased(KeyEvent e) {
                quickSearch.processKeyEvent(e);
            }
        });
        
        defaultHeaderRenderer = treeTable.getTableHeader().getDefaultRenderer();
        treeTable.getTableHeader().setDefaultRenderer(new SortingHeaderRenderer());

        // init listener & attach it to closing of
        managerListener = new TreePropertyListener();
        tree.addTreeExpansionListener(managerListener);

        defaultActionListener = new PopupSupport();
        Action popupWrapper = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent evt) {
                    SwingUtilities.invokeLater( defaultActionListener );
                }

                @Override
                public boolean isEnabled() {
                    return treeTable.isFocusOwner() || tree.isFocusOwner();
                }
            };
            
        treeTable.getInputMap( JTree.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( 
                KeyStroke.getKeyStroke( KeyEvent.VK_F10, KeyEvent.SHIFT_DOWN_MASK ), "org.openide.actions.PopupAction" );
        treeTable.getActionMap().put("org.openide.actions.PopupAction", popupWrapper);
        tree.addMouseListener(defaultActionListener);

        tableMouseListener = new MouseUtils.PopupMouseAdapter() {
                    @Override
                    public void showPopup(MouseEvent mevt) {
                        if (isPopupAllowed()) {
                            if (mevt.getY() > treeTable.getHeight()) {
                                // clear selection, if click under the table
                                treeTable.clearSelection();
                            } else {
                                int selRow = treeTable.rowAtPoint( mevt.getPoint() );
                                boolean isAlreadySelected = false;
                                int[] currentSelection = tree.getSelectionRows();
                                for( int i=0; null != currentSelection && i<currentSelection.length; i++ ) {
                                    if( selRow == currentSelection[i] ) {
                                        isAlreadySelected = true;
                                        break;
                                    }
                                }
                                if( !isAlreadySelected )
                                    tree.setSelectionRow( selRow );
                            }

                            createPopup(mevt);
                        }
                    }
                };
        treeTable.addMouseListener(tableMouseListener);

        if (UIManager.getColor("control") != null) { // NOI18N
            treeTable.setGridColor(UIManager.getColor("control")); // NOI18N
        }
    }

    @Override
    public void setSelectionMode(int mode) {
        super.setSelectionMode(mode);

        if (mode == TreeSelectionModel.SINGLE_TREE_SELECTION) {
            treeTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        } else if (mode == TreeSelectionModel.CONTIGUOUS_TREE_SELECTION) {
            treeTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
        } else if (mode == TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION) {
            treeTable.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
        }
    }

    /** Overrides JScrollPane's getAccessibleContext() method to use internal accessible context.
     */
    @Override
    public AccessibleContext getAccessibleContext() {
        if (accessContext == null) {
            accessContext = new AccessibleTreeTableView();
        }

        return accessContext;
    }

    /** Initialize full support for horizontal scrolling.
     */
    private void initializeTreeScrollSupport() {
        scrollPane = new JScrollPane();
        scrollPane.setName("TreeTableView.scrollpane"); //NOI18N
        scrollPane.setBorder(BorderFactory.createEmptyBorder());
        scrollPane.setViewportBorder(BorderFactory.createEmptyBorder());

        if (UIManager.getColor("Table.background") != null) { // NOI18N
            scrollPane.getViewport().setBackground(UIManager.getColor("Table.background")); // NOI18N
        }

        hScrollBar = new JScrollBar(JScrollBar.HORIZONTAL);
        hScrollBar.putClientProperty(MetalScrollBarUI.FREE_STANDING_PROP, Boolean.FALSE);
        hScrollBar.setVisible(false);

        listener = new ScrollListener();

        treeTable.addPropertyChangeListener(listener);
        scrollPane.getViewport().addComponentListener(listener);
        tree.addPropertyChangeListener(listener);
        hScrollBar.getModel().addChangeListener(listener);
    }

    /* Overriden to work well with treeTable.
     */
    @Override
    public void setPopupAllowed(boolean value) {
        if (tree == null) {
            return;
        }

        if ((popupListener == null) && value) {
            // on
            popupListener = new PopupAdapter() {
                @Override
                        protected void showPopup(MouseEvent e) {
                            int selRow = tree.getClosestRowForLocation(e.getX(), e.getY());

                            if (!tree.isRowSelected(selRow)) {
                                tree.setSelectionRow(selRow);
                            }
                        }
                    };

            tree.addMouseListener(popupListener);

            return;
        }

        if ((popupListener != null) && !value) {
            // off
            tree.removeMouseListener(popupListener);
            popupListener = null;

            return;
        }
    }

    /* Overriden to work well with treeTable.
     */
    @Override
    public void setDefaultActionAllowed(boolean value) {
        if (tree == null) {
            return;
        }

        defaultActionEnabled = value;

        if (value) {
            defaultTreeActionListener = new DefaultTreeAction();
            treeTable.registerKeyboardAction(
                defaultTreeActionListener, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), JComponent.WHEN_FOCUSED
            );
        } else {
            // Switch off.
            defaultTreeActionListener = null;
            treeTable.unregisterKeyboardAction(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false));
        }
    }

    @Override
    public boolean isQuickSearchAllowed() {
        return quickSearch.isEnabled();
    }
    
    @Override
    public void setQuickSearchAllowed(boolean allowedQuickSearch) {
        quickSearch.setEnabled(allowedQuickSearch);
    }

    /** Set columns.
     * @param props each column is constructed from Node.Property
     */
    public void setProperties(Property[] props) {
        tableModel.setProperties(props);
        treeColumnProperty.setProperty(tableModel.propertyForColumn(-1));

        if (treeColumnProperty.isComparable() || tableModel.existsComparableColumn()) {
            setAllowSortingByColumn(true);

            if (treeColumnProperty.isSortingColumn()) {
                getSortedNodeTreeModel().setSortedByName(true, !treeColumnProperty.isSortOrderDescending());
            } else {
                int index = tableModel.getSortingColumn();

                if (index != -1) {
                    getSortedNodeTreeModel().setSortedByProperty(
                        tableModel.propertyForColumnEx(index), !tableModel.isSortOrderDescending()
                    );
                }
            }
        }
    }

    /** Sets resize mode of table.
     *
     * @param mode - One of 5 legal values: <pre>JTable.AUTO_RESIZE_OFF,
     *                                           JTable.AUTO_RESIZE_NEXT_COLUMN,
     *                                           JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS,
     *                                           JTable.AUTO_RESIZE_LAST_COLUMN,
     *                                           JTable.AUTO_RESIZE_ALL_COLUMNS</pre>
     */
    public final void setTableAutoResizeMode(int mode) {
        treeTable.setAutoResizeMode(mode);
    }

    /** Gets resize mode of table.
     *
     * @return mode - One of 5 legal values: <pre>JTable.AUTO_RESIZE_OFF,
     *                                           JTable.AUTO_RESIZE_NEXT_COLUMN,
     *                                           JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS,
     *                                           JTable.AUTO_RESIZE_LAST_COLUMN,
     *                                           JTable.AUTO_RESIZE_ALL_COLUMNS</pre>
     */
    public final int getTableAutoResizeMode() {
        return treeTable.getAutoResizeMode();
    }

    /** Sets preferred width of table column
     * @param index column index
     * @param width preferred column width
     */
    public final void setTableColumnPreferredWidth(int index, int width) {
        if (index == -1) {
            //Issue 47969 - sometimes this is called with a -1 arg
            return;
        }

        tableModel.setArrayColumnWidth(index, width);

        int j = tableModel.getVisibleIndex(index);

        if (j != -1) {
            treeTable.getColumnModel().getColumn(j + 1).setPreferredWidth(width);
        }
    }

    /** Gets preferred width of table column
     * @param index column index
     * @return preferred column width
     */
    public final int getTableColumnPreferredWidth(int index) {
        int j = tableModel.getVisibleIndex(index);

        if (j != -1) {
            return treeTable.getColumnModel().getColumn(j + 1).getPreferredWidth();
        } else {
            return tableModel.getArrayColumnWidth(index);
        }
    }

    /** Set preferred size of tree view
     * @param width preferred width of tree view
     */
    public final void setTreePreferredWidth(int width) {
        treeTable.getColumnModel().getColumn(((TreeTable) treeTable).getTreeColumnIndex()).setPreferredWidth(width);
    }

    /** Get preferred size of tree view
     * @return preferred width of tree view
     */
    public final int getTreePreferredWidth() {
        return treeTable.getColumnModel().getColumn(((TreeTable) treeTable).getTreeColumnIndex()).getPreferredWidth();
    }

    @Override
    public void addNotify() {
        // to allow displaying popup also in blank area
        if (treeTable.getParent() != null) {
            treeTableParent = treeTable.getParent();
            treeTableParent.addMouseListener(tableMouseListener);
        }

        super.addNotify();
        if( tableModel.getRowCount() == 0 ) {
            //re-attach node listeners
            Node[] nodes = new Node[tree.getRowCount()];

            for (int i = 0; i < tree.getRowCount(); i++) {
                nodes[i] = Visualizer.findNode(tree.getPathForRow(i).getLastPathComponent());
            }

            tableModel.setNodes(nodes);
        }
        listener.revalidateScrollBar();
        ViewUtil.adjustBackground(treeTable);
        ViewUtil.adjustBackground(scrollPane);
        ViewUtil.adjustBackground(scrollPane.getViewport());
    }

    @Override
    public void removeNotify() {
        super.removeNotify();

        if (treeTableParent != null) { //IndexedEditorPanel
            treeTableParent.removeMouseListener(tableMouseListener);
        }

        treeTableParent = null;

        // clear node listeners
        tableModel.setNodes(new Node[] {  });
    }

    @Override
    public void addMouseListener(MouseListener l) {
        super.addMouseListener(l);
        treeTable.getTableHeader().addMouseListener(l);
    }

    @Override
    public void removeMouseListener(MouseListener l) {
        super.removeMouseListener(l);
        treeTable.getTableHeader().removeMouseListener(l);
    }

    /**
     * Drag and drop is not supported in TreeTableView.
     */
    @Override
    public void setDragSource(boolean state) {
    }

    /**
     * Drag and drop is not supported in TreeTableView.
     */
    @Override
    public void setDropTarget(boolean state) {
    }

    /* Overriden to get position for popup invoked by keyboard
     */
    @Override
    Point getPositionForPopup() {
        int row = treeTable.getSelectedRow();

        if (row < 0) {
            return null;
        }

        int col = treeTable.getSelectedColumn();

        if (col < 0) {
            col = 0;
        }

        Rectangle r;

        if (col == 0) {
            r = tree.getRowBounds(row);
        } else {
            r = treeTable.getCellRect(row, col, true);
        }

        Point p = SwingUtilities.convertPoint(treeTable, r.x, r.y, this);

        return p;
    }

    private void createPopup(MouseEvent e) {
        Point p = SwingUtilities.convertPoint(e.getComponent(), e.getX(), e.getY(), TreeTableView.this);

        createPopup(p.x, p.y);

        e.consume();
    }

    @Override
    void createPopup(int xpos, int ypos) {
        int treeXpos = xpos - ((TreeTable) treeTable).getPositionX();

        if (allowHideColumns || allowSortingByColumn) {
            int col = treeTable.getColumnModel().getColumnIndexAtX(treeXpos);
            super.createExtendedPopup(xpos, ypos, getListMenu(col));
        } else {
            super.createPopup(xpos, ypos);
        }
    }

    /* creates List Options menu
     */
    private JMenu getListMenu(final int col) {
        JMenu listItem = new JMenu(NbBundle.getMessage(NodeTableModel.class, "LBL_ListOptions"));

        if (allowHideColumns && (col > 0)) {
            JMenu colsItem = new JMenu(NbBundle.getMessage(NodeTableModel.class, "LBL_ColsMenu"));

            boolean addColsItem = false;

            if (col > 1) {
                JMenuItem moveLItem = new JMenuItem(NbBundle.getMessage(NodeTableModel.class, "LBL_MoveLeft"));
                moveLItem.addActionListener(
                    new ActionListener() {
                        @Override
                        public void actionPerformed(java.awt.event.ActionEvent actionEvent) {
                            treeTable.getColumnModel().moveColumn(col, col - 1);
                        }
                    }
                );
                colsItem.add(moveLItem);
                addColsItem = true;
            }

            if (col < tableModel.getColumnCount()) {
                JMenuItem moveRItem = new JMenuItem(
                        NbBundle.getMessage(NodeTableModel.class, "LBL_MoveRight")
                    );
                moveRItem.addActionListener(
                    new ActionListener() {
                        @Override
                        public void actionPerformed(java.awt.event.ActionEvent actionEvent) {
                            treeTable.getColumnModel().moveColumn(col, col + 1);
                        }
                    }
                );
                colsItem.add(moveRItem);
                addColsItem = true;
            }

            if (addColsItem) {
                listItem.add(colsItem);
            }
        }

        if (allowSortingByColumn) {
            JMenu sortItem = new JMenu(NbBundle.getMessage(NodeTableModel.class, "LBL_SortMenu"));
            JRadioButtonMenuItem noSortItem = new JRadioButtonMenuItem(
                    NbBundle.getMessage(NodeTableModel.class, "LBL_NoSort"),
                    !getSortedNodeTreeModel().isSortingActive()
                );
            noSortItem.addActionListener(
                new ActionListener() {
                    @Override
                    public void actionPerformed(java.awt.event.ActionEvent actionEvent) {
                        noSorting();
                    }
                }
            );
            sortItem.add(noSortItem);

            int visibleComparable = 0;
            JRadioButtonMenuItem colItem;

            if (treeColumnProperty.isComparable()) {
                visibleComparable++;
                colItem = new JRadioButtonMenuItem(treeTable.getColumnName(0), treeColumnProperty.isSortingColumn());
                colItem.setHorizontalTextPosition(SwingConstants.LEFT);
                colItem.addActionListener(
                    new ActionListener() {
                        @Override
                        public void actionPerformed(java.awt.event.ActionEvent actionEvent) {
                            setSortingColumn(-1);
                        }
                    }
                );
                sortItem.add(colItem);
            }

            for (int i = 0; i < tableModel.getColumnCount(); i++) {
                if (tableModel.isComparableColumn(i)) {
                    visibleComparable++;
                    colItem = new JRadioButtonMenuItem(
                            tableModel.getColumnName(i),
                            tableModel.isSortingColumnEx(tableModel.translateVisibleColumnIndex(i))
                        );
                    colItem.setHorizontalTextPosition(SwingConstants.LEFT);

                    final int index = tableModel.translateVisibleColumnIndex(i);
                    colItem.addActionListener(
                        new ActionListener() {
                            @Override
                            public void actionPerformed(java.awt.event.ActionEvent actionEvent) {
                                setSortingColumn(index);
                            }
                        }
                    );
                    sortItem.add(colItem);
                }
            }

            //add invisible columns
            for (int i = 0; i < tableModel.getColumnCountEx(); i++) {
                if (tableModel.isComparableColumnEx(i) && !tableModel.isVisibleColumnEx(i)) {
                    visibleComparable++;
                    colItem = new JRadioButtonMenuItem(tableModel.getColumnNameEx(i), tableModel.isSortingColumnEx(i));
                    colItem.setHorizontalTextPosition(SwingConstants.LEFT);

                    final int index = i;
                    colItem.addActionListener(
                        new ActionListener() {
                            @Override
                            public void actionPerformed(java.awt.event.ActionEvent actionEvent) {
                                setSortingColumn(index);
                            }
                        }
                    );
                    sortItem.add(colItem);
                }
            }

            if (visibleComparable > 0) {
                sortItem.addSeparator();

                boolean current_sort;

                if (treeColumnProperty.isSortingColumn()) {
                    current_sort = treeColumnProperty.isSortOrderDescending();
                } else {
                    current_sort = tableModel.isSortOrderDescending();
                }

                JRadioButtonMenuItem ascItem = new JRadioButtonMenuItem(
                        NbBundle.getMessage(NodeTableModel.class, "LBL_Ascending"), !current_sort
                    );
                ascItem.setHorizontalTextPosition(SwingConstants.LEFT);
                ascItem.addActionListener(
                    new ActionListener() {
                        @Override
                        public void actionPerformed(java.awt.event.ActionEvent actionEvent) {
                            setSortingOrder(true);
                        }
                    }
                );
                sortItem.add(ascItem);

                JRadioButtonMenuItem descItem = new JRadioButtonMenuItem(
                        NbBundle.getMessage(NodeTableModel.class, "LBL_Descending"), current_sort
                    );
                descItem.setHorizontalTextPosition(SwingConstants.LEFT);
                descItem.addActionListener(
                    new ActionListener() {
                        @Override
                        public void actionPerformed(java.awt.event.ActionEvent actionEvent) {
                            setSortingOrder(false);
                        }
                    }
                );
                sortItem.add(descItem);

                if (!getSortedNodeTreeModel().isSortingActive()) {
                    ascItem.setEnabled(false);
                    descItem.setEnabled(false);
                }

                listItem.add(sortItem);
            }
        }

        if (allowHideColumns) {
            JMenuItem visItem = new JMenuItem(NbBundle.getMessage(NodeTableModel.class, "LBL_ChangeColumns"));
            visItem.addActionListener(
                new ActionListener() {
                    @Override
                    public void actionPerformed(java.awt.event.ActionEvent actionEvent) {
                        selectVisibleColumns();
                    }
                }
            );

            listItem.add(visItem);
        }

        return listItem;
    }

    /* Sets column to be currently used for sorting
     */
    private void setSortingColumn(int index) {
        tableModel.setSortingColumnEx(index);

        if (index != -1) {
            getSortedNodeTreeModel().setSortedByProperty(
                tableModel.propertyForColumnEx(index), !tableModel.isSortOrderDescending()
            );
            treeColumnProperty.setSortingColumn(false);
        } else {
            getSortedNodeTreeModel().setSortedByName(true, !treeColumnProperty.isSortOrderDescending());
            treeColumnProperty.setSortingColumn(true);
        }

        // to change sort icon
        treeTable.getTableHeader().repaint();
    }

    private void noSorting() {
        tableModel.setSortingColumnEx(-1);
        getSortedNodeTreeModel().setNoSorting();
        treeColumnProperty.setSortingColumn(false);

        // to change sort icon
        treeTable.getTableHeader().repaint();
    }

    /* Sets sorting order for current sorting.
     */
    private void setSortingOrder(boolean ascending) {
        if (treeColumnProperty.isSortingColumn()) {
            treeColumnProperty.setSortOrderDescending(!ascending);
        } else {
            tableModel.setSortOrderDescending(!ascending);
        }

        getSortedNodeTreeModel().setSortOrder(ascending);

        // to change sort icon
        treeTable.getTableHeader().repaint();
    }

    private synchronized SortedNodeTreeModel getSortedNodeTreeModel() {
        if (sortedNodeTreeModel == null) {
            sortedNodeTreeModel = new SortedNodeTreeModel();
        }

        return sortedNodeTreeModel;
    }

    /** This is internal accessible context for TreeTableView.
     * It delegates setAccessibleName and setAccessibleDescription methods to set these properties
     * in underlying TreeTable as well.
     */
    private class AccessibleTreeTableView extends AccessibleJScrollPane {
        AccessibleTreeTableView() {
        }

        @Override
        public void setAccessibleName(String accessibleName) {
            super.setAccessibleName(accessibleName);

            if (treeTable != null) {
                treeTable.getAccessibleContext().setAccessibleName(accessibleName);
            }
        }

        @Override
        public void setAccessibleDescription(String accessibleDescription) {
            super.setAccessibleDescription(accessibleDescription);

            if (treeTable != null) {
                treeTable.getAccessibleContext().setAccessibleDescription(accessibleDescription);
            }
        }
    }

    /* Horizontal scrolling support.
     */
    private final class ScrollListener extends ComponentAdapter implements PropertyChangeListener, ChangeListener {
        boolean movecorrection = false;

        ScrollListener() {
        }

        //Column width
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if (((TreeTable) treeTable).getTreeColumnIndex() == -1) {
                return;
            }

            if ("width".equals(evt.getPropertyName())) { // NOI18N

                if (!treeTable.equals(evt.getSource())) {
                    Dimension dim = hScrollBar.getPreferredSize();
                    dim.width = treeTable.getColumnModel().getColumn(((TreeTable) treeTable).getTreeColumnIndex())
                                         .getWidth();
                    hScrollBar.setPreferredSize(dim);
                    hScrollBar.revalidate();
                    hScrollBar.repaint();
                }

                revalidateScrollBar();
            } else if ("positionX".equals(evt.getPropertyName())) { // NOI18N
                revalidateScrollBar();
            } else if ("treeColumnIndex".equals(evt.getPropertyName())) { // NOI18N
                treeTable.getColumnModel().getColumn(((TreeTable) treeTable).getTreeColumnIndex())
                         .addPropertyChangeListener(listener);
            } else if ("column_moved".equals(evt.getPropertyName())) { // NOI18N

                int from = ((Integer) evt.getOldValue()).intValue();
                int to = ((Integer) evt.getNewValue()).intValue();

                if ((from == 0) || (to == 0)) {
                    if (movecorrection) {
                        movecorrection = false;
                    } else {
                        movecorrection = true;

                        // not allowed to move first, tree column
                        treeTable.getColumnModel().moveColumn(to, from);
                    }

                    return;
                }

                // module will be revalidated in NodeTableModel
                treeTable.getTableHeader().getColumnModel().getColumn(from).setModelIndex(from);
                treeTable.getTableHeader().getColumnModel().getColumn(to).setModelIndex(to);
                tableModel.moveColumn(from - 1, to - 1);
            }
        }

        //Viewport height
        @Override
        public void componentResized(ComponentEvent e) {
            revalidateScrollBar();
        }

        //ScrollBar change
        @Override
        public void stateChanged(ChangeEvent evt) {
            int value = hScrollBar.getModel().getValue();
            ((TreeTable) treeTable).setPositionX(value);
        }

        private void revalidateScrollBar() {
            if (!isDisplayable()) {
                return;
            }

            if (
                (treeTable.getColumnModel().getColumnCount() > 0) &&
                    (((TreeTable) treeTable).getTreeColumnIndex() >= 0)
            ) {
                int extentWidth = treeTable.getColumnModel().getColumn(((TreeTable) treeTable).getTreeColumnIndex())
                                           .getWidth();
                int maxWidth = tree.getPreferredSize().width;
                int extentHeight = scrollPane.getViewport().getSize().height;
                int maxHeight = tree.getPreferredSize().height;
                int positionX = ((TreeTable) treeTable).getPositionX();

                int value = Math.max(0, Math.min(positionX, maxWidth - extentWidth));

                boolean hsbvisible = hScrollBar.isVisible();
                boolean vsbvisible = scrollPane.getVerticalScrollBar().isVisible();
                int hsbheight = hsbvisible ? hScrollBar.getHeight() : 0;
                int vsbwidth = scrollPane.getVerticalScrollBar().getWidth();

                hScrollBar.setValues(value, extentWidth, 0, maxWidth);

                if (
                    hideHScrollBar || (maxWidth <= extentWidth) ||
                        (vsbvisible &&
                        ((maxHeight <= (extentHeight + hsbheight)) && (maxWidth <= (extentWidth + vsbwidth))))
                ) {
                    hScrollBar.setVisible(false);
                } else {
                    hScrollBar.setVisible(true);
                }
            }
        }
    }

    /** Scrollable (better say not scrollable) pane. Used as container for
     * left (controlling) and rigth (controlled) scroll panes.
     */
    private static final class CompoundScrollPane extends JPanel implements Scrollable {
        CompoundScrollPane() {
        }

        @Override
        public void setBorder(Border b) {
            //do nothing
        }

        @Override
        public boolean getScrollableTracksViewportWidth() {
            return true;
        }

        @Override
        public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
            return 10;
        }

        @Override
        public boolean getScrollableTracksViewportHeight() {
            return true;
        }

        @Override
        public Dimension getPreferredScrollableViewportSize() {
            return this.getPreferredSize();
        }

        @Override
        public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
            return 10;
        }
    }

    @Override
    public Insets getInsets() {
        Insets res = getInnerInsets();
        res = new Insets(res.top, res.left, res.bottom, res.right);
        if( null != searchpanel && searchpanel.isVisible() ) {
            res.bottom += searchpanel.getPreferredSize().height;
        }
        return res;
    }

    private Insets getInnerInsets() {
        Insets res = super.getInsets();
        if( null == res ) {
            res = new Insets(0,0,0,0);
        }
        return res;
    }

    private class SearchScrollPaneLayout extends ScrollPaneLayout {

        public SearchScrollPaneLayout() {
        }
        
        @Override
        public void layoutContainer( Container parent ) {
            super.layoutContainer(parent);
            if( null != searchpanel && searchpanel.isVisible() ) {
                Insets innerInsets = getInnerInsets();
                Dimension prefSize = searchpanel.getPreferredSize();
                searchpanel.setBounds(innerInsets.left, parent.getHeight()-innerInsets.bottom-prefSize.height,
                        parent.getWidth()-innerInsets.left-innerInsets.right, prefSize.height);
            }
        }
        
    }

    /** Invokes default action.
     */
    private class DefaultTreeAction implements ActionListener {
        DefaultTreeAction() {
        }

        /**
         * Invoked when an action occurs.
         */
        @Override
        public void actionPerformed(ActionEvent e) {
            if (treeTable.getSelectedColumn() != ((TreeTable) treeTable).getTreeColumnIndex()) {
                return;
            }

            Node[] nodes = manager.getSelectedNodes();

            if (nodes.length == 1) {
                Action a = nodes[0].getPreferredAction();

                if (a != null) {
                    if (a.isEnabled()) {
                        a.actionPerformed(new ActionEvent(nodes[0], ActionEvent.ACTION_PERFORMED, "")); // NOI18N
                    } else {
                        Utilities.disabledActionBeep();
                    }
                }
            }
        }
    }

    @Override
    Node getOriginalNode (Node n) {
        if (n instanceof SortedNodeTreeModel.SortedNode) {
            SortedNodeTreeModel.SortedNode sn = (SortedNodeTreeModel.SortedNode) n;
            return sn.getOriginalNode ();
        }
        return n;
    }

    /* Synchronizes selected nodes from the manager of this Explorer.
    */
    @Override
    protected void showSelection(TreePath[] treePaths) {
        TreePath [] modifiedTreePaths = new TreePath [treePaths.length];
        for (int i = 0; i < treePaths.length; i++) {
            TreePath tp = treePaths [i];
            Node o = ((VisualizerNode) tp.getLastPathComponent ()).node;
            TreePath mtp = getTreePath (getSortedNodeFromOriginal (o));
            modifiedTreePaths [i] = mtp;
        }
        super.showSelection (modifiedTreePaths);
    }

    @Override
    public void collapseNode(Node n) {
        super.collapseNode(getSortedNodeFromOriginal(n));
    }

    @Override
    public void expandNode(Node n) {
        super.expandNode(getSortedNodeFromOriginal(n));
    }

    @Override
    public boolean isExpanded(Node n) {
        return super.isExpanded(getSortedNodeFromOriginal(n));
    }

    private Node getSortedNodeFromOriginal (Node orig) {
        if (getSortedNodeTreeModel () != null) {
            if (getSortedNodeTreeModel ().original2filter != null) {
                SortedNodeTreeModel.SortedNode sn = getSortedNodeTreeModel ().original2filter.get (orig);
                if (sn != null) {
                    return sn;
                }
            }
        }
        return orig;
    }

    /* node tree model with added sorting support
     */
    private class SortedNodeTreeModel extends NodeTreeModel {
        private Node.Property sortedByProperty;
        private boolean sortAscending = true;
        private Comparator<Node> rowComparator;
        private boolean sortedByName = false;
        private Map<Node, SortedNode> original2filter = new WeakHashMap<Node, SortedNode> (11);

        @Override
        void setNode (Node root, TreeView.VisualizerHolder visHolder) {
            visHolder.clear ();
            original2filter.clear ();
            super.setNode (new SortedNode (root), null);
        }

        private class SortedNode extends FilterNode {
            public SortedNode (Node original) {
                super(original, original.isLeaf() ? Children.LEAF : new SortedChildren(original));
                original2filter.put (original, this);
            }
            public Node getOriginalNode () {
                return super.getOriginal ();
            }

            @Override
            protected NodeListener createNodeListener() {
                return new FilterNode.NodeAdapter(this) {
                    @Override
                    protected void propertyChange(FilterNode fn, PropertyChangeEvent ev) {
                        super.propertyChange(fn, ev);
                        if (ev.getPropertyName().equals(Node.PROP_LEAF)) {
                            final org.openide.nodes.Children[] newChildren = new org.openide.nodes.Children[1];
                            Children.MUTEX.readAccess(new Runnable() {

                                @Override
                                public void run() {
                                    boolean origIsLeaf = getOriginal().isLeaf();
                                    boolean thisIsLeaf = isLeaf();
                                    if (origIsLeaf && !thisIsLeaf) {
                                        newChildren[0] = Children.LEAF;
                                    } else if (!origIsLeaf && thisIsLeaf) {
                                        newChildren[0] = new SortedChildren(getOriginal());
                                    }
                                }
                            });

                            if (newChildren[0] != null) {
                                Children.MUTEX.postWriteRequest(
                                        new Runnable() {

                                            @Override
                                            public void run() {
                                                setChildren(newChildren[0]);
                                            }
                                        }
                                );
                            }
                        }
                        if (ev.getPropertyName().equals(Node.PROP_PARENT_NODE)) {
                            final Node node = (Node)ev.getSource();
                            if (node.getParentNode() == null) {
                                original2filter.remove(node);
                            }
                        }
                    }
                };
            }
        }

        private class SortedChildren extends FilterNode.Children {
            public SortedChildren (Node n) {
                super (n);
                sortNodes ();
            }

            @Override
            protected Node[] createNodes (Node key) {
                return new Node [] { new SortedNode (key) };
            }

            @Override
            protected void addNotify () {
                super.addNotify ();
                sortNodes ();
            }

            @Override
            protected void filterChildrenAdded (NodeMemberEvent ev) {
                super.filterChildrenAdded (ev);
                sortNodes ();
            }

            @Override
            protected void filterChildrenRemoved (NodeMemberEvent ev) {
                super.filterChildrenRemoved (ev);
                sortNodes ();
            }

            @Override
            protected void filterChildrenReordered (NodeReorderEvent ev) {
                super.filterChildrenReordered (ev);
                sortNodes ();
            }

            private void sortNodes() {
                Node[] origNodes = original.getChildren().getNodes();
                if (isSortingActive()) {
                    Node[] sortedNodes = new Node[origNodes.length];
                    System.arraycopy(origNodes, 0, sortedNodes, 0, origNodes.length);
                    Collections.sort(Arrays.asList(sortedNodes), getRowComparator());
                    setKeys(sortedNodes);
                } else {
                    setKeys(origNodes);
                }
            }
        }

        void setNoSorting() {
            setSortedByProperty(null);
            setSortedByName(false);
            sortingChanged();
        }

        boolean isSortingActive() {
            return ((sortedByProperty != null) || sortedByName);
        }

        void setSortedByProperty(Node.Property prop) {
            if (sortedByProperty == prop) {
                return;
            }

            sortedByProperty = prop;

            if (prop == null) {
                rowComparator = null;
            } else {
                sortedByName = false;
            }

            sortingChanged();
        }

        void setSortedByProperty(Node.Property prop, boolean ascending) {
            if ((sortedByProperty == prop) && (ascending == sortAscending)) {
                return;
            }

            sortedByProperty = prop;
            sortAscending = ascending;

            if (prop == null) {
                rowComparator = null;
            } else {
                sortedByName = false;
            }

            sortingChanged();
        }

        void setSortedByName(boolean sorted, boolean ascending) {
            if ((sortedByName == sorted) && (ascending == sortAscending)) {
                return;
            }

            sortedByName = sorted;
            sortAscending = ascending;

            if (sortedByName) {
                sortedByProperty = null;
            }

            sortingChanged();
        }

        void setSortedByName(boolean sorted) {
            sortedByName = sorted;

            if (sortedByName) {
                sortedByProperty = null;
            }

            sortingChanged();
        }

        void setSortOrder(boolean ascending) {
            if (ascending == sortAscending) {
                return;
            }

            sortAscending = ascending;
            sortingChanged();
        }

        private Node.Property getNodeProperty(Node node, Node.Property prop) {
            Node.PropertySet[] propsets = node.getPropertySets();

            for (int i = 0, n = propsets.length; i < n; i++) {
                Node.Property[] props = propsets[i].getProperties();

                for (int j = 0, m = props.length; j < m; j++) {
                    if (props[j].equals(prop)) {
                        return props[j];
                    }
                }
            }

            return null;
        }

        synchronized Comparator<Node> getRowComparator() {
            if (rowComparator == null) {
                rowComparator = new Comparator<Node>() {

                    @Override
                    @SuppressWarnings("unchecked")
                    public int compare(Node n1, Node n2) {
                        if (n1 == n2) {
                            return 0;
                        }

                        if ((n1 == null) && (n2 == null)) {
                            return 0;
                        }
                        if (n1 == null) {
                            return 1;
                        }
                        if (n2 == null) {
                            return -1;
                        }
                        if ((n1.getParentNode() == null) ||
                            (n2.getParentNode() == null)) {
                            // PENDING: throw Exception
                            Logger.getAnonymousLogger().warning("TTV.compare: Node " +
                                                                n1 + " or " + n2 +
                                                                " has no parent!");
                            return 0;
                        }
                        if (!(n1.getParentNode().equals(n2.getParentNode()))) {
                            // PENDING: throw Exception
                            Logger.getAnonymousLogger().warning("TTV.compare: Nodes " +
                                                                n1 + " and " +
                                                                n2 +
                                                                " has different parent!");
                            return 0;
                        }
                        int res;

                        if (sortedByName) {
                            res = n1.getDisplayName().compareTo(n2.getDisplayName());
                            return sortAscending ? res
                                                 : (-res);
                        }
                        Property p1 = getNodeProperty(n1, sortedByProperty);
                        Property p2 = getNodeProperty(n2, sortedByProperty);

                        if ((p1 == null) && (p2 == null)) {
                            return 0;
                        }
                        try {
                            if (p1 == null) {
                                res = -1;
                            } else if (p2 == null) {
                                res = 1;
                            } else {
                                Object v1 = p1.getValue();
                                Object v2 = p2.getValue();

                                if ((v1 == null) && (v2 == null)) {
                                    return 0;
                                } else if (v1 == null) {
                                    res = -1;
                                } else if (v2 == null) {
                                    res = 1;
                                } else {
                                    if ((v1.getClass() != v2.getClass()) ||
                                        !(v1 instanceof Comparable)) {
                                        v1 = v1.toString();
                                        v2 = v2.toString();
                                    }
                                    res = ((Comparable) v1).compareTo(v2);
                                }
                            }
                            return sortAscending ? res
                                                 : (-res);
                        }
                        catch (Exception ex) {
                            Logger.getLogger(TreeTableView.class.getName()).log(Level.WARNING, null, ex);
                            return 0;
                        }
                    }
                };
            }

            return rowComparator;
        }

        void sortingChanged() {
            // PENDING: remember the last sorting to avoid multiple sorting
            // remenber expanded folders
            TreeNode tn = (TreeNode) (this.getRoot());
            java.util.List<TreePath> list = new ArrayList<TreePath>();
            Enumeration<TreePath> en = TreeTableView.this.tree.getExpandedDescendants(new TreePath(tn));

            while ((en != null) && en.hasMoreElements()) {
                TreePath path = en.nextElement();

                // bugfix #32328, don't sort whole subtree but only expanded folders
                Node n = ((VisualizerNode) path.getLastPathComponent ()).node;
                Children children = n.getChildren();
                if (children instanceof SortedChildren) {
                    ((SortedChildren) children).sortNodes ();
                    list.add(path);
                } // else Children.LEAF
            }

            // expand again folders
            for (int i = 0; i < list.size(); i++) {
                TreeTableView.this.tree.expandPath(list.get(i));
            }
        }

        String getRootDescription() {
            if (getRoot() instanceof VisualizerNode) {
                //#37802 commenting this out - unfathomable why you would need
                //to sort the root's children in order to get its short 
                //description - Tim
                //                sortChildren ((VisualizerNode)getRoot ());
                return ((VisualizerNode) getRoot()).getShortDescription();
            }

            return ""; // NOI18N
        }

    }

    /* Cell renderer for sorting column header.
     */
    private class SortingHeaderRenderer extends DefaultTableCellRenderer {
        SortingHeaderRenderer() {
        }

        /** Overrides superclass method. */
        @Override
        public Component getTableCellRendererComponent(
            JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column
        ) {
            Component comp = defaultHeaderRenderer.getTableCellRendererComponent(
                    table, value, isSelected, hasFocus, row, column
                );

            if (comp instanceof JLabel) {
                if ((column == 0) && treeColumnProperty.isSortingColumn()) {
                    ((JLabel) comp).setIcon(getProperIcon(treeColumnProperty.isSortOrderDescending()));
                    ((JLabel) comp).setHorizontalTextPosition(SwingConstants.LEFT);

                    if( Utilities.isWindows() ) {
                        comp.setFont(getFont().deriveFont(Font.BOLD, getFont().getSize()));
                    } else {
                        // don't use deriveFont() - see #49973 for details
                        comp.setFont(new Font(getFont().getName(), Font.BOLD, getFont().getSize()));
                    }
                } else if ((column != 0) && ((tableModel.getVisibleSortingColumn() + 1) == column)) {
                    ((JLabel) comp).setIcon(getProperIcon(tableModel.isSortOrderDescending()));
                    ((JLabel) comp).setHorizontalTextPosition(SwingConstants.LEFT);

                    if( Utilities.isWindows() ) {
                        comp.setFont(getFont().deriveFont(Font.BOLD, getFont().getSize()));
                    } else {
                        // don't use deriveFont() - see #49973 for details
                        comp.setFont(new Font(getFont().getName(), Font.BOLD, getFont().getSize()));
                    }
                } else {
                    ((JLabel) comp).setIcon(null);
                }
            }

            return comp;
        }

        private ImageIcon getProperIcon(boolean descending) {
            if (descending) {
                return ImageUtilities.loadImageIcon(SORT_DESC_ICON, false);
            } else {
                return ImageUtilities.loadImageIcon(SORT_ASC_ICON, false);
            }
        }
    }
     // End of inner class SortingHeaderRenderer.

    private static class TreeColumnProperty {
        private Property p = null;

        TreeColumnProperty() {
        }

        void setProperty(Property p) {
            this.p = p;
        }

        boolean isComparable() {
            if (p == null) {
                return false;
            }

            Object o = p.getValue(NodeTableModel.ATTR_COMPARABLE_COLUMN);

            if ((o != null) && o instanceof Boolean) {
                return ((Boolean) o).booleanValue();
            }

            return false;
        }

        boolean isSortingColumn() {
            if (p == null) {
                return false;
            }

            Object o = p.getValue(NodeTableModel.ATTR_SORTING_COLUMN);

            if ((o != null) && o instanceof Boolean) {
                return ((Boolean) o).booleanValue();
            }

            return false;
        }

        void setSortingColumn(boolean sorting) {
            if (p == null) {
                return;
            }

            p.setValue(NodeTableModel.ATTR_SORTING_COLUMN, sorting ? Boolean.TRUE : Boolean.FALSE);
        }

        boolean isSortOrderDescending() {
            if (p == null) {
                return false;
            }

            Object o = p.getValue(NodeTableModel.ATTR_DESCENDING_ORDER);

            if ((o != null) && o instanceof Boolean) {
                return ((Boolean) o).booleanValue();
            }

            return false;
        }

        void setSortOrderDescending(boolean descending) {
            if (p == null) {
                return;
            }

            p.setValue(NodeTableModel.ATTR_DESCENDING_ORDER, descending ? Boolean.TRUE : Boolean.FALSE);
        }
    }

    /* For testing - use internal execution
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                Node n = //new org.netbeans.core.ModuleNode();
                    RepositoryNodeFactory.getDefault().repository(DataFilter.ALL);

                org.openide.explorer.ExplorerManager em = new org.openide.explorer.ExplorerManager();
                em.setRootContext(n);

                org.openide.explorer.ExplorerPanel ep = new org.openide.explorer.ExplorerPanel(em);
                ep.setLayout (new BorderLayout ());
                ep.setBorder(new EmptyBorder(20, 20, 20, 20));

                TreeTableView ttv = new TreeTableView();
                ttv.setRootVisible(false);
                ttv.setPopupAllowed(true);
                ttv.setDefaultActionAllowed(true);
                ttv.setVerticalScrollBarPolicy( JScrollPane.VERTICAL_SCROLLBAR_ALWAYS );
                ttv.setHorizontalScrollBarPolicy( JScrollPane.HORIZONTAL_SCROLLBAR_NEVER );

                org.openide.nodes.PropertySupport.ReadOnly prop2
                    = new org.openide.nodes.PropertySupport.ReadOnly (
                            "name", // NOI18N
                            String.class,
                            "name",
                            "Name Tooltip"
                        ) {
                            public Object getValue () {
                                return null;
                            }

                        };
                //prop2.setValue( "InvisibleInTreeTableView", Boolean.TRUE );
                prop2.setValue( "SortingColumnTTV", Boolean.TRUE );
                prop2.setValue( "DescendingOrderTTV", Boolean.TRUE );
                prop2.setValue( "ComparableColumnTTV", Boolean.TRUE );

                ttv.setProperties(
    //                    n.getChildren().getNodes()[0].getPropertySets()[0].getProperties());
                    new Property[]{
                        new org.openide.nodes.PropertySupport.ReadWrite (
                            "hidden", // NOI18N
                            Boolean.TYPE,
                            "hidden",
                            "Hidden tooltip"
                        ) {
                            public Object getValue () {
                                return null;
                            }

                            public void setValue (Object o) {
                            }
                        },
                        prop2,
                        new org.openide.nodes.PropertySupport.ReadOnly (
                            "template", // NOI18N
                            Boolean.TYPE,
                            "template",
                            "Template Tooltip"
                        ) {
                            public Object getValue () {
                                return null;
                            }

                        }

                    }
                );
                ttv.setTreePreferredWidth(200);

                ttv.setTableColumnPreferredWidth(0, 60);
                ttv.setTableColumnPreferredWidth(1, 150);
                ttv.setTableColumnPreferredWidth(2, 100);


                ep.add("Center", ttv);
                ep.open();
            }
        });
    }
    */
}
