/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Contributor(s):
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 *
 * Contributor(s): Soot Phengsy
 */

package org.netbeans.swing.dirchooser;

import java.util.logging.Level;
import javax.swing.*;
import javax.swing.Timer;
import javax.swing.event.TreeExpansionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.filechooser.*;
import javax.swing.plaf.basic.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.security.AccessControlException;
import java.text.MessageFormat;
import java.util.*;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.border.EmptyBorder;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.plaf.ActionMapUIResource;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.UIResource;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreeSelectionModel;
import org.netbeans.swing.dirchooser.spi.CustomDirectoryProvider;
import org.openide.awt.HtmlRenderer;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.NbPreferences;
import org.openide.util.RequestProcessor;
import org.openide.util.Utilities;


/**
 * An implementation of a customized filechooser.
 *
 * @author Soot Phengsy, inspired by Jeff Dinkins' Swing version
 */
public class DirectoryChooserUI extends BasicFileChooserUI {

    private static final Dimension horizontalStrut1 = new Dimension(25, 1);
    private static final Dimension verticalStrut1  = new Dimension(1, 4);
    private static final Dimension verticalStrut2  = new Dimension(1, 6);
    private static final Dimension verticalStrut3  = new Dimension(1, 8);
    private static Dimension PREF_SIZE = new Dimension(425, 245);
    private static Dimension MIN_SIZE = new Dimension(425, 245);
    private static Dimension TREE_PREF_SIZE = new Dimension(380, 230);
    private static final int ACCESSORY_WIDTH = 250;
    
    private static final Logger LOG = Logger.getLogger(DirectoryChooserUI.class.getName());

    private static final String TIMEOUT_KEY="nb.fileChooser.timeout"; // NOI18N

    private JPanel centerPanel;
    
    private JLabel lookInComboBoxLabel;
    
    private JComboBox directoryComboBox;
    
    private DirectoryComboBoxModel directoryComboBoxModel;
    
    private ActionListener directoryComboBoxAction = new DirectoryComboBoxAction();
    
    private FilterTypeComboBoxModel filterTypeComboBoxModel;
    
    private JTextField filenameTextField;
    
    private JComponent placesBar;
    private boolean placesBarFailed = false;
    
    private JButton approveButton;
    private JButton cancelButton;
    
    private JPanel buttonPanel;
    private JPanel bottomPanel;
    
    private JComboBox filterTypeComboBox;
    
    private int lookInLabelMnemonic = 0;
    private String lookInLabelText = null;
    private String saveInLabelText = null;
    
    private int fileNameLabelMnemonic = 0;
    private String fileNameLabelText = null;
    
    private int filesOfTypeLabelMnemonic = 0;
    private String filesOfTypeLabelText = null;
    
    private String upFolderToolTipText = null;
    private String upFolderAccessibleName = null;
    
    private String newFolderToolTipText = null;
    
    private String homeFolderTooltipText = null;
    
    private Action newFolderAction = new NewDirectoryAction();
    
    private BasicFileView fileView = new DirectoryChooserFileView();
    
    private static JTree tree;
    
    private DirectoryTreeModel model;
    
    private DirectoryNode newFolderNode;
    
    private JComponent treeViewPanel;
    
    private InputBlocker blocker;
    
    private static JFileChooser fileChooser;
    
    private boolean changeDirectory = true;
    
    private boolean showPopupCompletion = false;
    
    private boolean addNewDirectory = false;
    
    private JPopupMenu popupMenu;
    
    private FileCompletionPopup completionPopup;
    
    private RequestProcessor.Task updateWorker;
    
    private boolean useShellFolder = false;
    
    private JButton upFolderButton;
    private JButton newFolderButton;
    
    private JComponent topCombo, topComboWrapper, topToolbar;
    private JPanel slownessPanel;

    private CustomDirectoryProvider customDirectoryProvider;

    public static ComponentUI createUI(JComponent c) {
        return new DirectoryChooserUI((JFileChooser) c);
    }

    public DirectoryChooserUI(JFileChooser filechooser) {
        super(filechooser);

        Collection<? extends CustomDirectoryProvider> directoryProviders =
                Lookup.getDefault().lookupAll(CustomDirectoryProvider.class);

        customDirectoryProvider = null;
        for (CustomDirectoryProvider directoryProvider : directoryProviders) {
            if (directoryProvider.isEnabled()) {
                this.customDirectoryProvider = directoryProvider;
                break;
            }
        }
    }
    
    @Override
    public void installUI(JComponent c) {
        super.installUI(c);
    }
    
    @Override
    public void uninstallComponents(JFileChooser fc) {
        fc.removeAll();
        super.uninstallComponents(fc);
    }
    
    @Override
    public void installComponents(JFileChooser fc) {
        fileChooser = fc;
        
        fc.setFocusCycleRoot(true);
        fc.setBorder(new EmptyBorder(4, 10, 10, 10));
        fc.setLayout(new BorderLayout(8, 8));
        
        updateUseShellFolder();
        createCenterPanel(fc);
        fc.add(centerPanel, BorderLayout.CENTER);
        
        if (fc.isMultiSelectionEnabled()) {
            setFileName(getStringOfFileNames(fc.getSelectedFiles()));
        } else {
            setFileName(getStringOfFileName(fc.getSelectedFile()));
        }
        
        if(fc.getControlButtonsAreShown()) {
            addControlButtons();
        }
        
        createPopup();
    }

    @Override
    public String getDialogTitle(JFileChooser fc) {
        String title = super.getDialogTitle(fc);
        fc.getAccessibleContext().setAccessibleDescription(title);
        return title;
    }
    
    private void updateUseShellFolder() {
        // Decide whether to use the ShellFolder class to populate shortcut
        // panel and combobox.
        
        Boolean prop =
                (Boolean)fileChooser.getClientProperty(DelegatingChooserUI.USE_SHELL_FOLDER);
        if (prop != null) {
            useShellFolder = prop.booleanValue();
        } else {
            // See if FileSystemView.getRoots() returns the desktop folder,
            // i.e. the normal Windows hierarchy.
            useShellFolder = false;
            File[] roots = fileChooser.getFileSystemView().getRoots();
            if (roots != null && roots.length == 1) {
                File[] cbFolders = getShellFolderRoots();
                if (cbFolders != null && cbFolders.length > 0 && roots[0] == cbFolders[0]) {
                    useShellFolder = true;
                }
            }
        }
        
        if (Utilities.isWindows()) {
            if (useShellFolder) {
                if (placesBar == null) {
                    placesBar = getPlacesBar();
                }
                if (placesBar != null) {
                    fileChooser.add(placesBar, BorderLayout.BEFORE_LINE_BEGINS);
                    if (placesBar instanceof PropertyChangeListener) {
                        fileChooser.addPropertyChangeListener((PropertyChangeListener)placesBar);
                    }
                }
            } else {
                if (placesBar != null) {
                    fileChooser.remove(placesBar);
                    if (placesBar instanceof PropertyChangeListener) {
                        fileChooser.removePropertyChangeListener((PropertyChangeListener)placesBar);
                    }
                    placesBar = null;
                }
            }
        }
    }

    /** Returns instance of WindowsPlacesBar class or null in case of failure
     */
    private JComponent getPlacesBar () {
        if (placesBarFailed) {
            return null;
        }
        try {
            Class<?> clazz = Class.forName("sun.swing.WindowsPlacesBar");
            Class[] params = new Class[] { JFileChooser.class, Boolean.TYPE };
            Constructor<?> constr = clazz.getConstructor(params);
            return (JComponent)constr.newInstance(fileChooser, isXPStyle().booleanValue());
        } catch (Exception exc) {
            // reflection not succesfull, just log the exception and return null
            Logger.getLogger(DirectoryChooserUI.class.getName()).log(
                    Level.FINE, "WindowsPlacesBar class can't be instantiated.", exc);
            placesBarFailed = true;
            return null;
        }
    }
    
    /** Reflection alternative of
     * sun.awt.shell.ShellFolder.getShellFolder(file)
     */
    private File getShellFolderForFile (File file) {
        try {
            Class<?> clazz = Class.forName("sun.awt.shell.ShellFolder");
            return (File) clazz.getMethod("getShellFolder", File.class).invoke(null, file);
        } catch (Exception exc) {
            // reflection not succesfull, just log the exception and return null
            Logger.getLogger(DirectoryChooserUI.class.getName()).log(
                    Level.FINE, "ShellFolder can't be used.", exc);
            return null;
        }
    }

    /** Reflection alternative of
     * sun.awt.shell.ShellFolder.getShellFolder(dir).getLinkLocation()
     */
    private File getShellFolderForFileLinkLoc (File file) {
        try {
            Class<?> clazz = Class.forName("sun.awt.shell.ShellFolder");
            Object sf = clazz.getMethod("getShellFolder", File.class).invoke(null, file);
            return (File) clazz.getMethod("getLinkLocation").invoke(sf);
        } catch (Exception exc) {
            // reflection not succesfull, just log the exception and return null
            Logger.getLogger(DirectoryChooserUI.class.getName()).log(
                    Level.FINE, "ShellFolder can't be used.", exc);
            return null;
        }
        
    }
    
    /** Reflection alternative of
     * sun.awt.shell.ShellFolder.get("fileChooserComboBoxFolders")
     */ 
    private File[] getShellFolderRoots () {
        try {
            Class<?> clazz = Class.forName("sun.awt.shell.ShellFolder");
            return (File[]) clazz.getMethod("get", String.class).invoke(null, "fileChooserComboBoxFolders");
        } catch (Exception exc) {
            // reflection not succesfull, just log the exception and return null
            Logger.getLogger(DirectoryChooserUI.class.getName()).log(
                    Level.FINE, "ShellFolder can't be used.", exc);
            return null;
        }
    }
    
