/*
 * Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.apple.laf;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.net.URI;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
import java.util.Vector;

import javax.swing.AbstractAction;
import javax.swing.AbstractListModel;
import javax.swing.Action;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultListSelectionModel;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileSystemView;
import javax.swing.filechooser.FileView;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.FileChooserUI;
import javax.swing.plaf.UIResource;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

import sun.swing.SwingUtilities2;

public class AquaFileChooserUI extends FileChooserUI {
    /* FileView icons */
    protected Icon directoryIcon = null;
    protected Icon fileIcon = null;
    protected Icon computerIcon = null;
    protected Icon hardDriveIcon = null;
    protected Icon floppyDriveIcon = null;

    protected Icon upFolderIcon = null;
    protected Icon homeFolderIcon = null;
    protected Icon listViewIcon = null;
    protected Icon detailsViewIcon = null;

    protected int saveButtonMnemonic = 0;
    protected int openButtonMnemonic = 0;
    protected int cancelButtonMnemonic = 0;
    protected int updateButtonMnemonic = 0;
    protected int helpButtonMnemonic = 0;
    protected int chooseButtonMnemonic = 0;

    private String saveTitleText = null;
    private String openTitleText = null;
    String newFolderTitleText = null;

    protected String saveButtonText = null;
    protected String openButtonText = null;
    protected String cancelButtonText = null;
    protected String updateButtonText = null;
    protected String helpButtonText = null;
    protected String newFolderButtonText = null;
    protected String chooseButtonText = null;

    //private String newFolderErrorSeparator = null;
    String newFolderErrorText = null;
    String newFolderExistsErrorText = null;
    protected String fileDescriptionText = null;
    protected String directoryDescriptionText = null;

    protected String saveButtonToolTipText = null;
    protected String openButtonToolTipText = null;
    protected String cancelButtonToolTipText = null;
    protected String updateButtonToolTipText = null;
    protected String helpButtonToolTipText = null;
    protected String chooseItemButtonToolTipText = null; // Choose anything
    protected String chooseFolderButtonToolTipText = null; // Choose folder
    protected String directoryComboBoxToolTipText = null;
    protected String filenameTextFieldToolTipText = null;
    protected String filterComboBoxToolTipText = null;
    protected String openDirectoryButtonToolTipText = null;
    protected String chooseButtonToolTipText = null;

    protected String cancelOpenButtonToolTipText = null;
    protected String cancelSaveButtonToolTipText = null;
    protected String cancelChooseButtonToolTipText = null;
    protected String cancelNewFolderButtonToolTipText = null;

    protected String desktopName = null;
    String newFolderDialogPrompt = null;
    String newFolderDefaultName = null;
    private String newFileDefaultName = null;
    String createButtonText = null;

    JFileChooser filechooser = null;

    private MouseListener doubleClickListener = null;
    private PropertyChangeListener propertyChangeListener = null;
    private AncestorListener ancestorListener = null;
    private DropTarget dragAndDropTarget = null;

    private static final AcceptAllFileFilter acceptAllFileFilter = new AcceptAllFileFilter();

    private AquaFileSystemModel model;

    final AquaFileView fileView = new AquaFileView(this);

    boolean selectionInProgress = false;

    // The accessoryPanel is a container to place the JFileChooser accessory component
    private JPanel accessoryPanel = null;

    //
    // ComponentUI Interface Implementation methods
    //
    public static ComponentUI createUI(final JComponent c) {
        return new AquaFileChooserUI((JFileChooser)c);
    }

    public AquaFileChooserUI(final JFileChooser filechooser) {
        super();
    }

    public void installUI(final JComponent c) {
        accessoryPanel = new JPanel(new BorderLayout());
        filechooser = (JFileChooser)c;

        createModel();

        installDefaults(filechooser);
        installComponents(filechooser);
        installListeners(filechooser);

        AquaUtils.enforceComponentOrientation(filechooser, ComponentOrientation.getOrientation(Locale.getDefault()));
    }

    public void uninstallUI(final JComponent c) {
        uninstallListeners(filechooser);
        uninstallComponents(filechooser);
        uninstallDefaults(filechooser);

        if (accessoryPanel != null) {
            accessoryPanel.removeAll();
        }

        accessoryPanel = null;
        getFileChooser().removeAll();
    }

    protected void installListeners(final JFileChooser fc) {
        doubleClickListener = createDoubleClickListener(fc, fFileList);
        fFileList.addMouseListener(doubleClickListener);

        propertyChangeListener = createPropertyChangeListener(fc);
        if (propertyChangeListener != null) {
            fc.addPropertyChangeListener(propertyChangeListener);
        }

        ancestorListener = new AncestorListener(){
            public void ancestorAdded(final AncestorEvent e) {
                // Request defaultness for the appropriate button based on mode
                setFocusForMode(getFileChooser());
                // Request defaultness for the appropriate button based on mode
                setDefaultButtonForMode(getFileChooser());
            }

            public void ancestorRemoved(final AncestorEvent e) {
            }

            public void ancestorMoved(final AncestorEvent e) {
            }
        };
        fc.addAncestorListener(ancestorListener);

        fc.registerKeyboardAction(new CancelSelectionAction(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        dragAndDropTarget = new DropTarget(fc, DnDConstants.ACTION_COPY, new DnDHandler(), true);
        fc.setDropTarget(dragAndDropTarget);
    }

    protected void uninstallListeners(final JFileChooser fc) {
        if (propertyChangeListener != null) {
            fc.removePropertyChangeListener(propertyChangeListener);
        }
        fFileList.removeMouseListener(doubleClickListener);
        fc.removePropertyChangeListener(filterComboBoxModel);
        fc.removePropertyChangeListener(model);
        fc.unregisterKeyboardAction(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0));
        fc.removeAncestorListener(ancestorListener);
        fc.setDropTarget(null);
        ancestorListener = null;
    }

    protected void installDefaults(final JFileChooser fc) {
        installIcons(fc);
        installStrings(fc);
        setPackageIsTraversable(fc.getClientProperty(PACKAGE_TRAVERSABLE_PROPERTY));
        setApplicationIsTraversable(fc.getClientProperty(APPLICATION_TRAVERSABLE_PROPERTY));
    }

    protected void installIcons(final JFileChooser fc) {
        directoryIcon = UIManager.getIcon("FileView.directoryIcon");
        fileIcon = UIManager.getIcon("FileView.fileIcon");
        computerIcon = UIManager.getIcon("FileView.computerIcon");
        hardDriveIcon = UIManager.getIcon("FileView.hardDriveIcon");
    }

    String getString(final String uiKey, final String fallback) {
        final String result = UIManager.getString(uiKey);
        return (result == null ? fallback : result);
    }

    protected void installStrings(final JFileChooser fc) {
        // Exist in basic.properties (though we might want to override)
        fileDescriptionText = UIManager.getString("FileChooser.fileDescriptionText");
        directoryDescriptionText = UIManager.getString("FileChooser.directoryDescriptionText");
        newFolderErrorText = getString("FileChooser.newFolderErrorText", "Error occurred during folder creation");

        saveButtonText = UIManager.getString("FileChooser.saveButtonText");
        openButtonText = UIManager.getString("FileChooser.openButtonText");
        cancelButtonText = UIManager.getString("FileChooser.cancelButtonText");
        updateButtonText = UIManager.getString("FileChooser.updateButtonText");
        helpButtonText = UIManager.getString("FileChooser.helpButtonText");

        saveButtonMnemonic = UIManager.getInt("FileChooser.saveButtonMnemonic");
        openButtonMnemonic = UIManager.getInt("FileChooser.openButtonMnemonic");
        cancelButtonMnemonic = UIManager.getInt("FileChooser.cancelButtonMnemonic");
        updateButtonMnemonic = UIManager.getInt("FileChooser.updateButtonMnemonic");
        helpButtonMnemonic = UIManager.getInt("FileChooser.helpButtonMnemonic");
        chooseButtonMnemonic = UIManager.getInt("FileChooser.chooseButtonMnemonic");

        saveButtonToolTipText = UIManager.getString("FileChooser.saveButtonToolTipText");
        openButtonToolTipText = UIManager.getString("FileChooser.openButtonToolTipText");
        cancelButtonToolTipText = UIManager.getString("FileChooser.cancelButtonToolTipText");
        updateButtonToolTipText = UIManager.getString("FileChooser.updateButtonToolTipText");
        helpButtonToolTipText = UIManager.getString("FileChooser.helpButtonToolTipText");

        // Mac-specific, but fallback to basic if it's missing
        saveTitleText = getString("FileChooser.saveTitleText", saveButtonText);
        openTitleText = getString("FileChooser.openTitleText", openButtonText);

        // Mac-specific, required
        newFolderExistsErrorText = getString("FileChooser.newFolderExistsErrorText", "That name is already taken");
        chooseButtonText = getString("FileChooser.chooseButtonText", "Choose");
        chooseButtonToolTipText = getString("FileChooser.chooseButtonToolTipText", "Choose selected file");
        newFolderButtonText = getString("FileChooser.newFolderButtonText", "New");
        newFolderTitleText = getString("FileChooser.newFolderTitleText", "New Folder");

        if (fc.getDialogType() == JFileChooser.SAVE_DIALOG) {
            fileNameLabelText = getString("FileChooser.saveDialogFileNameLabelText", "Save As:");
        } else {
            fileNameLabelText = getString("FileChooser.fileNameLabelText", "Name:");
        }

        filesOfTypeLabelText = getString("FileChooser.filesOfTypeLabelText", "Format:");

        desktopName = getString("FileChooser.desktopName", "Desktop");
        newFolderDialogPrompt = getString("FileChooser.newFolderPromptText", "Name of new folder:");
        newFolderDefaultName = getString("FileChooser.untitledFolderName", "untitled folder");
        newFileDefaultName = getString("FileChooser.untitledFileName", "untitled");
        createButtonText = getString("FileChooser.createButtonText", "Create");

        fColumnNames[1] = getString("FileChooser.byDateText", "Date Modified");
        fColumnNames[0] = getString("FileChooser.byNameText", "Name");

        // Mac-specific, optional
        chooseItemButtonToolTipText = UIManager.getString("FileChooser.chooseItemButtonToolTipText");
        chooseFolderButtonToolTipText = UIManager.getString("FileChooser.chooseFolderButtonToolTipText");
        openDirectoryButtonToolTipText = UIManager.getString("FileChooser.openDirectoryButtonToolTipText");

        directoryComboBoxToolTipText = UIManager.getString("FileChooser.directoryComboBoxToolTipText");
        filenameTextFieldToolTipText = UIManager.getString("FileChooser.filenameTextFieldToolTipText");
        filterComboBoxToolTipText = UIManager.getString("FileChooser.filterComboBoxToolTipText");

        cancelOpenButtonToolTipText = UIManager.getString("FileChooser.cancelOpenButtonToolTipText");
        cancelSaveButtonToolTipText = UIManager.getString("FileChooser.cancelSaveButtonToolTipText");
        cancelChooseButtonToolTipText = UIManager.getString("FileChooser.cancelChooseButtonToolTipText");
        cancelNewFolderButtonToolTipText = UIManager.getString("FileChooser.cancelNewFolderButtonToolTipText");

        newFolderTitleText = UIManager.getString("FileChooser.newFolderTitleText");
        newFolderToolTipText = UIManager.getString("FileChooser.newFolderToolTipText");
        newFolderAccessibleName = getString("FileChooser.newFolderAccessibleName", newFolderTitleText);
    }

    protected void uninstallDefaults(final JFileChooser fc) {
        uninstallIcons(fc);
        uninstallStrings(fc);
    }

    protected void uninstallIcons(final JFileChooser fc) {
        directoryIcon = null;
        fileIcon = null;
        computerIcon = null;
        hardDriveIcon = null;
        floppyDriveIcon = null;

        upFolderIcon = null;
        homeFolderIcon = null;
        detailsViewIcon = null;
        listViewIcon = null;
    }

    protected void uninstallStrings(final JFileChooser fc) {
        saveTitleText = null;
        openTitleText = null;
        newFolderTitleText = null;

        saveButtonText = null;
        openButtonText = null;
        cancelButtonText = null;
        updateButtonText = null;
        helpButtonText = null;
        newFolderButtonText = null;
        chooseButtonText = null;

        cancelOpenButtonToolTipText = null;
        cancelSaveButtonToolTipText = null;
        cancelChooseButtonToolTipText = null;
        cancelNewFolderButtonToolTipText = null;
        chooseButtonToolTipText = null;

        saveButtonToolTipText = null;
        openButtonToolTipText = null;
        cancelButtonToolTipText = null;
        updateButtonToolTipText = null;
        helpButtonToolTipText = null;
        chooseItemButtonToolTipText = null;
        chooseFolderButtonToolTipText = null;
        openDirectoryButtonToolTipText = null;
        directoryComboBoxToolTipText = null;
        filenameTextFieldToolTipText = null;
        filterComboBoxToolTipText = null;

        newFolderDefaultName = null;
        newFileDefaultName = null;

        desktopName = null;
    }

    protected void createModel() {
    }

    AquaFileSystemModel getModel() {
        return model;
    }

    /*
     * Listen for filechooser property changes, such as
     * the selected file changing, or the type of the dialog changing.
     */
    // Taken almost verbatim from Metal
    protected PropertyChangeListener createPropertyChangeListener(final JFileChooser fc) {
        return new PropertyChangeListener(){
            public void propertyChange(final PropertyChangeEvent e) {
                final String prop = e.getPropertyName();
                if (prop.equals(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)) {
                    final File f = (File)e.getNewValue();
                    if (f != null) {
                        // Select the file in the list if the selected file didn't change as
                        // a result of a list click.
                        if (!selectionInProgress && getModel().contains(f)) {
                            fFileList.setSelectedIndex(getModel().indexOf(f));
                        }

                        // [3643835] Need to populate the text field here.  No-op on Open dialogs
                        // Note that this was removed for 3514735, but should not have been.
                        if (!f.isDirectory()) {
                            setFileName(getFileChooser().getName(f));
                        }
                    }
                    updateButtonState(getFileChooser());
                } else if (prop.equals(JFileChooser.SELECTED_FILES_CHANGED_PROPERTY)) {
                    JFileChooser fileChooser = getFileChooser();
                    if (!fileChooser.isDirectorySelectionEnabled()) {
                        final File[] files = (File[]) e.getNewValue();
                        if (files != null) {
                            for (int selectedRow : fFileList.getSelectedRows()) {
                                File file = (File) fFileList.getValueAt(selectedRow, 0);
                                if (fileChooser.isTraversable(file)) {
                                    fFileList.removeSelectedIndex(selectedRow);
                                }
                            }
                        }
                    }
                } else if (prop.equals(JFileChooser.DIRECTORY_CHANGED_PROPERTY)) {
                    fFileList.clearSelection();
                    final File currentDirectory = getFileChooser().getCurrentDirectory();
                    if (currentDirectory != null) {
                        fDirectoryComboBoxModel.addItem(currentDirectory);
                        // Enable the newFolder action if the current directory
                        // is writable.
                        // PENDING(jeff) - broken - fix
                        getAction(kNewFolder).setEnabled(currentDirectory.canWrite());
                    }
                    updateButtonState(getFileChooser());
                } else if (prop.equals(JFileChooser.FILE_SELECTION_MODE_CHANGED_PROPERTY)) {
                    fFileList.clearSelection();
                    setBottomPanelForMode(getFileChooser()); // Also updates approve button
                } else if (prop == JFileChooser.ACCESSORY_CHANGED_PROPERTY) {
                    if (getAccessoryPanel() != null) {
                        if (e.getOldValue() != null) {
                            getAccessoryPanel().remove((JComponent)e.getOldValue());
                        }
                        final JComponent accessory = (JComponent)e.getNewValue();
                        if (accessory != null) {
                            getAccessoryPanel().add(accessory, BorderLayout.CENTER);
                        }
                    }
                } else if (prop == JFileChooser.APPROVE_BUTTON_TEXT_CHANGED_PROPERTY) {
                    updateApproveButton(getFileChooser());
                    getFileChooser().invalidate();
                } else if (prop == JFileChooser.DIALOG_TYPE_CHANGED_PROPERTY) {
                    if (getFileChooser().getDialogType() == JFileChooser.SAVE_DIALOG) {
                        fileNameLabelText = getString("FileChooser.saveDialogFileNameLabelText", "Save As:");
                    } else {
                        fileNameLabelText = getString("FileChooser.fileNameLabelText", "Name:");
                    }
                    fTextFieldLabel.setText(fileNameLabelText);

                    // Mac doesn't show the text field or "new folder" button in 'Open' dialogs
                    setBottomPanelForMode(getFileChooser()); // Also updates approve button
                } else if (prop.equals(JFileChooser.APPROVE_BUTTON_MNEMONIC_CHANGED_PROPERTY)) {
                    getApproveButton(getFileChooser()).setMnemonic(getApproveButtonMnemonic(getFileChooser()));
                } else if (prop.equals(PACKAGE_TRAVERSABLE_PROPERTY)) {
                    setPackageIsTraversable(e.getNewValue());
                } else if (prop.equals(APPLICATION_TRAVERSABLE_PROPERTY)) {
                    setApplicationIsTraversable(e.getNewValue());
                } else if (prop.equals(JFileChooser.MULTI_SELECTION_ENABLED_CHANGED_PROPERTY)) {
                    if (getFileChooser().isMultiSelectionEnabled()) {
                        fFileList.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
                    } else {
                        fFileList.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
                    }
                } else if (prop.equals(JFileChooser.CONTROL_BUTTONS_ARE_SHOWN_CHANGED_PROPERTY)) {
                    doControlButtonsChanged(e);
                }
            }
        };
    }

    void setPackageIsTraversable(final Object o) {
        int newProp = -1;
        if (o instanceof String s) newProp = parseTraversableProperty(s);
        if (newProp != -1) fPackageIsTraversable = newProp;
        else fPackageIsTraversable = sGlobalPackageIsTraversable;
    }

    void setApplicationIsTraversable(final Object o) {
        int newProp = -1;
        if (o instanceof String s) newProp = parseTraversableProperty(s);
        if (newProp != -1) fApplicationIsTraversable = newProp;
        else fApplicationIsTraversable = sGlobalApplicationIsTraversable;
    }

    void doControlButtonsChanged(final PropertyChangeEvent e) {
        if (getFileChooser().getControlButtonsAreShown()) {
            fBottomPanel.add(fDirectoryPanelSpacer);
            fBottomPanel.add(fDirectoryPanel);
        } else {
            fBottomPanel.remove(fDirectoryPanelSpacer);
            fBottomPanel.remove(fDirectoryPanel);
        }
    }

    public String getFileName() {
        if (filenameTextField != null) { return filenameTextField.getText(); }
        return null;
    }

    public String getDirectoryName() {
        // PENDING(jeff) - get the name from the directory combobox
        return null;
    }

    public void setFileName(final String filename) {
        if (filenameTextField != null) {
            filenameTextField.setText(filename);
        }
    }

    public void setDirectoryName(final String dirname) {
        // PENDING(jeff) - set the name in the directory combobox
    }

    public void rescanCurrentDirectory(final JFileChooser fc) {
        getModel().invalidateFileCache();
        getModel().validateFileCache();
    }

    public void ensureFileIsVisible(final JFileChooser fc, final File f) {
        if (f == null) {
            fFileList.requestFocusInWindow();
            fFileList.ensureIndexIsVisible(-1);
            return;
        }

        getModel().runWhenDone(new Runnable() {
            public void run() {
                fFileList.requestFocusInWindow();
                fFileList.ensureIndexIsVisible(getModel().indexOf(f));
            }
        });
    }

    public JFileChooser getFileChooser() {
        return filechooser;
    }

    public JPanel getAccessoryPanel() {
        return accessoryPanel;
    }

    protected JButton getApproveButton(final JFileChooser fc) {
        return fApproveButton;
    }

    @Override
    public JButton getDefaultButton(JFileChooser fc) {
        return getApproveButton(fc);
    }

    public int getApproveButtonMnemonic(final JFileChooser fc) {
        return fSubPanel.getApproveButtonMnemonic(fc);
    }

    public String getApproveButtonToolTipText(final JFileChooser fc) {
        return fSubPanel.getApproveButtonToolTipText(fc);
    }

    public String getApproveButtonText(final JFileChooser fc) {
        return fSubPanel.getApproveButtonText(fc);
    }

    protected String getCancelButtonToolTipText(final JFileChooser fc) {
        return fSubPanel.getCancelButtonToolTipText(fc);
    }

    // If the item's not selectable, it'll be visible but disabled in the list
    boolean isSelectableInList(final File f) {
        return fSubPanel.isSelectableInList(getFileChooser(), f);
    }

    // Is this a file that the JFileChooser wants?
    // Directories can be selected in the list regardless of mode
    boolean isSelectableForMode(final JFileChooser fc, final File f) {
        if (f == null) return false;
        final int mode = fc.getFileSelectionMode();
        if (mode == JFileChooser.FILES_AND_DIRECTORIES) return true;
        boolean traversable = fc.isTraversable(f);
        if (mode == JFileChooser.DIRECTORIES_ONLY) return traversable;
        return !traversable;
    }

    // ********************************************
    // ************ Create Listeners **************
    // ********************************************

    // From Basic
    public ListSelectionListener createListSelectionListener(final JFileChooser fc) {
        return new SelectionListener();
    }

    protected class SelectionListener implements ListSelectionListener {
        public void valueChanged(final ListSelectionEvent e) {
            if (e.getValueIsAdjusting()) return;

            File f = null;
            final int selectedRow = fFileList.getSelectedRow();
            final JFileChooser chooser = getFileChooser();
            boolean isSave = (chooser.getDialogType() == JFileChooser.SAVE_DIALOG);
            if (selectedRow >= 0) {
                f = (File)fFileList.getValueAt(selectedRow, 0);
            }

            // Save dialog lists can't be multi select, because all we're selecting is the next folder to open
            selectionInProgress = true;
            if (!isSave && chooser.isMultiSelectionEnabled()) {
                final int[] rows = fFileList.getSelectedRows();
                int selectableCount = 0;
                // Double-check that all the list selections are valid for this mode
                // Directories can be selected in the list regardless of mode
                for (final int element : rows) {
                    if (isSelectableForMode(chooser, (File) fFileList.getValueAt(element, 0))) selectableCount++;
                }
                if (selectableCount > 0) {
                    final File[] files = new File[selectableCount];
                    for (int i = 0, si = 0; i < rows.length; i++) {
                        f = (File)fFileList.getValueAt(rows[i], 0);
                        if (isSelectableForMode(chooser, f)) {
                            if (fileView.isAlias(f)) {
                                f = fileView.resolveAlias(f);
                            }
                            files[si++] = f;
                        }
                    }
                    chooser.setSelectedFiles(files);
                } else {
                    chooser.setSelectedFiles(null);
                }
            } else {
                chooser.setSelectedFiles(null);
                chooser.setSelectedFile(f);
            }
            selectionInProgress = false;
        }
    }

    // When the Save textfield has the focus, the button should say "Save"
    // Otherwise, it depends on the list selection
    protected class SaveTextFocusListener implements FocusListener {
        public void focusGained(final FocusEvent e) {
            updateButtonState(getFileChooser());
        }

        // Do nothing, we might be losing focus due to window deactivation
        public void focusLost(final FocusEvent e) {

        }
    }

    // When the Save textfield is empty and the button says "Save", it should be disabled
    // Otherwise, it depends on the list selection
    protected class SaveTextDocumentListener implements DocumentListener {
        public void insertUpdate(final DocumentEvent e) {
            textChanged();
        }

        public void removeUpdate(final DocumentEvent e) {
            textChanged();
        }

        public void changedUpdate(final DocumentEvent e) {

        }

        void textChanged() {
            updateButtonState(getFileChooser());
        }
    }

    // Opens the File object if it's a traversable directory
    protected boolean openDirectory(final File f) {
        if (getFileChooser().isTraversable(f)) {
            fFileList.clearSelection();
            // Resolve any aliases
            final File original = fileView.resolveAlias(f);
            getFileChooser().setCurrentDirectory(original);
            updateButtonState(getFileChooser());
            return true;
        }
        return false;
    }

    // From Basic
    protected class DoubleClickListener extends MouseAdapter {
        JTableExtension list;

        public DoubleClickListener(final JTableExtension list) {
            this.list = list;
        }

        public void mouseClicked(final MouseEvent e) {
            if (e.getClickCount() != 2) return;

            final int index = list.locationToIndex(e.getPoint());
            if (index < 0) return;

            final File f = (File)((AquaFileSystemModel)list.getModel()).getElementAt(index);
            if (openDirectory(f)) return;

            if (!isSelectableInList(f)) return;
            getFileChooser().approveSelection();
        }
    }

    protected MouseListener createDoubleClickListener(final JFileChooser fc, final JTableExtension list) {
        return new DoubleClickListener(list);
    }

    // listens for drag events onto the JFileChooser and sets the selected file or directory
    class DnDHandler extends DropTargetAdapter {
        public void dragEnter(final DropTargetDragEvent dtde) {
            tryToAcceptDrag(dtde);
        }

        public void dragOver(final DropTargetDragEvent dtde) {
            tryToAcceptDrag(dtde);
        }

        public void dropActionChanged(final DropTargetDragEvent dtde) {
            tryToAcceptDrag(dtde);
        }

        public void drop(final DropTargetDropEvent dtde) {
            if (dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
                handleFileDropEvent(dtde);
                return;
            }

            if (dtde.isDataFlavorSupported(DataFlavor.stringFlavor)) {
                handleStringDropEvent(dtde);
                return;
            }
        }

        protected void tryToAcceptDrag(final DropTargetDragEvent dtde) {
            if (dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor) || dtde.isDataFlavorSupported(DataFlavor.stringFlavor)) {
                dtde.acceptDrag(DnDConstants.ACTION_COPY);
                return;
            }

            dtde.rejectDrag();
        }

        protected void handleFileDropEvent(final DropTargetDropEvent dtde) {
            dtde.acceptDrop(dtde.getDropAction());
            final Transferable transferable = dtde.getTransferable();

            try {
                @SuppressWarnings("unchecked")
                final java.util.List<File> fileList = (java.util.List<File>)transferable.getTransferData(DataFlavor.javaFileListFlavor);
                dropFiles(fileList.toArray(new File[fileList.size()]));
                dtde.dropComplete(true);
            } catch (final Exception e) {
                dtde.dropComplete(false);
            }
        }

        protected void handleStringDropEvent(final DropTargetDropEvent dtde) {
            dtde.acceptDrop(dtde.getDropAction());
            final Transferable transferable = dtde.getTransferable();

            final String stringData;
            try {
                stringData = (String)transferable.getTransferData(DataFlavor.stringFlavor);
            } catch (final Exception e) {
                dtde.dropComplete(false);
                return;
            }

            try {
                final File fileAsPath = new File(stringData);
                if (fileAsPath.exists()) {
                    dropFiles(new File[] {fileAsPath});
                    dtde.dropComplete(true);
                    return;
                }
            } catch (final Exception e) {
                // try again
            }

            try {
                final File fileAsURI = new File(new URI(stringData));
                if (fileAsURI.exists()) {
                    dropFiles(new File[] {fileAsURI});
                    dtde.dropComplete(true);
                    return;
                }
            } catch (final Exception e) {
                // nothing more to do
            }

            dtde.dropComplete(false);
        }

        protected void dropFiles(final File[] files) {
            final JFileChooser jfc = getFileChooser();

            if (files.length == 1) {
                if (files[0].isDirectory()) {
                    jfc.setCurrentDirectory(files[0]);
                    return;
                }

                if (!isSelectableForMode(jfc, files[0])) {
                    return;
                }
            }

            jfc.setSelectedFiles(files);
            for (final File file : files) {
                jfc.ensureFileIsVisible(file);
            }
            getModel().runWhenDone(new Runnable() {
                public void run() {
                    final AquaFileSystemModel fileSystemModel = getModel();
                    for (final File element : files) {
                        final int index = fileSystemModel.indexOf(element);
                        if (index >= 0) fFileList.addRowSelectionInterval(index, index);
                    }
                }
            });
        }
    }

    // FileChooser UI PLAF methods

    /**
     * Returns the default accept all file filter
     */
    public FileFilter getAcceptAllFileFilter(final JFileChooser fc) {
        return acceptAllFileFilter;
    }

    public FileView getFileView(final JFileChooser fc) {
        return fileView;
    }

    /**
     * Returns the title of this dialog
     */
    public String getDialogTitle(final JFileChooser fc) {
        if (fc.getDialogTitle() == null) {
            if (getFileChooser().getDialogType() == JFileChooser.OPEN_DIALOG) {
                return openTitleText;
            } else if (getFileChooser().getDialogType() == JFileChooser.SAVE_DIALOG) { return saveTitleText; }
        }
        return fc.getDialogTitle();
    }

    // Utility to get the first selected item regardless of whether we're single or multi select
    File getFirstSelectedItem() {
        // Get the selected item
        File selectedFile = null;
        final int index = fFileList.getSelectedRow();
        if (index >= 0) {
            selectedFile = (File)((AquaFileSystemModel)fFileList.getModel()).getElementAt(index);
        }
        return selectedFile;
    }

    // Make a file from the filename
    File makeFile(final JFileChooser fc, final String filename) {
        File selectedFile = null;
        // whitespace is legal on Macs, even on beginning and end of filename
        if (filename != null && !filename.isEmpty()) {
            final FileSystemView fs = fc.getFileSystemView();
            selectedFile = fs.createFileObject(filename);
            if (!selectedFile.isAbsolute()) {
                selectedFile = fs.createFileObject(fc.getCurrentDirectory(), filename);
            }
        }
        return selectedFile;
    }

    // Utility to tell if the textfield has anything in it
    boolean textfieldIsValid() {
        final String s = getFileName();
        return (s != null && !s.isEmpty());
    }

    // Action to attach to the file list so we can override the default action
    // of the table for the return key, which is to select the next line.
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    protected class DefaultButtonAction extends AbstractAction {
        public void actionPerformed(final ActionEvent e) {
            final JRootPane root = AquaFileChooserUI.this.getFileChooser().getRootPane();
            final JFileChooser fc = AquaFileChooserUI.this.getFileChooser();
            final JButton owner = root.getDefaultButton();
            if (owner != null && SwingUtilities.getRootPane(owner) == root && owner.isEnabled()) {
                owner.doClick(20);
            } else if (!fc.getControlButtonsAreShown()) {
                final JButton defaultButton = AquaFileChooserUI.this.fSubPanel.getDefaultButton(fc);

                if (defaultButton != null) {
                    defaultButton.doClick(20);
                }
            } else {
                Toolkit.getDefaultToolkit().beep();
            }
        }

        public boolean isEnabled() {
            return true;
        }
    }

    /**
     * Creates a new folder.
     */
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    protected class NewFolderAction extends AbstractAction {
        protected NewFolderAction() {
            super(newFolderAccessibleName);
        }

        // Muchlike showInputDialog, but we give it options instead of selectionValues
        private Object showNewFolderDialog(final Component parentComponent, final Object message, final String title, final int messageType, final Icon icon, final Object[] options, final Object initialSelectionValue) {
            final JOptionPane pane = new JOptionPane(message, messageType, JOptionPane.OK_CANCEL_OPTION, icon, options, null);

            pane.setWantsInput(true);
            pane.setInitialSelectionValue(initialSelectionValue);

            final JDialog dialog = pane.createDialog(parentComponent, title);

            pane.selectInitialValue();
            dialog.setVisible(true);
            dialog.dispose();

            final Object value = pane.getValue();

            if (value == null || value.equals(cancelButtonText)
                    || value.equals(JOptionPane.CLOSED_OPTION)) {
                return null;
            }
            return pane.getInputValue();
        }

        public void actionPerformed(final ActionEvent e) {
            final JFileChooser fc = getFileChooser();
            final File currentDirectory = fc.getCurrentDirectory();
            File newFolder = null;
            final String[] options = {createButtonText, cancelButtonText};
            final String filename = (String)showNewFolderDialog(fc, //parentComponent
                    newFolderDialogPrompt, // message
                    newFolderTitleText, // title
                    JOptionPane.PLAIN_MESSAGE, // messageType
                    null, // icon
                    options, // selectionValues
                    newFolderDefaultName); // initialSelectionValue

            if (filename != null) {
                try {
                    newFolder = fc.getFileSystemView().createFileObject(currentDirectory, filename);
                    if (newFolder.exists()) {
                        JOptionPane.showMessageDialog(fc, newFolderExistsErrorText, "", JOptionPane.ERROR_MESSAGE);
                        return;
                    }

                    newFolder.mkdirs();
                } catch(final Exception exc) {
                    JOptionPane.showMessageDialog(fc, newFolderErrorText, "", JOptionPane.ERROR_MESSAGE);
                    return;
                }

                openDirectory(newFolder);
            }
        }
    }

    /**
     * Responds to an Open, Save, or Choose request
     */
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    protected class ApproveSelectionAction extends AbstractAction {
        public void actionPerformed(final ActionEvent e) {
            fSubPanel.approveSelection(getFileChooser());
        }
    }

    /**
     * Responds to an OpenDirectory request
     */
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    protected class OpenSelectionAction extends AbstractAction {
        public void actionPerformed(final ActionEvent e) {
            final int index = fFileList.getSelectedRow();
            if (index >= 0) {
                final File selectedFile = (File)((AquaFileSystemModel)fFileList.getModel()).getElementAt(index);
                if (selectedFile != null) openDirectory(selectedFile);
            }
        }
    }

    /**
     * Responds to a cancel request.
     */
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    protected class CancelSelectionAction extends AbstractAction {
        public void actionPerformed(final ActionEvent e) {
            getFileChooser().cancelSelection();
        }

        public boolean isEnabled() {
            return getFileChooser().isEnabled();
        }
    }

    /**
     * Rescans the files in the current directory
     */
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    protected class UpdateAction extends AbstractAction {
        public void actionPerformed(final ActionEvent e) {
            final JFileChooser fc = getFileChooser();
            fc.setCurrentDirectory(fc.getFileSystemView().createFileObject(getDirectoryName()));
            fc.rescanCurrentDirectory();
        }
    }

    // *****************************************
    // ***** default AcceptAll file filter *****
    // *****************************************
    private static class AcceptAllFileFilter extends FileFilter {
        public AcceptAllFileFilter() {
        }

        public boolean accept(final File f) {
            return true;
        }

        public String getDescription() {
            return UIManager.getString("FileChooser.acceptAllFileFilterText");
        }
    }

    // Penultimate superclass is JLabel
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    protected class MacFCTableCellRenderer extends DefaultTableCellRenderer {
        boolean fIsSelected = false;

        public MacFCTableCellRenderer(final Font f) {
            super();
            setFont(f);
            setIconTextGap(10);
        }

        public Component getTableCellRendererComponent(final JTable list, final Object value, final boolean isSelected, final boolean cellHasFocus, final int index, final int col) {
            super.getTableCellRendererComponent(list, value, isSelected, false, index, col); // No focus border, thanks
            fIsSelected = isSelected;
            return this;
        }

        public boolean isSelected() {
            return fIsSelected && isEnabled();
        }

        protected String layoutCL(final JLabel label, final FontMetrics fontMetrics, final String text, final Icon icon, final Rectangle viewR, final Rectangle iconR, final Rectangle textR) {
            return SwingUtilities.layoutCompoundLabel(label, fontMetrics, text, icon, label.getVerticalAlignment(), label.getHorizontalAlignment(), label.getVerticalTextPosition(), label.getHorizontalTextPosition(), viewR, iconR, textR, label.getIconTextGap());
        }

        protected void paintComponent(final Graphics g) {
            final String text = getText();
            Icon icon = getIcon();
            if (icon != null && !isEnabled()) {
                final Icon disabledIcon = getDisabledIcon();
                if (disabledIcon != null) icon = disabledIcon;
            }

            if ((icon == null) && (text == null)) { return; }

            // from ComponentUI update
            g.setColor(getBackground());
            g.fillRect(0, 0, getWidth(), getHeight());

            // from BasicLabelUI paint
            final FontMetrics fm = g.getFontMetrics();
            Insets paintViewInsets = getInsets(null);
            paintViewInsets.left += 10;

            Rectangle paintViewR = new Rectangle(paintViewInsets.left, paintViewInsets.top, getWidth() - (paintViewInsets.left + paintViewInsets.right), getHeight() - (paintViewInsets.top + paintViewInsets.bottom));

            Rectangle paintIconR = new Rectangle();
            Rectangle paintTextR = new Rectangle();

            final String clippedText = layoutCL(this, fm, text, icon, paintViewR, paintIconR, paintTextR);

            if (icon != null) {
                icon.paintIcon(this, g, paintIconR.x + 5, paintIconR.y);
            }

            if (text != null) {
                final int textX = paintTextR.x;
                final int textY = paintTextR.y + fm.getAscent() + 1;
                if (isEnabled()) {
                    // Color background = fIsSelected ? getForeground() : getBackground();
                    final Color background = getBackground();

                    g.setColor(background);
                    g.fillRect(textX - 1, paintTextR.y, paintTextR.width + 2, fm.getAscent() + 2);

                    g.setColor(getForeground());
                    SwingUtilities2.drawString(filechooser, g, clippedText, textX, textY);
                } else {
                    final Color background = getBackground();
                    g.setColor(background);
                    g.fillRect(textX - 1, paintTextR.y, paintTextR.width + 2, fm.getAscent() + 2);

                    g.setColor(background.brighter());
                    SwingUtilities2.drawString(filechooser, g, clippedText, textX, textY);
                    g.setColor(background.darker());
                    SwingUtilities2.drawString(filechooser, g, clippedText, textX + 1, textY + 1);
                }
            }
        }

    }

    @SuppressWarnings("serial") // Superclass is not serializable across versions
    protected class FileRenderer extends MacFCTableCellRenderer {
        public FileRenderer(final Font f) {
            super(f);
        }

        public Component getTableCellRendererComponent(final JTable list,
                                                       final Object value,
                                                       final boolean isSelected,
                                                       final boolean cellHasFocus,
                                                       final int index,
                                                       final int col) {
            super.getTableCellRendererComponent(list, value, isSelected, false,
                                                index,
                                                col); // No focus border, thanks
            final File file = (File)value;
            final JFileChooser fc = getFileChooser();
            setText(fc.getName(file));
            setIcon(fc.getIcon(file));
            setEnabled(isSelectableInList(file));
            return this;
        }
    }

    @SuppressWarnings("serial") // Superclass is not serializable across versions
    protected class DateRenderer extends MacFCTableCellRenderer {
        public DateRenderer(final Font f) {
            super(f);
        }

        public Component getTableCellRendererComponent(final JTable list,
                                                       final Object value,
                                                       final boolean isSelected,
                                                       final boolean cellHasFocus,
                                                       final int index,
                                                       final int col) {
            super.getTableCellRendererComponent(list, value, isSelected, false,
                                                index, col);
            final File file = (File)fFileList.getValueAt(index, 0);
            setEnabled(isSelectableInList(file));
            final DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.SHORT);
            final Date date = (Date)value;

            if (date != null) {
                setText(formatter.format(date));
            } else {
                setText("");
            }

            return this;
        }
    }

    @Override
    public Dimension getPreferredSize(final JComponent c) {
        return new Dimension(PREF_WIDTH, PREF_HEIGHT);
    }

    @Override
    public Dimension getMinimumSize(final JComponent c) {
        return new Dimension(MIN_WIDTH, MIN_HEIGHT);
    }

    @Override
    public Dimension getMaximumSize(final JComponent c) {
        return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
    }

    @SuppressWarnings("serial") // anonymous class
    protected ListCellRenderer<File> createDirectoryComboBoxRenderer(final JFileChooser fc) {
        return new AquaComboBoxRendererInternal<File>(directoryComboBox) {
            public Component getListCellRendererComponent(final JList<? extends File> list,
                                                          final File directory,
                                                          final int index,
                                                          final boolean isSelected,
                                                          final boolean cellHasFocus) {
                super.getListCellRendererComponent(list, directory, index, isSelected, cellHasFocus);
                if (directory == null) {
                    setText("");
                    return this;
                }

                final JFileChooser chooser = getFileChooser();
                setText(chooser.getName(directory));
                setIcon(chooser.getIcon(directory));
                return this;
            }
        };
    }

    //
    // DataModel for DirectoryComboxbox
    //
    protected DirectoryComboBoxModel createDirectoryComboBoxModel(final JFileChooser fc) {
        return new DirectoryComboBoxModel();
    }

    /**
     * Data model for a type-face selection combo-box.
     */
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    protected class DirectoryComboBoxModel extends AbstractListModel<File> implements ComboBoxModel<File> {
        Vector<File> fDirectories = new Vector<File>();
        int topIndex = -1;
        int fPathCount = 0;

        File fSelectedDirectory = null;

        public DirectoryComboBoxModel() {
            super();
            // Add the current directory to the model, and make it the
            // selectedDirectory
            addItem(getFileChooser().getCurrentDirectory());
        }

        /**
         * Removes the selected directory, and clears out the
         * path file entries leading up to that directory.
         */
        private void removeSelectedDirectory() {
            fDirectories.removeAllElements();
            fPathCount = 0;
            fSelectedDirectory = null;
            // dump();
        }

        /**
         * 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.
         */
        void addItem(final File directory) {
            if (directory == null) { return; }
            if (fSelectedDirectory != null) {
                removeSelectedDirectory();
            }

            // create File instances of each directory leading up to the top
            File f = directory.getAbsoluteFile();
            final ArrayList<File> path = new ArrayList<File>(10);
            while (f.getParent() != null) {
                path.add(f);
                f = getFileChooser().getFileSystemView().createFileObject(f.getParent());
            }

            // Add root file (the desktop) to the model
            final File[] roots = getFileChooser().getFileSystemView().getRoots();
            for (final File element : roots) {
                path.add(element);
            }
            fPathCount = path.size();

            // insert all the path fDirectories leading up to the
            // selected directory in reverse order (current directory at top)
            for (int i = 0; i < path.size(); i++) {
                fDirectories.addElement(path.get(i));
            }

            setSelectedItem(fDirectories.elementAt(0));

            // dump();
        }

        public void setSelectedItem(final Object selectedDirectory) {
            this.fSelectedDirectory = (File)selectedDirectory;
            fireContentsChanged(this, -1, -1);
        }

        public Object getSelectedItem() {
            return fSelectedDirectory;
        }

        public int getSize() {
            return fDirectories.size();
        }

        public File getElementAt(final int index) {
            return fDirectories.elementAt(index);
        }
    }

    //
    // Renderer for Types ComboBox
    //
    @SuppressWarnings("serial") // anonymous class
    protected ListCellRenderer<FileFilter> createFilterComboBoxRenderer() {
        return new AquaComboBoxRendererInternal<FileFilter>(filterComboBox) {
            public Component getListCellRendererComponent(final JList<? extends FileFilter> list,
                                                          final FileFilter filter,
                                                          final int index,
                                                          final boolean isSelected,
                                                          final boolean cellHasFocus) {
                super.getListCellRendererComponent(list, filter, index, isSelected, cellHasFocus);
                if (filter != null) setText(filter.getDescription());
                return this;
            }
        };
    }

    //
    // DataModel for Types Comboxbox
    //
    protected FilterComboBoxModel createFilterComboBoxModel() {
        return new FilterComboBoxModel();
    }

    /**
     * Data model for a type-face selection combo-box.
     */
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    protected class FilterComboBoxModel extends AbstractListModel<FileFilter> implements ComboBoxModel<FileFilter>,
            PropertyChangeListener {
        protected FileFilter[] filters;
        Object oldFileFilter = getFileChooser().getFileFilter();

        protected FilterComboBoxModel() {
            super();
            filters = getFileChooser().getChoosableFileFilters();
        }

        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) {
                setSelectedItem(e.getNewValue());
            }
        }

        public void setSelectedItem(Object filter) {
            if (filter != null && !isSelectedFileFilterInModel(filter)) {
                oldFileFilter = filter;
                getFileChooser().setFileFilter((FileFilter) filter);
                fireContentsChanged(this, -1, -1);
            }
        }

        private boolean isSelectedFileFilterInModel(Object filter) {
            return Objects.equals(filter, oldFileFilter);
        }

        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 (FileFilter filter : filters) {
                    if (filter == currentFilter) {
                        found = true;
                    }
                }
                if(found == false) {
                    getFileChooser().addChoosableFileFilter(currentFilter);
                }
            }
            return getFileChooser().getFileFilter();
        }

        public int getSize() {
            if(filters != null) {
                return filters.length;
            } else {
                return 0;
            }
        }

        public FileFilter 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;
            }
        }
    }

    private boolean containsFileFilter(Object fileFilter) {
        return Objects.equals(fileFilter, getFileChooser().getFileFilter());
    }

    /**
     * Acts when FilterComboBox has changed the selected item.
     */
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    protected class FilterComboBoxAction extends AbstractAction {
        protected FilterComboBoxAction() {
            super("FilterComboBoxAction");
        }

        public void actionPerformed(final ActionEvent e) {
            Object selectedFilter = filterComboBox.getSelectedItem();
            if (!containsFileFilter(selectedFilter)) {
                getFileChooser().setFileFilter((FileFilter) selectedFilter);
            }
        }
    }

    /**
     * Acts when DirectoryComboBox has changed the selected item.
     */
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    protected class DirectoryComboBoxAction extends AbstractAction {
        protected DirectoryComboBoxAction() {
            super("DirectoryComboBoxAction");
        }

        public void actionPerformed(final ActionEvent e) {
            getFileChooser().setCurrentDirectory((File)directoryComboBox.getSelectedItem());
        }
    }

    // Sorting Table operations
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    class JSortingTableHeader extends JTableHeader {
        public JSortingTableHeader(final TableColumnModel cm) {
            super(cm);
            setReorderingAllowed(true); // This causes mousePress to call setDraggedColumn
        }

        // One sort state for each column.  Both are ascending by default
        final boolean[] fSortAscending = {true, true};

        // Instead of dragging, it selects which one to sort by
        public void setDraggedColumn(final TableColumn aColumn) {
            if (aColumn != null) {
                final int colIndex = aColumn.getModelIndex();
                if (colIndex != fSortColumn) {
                    filechooser.firePropertyChange(AquaFileSystemModel.SORT_BY_CHANGED, fSortColumn, colIndex);
                    fSortColumn = colIndex;
                } else {
                    fSortAscending[colIndex] = !fSortAscending[colIndex];
                    filechooser.firePropertyChange(AquaFileSystemModel.SORT_ASCENDING_CHANGED, !fSortAscending[colIndex], fSortAscending[colIndex]);
                }
                // Need to repaint the highlighted column.
                repaint();
            }
        }

        // This stops mouseDrags from moving the column
        public TableColumn getDraggedColumn() {
            return null;
        }

        protected TableCellRenderer createDefaultRenderer() {
            final DefaultTableCellRenderer label = new AquaTableCellRenderer();
            label.setHorizontalAlignment(SwingConstants.LEFT);
            return label;
        }

        @SuppressWarnings("serial") // Superclass is not serializable across versions
        class AquaTableCellRenderer extends DefaultTableCellRenderer implements UIResource {
            public Component getTableCellRendererComponent(final JTable localTable, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column) {
                if (localTable != null) {
                    final JTableHeader header = localTable.getTableHeader();
                    if (header != null) {
                        setForeground(header.getForeground());
                        setBackground(header.getBackground());
                        setFont(UIManager.getFont("TableHeader.font"));
                    }
                }

                setText((value == null) ? "" : value.toString());

                // Modify the table "border" to draw smaller, and with the titles in the right position
                // and sort indicators, just like an NSSave/Open panel.
                final AquaTableHeaderBorder cellBorder = AquaTableHeaderBorder.getListHeaderBorder();
                cellBorder.setSelected(column == fSortColumn);
                final int horizontalShift = (column == 0 ? 35 : 10);
                cellBorder.setHorizontalShift(horizontalShift);

                if (column == fSortColumn) {
                    cellBorder.setSortOrder(fSortAscending[column] ? AquaTableHeaderBorder.SORT_ASCENDING : AquaTableHeaderBorder.SORT_DECENDING);
                } else {
                    cellBorder.setSortOrder(AquaTableHeaderBorder.SORT_NONE);
                }
                setBorder(cellBorder);
                return this;
            }
        }
    }

    public void installComponents(final JFileChooser fc) {
        JPanel tPanel; // temp panel
        // set to a Y BoxLayout. The chooser will be laid out top to bottom.
        fc.setLayout(new BoxLayout(fc, BoxLayout.Y_AXIS));
        fc.add(Box.createRigidArea(vstrut10));

        // construct the top panel

        final JPanel topPanel = new JPanel();
        topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));
        fc.add(topPanel);
        fc.add(Box.createRigidArea(vstrut10));

        // Add the textfield pane

        fTextfieldPanel = new JPanel();
        fTextfieldPanel.setLayout(new BorderLayout());
        // setBottomPanelForMode will make this visible if we need it
        fTextfieldPanel.setVisible(false);
        topPanel.add(fTextfieldPanel);

        tPanel = new JPanel();
        tPanel.setLayout(new BoxLayout(tPanel, BoxLayout.Y_AXIS));
        final JPanel labelArea = new JPanel();
        labelArea.setLayout(new FlowLayout(FlowLayout.CENTER));
        fTextFieldLabel = new JLabel(fileNameLabelText);
        labelArea.add(fTextFieldLabel);

        // text field
        filenameTextField = new JTextField();
        fTextFieldLabel.setLabelFor(filenameTextField);
        filenameTextField.addActionListener(getAction(kOpen));
        filenameTextField.addFocusListener(new SaveTextFocusListener());
        final Dimension minSize = filenameTextField.getMinimumSize();
        Dimension d = new Dimension(250, (int)minSize.getHeight());
        filenameTextField.setPreferredSize(d);
        filenameTextField.setMaximumSize(d);
        labelArea.add(filenameTextField);
        final File f = fc.getSelectedFile();
        if (f != null) {
            setFileName(fc.getName(f));
        } else if (fc.getDialogType() == JFileChooser.SAVE_DIALOG) {
            setFileName(newFileDefaultName);
        }

        tPanel.add(labelArea);
        // separator line
        @SuppressWarnings("serial") // anonymous class
        final JSeparator sep = new JSeparator(){
            public Dimension getPreferredSize() {
                return new Dimension(((JComponent)getParent()).getWidth(), 3);
            }
        };
        tPanel.add(Box.createRigidArea(new Dimension(1, 8)));
        tPanel.add(sep);
        tPanel.add(Box.createRigidArea(new Dimension(1, 7)));
        fTextfieldPanel.add(tPanel, BorderLayout.CENTER);

        // DirectoryComboBox, left-justified, 200x20 not including drop shadow
        directoryComboBox = new JComboBox<>();
        directoryComboBox.putClientProperty("JComboBox.lightweightKeyboardNavigation", "Lightweight");
        fDirectoryComboBoxModel = createDirectoryComboBoxModel(fc);
        directoryComboBox.setModel(fDirectoryComboBoxModel);
        directoryComboBox.addActionListener(directoryComboBoxAction);
        directoryComboBox.setRenderer(createDirectoryComboBoxRenderer(fc));
        directoryComboBox.setToolTipText(directoryComboBoxToolTipText);
        d = new Dimension(250, (int)directoryComboBox.getMinimumSize().getHeight());
        directoryComboBox.setPreferredSize(d);
        directoryComboBox.setMaximumSize(d);
        topPanel.add(directoryComboBox);

        // ************************************** //
        // ** Add the directory/Accessory pane ** //
        // ************************************** //
        final JPanel centerPanel = new JPanel(new BorderLayout());
        fc.add(centerPanel);

        // Accessory pane (equiv to Preview pane in NavServices)
        final JComponent accessory = fc.getAccessory();
        if (accessory != null) {
            getAccessoryPanel().add(accessory);
        }
        centerPanel.add(getAccessoryPanel(), BorderLayout.LINE_START);

        // Directory list(table), right-justified, resizable
        final JPanel p = createList(fc);
        p.setMinimumSize(LIST_MIN_SIZE);
        centerPanel.add(p, BorderLayout.CENTER);

        // ********************************** //
        // **** Construct the bottom panel ** //
        // ********************************** //
        fBottomPanel = new JPanel();
        fBottomPanel.setLayout(new BoxLayout(fBottomPanel, BoxLayout.Y_AXIS));
        fc.add(fBottomPanel);

        // Filter label and combobox.
        // I know it's unMaclike, but the filter goes on Directory_only too.
        tPanel = new JPanel();
        tPanel.setLayout(new FlowLayout(FlowLayout.CENTER));
        tPanel.setBorder(AquaGroupBorder.getTitlelessBorder());
        final JLabel formatLabel = new JLabel(filesOfTypeLabelText);
        tPanel.add(formatLabel);

        // Combobox
        filterComboBoxModel = createFilterComboBoxModel();
        fc.addPropertyChangeListener(filterComboBoxModel);
        filterComboBox = new JComboBox<>(filterComboBoxModel);
        formatLabel.setLabelFor(filterComboBox);
        filterComboBox.setRenderer(createFilterComboBoxRenderer());
        d = new Dimension(220, (int)filterComboBox.getMinimumSize().getHeight());
        filterComboBox.setPreferredSize(d);
        filterComboBox.setMaximumSize(d);
        filterComboBox.addActionListener(filterComboBoxAction);
        filterComboBox.setOpaque(false);
        tPanel.add(filterComboBox);

        fBottomPanel.add(tPanel);

        // fDirectoryPanel: New, Open, Cancel, Approve buttons, right-justified, 82x22
        // (sometimes the NewFolder and OpenFolder buttons are invisible)
        fDirectoryPanel = new JPanel();
        fDirectoryPanel.setLayout(new BoxLayout(fDirectoryPanel, BoxLayout.PAGE_AXIS));
        JPanel directoryPanel = new JPanel(new BorderLayout());
        JPanel newFolderButtonPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0));
        newFolderButtonPanel.add(Box.createHorizontalStrut(20));
        fNewFolderButton = createNewFolderButton(); // Because we hide it depending on style
        newFolderButtonPanel.add(fNewFolderButton);
        directoryPanel.add(newFolderButtonPanel, BorderLayout.LINE_START);
        JPanel approveCancelButtonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING, 0, 0));
        fOpenButton = createButton(kOpenDirectory, openButtonText);
        approveCancelButtonPanel.add(fOpenButton);
        approveCancelButtonPanel.add(Box.createHorizontalStrut(8));
        fCancelButton = createButton(kCancel, null);
        approveCancelButtonPanel.add(fCancelButton);
        approveCancelButtonPanel.add(Box.createHorizontalStrut(8));
        // The ApproveSelection button
        fApproveButton = new JButton();
        fApproveButton.addActionListener(fApproveSelectionAction);
        approveCancelButtonPanel.add(fApproveButton);
        approveCancelButtonPanel.add(Box.createHorizontalStrut(20));
        directoryPanel.add(approveCancelButtonPanel, BorderLayout.LINE_END);
        fDirectoryPanel.add(Box.createVerticalStrut(5));
        fDirectoryPanel.add(directoryPanel);
        fDirectoryPanel.add(Box.createVerticalStrut(12));
        fDirectoryPanelSpacer = Box.createRigidArea(hstrut10);

        if (fc.getControlButtonsAreShown()) {
            fBottomPanel.add(fDirectoryPanelSpacer);
            fBottomPanel.add(fDirectoryPanel);
        }

        setBottomPanelForMode(fc); // updates ApproveButtonText etc

        // don't create til after the FCSubpanel and buttons are made
        filenameTextField.getDocument().addDocumentListener(new SaveTextDocumentListener());
    }

    void setDefaultButtonForMode(final JFileChooser fc) {
        final JButton defaultButton = fSubPanel.getDefaultButton(fc);
        final JRootPane root = defaultButton.getRootPane();
        if (root != null) {
            root.setDefaultButton(defaultButton);
        }
    }

    // Macs start with their focus in text areas if they have them,
    // lists otherwise (the other plafs start with the focus on approveButton)
    void setFocusForMode(final JFileChooser fc) {
        final JComponent focusComponent = fSubPanel.getFocusComponent(fc);
        if (focusComponent != null) {
            focusComponent.requestFocus();
        }
    }

    // Enable/disable buttons as needed for the current selection/focus state
    void updateButtonState(final JFileChooser fc) {
        fSubPanel.updateButtonState(fc, getFirstSelectedItem());
        updateApproveButton(fc);
    }

    void updateApproveButton(final JFileChooser chooser) {
        fApproveButton.setText(getApproveButtonText(chooser));
        fApproveButton.setToolTipText(getApproveButtonToolTipText(chooser));
        fApproveButton.setMnemonic(getApproveButtonMnemonic(chooser));
        fCancelButton.setToolTipText(getCancelButtonToolTipText(chooser));
    }

    // Lazy-init the subpanels
    synchronized FCSubpanel getSaveFilePanel() {
        if (fSaveFilePanel == null) fSaveFilePanel = new SaveFilePanel();
        return fSaveFilePanel;
    }

    synchronized FCSubpanel getOpenFilePanel() {
        if (fOpenFilePanel == null) fOpenFilePanel = new OpenFilePanel();
        return fOpenFilePanel;
    }

    synchronized FCSubpanel getOpenDirOrAnyPanel() {
        if (fOpenDirOrAnyPanel == null) fOpenDirOrAnyPanel = new OpenDirOrAnyPanel();
        return fOpenDirOrAnyPanel;
    }

    synchronized FCSubpanel getCustomFilePanel() {
        if (fCustomFilePanel == null) fCustomFilePanel = new CustomFilePanel();
        return fCustomFilePanel;
    }

    synchronized FCSubpanel getCustomDirOrAnyPanel() {
        if (fCustomDirOrAnyPanel == null) fCustomDirOrAnyPanel = new CustomDirOrAnyPanel();
        return fCustomDirOrAnyPanel;
    }

    void setBottomPanelForMode(final JFileChooser fc) {
        if (fc.getDialogType() == JFileChooser.SAVE_DIALOG) fSubPanel = getSaveFilePanel();
        else if (fc.getDialogType() == JFileChooser.OPEN_DIALOG) {
            if (fc.getFileSelectionMode() == JFileChooser.FILES_ONLY) fSubPanel = getOpenFilePanel();
            else fSubPanel = getOpenDirOrAnyPanel();
        } else if (fc.getDialogType() == JFileChooser.CUSTOM_DIALOG) {
            if (fc.getFileSelectionMode() == JFileChooser.FILES_ONLY) fSubPanel = getCustomFilePanel();
            else fSubPanel = getCustomDirOrAnyPanel();
        }

        fSubPanel.installPanel(fc, true);
        updateApproveButton(fc);
        updateButtonState(fc);
        setDefaultButtonForMode(fc);
        setFocusForMode(fc);
        fc.invalidate();
    }

    // fTextfieldPanel and fDirectoryPanel both have NewFolder buttons; only one should be visible at a time
    JButton createNewFolderButton() {
        final JButton b = new JButton(newFolderButtonText);
        b.setToolTipText(newFolderToolTipText);
        b.getAccessibleContext().setAccessibleName(newFolderAccessibleName);
        b.setHorizontalTextPosition(SwingConstants.LEFT);
        b.setAlignmentX(Component.LEFT_ALIGNMENT);
        b.setAlignmentY(Component.CENTER_ALIGNMENT);
        b.addActionListener(getAction(kNewFolder));
        return b;
    }

    JButton createButton(final int which, String label) {
        if (label == null) label = UIManager.getString(sDataPrefix + sButtonKinds[which] + sButtonData[0]);
        final int mnemonic = UIManager.getInt(sDataPrefix + sButtonKinds[which] + sButtonData[1]);
        final String tipText = UIManager.getString(sDataPrefix + sButtonKinds[which] + sButtonData[2]);
        final JButton b = new JButton(label);
        b.setMnemonic(mnemonic);
        b.setToolTipText(tipText);
        b.addActionListener(getAction(which));
        return b;
    }

    AbstractAction getAction(final int which) {
        return fButtonActions[which];
    }

    public void uninstallComponents(final JFileChooser fc) {
        // AquaButtonUI install some listeners to all parents, which means that
        // we need to uninstall UI here to remove those listeners, because after
        // we remove them from FileChooser we lost the latest reference to them,
        // and our standard uninstallUI machinery will not call them.
        fApproveButton.getUI().uninstallUI(fApproveButton);
        fOpenButton.getUI().uninstallUI(fOpenButton);
        fNewFolderButton.getUI().uninstallUI(fNewFolderButton);
        fCancelButton.getUI().uninstallUI(fCancelButton);
        directoryComboBox.getUI().uninstallUI(directoryComboBox);
        filterComboBox.getUI().uninstallUI(filterComboBox);
    }

    // Consistent with the AppKit NSSavePanel, clicks on a file (not a directory) should populate the text field
    // with that file's display name.
    protected class FileListMouseListener extends MouseAdapter {
        public void mouseClicked(final MouseEvent e) {
            final Point p = e.getPoint();
            final int row = fFileList.rowAtPoint(p);
            final int column = fFileList.columnAtPoint(p);

            // The autoscroller can generate drag events outside the Table's range.
            if ((column == -1) || (row == -1)) { return; }

            final File clickedFile = (File)(fFileList.getValueAt(row, 0));

            // rdar://problem/3734130 -- don't populate the text field if this file isn't selectable in this mode.
            if (isSelectableForMode(getFileChooser(), clickedFile)) {
                // [3188387] Populate the file name field with the selected file name
                // [3484163] It should also use the display name, not the actual name.
                setFileName(fileView.getName(clickedFile));
            }
        }
    }

    protected JPanel createList(final JFileChooser fc) {
        // The first part is similar to MetalFileChooserUI.createList - same kind of listeners
        final JPanel p = new JPanel(new BorderLayout());
        fFileList = new JTableExtension();
        fFileList.setToolTipText(null); // Workaround for 2487689
        fFileList.addMouseListener(new FileListMouseListener());
        model = new AquaFileSystemModel(fc, fFileList, fColumnNames);
        final MacListSelectionModel listSelectionModel = new MacListSelectionModel(model);

        if (getFileChooser().isMultiSelectionEnabled()) {
            listSelectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
        } else {
            listSelectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        }

        fFileList.setModel(model);
        fFileList.setSelectionModel(listSelectionModel);
        fFileList.getSelectionModel().addListSelectionListener(createListSelectionListener(fc));

        // Now we're different, because we're a table, not a list
        fc.addPropertyChangeListener(model);
        fFileList.addFocusListener(new SaveTextFocusListener());
        final JTableHeader th = new JSortingTableHeader(fFileList.getColumnModel());
        fFileList.setTableHeader(th);
        fFileList.setRowMargin(0);
        fFileList.setIntercellSpacing(new Dimension(0, 1));
        fFileList.setShowVerticalLines(false);
        fFileList.setShowHorizontalLines(false);
        final Font f = fFileList.getFont(); //ThemeFont.GetThemeFont(AppearanceConstants.kThemeViewsFont);
        //fc.setFont(f);
        //fFileList.setFont(f);
        fFileList.setDefaultRenderer(File.class, new FileRenderer(f));
        fFileList.setDefaultRenderer(Date.class, new DateRenderer(f));
        final FontMetrics fm = fFileList.getFontMetrics(f);

        // Row height isn't based on the renderers.  It defaults to 16 so we have to set it
        fFileList.setRowHeight(Math.max(fm.getHeight(), fileIcon.getIconHeight() + 2));

        // Add a binding for the file list that triggers return and escape
        fFileList.registerKeyboardAction(new CancelSelectionAction(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_FOCUSED);
        // Add a binding for the file list that triggers the default button (see DefaultButtonAction)
        fFileList.registerKeyboardAction(new DefaultButtonAction(), KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), JComponent.WHEN_FOCUSED);
        fFileList.setDropTarget(dragAndDropTarget);

        final JScrollPane scrollpane = new JScrollPane(fFileList, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        scrollpane.setComponentOrientation(ComponentOrientation.getOrientation(Locale.getDefault()));
        scrollpane.setCorner(ScrollPaneConstants.UPPER_TRAILING_CORNER, new ScrollPaneCornerPanel());
        p.add(scrollpane, BorderLayout.CENTER);
        return p;
    }

    @SuppressWarnings("serial") // Superclass is not serializable across versions
    protected class ScrollPaneCornerPanel extends JPanel {
        final Border border = UIManager.getBorder("TableHeader.cellBorder");

        protected void paintComponent(final Graphics g) {
            border.paintBorder(this, g, 0, 0, getWidth() + 1, getHeight());
        }
    }

    JComboBox<File> directoryComboBox;
    DirectoryComboBoxModel fDirectoryComboBoxModel;
    private final Action directoryComboBoxAction = new DirectoryComboBoxAction();

    JTextField filenameTextField;

    JTableExtension fFileList;

    private FilterComboBoxModel filterComboBoxModel;
    JComboBox<FileFilter> filterComboBox;
    private final Action filterComboBoxAction = new FilterComboBoxAction();

    private static final Dimension hstrut10 = new Dimension(10, 1);
    private static final Dimension vstrut10 = new Dimension(1, 10);

    private static final int PREF_WIDTH = 550;
    private static final int PREF_HEIGHT = 400;
    private static final int MIN_WIDTH = 400;
    private static final int MIN_HEIGHT = 250;
    private static final int LIST_MIN_WIDTH = 400;
    private static final int LIST_MIN_HEIGHT = 100;
    private static final Dimension LIST_MIN_SIZE = new Dimension(LIST_MIN_WIDTH, LIST_MIN_HEIGHT);

    static String fileNameLabelText = null;
    JLabel fTextFieldLabel = null;

    private static String filesOfTypeLabelText = null;

    private static String newFolderToolTipText = null;
    static String newFolderAccessibleName = null;

    private static final String[] fColumnNames = new String[2];

    JPanel fTextfieldPanel; // Filename textfield for Save or Custom
    private JPanel fDirectoryPanel; // NewFolder/OpenFolder/Cancel/Approve buttons
    private Component fDirectoryPanelSpacer;
    private JPanel fBottomPanel; // The panel that holds fDirectoryPanel and filterComboBox

    private FCSubpanel fSaveFilePanel = null;
    private FCSubpanel fOpenFilePanel = null;
    private FCSubpanel fOpenDirOrAnyPanel = null;
    private FCSubpanel fCustomFilePanel = null;
    private FCSubpanel fCustomDirOrAnyPanel = null;

    FCSubpanel fSubPanel = null; // Current FCSubpanel

    JButton fApproveButton; // mode-specific behavior is managed by FCSubpanel.approveSelection
    JButton fOpenButton; // for Directories
    JButton fNewFolderButton; // for fDirectoryPanel

    // ToolTip text varies with type of dialog
    private JButton fCancelButton;

    private final ApproveSelectionAction fApproveSelectionAction = new ApproveSelectionAction();
    protected int fSortColumn = 0;
    protected int fPackageIsTraversable = -1;
    protected int fApplicationIsTraversable = -1;

    protected static final int sGlobalPackageIsTraversable;
    protected static final int sGlobalApplicationIsTraversable;

    protected static final String PACKAGE_TRAVERSABLE_PROPERTY = "JFileChooser.packageIsTraversable";
    protected static final String APPLICATION_TRAVERSABLE_PROPERTY = "JFileChooser.appBundleIsTraversable";
    protected static final String[] sTraversableProperties = {"always", // Bundle is always traversable
            "never", // Bundle is never traversable
            "conditional"}; // Bundle is traversable on command click
    protected static final int kOpenAlways = 0, kOpenNever = 1, kOpenConditional = 2;

    AbstractAction[] fButtonActions = {fApproveSelectionAction, fApproveSelectionAction, new CancelSelectionAction(), new OpenSelectionAction(), null, new NewFolderAction()};

    static int parseTraversableProperty(final String s) {
        if (s == null) return -1;
        for (int i = 0; i < sTraversableProperties.length; i++) {
            if (s.equals(sTraversableProperties[i])) return i;
        }
        return -1;
    }

    static {
        Object o = UIManager.get(PACKAGE_TRAVERSABLE_PROPERTY);
        if (o instanceof String s) sGlobalPackageIsTraversable = parseTraversableProperty(s);
        else sGlobalPackageIsTraversable = kOpenConditional;

        o = UIManager.get(APPLICATION_TRAVERSABLE_PROPERTY);
        if (o instanceof String s) sGlobalApplicationIsTraversable = parseTraversableProperty(s);
        else sGlobalApplicationIsTraversable = kOpenConditional;
    }
    static final String sDataPrefix = "FileChooser.";
    static final String[] sButtonKinds = {"openButton", "saveButton", "cancelButton", "openDirectoryButton", "helpButton", "newFolderButton"};
    static final String[] sButtonData = {"Text", "Mnemonic", "ToolTipText"};
    static final int kOpen = 0, kSave = 1, kCancel = 2, kOpenDirectory = 3, kHelp = 4, kNewFolder = 5;

    /*-------

     Possible states: Save, {Open, Custom}x{Files, File and Directory, Directory}
     --------- */

    // This class returns the values for the Custom type, to avoid duplicating code in the two Custom subclasses
    abstract class FCSubpanel {
        // Install the appropriate panels for this mode
        abstract void installPanel(JFileChooser fc, boolean controlButtonsAreShown);

        abstract void updateButtonState(JFileChooser fc, File f);

        // Can this item be selected?
        // if not, it's disabled in the list
        boolean isSelectableInList(final JFileChooser fc, final File f) {
            if (f == null) return false;
            if (fc.getFileSelectionMode() == JFileChooser.DIRECTORIES_ONLY) return fc.isTraversable(f);
            return fc.accept(f);
        }

        void approveSelection(final JFileChooser fc) {
            fc.approveSelection();
        }

        JButton getDefaultButton(final JFileChooser fc) {
            return fApproveButton;
        }

        // Default to the textfield, panels without one should subclass
        JComponent getFocusComponent(final JFileChooser fc) {
            return filenameTextField;
        }

        String getApproveButtonText(final JFileChooser fc) {
            // Fallback to "choose"
            return this.getApproveButtonText(fc, chooseButtonText);
        }

        // Try to get the custom text.  If none, use the fallback
        String getApproveButtonText(final JFileChooser fc, final String fallbackText) {
            final String buttonText = fc.getApproveButtonText();
            return buttonText != null
                    ? buttonText
                    : fallbackText;
        }

        int getApproveButtonMnemonic(final JFileChooser fc) {
            // Don't use a default
            return fc.getApproveButtonMnemonic();
        }

        String getApproveButtonToolTipText(final JFileChooser fc) {
            // Fallback to "Choose selected file"
            return getApproveButtonToolTipText(fc, chooseButtonToolTipText);
        }

        String getApproveButtonToolTipText(final JFileChooser fc, final String fallbackText) {
            final String tooltipText = fc.getApproveButtonToolTipText();
            return tooltipText != null
                    ? tooltipText
                    : fallbackText;
        }

        String getCancelButtonToolTipText(final JFileChooser fc) {
            return cancelChooseButtonToolTipText;
        }
    }

    // Custom FILES_ONLY dialog
    /*
     NavServices Save appearance with Open behavior
     Approve button label = Open when list has focus and a directory is selected, Custom otherwise
     No OpenDirectory button - Approve button is overloaded
     Default button / double click = Approve
     Has text field
     List - everything is enabled
     */
    class CustomFilePanel extends FCSubpanel {
        void installPanel(final JFileChooser fc, final boolean controlButtonsAreShown) {
            fTextfieldPanel.setVisible(true); // do we really want one in multi-select?  It's confusing
            fOpenButton.setVisible(false);
            fNewFolderButton.setVisible(true);
        }

        // If the list has focus, the mode depends on the selection
        // - directory = open, file = approve
        // If something else has focus and we have text, it's approve
        // otherwise, it depends on selection again.
        boolean inOpenDirectoryMode(final JFileChooser fc, final File f) {
            final boolean selectionIsDirectory = (f != null && fc.isTraversable(f));
            if (fFileList.hasFocus()) return selectionIsDirectory;
            else if (textfieldIsValid()) return false;
            return selectionIsDirectory;
        }

        // The approve button is overloaded to mean OpenDirectory or Save
        void approveSelection(final JFileChooser fc) {
            File f = getFirstSelectedItem();
            if (inOpenDirectoryMode(fc, f)) {
                openDirectory(f);
            } else {
                f = makeFile(fc, getFileName());
                if (f != null) {
                    selectionInProgress = true;
                    getFileChooser().setSelectedFile(f);
                    selectionInProgress = false;
                }
                getFileChooser().approveSelection();
            }
        }

        // The approve button should be enabled
        // - if something in the list can be opened
        // - if the textfield has something in it
        void updateButtonState(final JFileChooser fc, final File f) {
            boolean enabled = true;
            if (!inOpenDirectoryMode(fc, f)) {
                enabled = (f != null) || textfieldIsValid();
            }
            getApproveButton(fc).setEnabled(enabled);

            // The OpenDirectory button should be disabled if there's no directory selected
            fOpenButton.setEnabled(f != null && fc.isTraversable(f));

            // Update the default button, since we may have disabled the current default.
            setDefaultButtonForMode(fc);
        }

        // everything's enabled, because we don't know what they're doing with them
        boolean isSelectableInList(final JFileChooser fc, final File f) {
            if (f == null) return false;
            return fc.accept(f);
        }

        String getApproveButtonToolTipText(final JFileChooser fc) {
            // The approve Button should have openDirectoryButtonToolTipText when the selection is a folder...
            if (inOpenDirectoryMode(fc, getFirstSelectedItem())) return openDirectoryButtonToolTipText;
            return super.getApproveButtonToolTipText(fc);
        }
    }

    // All Save dialogs
    /*
     NavServices Save
     Approve button label = Open when list has focus and a directory is selected, Save otherwise
     No OpenDirectory button - Approve button is overloaded
     Default button / double click = Approve
     Has text field
     Has NewFolder button (by text field)
     List - only traversables are enabled
     List is always SINGLE_SELECT
     */
    // Subclasses CustomFilePanel because they look alike and have some common behavior
    class SaveFilePanel extends CustomFilePanel {
        void installPanel(final JFileChooser fc, final boolean controlButtonsAreShown) {
            fTextfieldPanel.setVisible(true);
            fOpenButton.setVisible(false);
            fNewFolderButton.setVisible(true);
        }

        // only traversables are enabled, regardless of mode
        // because all you can do is select the next folder to open
        boolean isSelectableInList(final JFileChooser fc, final File f) {
            return fc.accept(f) && fc.isTraversable(f);
        }

        // The approve button means 'approve the file name in the text field.'
        void approveSelection(final JFileChooser fc) {
            final File f = makeFile(fc, getFileName());
            if (f != null) {
                selectionInProgress = true;
                getFileChooser().setSelectedFile(f);
                selectionInProgress = false;
                getFileChooser().approveSelection();
            }
        }

        // The approve button should be enabled if the textfield has something in it
        void updateButtonState(final JFileChooser fc, final File f) {
            final boolean enabled = textfieldIsValid();
            getApproveButton(fc).setEnabled(enabled);
        }

        String getApproveButtonText(final JFileChooser fc) {
            // Get the custom text, or fallback to "Save"
            return this.getApproveButtonText(fc, saveButtonText);
        }

        int getApproveButtonMnemonic(final JFileChooser fc) {
            return saveButtonMnemonic;
        }

        String getApproveButtonToolTipText(final JFileChooser fc) {
            // The approve Button should have openDirectoryButtonToolTipText when the selection is a folder...
            if (inOpenDirectoryMode(fc, getFirstSelectedItem())) return openDirectoryButtonToolTipText;
            return this.getApproveButtonToolTipText(fc, saveButtonToolTipText);
        }

        String getCancelButtonToolTipText(final JFileChooser fc) {
            return cancelSaveButtonToolTipText;
        }
    }

    // Open FILES_ONLY
    /*
     NSOpenPanel-style
     Approve button label = Open
     Default button / double click = Approve
     No text field
     No NewFolder button
     List - all items are enabled
     */
    class OpenFilePanel extends FCSubpanel {
        void installPanel(final JFileChooser fc, final boolean controlButtonsAreShown) {
            fTextfieldPanel.setVisible(false);
            fOpenButton.setVisible(false);
            fNewFolderButton.setVisible(false);
            setDefaultButtonForMode(fc);
        }

        boolean inOpenDirectoryMode(final JFileChooser fc, final File f) {
            return (f != null && fc.isTraversable(f));
        }

        // Default to the list
        JComponent getFocusComponent(final JFileChooser fc) {
            return fFileList;
        }

        void updateButtonState(final JFileChooser fc, final File f) {
            // Button is disabled if there's nothing selected
            final boolean enabled = (f != null) && !fc.isTraversable(f);
            getApproveButton(fc).setEnabled(enabled);
        }

        // all items are enabled
        boolean isSelectableInList(final JFileChooser fc, final File f) {
            return f != null && fc.accept(f);
        }

        String getApproveButtonText(final JFileChooser fc) {
            // Get the custom text, or fallback to "Open"
            return this.getApproveButtonText(fc, openButtonText);
        }

        int getApproveButtonMnemonic(final JFileChooser fc) {
            return openButtonMnemonic;
        }

        String getApproveButtonToolTipText(final JFileChooser fc) {
            return this.getApproveButtonToolTipText(fc, openButtonToolTipText);
        }

        String getCancelButtonToolTipText(final JFileChooser fc) {
            return cancelOpenButtonToolTipText;
        }
    }

    // used by open and custom panels for Directory only or files and directories
    abstract class DirOrAnyPanel extends FCSubpanel {
        void installPanel(final JFileChooser fc, final boolean controlButtonsAreShown) {
            fOpenButton.setVisible(false);
        }

        JButton getDefaultButton(final JFileChooser fc) {
            return getApproveButton(fc);
        }

        void updateButtonState(final JFileChooser fc, final File f) {
            // Button is disabled if there's nothing selected
            // Approve button is handled by the subclasses
            // getApproveButton(fc).setEnabled(f != null);

            // The OpenDirectory button should be disabled if there's no directory selected
            // - we only check the first item

            fOpenButton.setEnabled(false);
            setDefaultButtonForMode(fc);
        }
    }

    // Open FILES_AND_DIRECTORIES or DIRECTORIES_ONLY
    /*
     NavServices Choose
     Approve button label = Choose/Custom
     Has OpenDirectory button
     Default button / double click = OpenDirectory
     No text field
     List - files are disabled in DIRECTORIES_ONLY
     */
    class OpenDirOrAnyPanel extends DirOrAnyPanel {
        void installPanel(final JFileChooser fc, final boolean controlButtonsAreShown) {
            super.installPanel(fc, controlButtonsAreShown);
            fTextfieldPanel.setVisible(false);
            fNewFolderButton.setVisible(false);
        }

        // Default to the list
        JComponent getFocusComponent(final JFileChooser fc) {
            return fFileList;
        }

        int getApproveButtonMnemonic(final JFileChooser fc) {
            return chooseButtonMnemonic;
        }

        String getApproveButtonToolTipText(final JFileChooser fc) {
            String fallbackText;
            if (fc.getFileSelectionMode() == JFileChooser.DIRECTORIES_ONLY) fallbackText = chooseFolderButtonToolTipText;
            else fallbackText = chooseItemButtonToolTipText;
            return this.getApproveButtonToolTipText(fc, fallbackText);
        }

        void updateButtonState(final JFileChooser fc, final File f) {
            // Button is disabled if there's nothing selected
            getApproveButton(fc).setEnabled(f != null);
            super.updateButtonState(fc, f);
        }
    }

    // Custom FILES_AND_DIRECTORIES or DIRECTORIES_ONLY
    /*
     No NavServices equivalent
     Approve button label = user defined or Choose
     Has OpenDirectory button
     Default button / double click = OpenDirectory
     Has text field
     Has NewFolder button (by text field)
     List - files are disabled in DIRECTORIES_ONLY
     */
    class CustomDirOrAnyPanel extends DirOrAnyPanel {
        void installPanel(final JFileChooser fc, final boolean controlButtonsAreShown) {
            super.installPanel(fc, controlButtonsAreShown);
            fTextfieldPanel.setVisible(true);
            fNewFolderButton.setVisible(true);
        }

        // If there's text, make a file and select it
        void approveSelection(final JFileChooser fc) {
            final File f = makeFile(fc, getFileName());
            if (f != null) {
                selectionInProgress = true;
                getFileChooser().setSelectedFile(f);
                selectionInProgress = false;
            }
            getFileChooser().approveSelection();
        }

        void updateButtonState(final JFileChooser fc, final File f) {
            // Button is disabled if there's nothing selected
            getApproveButton(fc).setEnabled(f != null || textfieldIsValid());
            super.updateButtonState(fc, f);
        }
    }

    // See FileRenderer - documents in Save dialogs draw disabled, so they shouldn't be selected
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    class MacListSelectionModel extends DefaultListSelectionModel {
        AquaFileSystemModel fModel;

        MacListSelectionModel(final AquaFileSystemModel model) {
            fModel = model;
        }

        // Can the file be selected in this mode?
        // (files are visible even if they can't be selected)
        boolean isSelectableInListIndex(final int index) {
            final File file = (File)fModel.getValueAt(index, 0);
            return (file != null && isSelectableInList(file));
        }

        // Make sure everything in the selection interval is valid
        void verifySelectionInterval(int index0, int index1, boolean isSetSelection) {
            if (index0 > index1) {
                final int tmp = index1;
                index1 = index0;
                index0 = tmp;
            }
            int start = index0;
            int end;
            do {
                // Find the first selectable file in the range
                for (; start <= index1; start++) {
                    if (isSelectableInListIndex(start)) break;
                }
                end = -1;
                // Find the last selectable file in the range
                for (int i = start; i <= index1; i++) {
                    if (!isSelectableInListIndex(i)) {
                        break;
                    }
                    end = i;
                }
                // Select the range
                if (end >= 0) {
                    // If setting the selection, do "set" the first time to clear the old one
                    // after that do "add" to extend it
                    if (isSetSelection) {
                        super.setSelectionInterval(start, end);
                        isSetSelection = false;
                    } else {
                        super.addSelectionInterval(start, end);
                    }
                    start = end + 1;
                } else {
                    break;
                }
            } while (start <= index1);
        }

        public void setAnchorSelectionIndex(final int anchorIndex) {
            if (isSelectableInListIndex(anchorIndex)) super.setAnchorSelectionIndex(anchorIndex);
        }

        public void setLeadSelectionIndex(final int leadIndex) {
            if (isSelectableInListIndex(leadIndex)) super.setLeadSelectionIndex(leadIndex);
        }

        public void setSelectionInterval(final int index0, final int index1) {
            if (index0 == -1 || index1 == -1) { return; }

            if ((getSelectionMode() == SINGLE_SELECTION) || (index0 == index1)) {
                if (isSelectableInListIndex(index1)) super.setSelectionInterval(index1, index1);
            } else {
                verifySelectionInterval(index0, index1, true);
            }
        }

        public void addSelectionInterval(final int index0, final int index1) {
            if (index0 == -1 || index1 == -1) { return; }

            if (index0 == index1) {
                if (isSelectableInListIndex(index1)) super.addSelectionInterval(index1, index1);
                return;
            }

            if (getSelectionMode() != MULTIPLE_INTERVAL_SELECTION) {
                setSelectionInterval(index0, index1);
                return;
            }

            verifySelectionInterval(index0, index1, false);
        }
    }

    // Convenience, to translate from the JList directory view to the Mac-style JTable
    //   & minimize diffs between this and BasicFileChooserUI
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    static class JTableExtension extends JTable {
        public void setSelectedIndex(final int index) {
            getSelectionModel().setSelectionInterval(index, index);
        }

        public void removeSelectedIndex(final int index) {
            getSelectionModel().removeSelectionInterval(index, index);
        }

        public void ensureIndexIsVisible(final int index) {
            final Rectangle cellBounds = getCellRect(index, 0, false);
            if (cellBounds != null) {
                scrollRectToVisible(cellBounds);
            }
        }

        public int locationToIndex(final Point location) {
            return rowAtPoint(location);
        }
    }
}