    private void createBottomPanel(JFileChooser fc) {
        bottomPanel = new JPanel();
        bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.LINE_AXIS));
        JPanel labelPanel = new JPanel();
        labelPanel.setLayout(new BoxLayout(labelPanel, BoxLayout.PAGE_AXIS));
        labelPanel.add(Box.createRigidArea(verticalStrut1));
        
        JLabel fnl = new JLabel(fileNameLabelText);
        fnl.setDisplayedMnemonic(fileNameLabelMnemonic);
        fnl.setAlignmentY(0);
        labelPanel.add(fnl);
        
        labelPanel.add(Box.createRigidArea(new Dimension(1,12)));
        
        JLabel ftl = new JLabel(filesOfTypeLabelText);
        ftl.setDisplayedMnemonic(filesOfTypeLabelMnemonic);
        labelPanel.add(ftl);
        
        bottomPanel.add(labelPanel);
        bottomPanel.add(Box.createRigidArea(new Dimension(15, 0)));
        
        JPanel fileAndFilterPanel = new JPanel();
        fileAndFilterPanel.add(Box.createRigidArea(verticalStrut3));
        fileAndFilterPanel.setLayout(new BoxLayout(fileAndFilterPanel, BoxLayout.Y_AXIS));
        
        filenameTextField = new JTextField(24) {
            @Override
            public Dimension getMaximumSize() {
                return new Dimension(Short.MAX_VALUE, super.getPreferredSize().height);
            }
        };
        
        filenameTextField.getDocument().addDocumentListener(new DocumentListener() {
            @Override
            public void insertUpdate(DocumentEvent e) {
                updateCompletions();
            }
            @Override
            public void removeUpdate(DocumentEvent e) {
                updateCompletions();
            }
            @Override
            public void changedUpdate(DocumentEvent e) {}
        });
        
        filenameTextField.addKeyListener(new TextFieldKeyListener());
        
        fnl.setLabelFor(filenameTextField);
        filenameTextField.addFocusListener(
                new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                if (!getFileChooser().isMultiSelectionEnabled()) {
                    tree.clearSelection();
                }
            }
        });
        
        // disable TAB focus transfer, we need it for completion
        Set<AWTKeyStroke> tKeys = filenameTextField.getFocusTraversalKeys(java.awt.KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS);
        Set<AWTKeyStroke> newTKeys = new HashSet<>(tKeys);
        newTKeys.remove(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_TAB, 0));
        // #107305: enable at least Ctrl+TAB if we have TAB for completion
        newTKeys.add(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_TAB, KeyEvent.CTRL_DOWN_MASK));
        filenameTextField.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, newTKeys);
        
        fileAndFilterPanel.add(filenameTextField);
        fileAndFilterPanel.add(Box.createRigidArea(verticalStrut3));
        
        filterTypeComboBoxModel = createFilterComboBoxModel();
        fc.addPropertyChangeListener(filterTypeComboBoxModel);
        filterTypeComboBox = new JComboBox(filterTypeComboBoxModel);
        ftl.setLabelFor(filterTypeComboBox);
        filterTypeComboBox.setRenderer(createFilterComboBoxRenderer());
        fileAndFilterPanel.add(filterTypeComboBox);
        
        bottomPanel.add(fileAndFilterPanel);
        bottomPanel.add(Box.createRigidArea(horizontalStrut1));
        createButtonsPanel(fc);
    }
    
    private void createButtonsPanel(JFileChooser fc) {
        buttonPanel = new JPanel();
        buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.Y_AXIS));
        
        approveButton = new JButton(getApproveButtonText(fc)) {
            @Override
            public Dimension getMaximumSize() {
                return approveButton.getPreferredSize().width > cancelButton.getPreferredSize().width ?
                    approveButton.getPreferredSize() : cancelButton.getPreferredSize();
            }
        };
        // #107791: No mnemonics desirable on Mac 
        if (!Utilities.isMac()) {
            approveButton.setMnemonic(getApproveButtonMnemonic(fc));
        }
        approveButton.addActionListener(getApproveSelectionAction());
        approveButton.setToolTipText(getApproveButtonToolTipText(fc));
        buttonPanel.add(Box.createRigidArea(verticalStrut1));
        buttonPanel.add(approveButton);
        buttonPanel.add(Box.createRigidArea(verticalStrut2));
        
        cancelButton = new JButton(cancelButtonText) {
            @Override
            public Dimension getMaximumSize() {
                return approveButton.getPreferredSize().width > cancelButton.getPreferredSize().width ?
                    approveButton.getPreferredSize() : cancelButton.getPreferredSize();
            }
        };
        // #107791: No mnemonics desirable on Mac 
        if (!Utilities.isMac()) {
            cancelButton.setMnemonic(cancelButtonMnemonic);
        }
        cancelButton.setToolTipText(cancelButtonToolTipText);
        cancelButton.addActionListener(getCancelSelectionAction());
        buttonPanel.add(cancelButton);

        //TODO initial approve button disabled code started
        if (customDirectoryProvider != null)
            approveButton.setEnabled(false);
        //TODO initial approve button disabled code ended
    }
    
    private void createCenterPanel(final JFileChooser fc) {
        centerPanel = new JPanel(new BorderLayout());
        treeViewPanel = createTree();
        treeViewPanel.setPreferredSize(TREE_PREF_SIZE);
        JPanel treePanel = new JPanel();
        treePanel.setLayout(new BorderLayout());
        JComponent accessory = fc.getAccessory();
        topToolbar = createTopToolbar();
        topCombo = createTopCombo(fc);
        topComboWrapper = new JPanel(new BorderLayout());
        topComboWrapper.add(topCombo, BorderLayout.CENTER);
        if (accessory == null) {
            topComboWrapper.add(topToolbar, BorderLayout.EAST);
        }
        treePanel.add(topComboWrapper, BorderLayout.NORTH);
        treePanel.add(treeViewPanel, BorderLayout.CENTER);
        centerPanel.add(treePanel, BorderLayout.CENTER);
        // control width of accessory panel, don't allow to jump (change width)
        JPanel wrapAccessory = new JPanel() {
            private Dimension prefSize = new Dimension(ACCESSORY_WIDTH, 0);
            private Dimension minSize = new Dimension(ACCESSORY_WIDTH, 0);
            
            @Override
            public Dimension getMinimumSize () {
                if (fc.getAccessory() != null) {
                    minSize.height = getAccessoryPanel().getMinimumSize().height;
                    return minSize;
                }
                return super.getMinimumSize();
            }
            @Override
            public Dimension getPreferredSize () {
                if (fc.getAccessory() != null) {
                    Dimension origPref = getAccessoryPanel().getPreferredSize();
                    LOG.fine("AccessoryWrapper.getPreferredSize: orig pref size: " + origPref);
                    
                    prefSize.height = origPref.height;
                    
                    prefSize.width = Math.max(prefSize.width, origPref.width);
                    int centerW = centerPanel.getWidth();
                    if (centerW != 0 && prefSize.width > centerW / 2) {
                        prefSize.width = centerW / 2;
                    }
                    LOG.fine("AccessoryWrapper.getPreferredSize: resulting pref size: " + prefSize);
                    
                    return prefSize;
                }
                return super.getPreferredSize();
            }
        };
        wrapAccessory.setLayout(new BorderLayout());
        JPanel accessoryPanel = getAccessoryPanel();
        if (accessory != null) {
            accessoryPanel.add(topToolbar, BorderLayout.NORTH);
            accessoryPanel.add(accessory, BorderLayout.CENTER);
        }
        wrapAccessory.add(accessoryPanel, BorderLayout.CENTER);
        centerPanel.add(wrapAccessory, BorderLayout.EAST);
        createBottomPanel(fc);
        centerPanel.add(bottomPanel, BorderLayout.SOUTH);
    }
    
    
    
    private JComponent createTopCombo(JFileChooser fc) {
        JPanel panel = new JPanel();
        if (fc.getAccessory() != null) {
            panel.setBorder(BorderFactory.createEmptyBorder(4, 0, 8, 0));
        } else {
            panel.setBorder(BorderFactory.createEmptyBorder(6, 0, 10, 0));
        }
        panel.setLayout(new BorderLayout());
        
        Box labelBox = Box.createHorizontalBox();
        
        lookInComboBoxLabel = new JLabel(lookInLabelText);
        lookInComboBoxLabel.setDisplayedMnemonic(lookInLabelMnemonic);
        lookInComboBoxLabel.setAlignmentX(JComponent.LEFT_ALIGNMENT);
        lookInComboBoxLabel.setAlignmentY(JComponent.CENTER_ALIGNMENT);
        
        labelBox.add(lookInComboBoxLabel);
        labelBox.add(Box.createRigidArea(new Dimension(9,0)));
        
        // fixed #97525, made the height of the
        // combo box bigger.
        directoryComboBox = new JComboBox() {
            @Override
            public Dimension getMinimumSize() {
                Dimension d = super.getMinimumSize();
                d.width = 60;
                return d;
            }
            
            @Override
            public Dimension getPreferredSize() {
                Dimension d = super.getPreferredSize();
                // Must be small enough to not affect total width and height.
                d.height = 24;
                d.width = 150;
                return d;
            }
        };
        directoryComboBox.putClientProperty( "JComboBox.lightweightKeyboardNavigation", "Lightweight" );
        lookInComboBoxLabel.setLabelFor(directoryComboBox);
        directoryComboBoxModel = createDirectoryComboBoxModel(fc);
        directoryComboBox.setModel(directoryComboBoxModel);
        directoryComboBox.addActionListener(directoryComboBoxAction);
        directoryComboBox.setRenderer(createDirectoryComboBoxRenderer(fc));
        directoryComboBox.setAlignmentX(JComponent.LEFT_ALIGNMENT);
        directoryComboBox.setAlignmentY(JComponent.CENTER_ALIGNMENT);
        directoryComboBox.setMaximumRowCount(8);
        
        panel.add(labelBox, BorderLayout.WEST);
        panel.add(directoryComboBox, BorderLayout.CENTER);
        
        return panel;
    }
    
    private JComponent createTopToolbar() {
        JToolBar topPanel = new JToolBar();
        topPanel.setFloatable(false);
        
        if (Utilities.isWindows()) {
            topPanel.putClientProperty("JToolBar.isRollover", Boolean.TRUE);
        }
        
        upFolderButton = new JButton(getChangeToParentDirectoryAction());
        upFolderButton.setText(null);
        // fixed bug #97049
        final boolean isMac = Utilities.isMac();
        Icon upFolderIcon = null;
        if (!isMac) {
            upFolderIcon = UIManager.getIcon("FileChooser.upFolderIcon");
        }
        // on Mac all icons from UIManager are the same, some default, so load our own.
        // it's also fallback if icon from UIManager not found, may happen
        if (isMac || upFolderIcon == null) {
            upFolderIcon = ImageUtilities.image2Icon(ImageUtilities.loadImage("/org/netbeans/swing/dirchooser/resources/upFolderIcon.gif", false));
        }
        upFolderButton.setIcon(upFolderIcon);
        upFolderButton.setToolTipText(upFolderToolTipText);
        upFolderButton.getAccessibleContext().setAccessibleName(upFolderAccessibleName);
        upFolderButton.setAlignmentX(JComponent.RIGHT_ALIGNMENT);
        upFolderButton.setAlignmentY(JComponent.CENTER_ALIGNMENT);
        
        if(useShellFolder) {
            upFolderButton.setFocusPainted(false);
        }
        
        topPanel.add(upFolderButton);
        topPanel.add(Box.createRigidArea(new Dimension(2, 0)));
        
        // no home on Win platform
        if (!Utilities.isWindows()) {
            JButton homeButton = new JButton(getGoHomeAction());
            Icon homeIcon = null;
            if (!isMac) {
                homeIcon = UIManager.getIcon("FileChooser.homeFolderIcon");
            }
            if (isMac || homeIcon == null) {
                homeIcon = ImageUtilities.image2Icon(ImageUtilities.loadImage("/org/netbeans/swing/dirchooser/resources/homeIcon.gif", false));
            }
            homeButton.setIcon(homeIcon);
            homeButton.setText(null);
            
            String tooltip = homeButton.getToolTipText();
            if (tooltip == null) {
                tooltip = homeFolderTooltipText;
                if (tooltip == null) {
                    tooltip = NbBundle.getMessage(DirectoryChooserUI.class,
                            "TLTP_HomeFolder");
                }
                homeButton.setToolTipText( tooltip );
            }

            topPanel.add(homeButton);
        }
        
        newFolderButton = new JButton(newFolderAction);
        newFolderButton.setText(null);
        // fixed bug #97049
        Icon newFolderIcon = null;
        if (!isMac) {
            newFolderIcon = UIManager.getIcon("FileChooser.newFolderIcon");
        }
        // on Mac all icons from UIManager are the same, some default, so load our own.
        // it's also fallback if icon from UIManager not found, may happen
        if (isMac || newFolderIcon == null) {
            newFolderIcon = ImageUtilities.image2Icon(ImageUtilities.loadImage("/org/netbeans/swing/dirchooser/resources/newFolderIcon.gif", false));
        }
        newFolderButton.setIcon(newFolderIcon);
        newFolderButton.setToolTipText(newFolderToolTipText);
        newFolderButton.setAlignmentX(JComponent.RIGHT_ALIGNMENT);
        newFolderButton.setAlignmentY(JComponent.CENTER_ALIGNMENT);
        
        if(useShellFolder) {
            newFolderButton.setFocusPainted(false);
        }
        
        topPanel.add(newFolderButton);
        topPanel.add(Box.createRigidArea(new Dimension(2, 0)));
        
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        panel.setBorder(BorderFactory.createEmptyBorder(4, 9, 8, 0));
        panel.add(topPanel, BorderLayout.CENTER);
        
        return panel;
    }
    
    private JComponent createTree() {
        final DirectoryHandler dirHandler = createDirectoryHandler(fileChooser);
        // #106011: don't show "colors, food, sports" sample model after init :-)
        tree = new JTree(new Object[0]) {

            @Override
            protected void processMouseEvent(MouseEvent e) {
                dirHandler.preprocessMouseEvent(e);
                super.processMouseEvent(e);
            }

            // For speed (#127170):
            @Override
            public boolean isLargeModel() {
                return true;
            }

            // To work with different font sizes (#106223); see: http://www.javalobby.org/java/forums/t19562.html
            private boolean firstPaint = true;
            @Override
            public void setFont(Font f) {
                firstPaint = true;
                super.setFont(f);
            }
            @Override
            public void paint(Graphics g) {
                if (firstPaint) {
                    g.setFont(getFont());
                    setRowHeight(Math.max(/* icon height plus insets? */17, g.getFontMetrics().getHeight()));
                    firstPaint = false;
                    // Setting the fixed height will generate another paint request, no need to complete this one
                    return;
                }
                super.paint(g);
            }

        };
        // #105642: start with right content in tree 
        File curDir = fileChooser.getCurrentDirectory();
        if (curDir == null) {
            curDir = fileChooser.getFileSystemView().getRoots()[0];
        }
        updateTree(curDir);
        
        tree.setFocusable(true);
        tree.setOpaque(true);
        tree.setRootVisible(false);
        tree.setShowsRootHandles(true);
        tree.setToggleClickCount(0);
        tree.addTreeExpansionListener(new TreeExpansionHandler());
        TreeKeyHandler keyHandler = new TreeKeyHandler();
        tree.addKeyListener(keyHandler);
        tree.addFocusListener(keyHandler);
        tree.addMouseListener(dirHandler);
        tree.addFocusListener(dirHandler);
        tree.addTreeSelectionListener(dirHandler);
        
        if(fileChooser.isMultiSelectionEnabled()) {
            tree.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
        } else {
            tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
        }
        
        TreeCellEditor tce = new DirectoryCellEditor(tree, fileChooser, new JTextField());
        tree.setCellEditor(tce);
        tce.addCellEditorListener(dirHandler);
        tree.setCellRenderer(new DirectoryTreeRenderer());
        JScrollPane scrollBar = new JScrollPane(tree);
        scrollBar.setViewportView(tree);
        tree.setInvokesStopCellEditing(true);
        
        return scrollBar;
    }

    /** 
     * Handles keyboard quick search in tree and delete action.
     */
    class TreeKeyHandler extends KeyAdapter implements FocusListener {
        
        StringBuffer searchBuf = new StringBuffer();
        
        java.util.List<TreePath> paths;
                
        @Override
        public void keyPressed(KeyEvent evt) {
            if(evt.getKeyCode() == KeyEvent.VK_DELETE) {
                deleteAction();
            }
            
            // F2 as rename shortcut
            if (evt.getKeyCode() == KeyEvent.VK_F2) {
                DirectoryNode node = (DirectoryNode)tree.getLastSelectedPathComponent();
                if (node != null) {
                    applyEdit(node);
                }
            }
            
            if (isCharForSearch(evt)) {
                evt.consume();
            } else {
                resetBuffer();
            }

            // #105527: keyboard invocation of tree's popup menu 
            if ((evt.getKeyCode() == KeyEvent.VK_F10) && evt.isShiftDown() && !popupMenu.isShowing()) {
                JTree tree = (JTree) evt.getSource();
                int selRow = tree.getLeadSelectionRow();
                if (selRow >= 0) {
                    Rectangle bounds = tree.getRowBounds(selRow);
                    popupMenu.show(tree, bounds.x + bounds.width / 2, bounds.y + bounds.height * 3 / 5);
                    evt.consume();
                }
            }
        }
        
        @Override
        public void keyTyped(KeyEvent evt) {
            char keyChar = evt.getKeyChar();
            if (isCharForSearch(evt)) {
                if (paths == null) {
                    paths = getVisiblePaths();
                }
                searchBuf.append(keyChar);
                String searchedText = searchBuf.toString().toLowerCase();
                String curFileName = null;
                for (TreePath path : paths) {
                    curFileName = fileChooser.getName(((DirectoryNode) path.getLastPathComponent()).getFile());
                    if (curFileName != null && curFileName.toLowerCase().startsWith(searchedText)) {
                        tree.makeVisible(path);
                        tree.scrollPathToVisible(path);
                        tree.setSelectionPath(path);
                        break;
                    }
                }
            } else {
                resetBuffer();
            }
        }
        
        @Override
        public void focusGained(FocusEvent e) {
            resetBuffer();
        }

        @Override
        public void focusLost(FocusEvent e) {
            resetBuffer();
        }
        
        private boolean isCharForSearch (KeyEvent evt) {
            char ch = evt.getKeyChar();
            // refuse backspace key
            if ((int)ch == 8) {
                return false;
            }
            // #110975: refuse modifiers
            if (evt.getModifiers() != 0) {
                return false;
            }
            return (Character.isJavaIdentifierPart(ch) && !Character.isIdentifierIgnorable(ch)) 
                    || Character.isSpaceChar(ch);
        }
        
        private void resetBuffer () {
            searchBuf.delete(0, searchBuf.length());
            paths = null;
        }
        
    }
    
    private java.util.List<TreePath> getVisiblePaths () {
        int rowCount = tree.getRowCount();
        DirectoryNode node = null;
        java.util.List<TreePath> result = new ArrayList<TreePath>(rowCount);
        for (int i = 0; i < rowCount; i++) {
            result.add(tree.getPathForRow(i));
        }
        return result;
    }
    
    private void createPopup() {
        popupMenu = new JPopupMenu();
        JMenuItem item1 = new JMenuItem(getBundle().getString("LBL_NewFolder"));
        item1.addActionListener(newFolderAction);
        
        JMenuItem item2 = new JMenuItem(getBundle().getString("LBL_Rename"));
        item2.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                DirectoryNode node = (DirectoryNode)tree.getLastSelectedPathComponent();
                applyEdit(node);
            }
        });
        
        JMenuItem item3 = new JMenuItem(getBundle().getString("LBL_Delete"));
        item3.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                deleteAction();
            }
        });
        
        popupMenu.add(item1);
        popupMenu.add(item2);
        popupMenu.add(item3);
            }

    // remove multiple directories
    private void deleteAction() {
        // fixed #97079 to be able to delete one or more folders
        final TreePath[] nodePath = tree.getSelectionPaths();
        
        if(nodePath == null) {
            return;
        }
        String message = "";
        
        if(nodePath.length == 1) {
            
            File file = ((DirectoryNode)nodePath[0].getLastPathComponent()).getFile();
            // Don't do anything if it's a special file
            if(!canWrite(file)) {
                return;
            }
            message = MessageFormat.format(getBundle().getString("MSG_Delete"), file.getName());
        } else {
            message = MessageFormat.format(getBundle().getString("MSG_Delete_Multiple"), nodePath.length);
        }
        
        int answer = JOptionPane.showConfirmDialog(fileChooser, message , getBundle().getString("MSG_Confirm"), JOptionPane.YES_NO_OPTION);
        
        if (answer == JOptionPane.YES_OPTION) {
            
            RequestProcessor.getDefault().post(new Runnable() {
                DirectoryNode node;
                ArrayList<File> list = new ArrayList<>();
                int cannotDelete;
                ArrayList<DirectoryNode> nodes2Remove = new ArrayList<>(nodePath.length);

                @Override
                public void run() {
                    if (!EventQueue.isDispatchThread()) {
                        // first pass, out of EQ thread, deletes files
                        setCursor(fileChooser, Cursor.WAIT_CURSOR);
                        cannotDelete = 0;
                        for(int i = 0; i < nodePath.length; i++) {
                            DirectoryNode nodeToDelete = (DirectoryNode)nodePath[i].getLastPathComponent();
                            try {
                                FileObject fo = FileUtil.toFileObject(nodeToDelete.getFile());
                                fo.delete();
                                nodes2Remove.add(nodeToDelete);
                            } catch (IOException ignore) {
                                cannotDelete++;

                                if(canWrite(nodeToDelete.getFile())) {
                                    list.add(nodeToDelete.getFile());
                                }
                            }
                        }
                        // send to second pass
                        EventQueue.invokeLater(this);
                    } else {
                        // second pass, in EQ thread
                        for (DirectoryNode curNode : nodes2Remove) {
                            model.removeNodeFromParent(curNode);
                        }

                        setCursor(fileChooser, Cursor.DEFAULT_CURSOR);
                        if(cannotDelete > 0) {
                            String message = "";
                            
                            if(cannotDelete == 1) {
                                message = cannotDelete + " " + getBundle().getString("MSG_Sing_Delete");
                            } else {
                                message = cannotDelete + " " + getBundle().getString("MSG_Plur_Delete");
                            }
                            
                            setSelected((File[])list.toArray(new File[list.size()]));
                            
                            JOptionPane.showConfirmDialog(fileChooser, message , getBundle().getString("MSG_Confirm"), JOptionPane.OK_OPTION);
                        } else {
                            setSelected(new File[] {null});
                            setFileName("");
                        }
                    }
                }
            });
        }
    }
    
    private void updateCompletions() {
        String name = normalizeFile(getFileName());
        int slash = name.lastIndexOf(File.separatorChar);
        if (slash != -1) {
            String prefix = name.substring(0, slash + 1);
            File d = new File(prefix);
            if (d.isDirectory()) {
                File[] children = d.listFiles();
                if(children != null) {
                    Vector list = buildList(name, children);
                    
                    if(completionPopup == null) {
                        completionPopup = new FileCompletionPopup(fileChooser, filenameTextField, list);
                    } else if (completionPopup.isShowing() || 
                            (showPopupCompletion && fileChooser.isShowing())) {
                        completionPopup.setDataList(list);
                    }
                    
                    if(showPopupCompletion && fileChooser.isShowing() && !completionPopup.isShowing()) {
                        java.awt.Point los = filenameTextField.getLocation();
                        int popX = los.x;
                        int popY = los.y + filenameTextField.getHeight() - 6;
                        completionPopup.showPopup(filenameTextField, popX, popY);
                    }
                }
            }
        }
    }
    
    public Vector<File> buildList(String text, File[] children) {
        Vector<File> files = new Vector<>(children.length);
        
        for(int i = children.length - 1; i >= 0; i--) {
            File completion = children[i];
            
            if(fileChooser.accept(completion)) {
                String path = completion.getAbsolutePath();
                
                if (path.regionMatches(true, 0, text, 0, text.length())) {
                    
                    if(fileChooser.getFileSelectionMode() == JFileChooser.DIRECTORIES_ONLY) {
                        if(completion.isDirectory()) {
                            files.add(completion);
                        }
                    } else if(fileChooser.getFileSelectionMode() == JFileChooser.FILES_ONLY) {
                        if(completion.isFile()) {
                            files.add(completion);
                        }
                    } else if(fileChooser.getFileSelectionMode() == JFileChooser.FILES_AND_DIRECTORIES) {
                        files.add(completion);
                    }
                }
            }
        }
        
        Collections.sort(files, DirectoryNode.FILE_NAME_COMPARATOR);
        return files;
    }
    
    
    private static String normalizeFile(String text) {
        // See #21690 for background.
        // XXX what are legal chars for var names? bash manual says only:
        // "The braces are required when PARAMETER [...] is followed by a
        // character that is not to be interpreted as part of its name."
        Pattern p = Pattern.compile("(^|[^\\\\])\\$([a-zA-Z_0-9.]+)");
        Matcher m;
        while ((m = p.matcher(text)).find()) {
            // Have an env var to subst...
            // XXX handle ${PATH} too? or don't bother
            String var = System.getenv(m.group(2));
            if (var == null) {
                // Try Java system props too, and fall back to "".
                var = System.getProperty(m.group(2), "");
            }
            // XXX full readline compat would mean vars were also completed with TAB...
            text = text.substring(0, m.end(1)) + var + text.substring(m.end(2));
        }
        if (text.equals("~")) {
            return System.getProperty("user.home");
        } else if (text.startsWith("~" + File.separatorChar)) {
            return System.getProperty("user.home") + text.substring(1);
        } else {
            int i = text.lastIndexOf("//");
            if (i != -1) {
                // Treat /home/me//usr/local as /usr/local
                // (so that you can use "//" to start a new path, without selecting & deleting)
                return text.substring(i + 1);
            }
            i = text.lastIndexOf(File.separatorChar + "~" + File.separatorChar);
            if (i != -1) {
                // Treat /usr/local/~/stuff as /home/me/stuff
                return System.getProperty("user.home") + text.substring(i + 2);
            }
            return text;
        }
    }
    
    private static ResourceBundle getBundle() {
        return NbBundle.getBundle(DirectoryChooserUI.class);
    }
    
    private void updateTree(final File file) {
        // fixed bug #97522
        if(updateWorker != null) {
            // try to cancel previous update if possible
            if (!updateWorker.isFinished()) {
                updateWorker.cancel();
            }
            // #105642 - wait for previous update, to keep time order
            updateWorker.waitFinished();
        }
        
        updateWorker = RequestProcessor.getDefault().post(new Runnable() {
            DirectoryNode node;
            long startTime;
            @Override
            public void run() {
                if (!EventQueue.isDispatchThread()) {
                    // first pass, out of EQ thread
                    markStartTime();
                    setCursor(fileChooser, Cursor.WAIT_CURSOR);
                    node = new DirectoryNode(file);
                    node.loadChildren(fileChooser, true);
                    // send to second pass
                    EventQueue.invokeLater(this);
                } else {
                    // second pass, in EQ thread
                    model = new DirectoryTreeModel(node);
                    tree.setModel(model);
                    tree.repaint();
                    setCursor(fileChooser, Cursor.DEFAULT_CURSOR);
                    checkUpdate();
                }
            }

        });
        
    }

    private void markStartTime () {
        if (fileChooser.getClientProperty(DelegatingChooserUI.START_TIME) == null) {
            fileChooser.putClientProperty(DelegatingChooserUI.START_TIME,
                    Long.valueOf(System.currentTimeMillis()));
        }
    }

    private void checkUpdate() {
        if (Utilities.isWindows() && useShellFolder) {
            Long startTime = (Long) fileChooser.getClientProperty(DelegatingChooserUI.START_TIME);
            if (startTime == null) {
                return;
            }
            // clean for future marking
            fileChooser.putClientProperty(DelegatingChooserUI.START_TIME, null);

            long elapsed = System.currentTimeMillis() - startTime.longValue();
            long timeOut = NbPreferences.forModule(DirectoryChooserUI.class).
                    getLong(TIMEOUT_KEY, 10000);
            if (timeOut > 0 && elapsed > timeOut && slownessPanel == null) {
                JLabel slownessNote = new JLabel(
                        NbBundle.getMessage(DirectoryChooserUI.class, "MSG_SlownessNote"));
                slownessNote.setForeground(Color.RED);
                slownessPanel = new JPanel();
                JButton notShow = new JButton(
                        NbBundle.getMessage(DirectoryChooserUI.class, "BTN_NotShow"));
                notShow.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        NbPreferences.forModule(DirectoryChooserUI.class).putLong(TIMEOUT_KEY, 0);
                        centerPanel.remove(slownessPanel);
                        centerPanel.revalidate();
                    }
                });
                JPanel notShowP = new JPanel();
                notShowP.add(notShow);
                slownessPanel.setLayout(new BorderLayout());
                slownessPanel.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));
                slownessPanel.add(BorderLayout.CENTER, slownessNote);
                slownessPanel.add(BorderLayout.SOUTH, notShowP);
                centerPanel.add(BorderLayout.NORTH, slownessPanel);
                centerPanel.revalidate();
            }
        }
    }

    private Boolean isXPStyle() {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        Boolean themeActive = (Boolean)toolkit.getDesktopProperty("win.xpstyle.themeActive");
        if(themeActive == null)
            themeActive = Boolean.FALSE;
        if (themeActive.booleanValue() && System.getProperty("swing.noxp") == null) {
            themeActive = Boolean.TRUE;
        }
        return themeActive;
    }
    
    @Override
    protected void installStrings(JFileChooser fc) {
        super.installStrings(fc);
        
        Locale l = fc.getLocale();
        
        lookInLabelMnemonic = UIManager.getInt("FileChooser.lookInLabelMnemonic");
        lookInLabelText = UIManager.getString("FileChooser.lookInLabelText",l);
        saveInLabelText = UIManager.getString("FileChooser.saveInLabelText",l);
        
        fileNameLabelMnemonic = UIManager.getInt("FileChooser.fileNameLabelMnemonic");
        fileNameLabelText = UIManager.getString("FileChooser.fileNameLabelText",l);
        
        filesOfTypeLabelMnemonic = UIManager.getInt("FileChooser.filesOfTypeLabelMnemonic");
        filesOfTypeLabelText = UIManager.getString("FileChooser.filesOfTypeLabelText",l);
        
        upFolderToolTipText =  UIManager.getString("FileChooser.upFolderToolTipText",l);
        upFolderAccessibleName = UIManager.getString("FileChooser.upFolderAccessibleName",l);
        
        newFolderToolTipText = UIManager.getString("FileChooser.newFolderToolTipText",l);
        homeFolderTooltipText = UIManager.getString("FileChooser.homeFolderToolTipText",l);
        
    }
    
    @Override
    protected void installListeners(JFileChooser fc) {
        super.installListeners(fc);
        ActionMap actionMap = getActionMap();
        SwingUtilities.replaceUIActionMap(fc, actionMap);
    }
    
    protected ActionMap getActionMap() {
        return createActionMap();
    }
    
    protected ActionMap createActionMap() {
        AbstractAction escAction = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                getFileChooser().cancelSelection();
            }
            @Override
            public boolean isEnabled(){
                return getFileChooser().isEnabled();
            }
        };
        ActionMap map = new ActionMapUIResource();
        map.put("approveSelection", getApproveSelectionAction());
        map.put("cancelSelection", escAction);
        map.put("Go Up", getChangeToParentDirectoryAction());
        return map;
    }
    
    @Override
    public Action getNewFolderAction() {
        return newFolderAction;
    }
    
    @Override
    public void uninstallUI(JComponent c) {
        c.removePropertyChangeListener(filterTypeComboBoxModel);
        cancelButton.removeActionListener(getCancelSelectionAction());
        approveButton.removeActionListener(getApproveSelectionAction());
        filenameTextField.removeActionListener(getApproveSelectionAction());
        super.uninstallUI(c);
    }
    
    /**
     * Returns the preferred size of the specified
     * <code>JFileChooser</code>.
     * The preferred size is at least as large,
     * in both height and width,
     * as the preferred size recommended
     * by the file chooser's layout manager.
     *
     * @param c  a <code>JFileChooser</code>
     * @return   a <code>Dimension</code> specifying the preferred
     *           width and height of the file chooser
     */
    @Override
    public Dimension getPreferredSize(JComponent c) {
        int prefWidth = PREF_SIZE.width;
        Dimension d = c.getLayout().preferredLayoutSize(c);
        if (d != null) {
            return new Dimension(d.width < prefWidth ? prefWidth : d.width,
                    d.height < PREF_SIZE.height ? PREF_SIZE.height : d.height);
        } else {
            return new Dimension(prefWidth, PREF_SIZE.height);
        }
    }
    
    /**
     * Returns the minimum size of the <code>JFileChooser</code>.
     *
     * @param c  a <code>JFileChooser</code>
     * @return   a <code>Dimension</code> specifying the minimum
     *           width and height of the file chooser
     */
    @Override
    public Dimension getMinimumSize(JComponent c) {
        return MIN_SIZE;
    }
    
    /**
     * Returns the maximum size of the <code>JFileChooser</code>.
     *
     * @param c  a <code>JFileChooser</code>
     * @return   a <code>Dimension</code> specifying the maximum
     *           width and height of the file chooser
     */
    @Override
    public Dimension getMaximumSize(JComponent c) {
        return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
    }
    
    private String getStringOfFileName(File file) {
        if (file == null) {
            return null;
        } else {
            JFileChooser fc = getFileChooser();
            if (fc.isDirectorySelectionEnabled() && !fc.isFileSelectionEnabled()) {
                return file.getPath();
            } else {
                return file.getName();
            }
        }
    }
    
    private String getStringOfFileNames(File[] files) {
        StringBuffer buf = new StringBuffer();
        for (int i = 0; files != null && i < files.length; i++) {
            if (i > 0) {
                buf.append(" ");
            }
            if (files.length > 1) {
                buf.append("\"");
            }
            buf.append(getStringOfFileName(files[i]));
            if (files.length > 1) {
                buf.append("\"");
            }
        }
        return buf.toString();
    }
    
    /* The following methods are used by the PropertyChange Listener */
    
    private void fireSelectedFileChanged(PropertyChangeEvent e) {
        File f = (File) e.getNewValue();
        JFileChooser fc = getFileChooser();
        if (f != null
                && ((fc.isFileSelectionEnabled() && !f.isDirectory())
                || (f.isDirectory() && fc.isDirectorySelectionEnabled()))) {
            
            setFileName(getStringOfFileName(f));
        }

        //TODO button visibility code started
        if (customDirectoryProvider != null)
            approveButton.setEnabled(customDirectoryProvider.isValidCustomDirectory(f));
        //TODO button visibility code ended
    }
    
    private void fireSelectedFilesChanged(PropertyChangeEvent e) {
        File[] files = (File[]) e.getNewValue();
        JFileChooser fc = getFileChooser();
        if (files != null
                && files.length > 0
                && (files.length > 1 || fc.isDirectorySelectionEnabled() || !files[0].isDirectory())) {
            setFileName(getStringOfFileNames(files));
        }
    }
    
    private void fireDirectoryChanged(PropertyChangeEvent e) {
        JFileChooser fc = getFileChooser();
        FileSystemView fsv = fc.getFileSystemView();
        showPopupCompletion = false;
        setFileName("");
        clearIconCache();
        File currentDirectory = fc.getCurrentDirectory();
        if(currentDirectory != null) {
            directoryComboBoxModel.addItem(currentDirectory);
            newFolderAction.setEnabled(currentDirectory.canWrite());
            getChangeToParentDirectoryAction().setEnabled(!fsv.isRoot(currentDirectory));
            updateTree(currentDirectory);
            if (fc.isDirectorySelectionEnabled() && !fc.isFileSelectionEnabled()) {
                if (fsv.isFileSystem(currentDirectory)) {
                    setFileName(getStringOfFileName(currentDirectory));
                } else {
                    setFileName(null);
                }
            }
        }
    }
    
    private void fireFilterChanged(PropertyChangeEvent e) {
        clearIconCache();
    }
    
    private void fireFileSelectionModeChanged(PropertyChangeEvent e) {
        clearIconCache();
        JFileChooser fc = getFileChooser();
        
        File currentDirectory = fc.getCurrentDirectory();
        if (currentDirectory != null
                && fc.isDirectorySelectionEnabled()
                && !fc.isFileSelectionEnabled()
                && fc.getFileSystemView().isFileSystem(currentDirectory)) {
            
            setFileName(currentDirectory.getPath());
        } else {
            setFileName(null);
        }
    }
    
    private void fireMultiSelectionChanged(PropertyChangeEvent e) {
        if (getFileChooser().isMultiSelectionEnabled()) {
            tree.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
        } else {
            tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
            getFileChooser().setSelectedFiles(null);
        }
    }
    
    private void fireAccessoryChanged(PropertyChangeEvent e) {
        if(getAccessoryPanel() != null) {
            JComponent oldAcc = (JComponent) e.getOldValue();
            JComponent newAcc = (JComponent) e.getNewValue();
            JComponent accessoryPanel = getAccessoryPanel();
            if(oldAcc != null) {
                accessoryPanel.remove(oldAcc);
            }
            if (oldAcc != null && newAcc == null) {
                accessoryPanel.remove(topToolbar);
                topComboWrapper.add(topToolbar, BorderLayout.EAST);
                topCombo.setBorder(BorderFactory.createEmptyBorder(6, 0, 10, 0));
                topCombo.revalidate();
            }
            
            if(newAcc != null) {
                getAccessoryPanel().add(newAcc, BorderLayout.CENTER);
            }
            
            if (oldAcc == null && newAcc != null) {
                topComboWrapper.remove(topToolbar);
                topCombo.setBorder(BorderFactory.createEmptyBorder(4, 0, 8, 0));
                accessoryPanel.add(topToolbar, BorderLayout.NORTH);
            }
            
        }
    }
    
    private void fireApproveButtonTextChanged(PropertyChangeEvent e) {
        JFileChooser chooser = getFileChooser();
        approveButton.setText(getApproveButtonText(chooser));
        approveButton.setToolTipText(getApproveButtonToolTipText(chooser));
        // #107791: No mnemonics desirable on Mac 
        if (!Utilities.isMac()) {
            approveButton.setMnemonic(getApproveButtonMnemonic(chooser));
        }
    }
    
    private void fireDialogTypeChanged(PropertyChangeEvent e) {
        JFileChooser chooser = getFileChooser();
        approveButton.setText(getApproveButtonText(chooser));
        approveButton.setToolTipText(getApproveButtonToolTipText(chooser));
        // #107791: No mnemonics desirable on Mac 
        if (!Utilities.isMac()) {
            approveButton.setMnemonic(getApproveButtonMnemonic(chooser));
        }
        if (chooser.getDialogType() == JFileChooser.SAVE_DIALOG) {
            lookInComboBoxLabel.setText(saveInLabelText);
        } else {
            lookInComboBoxLabel.setText(lookInLabelText);
        }
    }
    
    private void fireApproveButtonMnemonicChanged(PropertyChangeEvent e) {
        // #107791: No mnemonics desirable on Mac 
        if (!Utilities.isMac()) {
            approveButton.setMnemonic(getApproveButtonMnemonic(getFileChooser()));
        }
    }
    
    private void fireControlButtonsChanged(PropertyChangeEvent e) {
        if(getFileChooser().getControlButtonsAreShown()) {
            addControlButtons();
        } else {
            removeControlButtons();
        }
    }
    
    /*
     * Listen for filechooser property changes, such as
     * the selected file changing, or the type of the dialog changing.
     */
    @Override
    public PropertyChangeListener createPropertyChangeListener(JFileChooser fc) {
        return new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent e) {
                String s = e.getPropertyName();
                if(s.equals(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)) {
                    fireSelectedFileChanged(e);
                } else if (s.equals(JFileChooser.SELECTED_FILES_CHANGED_PROPERTY)) {
                    fireSelectedFilesChanged(e);
                } else if(s.equals(JFileChooser.DIRECTORY_CHANGED_PROPERTY) && changeDirectory) {
                    fireDirectoryChanged(e);
                } else if(s.equals(JFileChooser.FILE_FILTER_CHANGED_PROPERTY)) {
                    fireFilterChanged(e);
                } else if(s.equals(JFileChooser.FILE_SELECTION_MODE_CHANGED_PROPERTY)) {
                    fireFileSelectionModeChanged(e);
                } else if(s.equals(JFileChooser.MULTI_SELECTION_ENABLED_CHANGED_PROPERTY)) {
                    fireMultiSelectionChanged(e);
                } else if(s.equals(JFileChooser.ACCESSORY_CHANGED_PROPERTY)) {
                    fireAccessoryChanged(e);
                } else if (s.equals(JFileChooser.APPROVE_BUTTON_TEXT_CHANGED_PROPERTY) ||
                        s.equals(JFileChooser.APPROVE_BUTTON_TOOL_TIP_TEXT_CHANGED_PROPERTY)) {
                    fireApproveButtonTextChanged(e);
                } else if(s.equals(JFileChooser.DIALOG_TYPE_CHANGED_PROPERTY)) {
                    fireDialogTypeChanged(e);
                } else if(s.equals(JFileChooser.APPROVE_BUTTON_MNEMONIC_CHANGED_PROPERTY)) {
                    fireApproveButtonMnemonicChanged(e);
                } else if(s.equals(JFileChooser.CONTROL_BUTTONS_ARE_SHOWN_CHANGED_PROPERTY)) {
                    fireControlButtonsChanged(e);
                } else if (s.equals(DelegatingChooserUI.USE_SHELL_FOLDER)) {
                    updateUseShellFolder();
                    fireDirectoryChanged(e);
                } else if (s.equals("componentOrientation")) {
                    ComponentOrientation o = (ComponentOrientation)e.getNewValue();
                    JFileChooser cc = (JFileChooser)e.getSource();
                    if (o != (ComponentOrientation)e.getOldValue()) {
                        cc.applyComponentOrientation(o);
                    }
                } else if (s.equals("ancestor")) {
                    if (e.getOldValue() == null && e.getNewValue() != null) {
                        filenameTextField.selectAll();
                        filenameTextField.requestFocus();
                    }
                }
            }
        };
    }
    
    protected void removeControlButtons() {
        bottomPanel.remove(buttonPanel);
    }
    
    protected void addControlButtons() {
        bottomPanel.add(buttonPanel);
    }
    
    @Override
    public String getFileName() {
        if(filenameTextField != null) {
            return filenameTextField.getText();
        } else {
            return null;
        }
    }
    
    @Override
    public void setFileName(String filename) {
        if(filenameTextField != null) {
            filenameTextField.setText(filename);
        }
    }
    
    private DirectoryComboBoxRenderer createDirectoryComboBoxRenderer(JFileChooser fc) {
        return new DirectoryComboBoxRenderer();
    }
    
    protected DirectoryComboBoxModel createDirectoryComboBoxModel(JFileChooser fc) {
        return new DirectoryComboBoxModel();
    }
    
    protected FilterComboBoxRenderer createFilterComboBoxRenderer() {
        return new FilterComboBoxRenderer();
    }
    
    protected FilterTypeComboBoxModel createFilterComboBoxModel() {
        return new FilterTypeComboBoxModel();
    }
    
    @Override
    protected JButton getApproveButton(JFileChooser fc) {
        return approveButton;
    }
    
    @Override
    public FileView getFileView(JFileChooser fc) {
        
        // fix bug #96957, should use DirectoryChooserFileView
        // only on windows
        if (Utilities.isWindows()) {
            if (useShellFolder) {
                fileView = new DirectoryChooserFileView();
            }
        } else {
            fileView = (BasicFileView) super.getFileView(fileChooser);
        }
        return fileView;
    }
    
    private void setSelected(File[] files) {
        changeDirectory = false;
        fileChooser.setSelectedFiles(files);
        changeDirectory = true;
    }
    
    private DirectoryHandler createDirectoryHandler(JFileChooser chooser) {
        return new DirectoryHandler(chooser);
    }
    
    private void addNewDirectory(final TreePath path) {
        RequestProcessor.getDefault().post(new Runnable() {
            @Override
            public void run() {
                EventQueue.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        DirectoryNode selectedNode = (DirectoryNode)path.getLastPathComponent();
                        
                        if(selectedNode == null || !canWrite(selectedNode.getFile())) {
                            return;
                        }
                        
                        try {
                            newFolderNode = new DirectoryNode(fileChooser.getFileSystemView().createNewFolder(selectedNode.getFile()));
                            model.insertNodeInto(newFolderNode, selectedNode, selectedNode.getChildCount());
                            applyEdit(newFolderNode);
                        } catch (IOException ex) {
                            Exceptions.printStackTrace(ex);
                        }
                    }
                });
            }
        });
    }
    
    private void applyEdit(DirectoryNode node) {
        TreeNode[] nodes = model.getPathToRoot(node);
        TreePath editingPath = new TreePath(nodes);
        tree.setEditable(true);
        tree.makeVisible(editingPath);
        tree.scrollPathToVisible(editingPath);
        tree.setSelectionPath(editingPath);
        tree.startEditingAtPath(editingPath);
        
        JTextField editField = DirectoryCellEditor.getTextField();
        editField.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
        editField.setRequestFocusEnabled(true);
        editField.requestFocus();
        editField.setSelectionStart(0);
        editField.setSelectionEnd(editField.getText().length());
    }
    
    private static boolean canWrite(File f) {
        boolean writeable = false;
        if (f != null) {
            try {
                writeable = f.canWrite();
            } catch (AccessControlException ex) {
                writeable = false;
            }
        }
        return writeable;
    }
    
    private void expandNode(final JFileChooser fileChooser, final TreePath path) {
        
        RequestProcessor.getDefault().post(new Runnable() {
            DirectoryNode node;
            
            @Override
            public void run() {
                if (!EventQueue.isDispatchThread()) {
                    // first pass, out of EQ thread, loads data
                    markStartTime();
                    setCursor(fileChooser, Cursor.WAIT_CURSOR);
                    node = (DirectoryNode) path.getLastPathComponent();
                    node.loadChildren(fileChooser, true);
                    // send to second pass
                    EventQueue.invokeLater(this);
                } else {
                    // second pass, in EQ thread
                    ((DefaultTreeModel) tree.getModel()).nodeStructureChanged(node);
                  /*
                   * This happens when the add new directory action is called
                   * and the node is not loaded.  Furthermore, it ensures that
                   * adding a new directory execute after the UI has finished
                   * displaying the children of the expanded node.
                   */
                    if(addNewDirectory) {
                        addNewDirectory(path);
                        addNewDirectory = false;
                    }
                    setCursor(fileChooser, Cursor.DEFAULT_CURSOR);
                    checkUpdate();
                }
            }
        });
    }
    
    private void setCursor(JComponent comp, int type) {
        Window window = SwingUtilities.getWindowAncestor(comp);
        if (window != null) {
            Cursor cursor = Cursor.getPredefinedCursor(type);
            window.setCursor(cursor);
            window.setFocusable(true);
        }
        
        JRootPane pane = fileChooser.getRootPane();
        if( null == blocker )
            blocker = new InputBlocker();
        
        if(type == Cursor.WAIT_CURSOR) {
            blocker.block(pane);
        } else if (type == Cursor.DEFAULT_CURSOR){
            blocker.unBlock(pane);
        }
    }
    
    /*************** HELPER CLASSES ***************/
    
    private class IconIndenter implements Icon {
        final static int space = 10;
        Icon icon = null;
        int depth = 0;
        
        @Override
        public void paintIcon(Component c, Graphics g, int x, int y) {
            if (icon == null) {
                return;
            }
            if (c.getComponentOrientation().isLeftToRight()) {
                icon.paintIcon(c, g, x+depth*space, y);
            } else {
                icon.paintIcon(c, g, x, y);
            }
        }
        
        @Override
        public int getIconWidth() {
            return icon != null ? icon.getIconWidth() + depth*space : null;
        }
        
        @Override
        public int getIconHeight() {
            return icon != null ? icon.getIconHeight() : null;
        }
        
    }
    
    private class DirectoryComboBoxRenderer  extends JLabel implements ListCellRenderer, UIResource {
        IconIndenter indenter = new IconIndenter();
        
        public DirectoryComboBoxRenderer() {
            setOpaque(true);
        }
        
        @Override
        public Component getListCellRendererComponent(JList list, Object value,
                int index, boolean isSelected,
                boolean cellHasFocus) {
            // #89393: GTK needs name to render cell renderer "natively"
            setName("ComboBox.listRenderer"); // NOI18N
            
            if (value == null) {
                setText("");
                return this;
            }
            File directory = (File)value;
            setText(getFileChooser().getName(directory));
            Icon icon = getFileChooser().getIcon(directory);
            indenter.icon = icon;
            indenter.depth = directoryComboBoxModel.getDepth(index);
            setIcon(indenter);
            if ( isSelected ) {
                setBackground(list.getSelectionBackground());
                setForeground(list.getSelectionForeground());
            } else {
                setBackground(list.getBackground());
                setForeground(list.getForeground());
            }
            
            return this;
        }
        
        // #89393: GTK needs name to render cell renderer "natively"
        @Override
        public String getName() {
            String name = super.getName();
            return name == null ? "ComboBox.renderer" : name;  // NOI18N
        }
    } // end of DirectoryComboBoxRenderer
    
    /**
     * Data model for a type-face selection combo-box.
     */
    private class DirectoryComboBoxModel extends AbstractListModel implements ComboBoxModel {
        Vector<File> directories = new Vector<>();
        int[] depths = null;
        File selectedDirectory = null;
        JFileChooser chooser = getFileChooser();
        FileSystemView fsv = chooser.getFileSystemView();
        
        public DirectoryComboBoxModel() {
            // Add the current directory to the model, and make it the
            // selectedDirectory
            File dir = getFileChooser().getCurrentDirectory();
            if(dir != null) {
                addItem(dir);
            }
        }
        
        /**
         * Adds the directory to the model and sets it to be selected,
         * additionally clears out the previous selected directory and
         * the paths leading up to it, if any.
         */
        private void addItem(File directory) {
            
            if(directory == null) {
                return;
            }
            
            directories.clear();
            
            if(useShellFolder) {
                directories.addAll(Arrays.asList(getShellFolderRoots()));
            } else {
                directories.addAll(Arrays.asList(fileChooser.getFileSystemView().getRoots()));
            }
            
            
            // Get the canonical (full) path. This has the side
            // benefit of removing extraneous chars from the path,
            // for example /foo/bar/ becomes /foo/bar
            File canonical = null;
            try {
                canonical = directory.getCanonicalFile();
            } catch (IOException e) {
                // Maybe drive is not ready. Can't abort here.
                canonical = directory;
            }
            
            // create File instances of each directory leading up to the top
            File sf = getShellFolderForFile(canonical);
            File f = sf;
            Vector<File> path = new Vector<>(10);

            
            /*
             * Fix for IZ#122534 :
             * NullPointerException at 
             * org.netbeans.swing.dirchooser.DirectoryChooserUI$DirectoryComboBoxModel.addItem
             * 
             */
            while( f!= null) {
                path.addElement(f);
                f = f.getParentFile();
            }

            int pathCount = path.size();
            // Insert chain at appropriate place in vector
            for (int i = 0; i < pathCount; i++) {
                f = path.get(i);
                if (directories.contains(f)) {
                    int topIndex = directories.indexOf(f);
                    for (int j = i-1; j >= 0; j--) {
                        directories.insertElementAt(path.get(j), topIndex+i-j);
                    }
                    break;
                }
            }
            calculateDepths();
            setSelectedItem(sf);
        }
        
        private void calculateDepths() {
            depths = new int[directories.size()];
            for (int i = 0; i < depths.length; i++) {
                File dir = directories.get(i);
                File parent = dir.getParentFile();
                depths[i] = 0;
                if (parent != null) {
                    for (int j = i-1; j >= 0; j--) {
                        if (parent.equals(directories.get(j))) {
                            depths[i] = depths[j] + 1;
                            break;
                        }
                    }
                }
            }
        }
        
        public int getDepth(int i) {
            return (depths != null && i >= 0 && i < depths.length) ? depths[i] : 0;
        }
        
        @Override
        public void setSelectedItem(Object selectedDirectory) {
            this.selectedDirectory = (File)selectedDirectory;
            fireContentsChanged(this, -1, -1);
        }
        
        @Override
        public Object getSelectedItem() {
            return selectedDirectory;
        }
        
        @Override
        public int getSize() {
            return directories.size();
        }
        
        @Override
        public Object getElementAt(int index) {
            return directories.elementAt(index);
        }
    }
    
    /**
     * Render different type sizes and styles.
     */
    private class FilterComboBoxRenderer extends JLabel implements ListCellRenderer, UIResource {
        
        public FilterComboBoxRenderer() {
            setOpaque(true);
        }
        
        @Override
        public Component getListCellRendererComponent(JList list,
                Object value, int index, boolean isSelected,
                boolean cellHasFocus) {
            
            // #89393: GTK needs name to render cell renderer "natively"
            setName("ComboBox.listRenderer"); // NOI18N
            
            if (value != null && value instanceof FileFilter) {
                setText(((FileFilter)value).getDescription());
            }
            
            if ( isSelected ) {
                setBackground(list.getSelectionBackground());
                setForeground(list.getSelectionForeground());
            } else {
                setBackground(list.getBackground());
                setForeground(list.getForeground());
            }
            
            return this;
        }
        
        // #89393: GTK needs name to render cell renderer "natively"
        @Override
        public String getName() {
            String name = super.getName();
            return name == null ? "ComboBox.renderer" : name;  // NOI18N
        }
        
    } // end of FilterComboBoxRenderer
    
    /**
     * Data model for a type-face selection combo-box.
     */
    protected class FilterTypeComboBoxModel extends AbstractListModel implements ComboBoxModel, PropertyChangeListener {
        protected FileFilter[] filters;
        protected FilterTypeComboBoxModel() {
            super();
            filters = getFileChooser().getChoosableFileFilters();
        }
        
        @Override
        public void propertyChange(PropertyChangeEvent e) {
            String prop = e.getPropertyName();
            if(prop == JFileChooser.CHOOSABLE_FILE_FILTER_CHANGED_PROPERTY) {
                filters = (FileFilter[]) e.getNewValue();
                fireContentsChanged(this, -1, -1);
            } else if (prop == JFileChooser.FILE_FILTER_CHANGED_PROPERTY) {
                fireContentsChanged(this, -1, -1);
            }
        }
        
        @Override
        public void setSelectedItem(Object filter) {
            if(filter != null) {
                getFileChooser().setFileFilter((FileFilter) filter);
                setFileName(null);
                fireContentsChanged(this, -1, -1);
            }
        }
        
        @Override
        public Object getSelectedItem() {
            // Ensure that the current filter is in the list.
            // NOTE: we shouldnt' have to do this, since JFileChooser adds
            // the filter to the choosable filters list when the filter
            // is set. Lets be paranoid just in case someone overrides
            // setFileFilter in JFileChooser.
            FileFilter currentFilter = getFileChooser().getFileFilter();
            boolean found = false;
            if(currentFilter != null) {
                for(int i=0; i < filters.length; i++) {
                    if(filters[i] == currentFilter) {
                        found = true;
                    }
                }
                if(found == false) {
                    getFileChooser().addChoosableFileFilter(currentFilter);
                }
            }
            return getFileChooser().getFileFilter();
        }
        
        @Override
        public int getSize() {
            if(filters != null) {
                return filters.length;
            } else {
                return 0;
            }
        }
        
        @Override
        public Object getElementAt(int index) {
            if(index > getSize() - 1) {
                // This shouldn't happen. Try to recover gracefully.
                return getFileChooser().getFileFilter();
            }
            if(filters != null) {
                return filters[index];
            } else {
                return null;
            }
        }
    }
    
    /**
     * Gets calls when the ComboBox has changed the selected item.
     */
    private class DirectoryComboBoxAction implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            File f = (File)directoryComboBox.getSelectedItem();
            getFileChooser().setCurrentDirectory(f);
        }
    }
    
    private class DirectoryChooserFileView extends BasicFileView {
        
        @Override
        public Icon getIcon(File f) {
            Icon icon = getCachedIcon(f);
            if (icon != null) {
                return icon;
            }
            
            if (f != null) {
                try {
                    icon = fileChooser.getFileSystemView().getSystemIcon(f);
                } catch (NullPointerException exc) {
                    // workaround for JDK bug 6357445, in IZ: 145832, please remove when fixed
                    LOG.log(Level.FINE, "JDK bug 6357445 encountered, NPE caught", exc); // NOI18N
                }
            }
            
            if (icon == null) {
                icon = super.getIcon(f);
            }
            
            cacheIcon(f, icon);
            return icon;
        }
    }
    
    private class TextFieldKeyListener extends KeyAdapter {
        @Override
        public void keyPressed(KeyEvent evt) {
            showPopupCompletion = true;
            int keyCode = evt.getKeyCode();
            // #105801: completionPopup might not be ready when updateCompletions not called (empty text field)
            if (completionPopup != null && !completionPopup.isVisible()) {
                if (keyCode == KeyEvent.VK_ENTER) {
                    File file = new File(filenameTextField.getText());
                    if(file.exists() && file.isDirectory()) {
                        setSelected(new File[] {file});
                        fileChooser.approveSelection();
                    }
                }
                
                if ((keyCode == KeyEvent.VK_TAB || keyCode == KeyEvent.VK_DOWN) ||
                    (keyCode == KeyEvent.VK_RIGHT && 
                    (filenameTextField.getCaretPosition() >= (filenameTextField.getDocument().getLength() - 1)))) {
                    updateCompletions();
                }
                
            }
            
            if(filenameTextField.isFocusOwner() && 
                    (completionPopup == null || !completionPopup.isVisible()) &&
                    keyCode == KeyEvent.VK_ESCAPE) {
                fileChooser.cancelSelection();
            }
        }
    }
    
    private class DirectoryHandler extends MouseAdapter 
            implements TreeSelectionListener, CellEditorListener, ActionListener,
                        FocusListener, Runnable {
        private JFileChooser fileChooser;
        /** current selection holder */
        private WeakReference<TreePath> curSelPath;
        /** timer for slow click to rename feature */ 
        private Timer renameTimer;
        /** path to rename for slow click to rename feature */
        private TreePath pathToRename;
        
        public DirectoryHandler(JFileChooser fileChooser) {
            this.fileChooser = fileChooser;
        }
        
        /************ imple of TreeSelectionListener *******/
        
        @Override
        public void valueChanged(TreeSelectionEvent e) {
            showPopupCompletion = false;
            FileSystemView fsv = fileChooser.getFileSystemView();
            JTree tree = (JTree) e.getSource();
            TreePath path = tree.getSelectionPath();
            TreePath curSel = e.getNewLeadSelectionPath();
            curSelPath = (curSel != null) ? new WeakReference<TreePath>(curSel) : null;
            
            if(path != null) {
                
                DirectoryNode node = (DirectoryNode)path.getLastPathComponent();
                File file = node.getFile();
                
                if(file != null) {
                    setSelected(getSelectedNodes(tree.getSelectionPaths()));
                    newFolderAction.setEnabled(canWrite(file) && file.isDirectory());
                    
                    if(file.isDirectory()) {
                        setDirectorySelected(true);
                    }
                }
            }
        }
        
        private File[] getSelectedNodes(TreePath[] paths) {
            Vector<File> files = new Vector<>();
            for(int i = 0; i < paths.length; i++) {
                File file = ((DirectoryNode)paths[i].getLastPathComponent()).getFile();
                if(file.isDirectory()
                        && fileChooser.isTraversable(file)
                        && !fileChooser.getFileSystemView().isFileSystem(file)) {
                    continue;
                }
                files.add(file);
            }
            return files.toArray(new File[files.size()]);
        }
        
        /********* impl of MouseListener ***********/
        
        @Override
        public void mouseClicked(MouseEvent e) {
            final JTree tree = (JTree) e.getSource();
            Point p = e.getPoint();
            final int x = e.getX();
            final int y = e.getY();
            int row = tree.getRowForLocation(x, y);
            TreePath path = tree.getPathForRow(row);
            
            if (path != null) {
                
                DirectoryNode node = (DirectoryNode) path.getLastPathComponent();
                newFolderAction.setEnabled(canWrite(node.getFile()));
    
                if (SwingUtilities.isLeftMouseButton(e) && (e.getClickCount() == 2)) {
                    cancelRename();
                    if(node.isNetBeansProject()) {
                        fileChooser.approveSelection();
                    } else if (node.getFile().isFile() && !node.getFile().getPath().endsWith(".lnk")){
                        fileChooser.approveSelection();
                    } else {
                        changeTreeDirectory(node.getFile());
                    }
                    
                }
                
                // handles click to rename feature
                if (SwingUtilities.isLeftMouseButton(e) && (e.getClickCount() == 1)) {
                    if (pathToRename != null) {
                        if (renameTimer != null) {
                            renameTimer.stop();
                        }
                        // start slow click rename timer
                        renameTimer = new Timer(800, this);
                        renameTimer.setRepeats(false);
                        renameTimer.start();
                    }
                }
                
                ((DirectoryTreeModel) tree.getModel()).nodeChanged(node);
                if (row == 0) {
                    tree.revalidate();
                    tree.repaint();
                }
            }
        }

        @Override
        public void mousePressed(MouseEvent e) {
            handlePopupMenu(e);
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            handlePopupMenu(e);
        }
        
        private void handlePopupMenu (MouseEvent e) {
            if (!e.isPopupTrigger()) {
                return;
            }
            final JTree tree = (JTree) e.getSource();
            Point p = e.getPoint();
            int x = e.getX();
            int y = e.getY();
            int row = tree.getRowForLocation(x, y);
            TreePath path = tree.getPathForRow(row);
            
            if (path != null) {
                DirectoryNode node = (DirectoryNode) path.getLastPathComponent();
                ((DirectoryTreeModel) tree.getModel()).nodeChanged(node);
                if(!fileChooser.getFileSystemView().isFileSystem(node.getFile())) {
                    return;
                }

                tree.setSelectionPath(path);
                popupMenu.show(tree, x, y);
            }            
        }
        
        private void changeTreeDirectory(File dir) {
            if (File.separatorChar == '\\' && dir.getPath().endsWith(".lnk")) {
                File linkLocation = getShellFolderForFileLinkLoc(dir);
                if (linkLocation != null && fileChooser.isTraversable(linkLocation)) {
                    dir = linkLocation;
                } else {
                    return;
                }
            }
            fileChooser.setCurrentDirectory(dir);
        }
        
        /********** implementation of CellEditorListener ****************/

        /** Refresh filename text field after rename */
        @Override
        public void editingStopped(ChangeEvent e) {
            DirectoryNode node = (DirectoryNode) tree.getLastSelectedPathComponent();
            if (node != null) {
                setFileName(getStringOfFileName(node.getFile()));
            }
        }

        @Override
        public void editingCanceled(ChangeEvent e) {
            // no operation
        }

        /********** ActionListener impl, slow-double-click rename ******/ 
        
        @Override
        public void actionPerformed(ActionEvent e) {
            if (tree.isFocusOwner() && isSelectionKept(pathToRename)) {
                DirectoryNode node = (DirectoryNode)tree.getLastSelectedPathComponent();
                if (node != null) {
                    applyEdit(node);
                }
            }
            // clear
            cancelRename();
        }
        
        void preprocessMouseEvent (MouseEvent e) {
            if ((e.getID() != MouseEvent.MOUSE_PRESSED) || (e.getButton() != MouseEvent.BUTTON1)) {
                return;
            }
            TreePath clickedPath = tree.getPathForLocation(e.getX(), e.getY());
            if (clickedPath != null && isSelectionKept(clickedPath)) {
                pathToRename = clickedPath;
            }
        }
        
        private boolean isSelectionKept (TreePath selPath) {
            if (curSelPath != null) {
                TreePath oldSel = curSelPath.get();
                if (oldSel != null && oldSel.equals(selPath)) {
                    return true;
                }
            }
            return false;
        }
        
        private void cancelRename () {
            if (renameTimer != null) {
                renameTimer.stop();
                renameTimer = null;
            }
            pathToRename = null;
        }
        
        /******** implementation of focus listener, for slow click rename cancelling ******/ 

        @Override
        public void focusGained(FocusEvent e) {
            // don't allow to invoke click to rename immediatelly after focus gain
            // what may happen is that tree gains focus by mouse
            // click on selected item - on some platforms selected item
            // is not visible without focus and click to rename will
            // be unwanted and surprising for users
            
            // see run method
            SwingUtilities.invokeLater(this);
        }

        @Override
        public void run() {
            cancelRename();
        }

        @Override
        public void focusLost(FocusEvent e) {
            cancelRename();
        }

    }
    
    private class TreeExpansionHandler implements  TreeExpansionListener  {
        @Override
        public void treeExpanded(TreeExpansionEvent evt) {
            TreePath path = evt.getPath();
            DirectoryNode node = (DirectoryNode) path
                    .getLastPathComponent();
            if(!node.isLoaded()) {
                expandNode(fileChooser, path);
            } else {
                // fixed #96954, to be able to add a new directory
                // when the node has been already loaded
                if(addNewDirectory) {
                    addNewDirectory(path);
                    addNewDirectory = false;
                }
                // Fix for IZ#123815 : Cannot refresh the tree content
                refreshNode( path , node );
            }
        }
        @Override
        public void treeCollapsed(TreeExpansionEvent event) {
        }
        
    }

    // Fix for IZ#123815 : Cannot refresh the tree content
    private void refreshNode( final TreePath path, final DirectoryNode node ){
        final File folder = node.getFile();

        // Additional fixes for IZ#116859 [60cat] Node update bug in the "open project" panel while deleting directories
        if ( !folder.exists() ){
            TreePath parentPath = path.getParentPath();
            boolean refreshTree = false;

            if(tree.isExpanded(path)) {
                tree.collapsePath(path);
                refreshTree = true;
            }
            model.removeNodeFromParent( node );
            if ( refreshTree ){
                tree.expandPath( parentPath );
            }
            return;
        }

        RequestProcessor.getDefault().post(new Runnable() {
            private Set<String> realDirs;
            @Override
            public void run() {
                if (!EventQueue.isDispatchThread()) {
                    // first phase
                    realDirs = new HashSet<String>();
                    File[] files = folder.listFiles();
                    files = files == null ? new File[0] : files;
                    for (File file : files) {
                        if ( !file.isDirectory() ){
                            continue;
                        }
                        String name = file.getName();
                        realDirs.add( name );
                    }
                    SwingUtilities.invokeLater(this);
                } else {
                    // second phase, in EQ thread, invoked from first phase
                    int count = node.getChildCount();
                    Map<String,DirectoryNode> currentFiles = new HashMap<>();
                    for( int i=0; i< count ; i++ ){
                        TreeNode child = node.getChildAt(i);
                        if ( child instanceof DirectoryNode ){
                            File file = ((DirectoryNode)child).getFile();
                            currentFiles.put( file.getName() , (DirectoryNode)child);
                        }
                    }

                    Set<String> realCloned = new HashSet<>(realDirs);
                    if ( realCloned.removeAll( currentFiles.keySet()) ){
                        // Handle added folders
                        for ( String name : realCloned ){
                            DirectoryNode added = new DirectoryNode( new File( folder, name ) );
                            model.insertNodeInto( added, node, node.getChildCount());
                        }
                    }
                    Set<String> currentNames = new HashSet<>(currentFiles.keySet());
                    if ( currentNames.removeAll( realDirs )){
                        // Handle deleted folders
                        for ( String name : currentNames ){
                            DirectoryNode removed = currentFiles.get( name );
                            model.removeNodeFromParent( removed );
                        }
                    }
                }
            }
        });
    }

    
    private class NewDirectoryAction extends AbstractAction {
        @Override
        public void actionPerformed(ActionEvent e) {
            final TreePath path = tree.getSelectionPath();
            
            if(path == null) {
                // if no nodes are selected, get the root node
                // fixed #96954, to be able to add a new directory
                // in the current directory shown in the tree
                addNewDirectory(new TreePath(model.getPathToRoot((DirectoryNode)tree.getModel().getRoot())));
            }
            
            if(path != null) {
                if(tree.isExpanded(path)) {
                    addNewDirectory(path);
                } else {
                    addNewDirectory = true;
                    tree.expandPath(path);
                }
            }
        }
    }
    
    private class DirectoryTreeRenderer implements TreeCellRenderer {
        HtmlRenderer.Renderer renderer = HtmlRenderer.createRenderer();

        @Override
        public Component getTreeCellRendererComponent(
                JTree tree,
                Object value,
                boolean isSelected,
                boolean expanded,
                boolean leaf,
                int row,
                boolean hasFocus) {
            
            Component stringDisplayer = renderer.getTreeCellRendererComponent(tree,
                    value,
                    isSelected,
                    expanded,
                    leaf,
                    row,
                    hasFocus);
            
            if(value instanceof DirectoryNode) {
                tree.setShowsRootHandles(true);
                DirectoryNode node = (DirectoryNode)value;
                ((JLabel)stringDisplayer).setIcon(getNodeIcon(node));
                ((JLabel)stringDisplayer).setText(getNodeText(node.getFile()));
            }
                Font f = stringDisplayer.getFont();
                stringDisplayer.setPreferredSize(new Dimension(stringDisplayer.getPreferredSize().width, 30));

            // allow some space around icon of items
            ((JComponent)stringDisplayer).setBorder(BorderFactory.createEmptyBorder(1, 0, 1, 0));
            stringDisplayer.setSize(stringDisplayer.getPreferredSize());
            
            return stringDisplayer;
        }
        
        private Icon getNodeIcon(DirectoryNode node) {
            File file = node.getFile();
            if(file.exists()) {
                //TODO icon changer code started
                if (customDirectoryProvider != null && customDirectoryProvider.isValidCustomDirectory(file)) {
                    return customDirectoryProvider.getCustomDirectoryIcon();
                }
                //TODO icon changer code ended

                return fileChooser.getIcon(file);
            } else {
                return null;
            }
        }
        
        private String getNodeText(File file) {
            if(file.exists()) {
                return "<html>" + fileChooser.getName(file) + "</html>";
            } else {
                return "<html></html>";
            }
        }
    }
    
    private class DirectoryTreeModel extends DefaultTreeModel {
        
        public DirectoryTreeModel(TreeNode root) {
            super(root);
        }
        
        @Override
        public void valueForPathChanged(TreePath path, Object newValue) {
            boolean refreshTree = false;
            DirectoryNode node = (DirectoryNode)path.getLastPathComponent();
            File f = node.getFile();
            File newFile = new File(f.getParentFile(), (String)newValue);
            
            if(f.renameTo(newFile)) {
                // fix bug #97521, #96960
                if(tree.isExpanded(path)) {
                    tree.collapsePath(path);
                    refreshTree = true;
                }
                
                node.setFile(newFile);
                node.removeAllChildren();
                
                ((DefaultTreeModel) tree.getModel()).nodeStructureChanged(node);
                if(refreshTree) {
                    tree.expandPath(path);
                }
            }
        }
    }
}
