1/*
2 * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package com.apple.laf;
27
28import java.awt.*;
29import java.awt.datatransfer.*;
30import java.awt.dnd.*;
31import java.awt.event.*;
32import java.beans.*;
33import java.io.File;
34import java.net.URI;
35import java.text.DateFormat;
36import java.util.*;
37
38import javax.swing.*;
39import javax.swing.border.Border;
40import javax.swing.event.*;
41import javax.swing.filechooser.*;
42import javax.swing.plaf.*;
43import javax.swing.table.*;
44
45import sun.swing.SwingUtilities2;
46
47public class AquaFileChooserUI extends FileChooserUI {
48    /* FileView icons */
49    protected Icon directoryIcon = null;
50    protected Icon fileIcon = null;
51    protected Icon computerIcon = null;
52    protected Icon hardDriveIcon = null;
53    protected Icon floppyDriveIcon = null;
54
55    protected Icon upFolderIcon = null;
56    protected Icon homeFolderIcon = null;
57    protected Icon listViewIcon = null;
58    protected Icon detailsViewIcon = null;
59
60    protected int saveButtonMnemonic = 0;
61    protected int openButtonMnemonic = 0;
62    protected int cancelButtonMnemonic = 0;
63    protected int updateButtonMnemonic = 0;
64    protected int helpButtonMnemonic = 0;
65    protected int chooseButtonMnemonic = 0;
66
67    private String saveTitleText = null;
68    private String openTitleText = null;
69    String newFolderTitleText = null;
70
71    protected String saveButtonText = null;
72    protected String openButtonText = null;
73    protected String cancelButtonText = null;
74    protected String updateButtonText = null;
75    protected String helpButtonText = null;
76    protected String newFolderButtonText = null;
77    protected String chooseButtonText = null;
78
79    //private String newFolderErrorSeparator = null;
80    String newFolderErrorText = null;
81    String newFolderExistsErrorText = null;
82    protected String fileDescriptionText = null;
83    protected String directoryDescriptionText = null;
84
85    protected String saveButtonToolTipText = null;
86    protected String openButtonToolTipText = null;
87    protected String cancelButtonToolTipText = null;
88    protected String updateButtonToolTipText = null;
89    protected String helpButtonToolTipText = null;
90    protected String chooseItemButtonToolTipText = null; // Choose anything
91    protected String chooseFolderButtonToolTipText = null; // Choose folder
92    protected String directoryComboBoxToolTipText = null;
93    protected String filenameTextFieldToolTipText = null;
94    protected String filterComboBoxToolTipText = null;
95    protected String openDirectoryButtonToolTipText = null;
96
97    protected String cancelOpenButtonToolTipText = null;
98    protected String cancelSaveButtonToolTipText = null;
99    protected String cancelChooseButtonToolTipText = null;
100    protected String cancelNewFolderButtonToolTipText = null;
101
102    protected String desktopName = null;
103    String newFolderDialogPrompt = null;
104    String newFolderDefaultName = null;
105    private String newFileDefaultName = null;
106    String createButtonText = null;
107
108    JFileChooser filechooser = null;
109
110    private MouseListener doubleClickListener = null;
111    private PropertyChangeListener propertyChangeListener = null;
112    private AncestorListener ancestorListener = null;
113    private DropTarget dragAndDropTarget = null;
114
115    private static final AcceptAllFileFilter acceptAllFileFilter = new AcceptAllFileFilter();
116
117    private AquaFileSystemModel model;
118
119    final AquaFileView fileView = new AquaFileView(this);
120
121    boolean selectionInProgress = false;
122
123    // The accessoryPanel is a container to place the JFileChooser accessory component
124    private JPanel accessoryPanel = null;
125
126    //
127    // ComponentUI Interface Implementation methods
128    //
129    public static ComponentUI createUI(final JComponent c) {
130        return new AquaFileChooserUI((JFileChooser)c);
131    }
132
133    public AquaFileChooserUI(final JFileChooser filechooser) {
134        super();
135    }
136
137    public void installUI(final JComponent c) {
138        accessoryPanel = new JPanel(new BorderLayout());
139        filechooser = (JFileChooser)c;
140
141        createModel();
142
143        installDefaults(filechooser);
144        installComponents(filechooser);
145        installListeners(filechooser);
146
147        AquaUtils.enforceComponentOrientation(filechooser, ComponentOrientation.getOrientation(Locale.getDefault()));
148    }
149
150    public void uninstallUI(final JComponent c) {
151        uninstallListeners(filechooser);
152        uninstallComponents(filechooser);
153        uninstallDefaults(filechooser);
154
155        if (accessoryPanel != null) {
156            accessoryPanel.removeAll();
157        }
158
159        accessoryPanel = null;
160        getFileChooser().removeAll();
161    }
162
163    protected void installListeners(final JFileChooser fc) {
164        doubleClickListener = createDoubleClickListener(fc, fFileList);
165        fFileList.addMouseListener(doubleClickListener);
166
167        propertyChangeListener = createPropertyChangeListener(fc);
168        if (propertyChangeListener != null) {
169            fc.addPropertyChangeListener(propertyChangeListener);
170        }
171        if (model != null) fc.addPropertyChangeListener(model);
172
173        ancestorListener = new AncestorListener(){
174            public void ancestorAdded(final AncestorEvent e) {
175                // Request defaultness for the appropriate button based on mode
176                setFocusForMode(getFileChooser());
177                // Request defaultness for the appropriate button based on mode
178                setDefaultButtonForMode(getFileChooser());
179            }
180
181            public void ancestorRemoved(final AncestorEvent e) {
182            }
183
184            public void ancestorMoved(final AncestorEvent e) {
185            }
186        };
187        fc.addAncestorListener(ancestorListener);
188
189        fc.registerKeyboardAction(new CancelSelectionAction(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
190        dragAndDropTarget = new DropTarget(fc, DnDConstants.ACTION_COPY, new DnDHandler(), true);
191        fc.setDropTarget(dragAndDropTarget);
192    }
193
194    protected void uninstallListeners(final JFileChooser fc) {
195        if (propertyChangeListener != null) {
196            fc.removePropertyChangeListener(propertyChangeListener);
197        }
198        fFileList.removeMouseListener(doubleClickListener);
199        fc.removePropertyChangeListener(model);
200        fc.unregisterKeyboardAction(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0));
201        fc.removeAncestorListener(ancestorListener);
202        fc.setDropTarget(null);
203        ancestorListener = null;
204    }
205
206    protected void installDefaults(final JFileChooser fc) {
207        installIcons(fc);
208        installStrings(fc);
209        setPackageIsTraversable(fc.getClientProperty(PACKAGE_TRAVERSABLE_PROPERTY));
210        setApplicationIsTraversable(fc.getClientProperty(APPLICATION_TRAVERSABLE_PROPERTY));
211    }
212
213    protected void installIcons(final JFileChooser fc) {
214        directoryIcon = UIManager.getIcon("FileView.directoryIcon");
215        fileIcon = UIManager.getIcon("FileView.fileIcon");
216        computerIcon = UIManager.getIcon("FileView.computerIcon");
217        hardDriveIcon = UIManager.getIcon("FileView.hardDriveIcon");
218    }
219
220    String getString(final String uiKey, final String fallback) {
221        final String result = UIManager.getString(uiKey);
222        return (result == null ? fallback : result);
223    }
224
225    protected void installStrings(final JFileChooser fc) {
226        // Exist in basic.properties (though we might want to override)
227        fileDescriptionText = UIManager.getString("FileChooser.fileDescriptionText");
228        directoryDescriptionText = UIManager.getString("FileChooser.directoryDescriptionText");
229        newFolderErrorText = getString("FileChooser.newFolderErrorText", "Error occurred during folder creation");
230
231        saveButtonText = UIManager.getString("FileChooser.saveButtonText");
232        openButtonText = UIManager.getString("FileChooser.openButtonText");
233        cancelButtonText = UIManager.getString("FileChooser.cancelButtonText");
234        updateButtonText = UIManager.getString("FileChooser.updateButtonText");
235        helpButtonText = UIManager.getString("FileChooser.helpButtonText");
236
237        saveButtonMnemonic = UIManager.getInt("FileChooser.saveButtonMnemonic");
238        openButtonMnemonic = UIManager.getInt("FileChooser.openButtonMnemonic");
239        cancelButtonMnemonic = UIManager.getInt("FileChooser.cancelButtonMnemonic");
240        updateButtonMnemonic = UIManager.getInt("FileChooser.updateButtonMnemonic");
241        helpButtonMnemonic = UIManager.getInt("FileChooser.helpButtonMnemonic");
242        chooseButtonMnemonic = UIManager.getInt("FileChooser.chooseButtonMnemonic");
243
244        saveButtonToolTipText = UIManager.getString("FileChooser.saveButtonToolTipText");
245        openButtonToolTipText = UIManager.getString("FileChooser.openButtonToolTipText");
246        cancelButtonToolTipText = UIManager.getString("FileChooser.cancelButtonToolTipText");
247        updateButtonToolTipText = UIManager.getString("FileChooser.updateButtonToolTipText");
248        helpButtonToolTipText = UIManager.getString("FileChooser.helpButtonToolTipText");
249
250        // Mac-specific, but fallback to basic if it's missing
251        saveTitleText = getString("FileChooser.saveTitleText", saveButtonText);
252        openTitleText = getString("FileChooser.openTitleText", openButtonText);
253
254        // Mac-specific, required
255        newFolderExistsErrorText = getString("FileChooser.newFolderExistsErrorText", "That name is already taken");
256        chooseButtonText = getString("FileChooser.chooseButtonText", "Choose");
257        newFolderButtonText = getString("FileChooser.newFolderButtonText", "New");
258        newFolderTitleText = getString("FileChooser.newFolderTitleText", "New Folder");
259
260        if (fc.getDialogType() == JFileChooser.SAVE_DIALOG) {
261            fileNameLabelText = getString("FileChooser.saveDialogFileNameLabelText", "Save As:");
262        } else {
263            fileNameLabelText = getString("FileChooser.fileNameLabelText", "Name:");
264        }
265
266        filesOfTypeLabelText = getString("FileChooser.filesOfTypeLabelText", "Format:");
267
268        desktopName = getString("FileChooser.desktopName", "Desktop");
269        newFolderDialogPrompt = getString("FileChooser.newFolderPromptText", "Name of new folder:");
270        newFolderDefaultName = getString("FileChooser.untitledFolderName", "untitled folder");
271        newFileDefaultName = getString("FileChooser.untitledFileName", "untitled");
272        createButtonText = getString("FileChooser.createButtonText", "Create");
273
274        fColumnNames[1] = getString("FileChooser.byDateText", "Date Modified");
275        fColumnNames[0] = getString("FileChooser.byNameText", "Name");
276
277        // Mac-specific, optional
278        chooseItemButtonToolTipText = UIManager.getString("FileChooser.chooseItemButtonToolTipText");
279        chooseFolderButtonToolTipText = UIManager.getString("FileChooser.chooseFolderButtonToolTipText");
280        openDirectoryButtonToolTipText = UIManager.getString("FileChooser.openDirectoryButtonToolTipText");
281
282        directoryComboBoxToolTipText = UIManager.getString("FileChooser.directoryComboBoxToolTipText");
283        filenameTextFieldToolTipText = UIManager.getString("FileChooser.filenameTextFieldToolTipText");
284        filterComboBoxToolTipText = UIManager.getString("FileChooser.filterComboBoxToolTipText");
285
286        cancelOpenButtonToolTipText = UIManager.getString("FileChooser.cancelOpenButtonToolTipText");
287        cancelSaveButtonToolTipText = UIManager.getString("FileChooser.cancelSaveButtonToolTipText");
288        cancelChooseButtonToolTipText = UIManager.getString("FileChooser.cancelChooseButtonToolTipText");
289        cancelNewFolderButtonToolTipText = UIManager.getString("FileChooser.cancelNewFolderButtonToolTipText");
290
291        newFolderTitleText = UIManager.getString("FileChooser.newFolderTitleText");
292        newFolderToolTipText = UIManager.getString("FileChooser.newFolderToolTipText");
293        newFolderAccessibleName = getString("FileChooser.newFolderAccessibleName", newFolderTitleText);
294    }
295
296    protected void uninstallDefaults(final JFileChooser fc) {
297        uninstallIcons(fc);
298        uninstallStrings(fc);
299    }
300
301    protected void uninstallIcons(final JFileChooser fc) {
302        directoryIcon = null;
303        fileIcon = null;
304        computerIcon = null;
305        hardDriveIcon = null;
306        floppyDriveIcon = null;
307
308        upFolderIcon = null;
309        homeFolderIcon = null;
310        detailsViewIcon = null;
311        listViewIcon = null;
312    }
313
314    protected void uninstallStrings(final JFileChooser fc) {
315        saveTitleText = null;
316        openTitleText = null;
317        newFolderTitleText = null;
318
319        saveButtonText = null;
320        openButtonText = null;
321        cancelButtonText = null;
322        updateButtonText = null;
323        helpButtonText = null;
324        newFolderButtonText = null;
325        chooseButtonText = null;
326
327        cancelOpenButtonToolTipText = null;
328        cancelSaveButtonToolTipText = null;
329        cancelChooseButtonToolTipText = null;
330        cancelNewFolderButtonToolTipText = null;
331
332        saveButtonToolTipText = null;
333        openButtonToolTipText = null;
334        cancelButtonToolTipText = null;
335        updateButtonToolTipText = null;
336        helpButtonToolTipText = null;
337        chooseItemButtonToolTipText = null;
338        chooseFolderButtonToolTipText = null;
339        openDirectoryButtonToolTipText = null;
340        directoryComboBoxToolTipText = null;
341        filenameTextFieldToolTipText = null;
342        filterComboBoxToolTipText = null;
343
344        newFolderDefaultName = null;
345        newFileDefaultName = null;
346
347        desktopName = null;
348    }
349
350    protected void createModel() {
351    }
352
353    AquaFileSystemModel getModel() {
354        return model;
355    }
356
357    /*
358     * Listen for filechooser property changes, such as
359     * the selected file changing, or the type of the dialog changing.
360     */
361    // Taken almost verbatim from Metal
362    protected PropertyChangeListener createPropertyChangeListener(final JFileChooser fc) {
363        return new PropertyChangeListener(){
364            public void propertyChange(final PropertyChangeEvent e) {
365                final String prop = e.getPropertyName();
366                if (prop.equals(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)) {
367                    final File f = (File)e.getNewValue();
368                    if (f != null) {
369                        // Select the file in the list if the selected file didn't change as
370                        // a result of a list click.
371                        if (!selectionInProgress && getModel().contains(f)) {
372                            fFileList.setSelectedIndex(getModel().indexOf(f));
373                        }
374
375                        // [3643835] Need to populate the text field here.  No-op on Open dialogs
376                        // Note that this was removed for 3514735, but should not have been.
377                        if (!f.isDirectory()) {
378                            setFileName(getFileChooser().getName(f));
379                        }
380                    }
381                    updateButtonState(getFileChooser());
382                } else if (prop.equals(JFileChooser.SELECTED_FILES_CHANGED_PROPERTY)) {
383                    JFileChooser fileChooser = getFileChooser();
384                    if (!fileChooser.isDirectorySelectionEnabled()) {
385                        final File[] files = (File[]) e.getNewValue();
386                        if (files != null) {
387                            for (int selectedRow : fFileList.getSelectedRows()) {
388                                File file = (File) fFileList.getValueAt(selectedRow, 0);
389                                if (fileChooser.isTraversable(file)) {
390                                    fFileList.removeSelectedIndex(selectedRow);
391                                }
392                            }
393                        }
394                    }
395                } else if (prop.equals(JFileChooser.DIRECTORY_CHANGED_PROPERTY)) {
396                    fFileList.clearSelection();
397                    final File currentDirectory = getFileChooser().getCurrentDirectory();
398                    if (currentDirectory != null) {
399                        fDirectoryComboBoxModel.addItem(currentDirectory);
400                        // Enable the newFolder action if the current directory
401                        // is writable.
402                        // PENDING(jeff) - broken - fix
403                        getAction(kNewFolder).setEnabled(currentDirectory.canWrite());
404                    }
405                    updateButtonState(getFileChooser());
406                } else if (prop.equals(JFileChooser.FILE_SELECTION_MODE_CHANGED_PROPERTY)) {
407                    fFileList.clearSelection();
408                    setBottomPanelForMode(getFileChooser()); // Also updates approve button
409                } else if (prop == JFileChooser.ACCESSORY_CHANGED_PROPERTY) {
410                    if (getAccessoryPanel() != null) {
411                        if (e.getOldValue() != null) {
412                            getAccessoryPanel().remove((JComponent)e.getOldValue());
413                        }
414                        final JComponent accessory = (JComponent)e.getNewValue();
415                        if (accessory != null) {
416                            getAccessoryPanel().add(accessory, BorderLayout.CENTER);
417                        }
418                    }
419                } else if (prop == JFileChooser.APPROVE_BUTTON_TEXT_CHANGED_PROPERTY) {
420                    updateApproveButton(getFileChooser());
421                    getFileChooser().invalidate();
422                } else if (prop == JFileChooser.DIALOG_TYPE_CHANGED_PROPERTY) {
423                    if (getFileChooser().getDialogType() == JFileChooser.SAVE_DIALOG) {
424                        fileNameLabelText = getString("FileChooser.saveDialogFileNameLabelText", "Save As:");
425                    } else {
426                        fileNameLabelText = getString("FileChooser.fileNameLabelText", "Name:");
427                    }
428                    fTextFieldLabel.setText(fileNameLabelText);
429
430                    // Mac doesn't show the text field or "new folder" button in 'Open' dialogs
431                    setBottomPanelForMode(getFileChooser()); // Also updates approve button
432                } else if (prop.equals(JFileChooser.APPROVE_BUTTON_MNEMONIC_CHANGED_PROPERTY)) {
433                    getApproveButton(getFileChooser()).setMnemonic(getApproveButtonMnemonic(getFileChooser()));
434                } else if (prop.equals(PACKAGE_TRAVERSABLE_PROPERTY)) {
435                    setPackageIsTraversable(e.getNewValue());
436                } else if (prop.equals(APPLICATION_TRAVERSABLE_PROPERTY)) {
437                    setApplicationIsTraversable(e.getNewValue());
438                } else if (prop.equals(JFileChooser.MULTI_SELECTION_ENABLED_CHANGED_PROPERTY)) {
439                    if (getFileChooser().isMultiSelectionEnabled()) {
440                        fFileList.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
441                    } else {
442                        fFileList.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
443                    }
444                } else if (prop.equals(JFileChooser.CONTROL_BUTTONS_ARE_SHOWN_CHANGED_PROPERTY)) {
445                    doControlButtonsChanged(e);
446                }
447            }
448        };
449    }
450
451    void setPackageIsTraversable(final Object o) {
452        int newProp = -1;
453        if (o != null && o instanceof String) newProp = parseTraversableProperty((String)o);
454        if (newProp != -1) fPackageIsTraversable = newProp;
455        else fPackageIsTraversable = sGlobalPackageIsTraversable;
456    }
457
458    void setApplicationIsTraversable(final Object o) {
459        int newProp = -1;
460        if (o != null && o instanceof String) newProp = parseTraversableProperty((String)o);
461        if (newProp != -1) fApplicationIsTraversable = newProp;
462        else fApplicationIsTraversable = sGlobalApplicationIsTraversable;
463    }
464
465    void doControlButtonsChanged(final PropertyChangeEvent e) {
466        if (getFileChooser().getControlButtonsAreShown()) {
467            fBottomPanel.add(fDirectoryPanelSpacer);
468            fBottomPanel.add(fDirectoryPanel);
469        } else {
470            fBottomPanel.remove(fDirectoryPanelSpacer);
471            fBottomPanel.remove(fDirectoryPanel);
472        }
473    }
474
475    public String getFileName() {
476        if (filenameTextField != null) { return filenameTextField.getText(); }
477        return null;
478    }
479
480    public String getDirectoryName() {
481        // PENDING(jeff) - get the name from the directory combobox
482        return null;
483    }
484
485    public void setFileName(final String filename) {
486        if (filenameTextField != null) {
487            filenameTextField.setText(filename);
488        }
489    }
490
491    public void setDirectoryName(final String dirname) {
492        // PENDING(jeff) - set the name in the directory combobox
493    }
494
495    public void rescanCurrentDirectory(final JFileChooser fc) {
496        getModel().invalidateFileCache();
497        getModel().validateFileCache();
498    }
499
500    public void ensureFileIsVisible(final JFileChooser fc, final File f) {
501        if (f == null) {
502            fFileList.requestFocusInWindow();
503            fFileList.ensureIndexIsVisible(-1);
504            return;
505        }
506
507        getModel().runWhenDone(new Runnable() {
508            public void run() {
509                fFileList.requestFocusInWindow();
510                fFileList.ensureIndexIsVisible(getModel().indexOf(f));
511            }
512        });
513    }
514
515    public JFileChooser getFileChooser() {
516        return filechooser;
517    }
518
519    public JPanel getAccessoryPanel() {
520        return accessoryPanel;
521    }
522
523    protected JButton getApproveButton(final JFileChooser fc) {
524        return fApproveButton;
525    }
526
527    public int getApproveButtonMnemonic(final JFileChooser fc) {
528        return fSubPanel.getApproveButtonMnemonic(fc);
529    }
530
531    public String getApproveButtonToolTipText(final JFileChooser fc) {
532        return fSubPanel.getApproveButtonToolTipText(fc);
533    }
534
535    public String getApproveButtonText(final JFileChooser fc) {
536        return fSubPanel.getApproveButtonText(fc);
537    }
538
539    protected String getCancelButtonToolTipText(final JFileChooser fc) {
540        return fSubPanel.getCancelButtonToolTipText(fc);
541    }
542
543    // If the item's not selectable, it'll be visible but disabled in the list
544    boolean isSelectableInList(final File f) {
545        return fSubPanel.isSelectableInList(getFileChooser(), f);
546    }
547
548    // Is this a file that the JFileChooser wants?
549    // Directories can be selected in the list regardless of mode
550    boolean isSelectableForMode(final JFileChooser fc, final File f) {
551        if (f == null) return false;
552        final int mode = fc.getFileSelectionMode();
553        if (mode == JFileChooser.FILES_AND_DIRECTORIES) return true;
554        boolean traversable = fc.isTraversable(f);
555        if (mode == JFileChooser.DIRECTORIES_ONLY) return traversable;
556        return !traversable;
557    }
558
559    // ********************************************
560    // ************ Create Listeners **************
561    // ********************************************
562
563    // From Basic
564    public ListSelectionListener createListSelectionListener(final JFileChooser fc) {
565        return new SelectionListener();
566    }
567
568    protected class SelectionListener implements ListSelectionListener {
569        public void valueChanged(final ListSelectionEvent e) {
570            if (e.getValueIsAdjusting()) return;
571
572            File f = null;
573            final int selectedRow = fFileList.getSelectedRow();
574            final JFileChooser chooser = getFileChooser();
575            boolean isSave = (chooser.getDialogType() == JFileChooser.SAVE_DIALOG);
576            if (selectedRow >= 0) {
577                f = (File)fFileList.getValueAt(selectedRow, 0);
578            }
579
580            // Save dialog lists can't be multi select, because all we're selecting is the next folder to open
581            selectionInProgress = true;
582            if (!isSave && chooser.isMultiSelectionEnabled()) {
583                final int[] rows = fFileList.getSelectedRows();
584                int selectableCount = 0;
585                // Double-check that all the list selections are valid for this mode
586                // Directories can be selected in the list regardless of mode
587                if (rows.length > 0) {
588                    for (final int element : rows) {
589                        if (isSelectableForMode(chooser, (File)fFileList.getValueAt(element, 0))) selectableCount++;
590                    }
591                }
592                if (selectableCount > 0) {
593                    final File[] files = new File[selectableCount];
594                    for (int i = 0, si = 0; i < rows.length; i++) {
595                        f = (File)fFileList.getValueAt(rows[i], 0);
596                        if (isSelectableForMode(chooser, f)) {
597                            if (fileView.isAlias(f)) {
598                                f = fileView.resolveAlias(f);
599                            }
600                            files[si++] = f;
601                        }
602                    }
603                    chooser.setSelectedFiles(files);
604                } else {
605                    chooser.setSelectedFiles(null);
606                }
607            } else {
608                chooser.setSelectedFiles(null);
609                chooser.setSelectedFile(f);
610            }
611            selectionInProgress = false;
612        }
613    }
614
615    // When the Save textfield has the focus, the button should say "Save"
616    // Otherwise, it depends on the list selection
617    protected class SaveTextFocusListener implements FocusListener {
618        public void focusGained(final FocusEvent e) {
619            updateButtonState(getFileChooser());
620        }
621
622        // Do nothing, we might be losing focus due to window deactivation
623        public void focusLost(final FocusEvent e) {
624
625        }
626    }
627
628    // When the Save textfield is empty and the button says "Save", it should be disabled
629    // Otherwise, it depends on the list selection
630    protected class SaveTextDocumentListener implements DocumentListener {
631        public void insertUpdate(final DocumentEvent e) {
632            textChanged();
633        }
634
635        public void removeUpdate(final DocumentEvent e) {
636            textChanged();
637        }
638
639        public void changedUpdate(final DocumentEvent e) {
640
641        }
642
643        void textChanged() {
644            updateButtonState(getFileChooser());
645        }
646    }
647
648    // Opens the File object if it's a traversable directory
649    protected boolean openDirectory(final File f) {
650        if (getFileChooser().isTraversable(f)) {
651            fFileList.clearSelection();
652            // Resolve any aliases
653            final File original = fileView.resolveAlias(f);
654            getFileChooser().setCurrentDirectory(original);
655            updateButtonState(getFileChooser());
656            return true;
657        }
658        return false;
659    }
660
661    // From Basic
662    protected class DoubleClickListener extends MouseAdapter {
663        JTableExtension list;
664
665        public DoubleClickListener(final JTableExtension list) {
666            this.list = list;
667        }
668
669        public void mouseClicked(final MouseEvent e) {
670            if (e.getClickCount() != 2) return;
671
672            final int index = list.locationToIndex(e.getPoint());
673            if (index < 0) return;
674
675            final File f = (File)((AquaFileSystemModel)list.getModel()).getElementAt(index);
676            if (openDirectory(f)) return;
677
678            if (!isSelectableInList(f)) return;
679            getFileChooser().approveSelection();
680        }
681    }
682
683    protected MouseListener createDoubleClickListener(final JFileChooser fc, final JTableExtension list) {
684        return new DoubleClickListener(list);
685    }
686
687    // listens for drag events onto the JFileChooser and sets the selected file or directory
688    class DnDHandler extends DropTargetAdapter {
689        public void dragEnter(final DropTargetDragEvent dtde) {
690            tryToAcceptDrag(dtde);
691        }
692
693        public void dragOver(final DropTargetDragEvent dtde) {
694            tryToAcceptDrag(dtde);
695        }
696
697        public void dropActionChanged(final DropTargetDragEvent dtde) {
698            tryToAcceptDrag(dtde);
699        }
700
701        public void drop(final DropTargetDropEvent dtde) {
702            if (dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
703                handleFileDropEvent(dtde);
704                return;
705            }
706
707            if (dtde.isDataFlavorSupported(DataFlavor.stringFlavor)) {
708                handleStringDropEvent(dtde);
709                return;
710            }
711        }
712
713        protected void tryToAcceptDrag(final DropTargetDragEvent dtde) {
714            if (dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor) || dtde.isDataFlavorSupported(DataFlavor.stringFlavor)) {
715                dtde.acceptDrag(DnDConstants.ACTION_COPY);
716                return;
717            }
718
719            dtde.rejectDrag();
720        }
721
722        protected void handleFileDropEvent(final DropTargetDropEvent dtde) {
723            dtde.acceptDrop(dtde.getDropAction());
724            final Transferable transferable = dtde.getTransferable();
725
726            try {
727                @SuppressWarnings("unchecked")
728                final java.util.List<File> fileList = (java.util.List<File>)transferable.getTransferData(DataFlavor.javaFileListFlavor);
729                dropFiles(fileList.toArray(new File[fileList.size()]));
730                dtde.dropComplete(true);
731            } catch (final Exception e) {
732                dtde.dropComplete(false);
733            }
734        }
735
736        protected void handleStringDropEvent(final DropTargetDropEvent dtde) {
737            dtde.acceptDrop(dtde.getDropAction());
738            final Transferable transferable = dtde.getTransferable();
739
740            final String stringData;
741            try {
742                stringData = (String)transferable.getTransferData(DataFlavor.stringFlavor);
743            } catch (final Exception e) {
744                dtde.dropComplete(false);
745                return;
746            }
747
748            try {
749                final File fileAsPath = new File(stringData);
750                if (fileAsPath.exists()) {
751                    dropFiles(new File[] {fileAsPath});
752                    dtde.dropComplete(true);
753                    return;
754                }
755            } catch (final Exception e) {
756                // try again
757            }
758
759            try {
760                final File fileAsURI = new File(new URI(stringData));
761                if (fileAsURI.exists()) {
762                    dropFiles(new File[] {fileAsURI});
763                    dtde.dropComplete(true);
764                    return;
765                }
766            } catch (final Exception e) {
767                // nothing more to do
768            }
769
770            dtde.dropComplete(false);
771        }
772
773        protected void dropFiles(final File[] files) {
774            final JFileChooser jfc = getFileChooser();
775
776            if (files.length == 1) {
777                if (files[0].isDirectory()) {
778                    jfc.setCurrentDirectory(files[0]);
779                    return;
780                }
781
782                if (!isSelectableForMode(jfc, files[0])) {
783                    return;
784                }
785            }
786
787            jfc.setSelectedFiles(files);
788            for (final File file : files) {
789                jfc.ensureFileIsVisible(file);
790            }
791            getModel().runWhenDone(new Runnable() {
792                public void run() {
793                    final AquaFileSystemModel fileSystemModel = getModel();
794                    for (final File element : files) {
795                        final int index = fileSystemModel.indexOf(element);
796                        if (index >= 0) fFileList.addRowSelectionInterval(index, index);
797                    }
798                }
799            });
800        }
801    }
802
803    // FileChooser UI PLAF methods
804
805    /**
806     * Returns the default accept all file filter
807     */
808    public FileFilter getAcceptAllFileFilter(final JFileChooser fc) {
809        return acceptAllFileFilter;
810    }
811
812    public FileView getFileView(final JFileChooser fc) {
813        return fileView;
814    }
815
816    /**
817     * Returns the title of this dialog
818     */
819    public String getDialogTitle(final JFileChooser fc) {
820        if (fc.getDialogTitle() == null) {
821            if (getFileChooser().getDialogType() == JFileChooser.OPEN_DIALOG) {
822                return openTitleText;
823            } else if (getFileChooser().getDialogType() == JFileChooser.SAVE_DIALOG) { return saveTitleText; }
824        }
825        return fc.getDialogTitle();
826    }
827
828    // Utility to get the first selected item regardless of whether we're single or multi select
829    File getFirstSelectedItem() {
830        // Get the selected item
831        File selectedFile = null;
832        final int index = fFileList.getSelectedRow();
833        if (index >= 0) {
834            selectedFile = (File)((AquaFileSystemModel)fFileList.getModel()).getElementAt(index);
835        }
836        return selectedFile;
837    }
838
839    // Make a file from the filename
840    File makeFile(final JFileChooser fc, final String filename) {
841        File selectedFile = null;
842        // whitespace is legal on Macs, even on beginning and end of filename
843        if (filename != null && !filename.equals("")) {
844            final FileSystemView fs = fc.getFileSystemView();
845            selectedFile = fs.createFileObject(filename);
846            if (!selectedFile.isAbsolute()) {
847                selectedFile = fs.createFileObject(fc.getCurrentDirectory(), filename);
848            }
849        }
850        return selectedFile;
851    }
852
853    // Utility to tell if the textfield has anything in it
854    boolean textfieldIsValid() {
855        final String s = getFileName();
856        return (s != null && !s.equals(""));
857    }
858
859    // Action to attach to the file list so we can override the default action
860    // of the table for the return key, which is to select the next line.
861    @SuppressWarnings("serial") // Superclass is not serializable across versions
862    protected class DefaultButtonAction extends AbstractAction {
863        public void actionPerformed(final ActionEvent e) {
864            final JRootPane root = AquaFileChooserUI.this.getFileChooser().getRootPane();
865            final JFileChooser fc = AquaFileChooserUI.this.getFileChooser();
866            final JButton owner = root.getDefaultButton();
867            if (owner != null && SwingUtilities.getRootPane(owner) == root && owner.isEnabled()) {
868                owner.doClick(20);
869            } else if (!fc.getControlButtonsAreShown()) {
870                final JButton defaultButton = AquaFileChooserUI.this.fSubPanel.getDefaultButton(fc);
871
872                if (defaultButton != null) {
873                    defaultButton.doClick(20);
874                }
875            } else {
876                Toolkit.getDefaultToolkit().beep();
877            }
878        }
879
880        public boolean isEnabled() {
881            return true;
882        }
883    }
884
885    /**
886     * Creates a new folder.
887     */
888    @SuppressWarnings("serial") // Superclass is not serializable across versions
889    protected class NewFolderAction extends AbstractAction {
890        protected NewFolderAction() {
891            super(newFolderAccessibleName);
892        }
893
894        // Muchlike showInputDialog, but we give it options instead of selectionValues
895        private Object showNewFolderDialog(final Component parentComponent, final Object message, final String title, final int messageType, final Icon icon, final Object[] options, final Object initialSelectionValue) {
896            final JOptionPane pane = new JOptionPane(message, messageType, JOptionPane.OK_CANCEL_OPTION, icon, options, null);
897
898            pane.setWantsInput(true);
899            pane.setInitialSelectionValue(initialSelectionValue);
900
901            final JDialog dialog = pane.createDialog(parentComponent, title);
902
903            pane.selectInitialValue();
904            dialog.setVisible(true);
905            dialog.dispose();
906
907            final Object value = pane.getValue();
908
909            if (value == null || value.equals(cancelButtonText)) {
910                return null;
911            }
912            return pane.getInputValue();
913        }
914
915        public void actionPerformed(final ActionEvent e) {
916            final JFileChooser fc = getFileChooser();
917            final File currentDirectory = fc.getCurrentDirectory();
918            File newFolder = null;
919            final String[] options = {createButtonText, cancelButtonText};
920            final String filename = (String)showNewFolderDialog(fc, //parentComponent
921                    newFolderDialogPrompt, // message
922                    newFolderTitleText, // title
923                    JOptionPane.PLAIN_MESSAGE, // messageType
924                    null, // icon
925                    options, // selectionValues
926                    newFolderDefaultName); // initialSelectionValue
927
928            if (filename != null) {
929                try {
930                    newFolder = fc.getFileSystemView().createFileObject(currentDirectory, filename);
931                    if (newFolder.exists()) {
932                        JOptionPane.showMessageDialog(fc, newFolderExistsErrorText, "", JOptionPane.ERROR_MESSAGE);
933                        return;
934                    }
935
936                    newFolder.mkdirs();
937                } catch(final Exception exc) {
938                    JOptionPane.showMessageDialog(fc, newFolderErrorText, "", JOptionPane.ERROR_MESSAGE);
939                    return;
940                }
941
942                openDirectory(newFolder);
943            }
944        }
945    }
946
947    /**
948     * Responds to an Open, Save, or Choose request
949     */
950    @SuppressWarnings("serial") // Superclass is not serializable across versions
951    protected class ApproveSelectionAction extends AbstractAction {
952        public void actionPerformed(final ActionEvent e) {
953            fSubPanel.approveSelection(getFileChooser());
954        }
955    }
956
957    /**
958     * Responds to an OpenDirectory request
959     */
960    @SuppressWarnings("serial") // Superclass is not serializable across versions
961    protected class OpenSelectionAction extends AbstractAction {
962        public void actionPerformed(final ActionEvent e) {
963            final int index = fFileList.getSelectedRow();
964            if (index >= 0) {
965                final File selectedFile = (File)((AquaFileSystemModel)fFileList.getModel()).getElementAt(index);
966                if (selectedFile != null) openDirectory(selectedFile);
967            }
968        }
969    }
970
971    /**
972     * Responds to a cancel request.
973     */
974    @SuppressWarnings("serial") // Superclass is not serializable across versions
975    protected class CancelSelectionAction extends AbstractAction {
976        public void actionPerformed(final ActionEvent e) {
977            getFileChooser().cancelSelection();
978        }
979
980        public boolean isEnabled() {
981            return getFileChooser().isEnabled();
982        }
983    }
984
985    /**
986     * Rescans the files in the current directory
987     */
988    @SuppressWarnings("serial") // Superclass is not serializable across versions
989    protected class UpdateAction extends AbstractAction {
990        public void actionPerformed(final ActionEvent e) {
991            final JFileChooser fc = getFileChooser();
992            fc.setCurrentDirectory(fc.getFileSystemView().createFileObject(getDirectoryName()));
993            fc.rescanCurrentDirectory();
994        }
995    }
996
997    // *****************************************
998    // ***** default AcceptAll file filter *****
999    // *****************************************
1000    private static class AcceptAllFileFilter extends FileFilter {
1001        public AcceptAllFileFilter() {
1002        }
1003
1004        public boolean accept(final File f) {
1005            return true;
1006        }
1007
1008        public String getDescription() {
1009            return UIManager.getString("FileChooser.acceptAllFileFilterText");
1010        }
1011    }
1012
1013    // Penultimate superclass is JLabel
1014    @SuppressWarnings("serial") // Superclass is not serializable across versions
1015    protected class MacFCTableCellRenderer extends DefaultTableCellRenderer {
1016        boolean fIsSelected = false;
1017
1018        public MacFCTableCellRenderer(final Font f) {
1019            super();
1020            setFont(f);
1021            setIconTextGap(10);
1022        }
1023
1024        public Component getTableCellRendererComponent(final JTable list, final Object value, final boolean isSelected, final boolean cellHasFocus, final int index, final int col) {
1025            super.getTableCellRendererComponent(list, value, isSelected, false, index, col); // No focus border, thanks
1026            fIsSelected = isSelected;
1027            return this;
1028        }
1029
1030        public boolean isSelected() {
1031            return fIsSelected && isEnabled();
1032        }
1033
1034        protected String layoutCL(final JLabel label, final FontMetrics fontMetrics, final String text, final Icon icon, final Rectangle viewR, final Rectangle iconR, final Rectangle textR) {
1035            return SwingUtilities.layoutCompoundLabel(label, fontMetrics, text, icon, label.getVerticalAlignment(), label.getHorizontalAlignment(), label.getVerticalTextPosition(), label.getHorizontalTextPosition(), viewR, iconR, textR, label.getIconTextGap());
1036        }
1037
1038        protected void paintComponent(final Graphics g) {
1039            final String text = getText();
1040            Icon icon = getIcon();
1041            if (icon != null && !isEnabled()) {
1042                final Icon disabledIcon = getDisabledIcon();
1043                if (disabledIcon != null) icon = disabledIcon;
1044            }
1045
1046            if ((icon == null) && (text == null)) { return; }
1047
1048            // from ComponentUI update
1049            g.setColor(getBackground());
1050            g.fillRect(0, 0, getWidth(), getHeight());
1051
1052            // from BasicLabelUI paint
1053            final FontMetrics fm = g.getFontMetrics();
1054            Insets paintViewInsets = getInsets(null);
1055            paintViewInsets.left += 10;
1056
1057            Rectangle paintViewR = new Rectangle(paintViewInsets.left, paintViewInsets.top, getWidth() - (paintViewInsets.left + paintViewInsets.right), getHeight() - (paintViewInsets.top + paintViewInsets.bottom));
1058
1059            Rectangle paintIconR = new Rectangle();
1060            Rectangle paintTextR = new Rectangle();
1061
1062            final String clippedText = layoutCL(this, fm, text, icon, paintViewR, paintIconR, paintTextR);
1063
1064            if (icon != null) {
1065                icon.paintIcon(this, g, paintIconR.x + 5, paintIconR.y);
1066            }
1067
1068            if (text != null) {
1069                final int textX = paintTextR.x;
1070                final int textY = paintTextR.y + fm.getAscent() + 1;
1071                if (isEnabled()) {
1072                    // Color background = fIsSelected ? getForeground() : getBackground();
1073                    final Color background = getBackground();
1074
1075                    g.setColor(background);
1076                    g.fillRect(textX - 1, paintTextR.y, paintTextR.width + 2, fm.getAscent() + 2);
1077
1078                    g.setColor(getForeground());
1079                    SwingUtilities2.drawString(filechooser, g, clippedText, textX, textY);
1080                } else {
1081                    final Color background = getBackground();
1082                    g.setColor(background);
1083                    g.fillRect(textX - 1, paintTextR.y, paintTextR.width + 2, fm.getAscent() + 2);
1084
1085                    g.setColor(background.brighter());
1086                    SwingUtilities2.drawString(filechooser, g, clippedText, textX, textY);
1087                    g.setColor(background.darker());
1088                    SwingUtilities2.drawString(filechooser, g, clippedText, textX + 1, textY + 1);
1089                }
1090            }
1091        }
1092
1093    }
1094
1095    @SuppressWarnings("serial") // Superclass is not serializable across versions
1096    protected class FileRenderer extends MacFCTableCellRenderer {
1097        public FileRenderer(final Font f) {
1098            super(f);
1099        }
1100
1101        public Component getTableCellRendererComponent(final JTable list,
1102                                                       final Object value,
1103                                                       final boolean isSelected,
1104                                                       final boolean cellHasFocus,
1105                                                       final int index,
1106                                                       final int col) {
1107            super.getTableCellRendererComponent(list, value, isSelected, false,
1108                                                index,
1109                                                col); // No focus border, thanks
1110            final File file = (File)value;
1111            final JFileChooser fc = getFileChooser();
1112            setText(fc.getName(file));
1113            setIcon(fc.getIcon(file));
1114            setEnabled(isSelectableInList(file));
1115            return this;
1116        }
1117    }
1118
1119    @SuppressWarnings("serial") // Superclass is not serializable across versions
1120    protected class DateRenderer extends MacFCTableCellRenderer {
1121        public DateRenderer(final Font f) {
1122            super(f);
1123        }
1124
1125        public Component getTableCellRendererComponent(final JTable list,
1126                                                       final Object value,
1127                                                       final boolean isSelected,
1128                                                       final boolean cellHasFocus,
1129                                                       final int index,
1130                                                       final int col) {
1131            super.getTableCellRendererComponent(list, value, isSelected, false,
1132                                                index, col);
1133            final File file = (File)fFileList.getValueAt(index, 0);
1134            setEnabled(isSelectableInList(file));
1135            final DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.SHORT);
1136            final Date date = (Date)value;
1137
1138            if (date != null) {
1139                setText(formatter.format(date));
1140            } else {
1141                setText("");
1142            }
1143
1144            return this;
1145        }
1146    }
1147
1148    @Override
1149    public Dimension getPreferredSize(final JComponent c) {
1150        return new Dimension(PREF_WIDTH, PREF_HEIGHT);
1151    }
1152
1153    @Override
1154    public Dimension getMinimumSize(final JComponent c) {
1155        return new Dimension(MIN_WIDTH, MIN_HEIGHT);
1156    }
1157
1158    @Override
1159    public Dimension getMaximumSize(final JComponent c) {
1160        return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
1161    }
1162
1163    @SuppressWarnings("serial") // anonymous class
1164    protected ListCellRenderer<File> createDirectoryComboBoxRenderer(final JFileChooser fc) {
1165        return new AquaComboBoxRendererInternal<File>(directoryComboBox) {
1166            public Component getListCellRendererComponent(final JList<? extends File> list,
1167                                                          final File directory,
1168                                                          final int index,
1169                                                          final boolean isSelected,
1170                                                          final boolean cellHasFocus) {
1171                super.getListCellRendererComponent(list, directory, index, isSelected, cellHasFocus);
1172                if (directory == null) {
1173                    setText("");
1174                    return this;
1175                }
1176
1177                final JFileChooser chooser = getFileChooser();
1178                setText(chooser.getName(directory));
1179                setIcon(chooser.getIcon(directory));
1180                return this;
1181            }
1182        };
1183    }
1184
1185    //
1186    // DataModel for DirectoryComboxbox
1187    //
1188    protected DirectoryComboBoxModel createDirectoryComboBoxModel(final JFileChooser fc) {
1189        return new DirectoryComboBoxModel();
1190    }
1191
1192    /**
1193     * Data model for a type-face selection combo-box.
1194     */
1195    @SuppressWarnings("serial") // Superclass is not serializable across versions
1196    protected class DirectoryComboBoxModel extends AbstractListModel<File> implements ComboBoxModel<File> {
1197        Vector<File> fDirectories = new Vector<File>();
1198        int topIndex = -1;
1199        int fPathCount = 0;
1200
1201        File fSelectedDirectory = null;
1202
1203        public DirectoryComboBoxModel() {
1204            super();
1205            // Add the current directory to the model, and make it the
1206            // selectedDirectory
1207            addItem(getFileChooser().getCurrentDirectory());
1208        }
1209
1210        /**
1211         * Removes the selected directory, and clears out the
1212         * path file entries leading up to that directory.
1213         */
1214        private void removeSelectedDirectory() {
1215            fDirectories.removeAllElements();
1216            fPathCount = 0;
1217            fSelectedDirectory = null;
1218            // dump();
1219        }
1220
1221        /**
1222         * Adds the directory to the model and sets it to be selected,
1223         * additionally clears out the previous selected directory and
1224         * the paths leading up to it, if any.
1225         */
1226        void addItem(final File directory) {
1227            if (directory == null) { return; }
1228            if (fSelectedDirectory != null) {
1229                removeSelectedDirectory();
1230            }
1231
1232            // create File instances of each directory leading up to the top
1233            File f = directory.getAbsoluteFile();
1234            final Vector<File> path = new Vector<File>(10);
1235            while (f.getParent() != null) {
1236                path.addElement(f);
1237                f = getFileChooser().getFileSystemView().createFileObject(f.getParent());
1238            };
1239
1240            // Add root file (the desktop) to the model
1241            final File[] roots = getFileChooser().getFileSystemView().getRoots();
1242            for (final File element : roots) {
1243                path.addElement(element);
1244            }
1245            fPathCount = path.size();
1246
1247            // insert all the path fDirectories leading up to the
1248            // selected directory in reverse order (current directory at top)
1249            for (int i = 0; i < path.size(); i++) {
1250                fDirectories.addElement(path.elementAt(i));
1251            }
1252
1253            setSelectedItem(fDirectories.elementAt(0));
1254
1255            // dump();
1256        }
1257
1258        public void setSelectedItem(final Object selectedDirectory) {
1259            this.fSelectedDirectory = (File)selectedDirectory;
1260            fireContentsChanged(this, -1, -1);
1261        }
1262
1263        public Object getSelectedItem() {
1264            return fSelectedDirectory;
1265        }
1266
1267        public int getSize() {
1268            return fDirectories.size();
1269        }
1270
1271        public File getElementAt(final int index) {
1272            return fDirectories.elementAt(index);
1273        }
1274    }
1275
1276    //
1277    // Renderer for Types ComboBox
1278    //
1279    @SuppressWarnings("serial") // anonymous class
1280    protected ListCellRenderer<FileFilter> createFilterComboBoxRenderer() {
1281        return new AquaComboBoxRendererInternal<FileFilter>(filterComboBox) {
1282            public Component getListCellRendererComponent(final JList<? extends FileFilter> list,
1283                                                          final FileFilter filter,
1284                                                          final int index,
1285                                                          final boolean isSelected,
1286                                                          final boolean cellHasFocus) {
1287                super.getListCellRendererComponent(list, filter, index, isSelected, cellHasFocus);
1288                if (filter != null) setText(filter.getDescription());
1289                return this;
1290            }
1291        };
1292    }
1293
1294    //
1295    // DataModel for Types Comboxbox
1296    //
1297    protected FilterComboBoxModel createFilterComboBoxModel() {
1298        return new FilterComboBoxModel();
1299    }
1300
1301    /**
1302     * Data model for a type-face selection combo-box.
1303     */
1304    @SuppressWarnings("serial") // Superclass is not serializable across versions
1305    protected class FilterComboBoxModel extends AbstractListModel<FileFilter> implements ComboBoxModel<FileFilter>,
1306            PropertyChangeListener {
1307        protected FileFilter[] filters;
1308        Object oldFileFilter = getFileChooser().getFileFilter();
1309
1310        protected FilterComboBoxModel() {
1311            super();
1312            filters = getFileChooser().getChoosableFileFilters();
1313        }
1314
1315        public void propertyChange(PropertyChangeEvent e) {
1316            String prop = e.getPropertyName();
1317            if(prop == JFileChooser.CHOOSABLE_FILE_FILTER_CHANGED_PROPERTY) {
1318                filters = (FileFilter[]) e.getNewValue();
1319                fireContentsChanged(this, -1, -1);
1320            } else if (prop == JFileChooser.FILE_FILTER_CHANGED_PROPERTY) {
1321                setSelectedItem(e.getNewValue());
1322            }
1323        }
1324
1325        public void setSelectedItem(Object filter) {
1326            if (filter != null && !isSelectedFileFilterInModel(filter)) {
1327                oldFileFilter = filter;
1328                getFileChooser().setFileFilter((FileFilter) filter);
1329                fireContentsChanged(this, -1, -1);
1330            }
1331        }
1332
1333        private boolean isSelectedFileFilterInModel(Object filter) {
1334            return Objects.equals(filter, oldFileFilter);
1335        }
1336
1337        public Object getSelectedItem() {
1338            // Ensure that the current filter is in the list.
1339            // NOTE: we shouldnt' have to do this, since JFileChooser adds
1340            // the filter to the choosable filters list when the filter
1341            // is set. Lets be paranoid just in case someone overrides
1342            // setFileFilter in JFileChooser.
1343            FileFilter currentFilter = getFileChooser().getFileFilter();
1344            boolean found = false;
1345            if(currentFilter != null) {
1346                for (FileFilter filter : filters) {
1347                    if (filter == currentFilter) {
1348                        found = true;
1349                    }
1350                }
1351                if(found == false) {
1352                    getFileChooser().addChoosableFileFilter(currentFilter);
1353                }
1354            }
1355            return getFileChooser().getFileFilter();
1356        }
1357
1358        public int getSize() {
1359            if(filters != null) {
1360                return filters.length;
1361            } else {
1362                return 0;
1363            }
1364        }
1365
1366        public FileFilter getElementAt(int index) {
1367            if(index > getSize() - 1) {
1368                // This shouldn't happen. Try to recover gracefully.
1369                return getFileChooser().getFileFilter();
1370            }
1371            if(filters != null) {
1372                return filters[index];
1373            } else {
1374                return null;
1375            }
1376        }
1377    }
1378
1379    private boolean containsFileFilter(Object fileFilter) {
1380        return Objects.equals(fileFilter, getFileChooser().getFileFilter());
1381    }
1382
1383    /**
1384     * Acts when FilterComboBox has changed the selected item.
1385     */
1386    @SuppressWarnings("serial") // Superclass is not serializable across versions
1387    protected class FilterComboBoxAction extends AbstractAction {
1388        protected FilterComboBoxAction() {
1389            super("FilterComboBoxAction");
1390        }
1391
1392        public void actionPerformed(final ActionEvent e) {
1393            Object selectedFilter = filterComboBox.getSelectedItem();
1394            if (!containsFileFilter(selectedFilter)) {
1395                getFileChooser().setFileFilter((FileFilter) selectedFilter);
1396            }
1397        }
1398    }
1399
1400    /**
1401     * Acts when DirectoryComboBox has changed the selected item.
1402     */
1403    @SuppressWarnings("serial") // Superclass is not serializable across versions
1404    protected class DirectoryComboBoxAction extends AbstractAction {
1405        protected DirectoryComboBoxAction() {
1406            super("DirectoryComboBoxAction");
1407        }
1408
1409        public void actionPerformed(final ActionEvent e) {
1410            getFileChooser().setCurrentDirectory((File)directoryComboBox.getSelectedItem());
1411        }
1412    }
1413
1414    // Sorting Table operations
1415    @SuppressWarnings("serial") // Superclass is not serializable across versions
1416    class JSortingTableHeader extends JTableHeader {
1417        public JSortingTableHeader(final TableColumnModel cm) {
1418            super(cm);
1419            setReorderingAllowed(true); // This causes mousePress to call setDraggedColumn
1420        }
1421
1422        // One sort state for each column.  Both are ascending by default
1423        final boolean fSortAscending[] = {true, true};
1424
1425        // Instead of dragging, it selects which one to sort by
1426        public void setDraggedColumn(final TableColumn aColumn) {
1427            if (aColumn != null) {
1428                final int colIndex = aColumn.getModelIndex();
1429                if (colIndex != fSortColumn) {
1430                    filechooser.firePropertyChange(AquaFileSystemModel.SORT_BY_CHANGED, fSortColumn, colIndex);
1431                    fSortColumn = colIndex;
1432                } else {
1433                    fSortAscending[colIndex] = !fSortAscending[colIndex];
1434                    filechooser.firePropertyChange(AquaFileSystemModel.SORT_ASCENDING_CHANGED, !fSortAscending[colIndex], fSortAscending[colIndex]);
1435                }
1436                // Need to repaint the highlighted column.
1437                repaint();
1438            }
1439        }
1440
1441        // This stops mouseDrags from moving the column
1442        public TableColumn getDraggedColumn() {
1443            return null;
1444        }
1445
1446        protected TableCellRenderer createDefaultRenderer() {
1447            final DefaultTableCellRenderer label = new AquaTableCellRenderer();
1448            label.setHorizontalAlignment(SwingConstants.LEFT);
1449            return label;
1450        }
1451
1452        @SuppressWarnings("serial") // Superclass is not serializable across versions
1453        class AquaTableCellRenderer extends DefaultTableCellRenderer implements UIResource {
1454            public Component getTableCellRendererComponent(final JTable localTable, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column) {
1455                if (localTable != null) {
1456                    final JTableHeader header = localTable.getTableHeader();
1457                    if (header != null) {
1458                        setForeground(header.getForeground());
1459                        setBackground(header.getBackground());
1460                        setFont(UIManager.getFont("TableHeader.font"));
1461                    }
1462                }
1463
1464                setText((value == null) ? "" : value.toString());
1465
1466                // Modify the table "border" to draw smaller, and with the titles in the right position
1467                // and sort indicators, just like an NSSave/Open panel.
1468                final AquaTableHeaderBorder cellBorder = AquaTableHeaderBorder.getListHeaderBorder();
1469                cellBorder.setSelected(column == fSortColumn);
1470                final int horizontalShift = (column == 0 ? 35 : 10);
1471                cellBorder.setHorizontalShift(horizontalShift);
1472
1473                if (column == fSortColumn) {
1474                    cellBorder.setSortOrder(fSortAscending[column] ? AquaTableHeaderBorder.SORT_ASCENDING : AquaTableHeaderBorder.SORT_DECENDING);
1475                } else {
1476                    cellBorder.setSortOrder(AquaTableHeaderBorder.SORT_NONE);
1477                }
1478                setBorder(cellBorder);
1479                return this;
1480            }
1481        }
1482    }
1483
1484    public void installComponents(final JFileChooser fc) {
1485        JPanel tPanel; // temp panel
1486        // set to a Y BoxLayout. The chooser will be laid out top to bottom.
1487        fc.setLayout(new BoxLayout(fc, BoxLayout.Y_AXIS));
1488        fc.add(Box.createRigidArea(vstrut10));
1489
1490        // construct the top panel
1491
1492        final JPanel topPanel = new JPanel();
1493        topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));
1494        fc.add(topPanel);
1495        fc.add(Box.createRigidArea(vstrut10));
1496
1497        // Add the textfield pane
1498
1499        fTextfieldPanel = new JPanel();
1500        fTextfieldPanel.setLayout(new BorderLayout());
1501        // setBottomPanelForMode will make this visible if we need it
1502        fTextfieldPanel.setVisible(false);
1503        topPanel.add(fTextfieldPanel);
1504
1505        tPanel = new JPanel();
1506        tPanel.setLayout(new BoxLayout(tPanel, BoxLayout.Y_AXIS));
1507        final JPanel labelArea = new JPanel();
1508        labelArea.setLayout(new FlowLayout(FlowLayout.CENTER));
1509        fTextFieldLabel = new JLabel(fileNameLabelText);
1510        labelArea.add(fTextFieldLabel);
1511
1512        // text field
1513        filenameTextField = new JTextField();
1514        fTextFieldLabel.setLabelFor(filenameTextField);
1515        filenameTextField.addActionListener(getAction(kOpen));
1516        filenameTextField.addFocusListener(new SaveTextFocusListener());
1517        final Dimension minSize = filenameTextField.getMinimumSize();
1518        Dimension d = new Dimension(250, (int)minSize.getHeight());
1519        filenameTextField.setPreferredSize(d);
1520        filenameTextField.setMaximumSize(d);
1521        labelArea.add(filenameTextField);
1522        final File f = fc.getSelectedFile();
1523        if (f != null) {
1524            setFileName(fc.getName(f));
1525        } else if (fc.getDialogType() == JFileChooser.SAVE_DIALOG) {
1526            setFileName(newFileDefaultName);
1527        }
1528
1529        tPanel.add(labelArea);
1530        // separator line
1531        @SuppressWarnings("serial") // anonymous class
1532        final JSeparator sep = new JSeparator(){
1533            public Dimension getPreferredSize() {
1534                return new Dimension(((JComponent)getParent()).getWidth(), 3);
1535            }
1536        };
1537        tPanel.add(Box.createRigidArea(new Dimension(1, 8)));
1538        tPanel.add(sep);
1539        tPanel.add(Box.createRigidArea(new Dimension(1, 7)));
1540        fTextfieldPanel.add(tPanel, BorderLayout.CENTER);
1541
1542        // DirectoryComboBox, left-justified, 200x20 not including drop shadow
1543        directoryComboBox = new JComboBox<>();
1544        directoryComboBox.putClientProperty("JComboBox.lightweightKeyboardNavigation", "Lightweight");
1545        fDirectoryComboBoxModel = createDirectoryComboBoxModel(fc);
1546        directoryComboBox.setModel(fDirectoryComboBoxModel);
1547        directoryComboBox.addActionListener(directoryComboBoxAction);
1548        directoryComboBox.setRenderer(createDirectoryComboBoxRenderer(fc));
1549        directoryComboBox.setToolTipText(directoryComboBoxToolTipText);
1550        d = new Dimension(250, (int)directoryComboBox.getMinimumSize().getHeight());
1551        directoryComboBox.setPreferredSize(d);
1552        directoryComboBox.setMaximumSize(d);
1553        topPanel.add(directoryComboBox);
1554
1555        // ************************************** //
1556        // ** Add the directory/Accessory pane ** //
1557        // ************************************** //
1558        final JPanel centerPanel = new JPanel(new BorderLayout());
1559        fc.add(centerPanel);
1560
1561        // Accessory pane (equiv to Preview pane in NavServices)
1562        final JComponent accessory = fc.getAccessory();
1563        if (accessory != null) {
1564            getAccessoryPanel().add(accessory);
1565        }
1566        centerPanel.add(getAccessoryPanel(), BorderLayout.LINE_START);
1567
1568        // Directory list(table), right-justified, resizable
1569        final JPanel p = createList(fc);
1570        p.setMinimumSize(LIST_MIN_SIZE);
1571        centerPanel.add(p, BorderLayout.CENTER);
1572
1573        // ********************************** //
1574        // **** Construct the bottom panel ** //
1575        // ********************************** //
1576        fBottomPanel = new JPanel();
1577        fBottomPanel.setLayout(new BoxLayout(fBottomPanel, BoxLayout.Y_AXIS));
1578        fc.add(fBottomPanel);
1579
1580        // Filter label and combobox.
1581        // I know it's unMaclike, but the filter goes on Directory_only too.
1582        tPanel = new JPanel();
1583        tPanel.setLayout(new FlowLayout(FlowLayout.CENTER));
1584        tPanel.setBorder(AquaGroupBorder.getTitlelessBorder());
1585        final JLabel formatLabel = new JLabel(filesOfTypeLabelText);
1586        tPanel.add(formatLabel);
1587
1588        // Combobox
1589        filterComboBoxModel = createFilterComboBoxModel();
1590        fc.addPropertyChangeListener(filterComboBoxModel);
1591        filterComboBox = new JComboBox<>(filterComboBoxModel);
1592        formatLabel.setLabelFor(filterComboBox);
1593        filterComboBox.setRenderer(createFilterComboBoxRenderer());
1594        d = new Dimension(220, (int)filterComboBox.getMinimumSize().getHeight());
1595        filterComboBox.setPreferredSize(d);
1596        filterComboBox.setMaximumSize(d);
1597        filterComboBox.addActionListener(filterComboBoxAction);
1598        filterComboBox.setOpaque(false);
1599        tPanel.add(filterComboBox);
1600
1601        fBottomPanel.add(tPanel);
1602
1603        // fDirectoryPanel: New, Open, Cancel, Approve buttons, right-justified, 82x22
1604        // (sometimes the NewFolder and OpenFolder buttons are invisible)
1605        fDirectoryPanel = new JPanel();
1606        fDirectoryPanel.setLayout(new BoxLayout(fDirectoryPanel, BoxLayout.PAGE_AXIS));
1607        JPanel directoryPanel = new JPanel(new BorderLayout());
1608        JPanel newFolderButtonPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0));
1609        newFolderButtonPanel.add(Box.createHorizontalStrut(20));
1610        fNewFolderButton = createNewFolderButton(); // Because we hide it depending on style
1611        newFolderButtonPanel.add(fNewFolderButton);
1612        directoryPanel.add(newFolderButtonPanel, BorderLayout.LINE_START);
1613        JPanel approveCancelButtonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING, 0, 0));
1614        fOpenButton = createButton(kOpenDirectory, openButtonText);
1615        approveCancelButtonPanel.add(fOpenButton);
1616        approveCancelButtonPanel.add(Box.createHorizontalStrut(8));
1617        fCancelButton = createButton(kCancel, null);
1618        approveCancelButtonPanel.add(fCancelButton);
1619        approveCancelButtonPanel.add(Box.createHorizontalStrut(8));
1620        // The ApproveSelection button
1621        fApproveButton = new JButton();
1622        fApproveButton.addActionListener(fApproveSelectionAction);
1623        approveCancelButtonPanel.add(fApproveButton);
1624        approveCancelButtonPanel.add(Box.createHorizontalStrut(20));
1625        directoryPanel.add(approveCancelButtonPanel, BorderLayout.LINE_END);
1626        fDirectoryPanel.add(Box.createVerticalStrut(5));
1627        fDirectoryPanel.add(directoryPanel);
1628        fDirectoryPanel.add(Box.createVerticalStrut(12));
1629        fDirectoryPanelSpacer = Box.createRigidArea(hstrut10);
1630
1631        if (fc.getControlButtonsAreShown()) {
1632            fBottomPanel.add(fDirectoryPanelSpacer);
1633            fBottomPanel.add(fDirectoryPanel);
1634        }
1635
1636        setBottomPanelForMode(fc); // updates ApproveButtonText etc
1637
1638        // don't create til after the FCSubpanel and buttons are made
1639        filenameTextField.getDocument().addDocumentListener(new SaveTextDocumentListener());
1640    }
1641
1642    void setDefaultButtonForMode(final JFileChooser fc) {
1643        final JButton defaultButton = fSubPanel.getDefaultButton(fc);
1644        final JRootPane root = defaultButton.getRootPane();
1645        if (root != null) {
1646            root.setDefaultButton(defaultButton);
1647        }
1648    }
1649
1650    // Macs start with their focus in text areas if they have them,
1651    // lists otherwise (the other plafs start with the focus on approveButton)
1652    void setFocusForMode(final JFileChooser fc) {
1653        final JComponent focusComponent = fSubPanel.getFocusComponent(fc);
1654        if (focusComponent != null) {
1655            focusComponent.requestFocus();
1656        }
1657    }
1658
1659    // Enable/disable buttons as needed for the current selection/focus state
1660    void updateButtonState(final JFileChooser fc) {
1661        fSubPanel.updateButtonState(fc, getFirstSelectedItem());
1662        updateApproveButton(fc);
1663    }
1664
1665    void updateApproveButton(final JFileChooser chooser) {
1666        fApproveButton.setText(getApproveButtonText(chooser));
1667        fApproveButton.setToolTipText(getApproveButtonToolTipText(chooser));
1668        fApproveButton.setMnemonic(getApproveButtonMnemonic(chooser));
1669        fCancelButton.setToolTipText(getCancelButtonToolTipText(chooser));
1670    }
1671
1672    // Lazy-init the subpanels
1673    synchronized FCSubpanel getSaveFilePanel() {
1674        if (fSaveFilePanel == null) fSaveFilePanel = new SaveFilePanel();
1675        return fSaveFilePanel;
1676    }
1677
1678    synchronized FCSubpanel getOpenFilePanel() {
1679        if (fOpenFilePanel == null) fOpenFilePanel = new OpenFilePanel();
1680        return fOpenFilePanel;
1681    }
1682
1683    synchronized FCSubpanel getOpenDirOrAnyPanel() {
1684        if (fOpenDirOrAnyPanel == null) fOpenDirOrAnyPanel = new OpenDirOrAnyPanel();
1685        return fOpenDirOrAnyPanel;
1686    }
1687
1688    synchronized FCSubpanel getCustomFilePanel() {
1689        if (fCustomFilePanel == null) fCustomFilePanel = new CustomFilePanel();
1690        return fCustomFilePanel;
1691    }
1692
1693    synchronized FCSubpanel getCustomDirOrAnyPanel() {
1694        if (fCustomDirOrAnyPanel == null) fCustomDirOrAnyPanel = new CustomDirOrAnyPanel();
1695        return fCustomDirOrAnyPanel;
1696    }
1697
1698    void setBottomPanelForMode(final JFileChooser fc) {
1699        if (fc.getDialogType() == JFileChooser.SAVE_DIALOG) fSubPanel = getSaveFilePanel();
1700        else if (fc.getDialogType() == JFileChooser.OPEN_DIALOG) {
1701            if (fc.getFileSelectionMode() == JFileChooser.FILES_ONLY) fSubPanel = getOpenFilePanel();
1702            else fSubPanel = getOpenDirOrAnyPanel();
1703        } else if (fc.getDialogType() == JFileChooser.CUSTOM_DIALOG) {
1704            if (fc.getFileSelectionMode() == JFileChooser.FILES_ONLY) fSubPanel = getCustomFilePanel();
1705            else fSubPanel = getCustomDirOrAnyPanel();
1706        }
1707
1708        fSubPanel.installPanel(fc, true);
1709        updateApproveButton(fc);
1710        updateButtonState(fc);
1711        setDefaultButtonForMode(fc);
1712        setFocusForMode(fc);
1713        fc.invalidate();
1714    }
1715
1716    // fTextfieldPanel and fDirectoryPanel both have NewFolder buttons; only one should be visible at a time
1717    JButton createNewFolderButton() {
1718        final JButton b = new JButton(newFolderButtonText);
1719        b.setToolTipText(newFolderToolTipText);
1720        b.getAccessibleContext().setAccessibleName(newFolderAccessibleName);
1721        b.setHorizontalTextPosition(SwingConstants.LEFT);
1722        b.setAlignmentX(Component.LEFT_ALIGNMENT);
1723        b.setAlignmentY(Component.CENTER_ALIGNMENT);
1724        b.addActionListener(getAction(kNewFolder));
1725        return b;
1726    }
1727
1728    JButton createButton(final int which, String label) {
1729        if (label == null) label = UIManager.getString(sDataPrefix + sButtonKinds[which] + sButtonData[0]);
1730        final int mnemonic = UIManager.getInt(sDataPrefix + sButtonKinds[which] + sButtonData[1]);
1731        final String tipText = UIManager.getString(sDataPrefix + sButtonKinds[which] + sButtonData[2]);
1732        final JButton b = new JButton(label);
1733        b.setMnemonic(mnemonic);
1734        b.setToolTipText(tipText);
1735        b.addActionListener(getAction(which));
1736        return b;
1737    }
1738
1739    AbstractAction getAction(final int which) {
1740        return fButtonActions[which];
1741    }
1742
1743    public void uninstallComponents(final JFileChooser fc) { //$ Metal (on which this is based) doesn't uninstall its components.
1744    }
1745
1746    // Consistent with the AppKit NSSavePanel, clicks on a file (not a directory) should populate the text field
1747    // with that file's display name.
1748    protected class FileListMouseListener extends MouseAdapter {
1749        public void mouseClicked(final MouseEvent e) {
1750            final Point p = e.getPoint();
1751            final int row = fFileList.rowAtPoint(p);
1752            final int column = fFileList.columnAtPoint(p);
1753
1754            // The autoscroller can generate drag events outside the Table's range.
1755            if ((column == -1) || (row == -1)) { return; }
1756
1757            final File clickedFile = (File)(fFileList.getValueAt(row, 0));
1758
1759            // rdar://problem/3734130 -- don't populate the text field if this file isn't selectable in this mode.
1760            if (isSelectableForMode(getFileChooser(), clickedFile)) {
1761                // [3188387] Populate the file name field with the selected file name
1762                // [3484163] It should also use the display name, not the actual name.
1763                setFileName(fileView.getName(clickedFile));
1764            }
1765        }
1766    }
1767
1768    protected JPanel createList(final JFileChooser fc) {
1769        // The first part is similar to MetalFileChooserUI.createList - same kind of listeners
1770        final JPanel p = new JPanel(new BorderLayout());
1771        fFileList = new JTableExtension();
1772        fFileList.setToolTipText(null); // Workaround for 2487689
1773        fFileList.addMouseListener(new FileListMouseListener());
1774        model = new AquaFileSystemModel(fc, fFileList, fColumnNames);
1775        final MacListSelectionModel listSelectionModel = new MacListSelectionModel(model);
1776
1777        if (getFileChooser().isMultiSelectionEnabled()) {
1778            listSelectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
1779        } else {
1780            listSelectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
1781        }
1782
1783        fFileList.setModel(model);
1784        fFileList.setSelectionModel(listSelectionModel);
1785        fFileList.getSelectionModel().addListSelectionListener(createListSelectionListener(fc));
1786
1787        // Now we're different, because we're a table, not a list
1788        fc.addPropertyChangeListener(model);
1789        fFileList.addFocusListener(new SaveTextFocusListener());
1790        final JTableHeader th = new JSortingTableHeader(fFileList.getColumnModel());
1791        fFileList.setTableHeader(th);
1792        fFileList.setRowMargin(0);
1793        fFileList.setIntercellSpacing(new Dimension(0, 1));
1794        fFileList.setShowVerticalLines(false);
1795        fFileList.setShowHorizontalLines(false);
1796        final Font f = fFileList.getFont(); //ThemeFont.GetThemeFont(AppearanceConstants.kThemeViewsFont);
1797        //fc.setFont(f);
1798        //fFileList.setFont(f);
1799        fFileList.setDefaultRenderer(File.class, new FileRenderer(f));
1800        fFileList.setDefaultRenderer(Date.class, new DateRenderer(f));
1801        final FontMetrics fm = fFileList.getFontMetrics(f);
1802
1803        // Row height isn't based on the renderers.  It defaults to 16 so we have to set it
1804        fFileList.setRowHeight(Math.max(fm.getHeight(), fileIcon.getIconHeight() + 2));
1805
1806        // Add a binding for the file list that triggers return and escape
1807        fFileList.registerKeyboardAction(new CancelSelectionAction(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_FOCUSED);
1808        // Add a binding for the file list that triggers the default button (see DefaultButtonAction)
1809        fFileList.registerKeyboardAction(new DefaultButtonAction(), KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), JComponent.WHEN_FOCUSED);
1810        fFileList.setDropTarget(dragAndDropTarget);
1811
1812        final JScrollPane scrollpane = new JScrollPane(fFileList, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
1813        scrollpane.setComponentOrientation(ComponentOrientation.getOrientation(Locale.getDefault()));
1814        scrollpane.setCorner(ScrollPaneConstants.UPPER_TRAILING_CORNER, new ScrollPaneCornerPanel());
1815        p.add(scrollpane, BorderLayout.CENTER);
1816        return p;
1817    }
1818
1819    @SuppressWarnings("serial") // Superclass is not serializable across versions
1820    protected class ScrollPaneCornerPanel extends JPanel {
1821        final Border border = UIManager.getBorder("TableHeader.cellBorder");
1822
1823        protected void paintComponent(final Graphics g) {
1824            border.paintBorder(this, g, 0, 0, getWidth() + 1, getHeight());
1825        }
1826    }
1827
1828    JComboBox<File> directoryComboBox;
1829    DirectoryComboBoxModel fDirectoryComboBoxModel;
1830    private final Action directoryComboBoxAction = new DirectoryComboBoxAction();
1831
1832    JTextField filenameTextField;
1833
1834    JTableExtension fFileList;
1835
1836    private FilterComboBoxModel filterComboBoxModel;
1837    JComboBox<FileFilter> filterComboBox;
1838    private final Action filterComboBoxAction = new FilterComboBoxAction();
1839
1840    private static final Dimension hstrut10 = new Dimension(10, 1);
1841    private static final Dimension vstrut10 = new Dimension(1, 10);
1842
1843    private static final int PREF_WIDTH = 550;
1844    private static final int PREF_HEIGHT = 400;
1845    private static final int MIN_WIDTH = 400;
1846    private static final int MIN_HEIGHT = 250;
1847    private static final int LIST_MIN_WIDTH = 400;
1848    private static final int LIST_MIN_HEIGHT = 100;
1849    private static final Dimension LIST_MIN_SIZE = new Dimension(LIST_MIN_WIDTH, LIST_MIN_HEIGHT);
1850
1851    static String fileNameLabelText = null;
1852    JLabel fTextFieldLabel = null;
1853
1854    private static String filesOfTypeLabelText = null;
1855
1856    private static String newFolderToolTipText = null;
1857    static String newFolderAccessibleName = null;
1858
1859    private static final String[] fColumnNames = new String[2];
1860
1861    JPanel fTextfieldPanel; // Filename textfield for Save or Custom
1862    private JPanel fDirectoryPanel; // NewFolder/OpenFolder/Cancel/Approve buttons
1863    private Component fDirectoryPanelSpacer;
1864    private JPanel fBottomPanel; // The panel that holds fDirectoryPanel and filterComboBox
1865
1866    private FCSubpanel fSaveFilePanel = null;
1867    private FCSubpanel fOpenFilePanel = null;
1868    private FCSubpanel fOpenDirOrAnyPanel = null;
1869    private FCSubpanel fCustomFilePanel = null;
1870    private FCSubpanel fCustomDirOrAnyPanel = null;
1871
1872    FCSubpanel fSubPanel = null; // Current FCSubpanel
1873
1874    JButton fApproveButton; // mode-specific behavior is managed by FCSubpanel.approveSelection
1875    JButton fOpenButton; // for Directories
1876    JButton fNewFolderButton; // for fDirectoryPanel
1877
1878    // ToolTip text varies with type of dialog
1879    private JButton fCancelButton;
1880
1881    private final ApproveSelectionAction fApproveSelectionAction = new ApproveSelectionAction();
1882    protected int fSortColumn = 0;
1883    protected int fPackageIsTraversable = -1;
1884    protected int fApplicationIsTraversable = -1;
1885
1886    protected static final int sGlobalPackageIsTraversable;
1887    protected static final int sGlobalApplicationIsTraversable;
1888
1889    protected static final String PACKAGE_TRAVERSABLE_PROPERTY = "JFileChooser.packageIsTraversable";
1890    protected static final String APPLICATION_TRAVERSABLE_PROPERTY = "JFileChooser.appBundleIsTraversable";
1891    protected static final String[] sTraversableProperties = {"always", // Bundle is always traversable
1892            "never", // Bundle is never traversable
1893            "conditional"}; // Bundle is traversable on command click
1894    protected static final int kOpenAlways = 0, kOpenNever = 1, kOpenConditional = 2;
1895
1896    AbstractAction[] fButtonActions = {fApproveSelectionAction, fApproveSelectionAction, new CancelSelectionAction(), new OpenSelectionAction(), null, new NewFolderAction()};
1897
1898    static int parseTraversableProperty(final String s) {
1899        if (s == null) return -1;
1900        for (int i = 0; i < sTraversableProperties.length; i++) {
1901            if (s.equals(sTraversableProperties[i])) return i;
1902        }
1903        return -1;
1904    }
1905
1906    static {
1907        Object o = UIManager.get(PACKAGE_TRAVERSABLE_PROPERTY);
1908        if (o != null && o instanceof String) sGlobalPackageIsTraversable = parseTraversableProperty((String)o);
1909        else sGlobalPackageIsTraversable = kOpenConditional;
1910
1911        o = UIManager.get(APPLICATION_TRAVERSABLE_PROPERTY);
1912        if (o != null && o instanceof String) sGlobalApplicationIsTraversable = parseTraversableProperty((String)o);
1913        else sGlobalApplicationIsTraversable = kOpenConditional;
1914    }
1915    static final String sDataPrefix = "FileChooser.";
1916    static final String[] sButtonKinds = {"openButton", "saveButton", "cancelButton", "openDirectoryButton", "helpButton", "newFolderButton"};
1917    static final String[] sButtonData = {"Text", "Mnemonic", "ToolTipText"};
1918    static final int kOpen = 0, kSave = 1, kCancel = 2, kOpenDirectory = 3, kHelp = 4, kNewFolder = 5;
1919
1920    /*-------
1921
1922     Possible states: Save, {Open, Custom}x{Files, File and Directory, Directory}
1923     --------- */
1924
1925    // This class returns the values for the Custom type, to avoid duplicating code in the two Custom subclasses
1926    abstract class FCSubpanel {
1927        // Install the appropriate panels for this mode
1928        abstract void installPanel(JFileChooser fc, boolean controlButtonsAreShown);
1929
1930        abstract void updateButtonState(JFileChooser fc, File f);
1931
1932        // Can this item be selected?
1933        // if not, it's disabled in the list
1934        boolean isSelectableInList(final JFileChooser fc, final File f) {
1935            if (f == null) return false;
1936            if (fc.getFileSelectionMode() == JFileChooser.DIRECTORIES_ONLY) return fc.isTraversable(f);
1937            return fc.accept(f);
1938        }
1939
1940        void approveSelection(final JFileChooser fc) {
1941            fc.approveSelection();
1942        }
1943
1944        JButton getDefaultButton(final JFileChooser fc) {
1945            return fApproveButton;
1946        }
1947
1948        // Default to the textfield, panels without one should subclass
1949        JComponent getFocusComponent(final JFileChooser fc) {
1950            return filenameTextField;
1951        }
1952
1953        String getApproveButtonText(final JFileChooser fc) {
1954            // Fallback to "choose"
1955            return this.getApproveButtonText(fc, chooseButtonText);
1956        }
1957
1958        // Try to get the custom text.  If none, use the fallback
1959        String getApproveButtonText(final JFileChooser fc, final String fallbackText) {
1960            final String buttonText = fc.getApproveButtonText();
1961            if (buttonText != null) {
1962                buttonText.trim();
1963                if (!buttonText.equals("")) return buttonText;
1964            }
1965            return fallbackText;
1966        }
1967
1968        int getApproveButtonMnemonic(final JFileChooser fc) {
1969            // Don't use a default
1970            return fc.getApproveButtonMnemonic();
1971        }
1972
1973        // No fallback
1974        String getApproveButtonToolTipText(final JFileChooser fc) {
1975            return getApproveButtonToolTipText(fc, null);
1976        }
1977
1978        String getApproveButtonToolTipText(final JFileChooser fc, final String fallbackText) {
1979            final String tooltipText = fc.getApproveButtonToolTipText();
1980            if (tooltipText != null) {
1981                tooltipText.trim();
1982                if (!tooltipText.equals("")) return tooltipText;
1983            }
1984            return fallbackText;
1985        }
1986
1987        String getCancelButtonToolTipText(final JFileChooser fc) {
1988            return cancelChooseButtonToolTipText;
1989        }
1990    }
1991
1992    // Custom FILES_ONLY dialog
1993    /*
1994     NavServices Save appearance with Open behavior
1995     Approve button label = Open when list has focus and a directory is selected, Custom otherwise
1996     No OpenDirectory button - Approve button is overloaded
1997     Default button / double click = Approve
1998     Has text field
1999     List - everything is enabled
2000     */
2001    class CustomFilePanel extends FCSubpanel {
2002        void installPanel(final JFileChooser fc, final boolean controlButtonsAreShown) {
2003            fTextfieldPanel.setVisible(true); // do we really want one in multi-select?  It's confusing
2004            fOpenButton.setVisible(false);
2005            fNewFolderButton.setVisible(true);
2006        }
2007
2008        // If the list has focus, the mode depends on the selection
2009        // - directory = open, file = approve
2010        // If something else has focus and we have text, it's approve
2011        // otherwise, it depends on selection again.
2012        boolean inOpenDirectoryMode(final JFileChooser fc, final File f) {
2013            final boolean selectionIsDirectory = (f != null && fc.isTraversable(f));
2014            if (fFileList.hasFocus()) return selectionIsDirectory;
2015            else if (textfieldIsValid()) return false;
2016            return selectionIsDirectory;
2017        }
2018
2019        // The approve button is overloaded to mean OpenDirectory or Save
2020        void approveSelection(final JFileChooser fc) {
2021            File f = getFirstSelectedItem();
2022            if (inOpenDirectoryMode(fc, f)) {
2023                openDirectory(f);
2024            } else {
2025                f = makeFile(fc, getFileName());
2026                if (f != null) {
2027                    selectionInProgress = true;
2028                    getFileChooser().setSelectedFile(f);
2029                    selectionInProgress = false;
2030                }
2031                getFileChooser().approveSelection();
2032            }
2033        }
2034
2035        // The approve button should be enabled
2036        // - if something in the list can be opened
2037        // - if the textfield has something in it
2038        void updateButtonState(final JFileChooser fc, final File f) {
2039            boolean enabled = true;
2040            if (!inOpenDirectoryMode(fc, f)) {
2041                enabled = (f != null) || textfieldIsValid();
2042            }
2043            getApproveButton(fc).setEnabled(enabled);
2044
2045            // The OpenDirectory button should be disabled if there's no directory selected
2046            fOpenButton.setEnabled(f != null && fc.isTraversable(f));
2047
2048            // Update the default button, since we may have disabled the current default.
2049            setDefaultButtonForMode(fc);
2050        }
2051
2052        // everything's enabled, because we don't know what they're doing with them
2053        boolean isSelectableInList(final JFileChooser fc, final File f) {
2054            if (f == null) return false;
2055            return fc.accept(f);
2056        }
2057
2058        String getApproveButtonToolTipText(final JFileChooser fc) {
2059            // The approve Button should have openDirectoryButtonToolTipText when the selection is a folder...
2060            if (inOpenDirectoryMode(fc, getFirstSelectedItem())) return openDirectoryButtonToolTipText;
2061            return super.getApproveButtonToolTipText(fc);
2062        }
2063    }
2064
2065    // All Save dialogs
2066    /*
2067     NavServices Save
2068     Approve button label = Open when list has focus and a directory is selected, Save otherwise
2069     No OpenDirectory button - Approve button is overloaded
2070     Default button / double click = Approve
2071     Has text field
2072     Has NewFolder button (by text field)
2073     List - only traversables are enabled
2074     List is always SINGLE_SELECT
2075     */
2076    // Subclasses CustomFilePanel because they look alike and have some common behavior
2077    class SaveFilePanel extends CustomFilePanel {
2078        void installPanel(final JFileChooser fc, final boolean controlButtonsAreShown) {
2079            fTextfieldPanel.setVisible(true);
2080            fOpenButton.setVisible(false);
2081            fNewFolderButton.setVisible(true);
2082        }
2083
2084        // only traversables are enabled, regardless of mode
2085        // because all you can do is select the next folder to open
2086        boolean isSelectableInList(final JFileChooser fc, final File f) {
2087            return fc.accept(f) && fc.isTraversable(f);
2088        }
2089
2090        // The approve button means 'approve the file name in the text field.'
2091        void approveSelection(final JFileChooser fc) {
2092            final File f = makeFile(fc, getFileName());
2093            if (f != null) {
2094                selectionInProgress = true;
2095                getFileChooser().setSelectedFile(f);
2096                selectionInProgress = false;
2097                getFileChooser().approveSelection();
2098            }
2099        }
2100
2101        // The approve button should be enabled if the textfield has something in it
2102        void updateButtonState(final JFileChooser fc, final File f) {
2103            final boolean enabled = textfieldIsValid();
2104            getApproveButton(fc).setEnabled(enabled);
2105        }
2106
2107        String getApproveButtonText(final JFileChooser fc) {
2108            // Get the custom text, or fallback to "Save"
2109            return this.getApproveButtonText(fc, saveButtonText);
2110        }
2111
2112        int getApproveButtonMnemonic(final JFileChooser fc) {
2113            return saveButtonMnemonic;
2114        }
2115
2116        String getApproveButtonToolTipText(final JFileChooser fc) {
2117            // The approve Button should have openDirectoryButtonToolTipText when the selection is a folder...
2118            if (inOpenDirectoryMode(fc, getFirstSelectedItem())) return openDirectoryButtonToolTipText;
2119            return this.getApproveButtonToolTipText(fc, saveButtonToolTipText);
2120        }
2121
2122        String getCancelButtonToolTipText(final JFileChooser fc) {
2123            return cancelSaveButtonToolTipText;
2124        }
2125    }
2126
2127    // Open FILES_ONLY
2128    /*
2129     NSOpenPanel-style
2130     Approve button label = Open
2131     Default button / double click = Approve
2132     No text field
2133     No NewFolder button
2134     List - all items are enabled
2135     */
2136    class OpenFilePanel extends FCSubpanel {
2137        void installPanel(final JFileChooser fc, final boolean controlButtonsAreShown) {
2138            fTextfieldPanel.setVisible(false);
2139            fOpenButton.setVisible(false);
2140            fNewFolderButton.setVisible(false);
2141            setDefaultButtonForMode(fc);
2142        }
2143
2144        boolean inOpenDirectoryMode(final JFileChooser fc, final File f) {
2145            return (f != null && fc.isTraversable(f));
2146        }
2147
2148        // Default to the list
2149        JComponent getFocusComponent(final JFileChooser fc) {
2150            return fFileList;
2151        }
2152
2153        void updateButtonState(final JFileChooser fc, final File f) {
2154            // Button is disabled if there's nothing selected
2155            final boolean enabled = (f != null) && !fc.isTraversable(f);
2156            getApproveButton(fc).setEnabled(enabled);
2157        }
2158
2159        // all items are enabled
2160        boolean isSelectableInList(final JFileChooser fc, final File f) {
2161            return f != null && fc.accept(f);
2162        }
2163
2164        String getApproveButtonText(final JFileChooser fc) {
2165            // Get the custom text, or fallback to "Open"
2166            return this.getApproveButtonText(fc, openButtonText);
2167        }
2168
2169        int getApproveButtonMnemonic(final JFileChooser fc) {
2170            return openButtonMnemonic;
2171        }
2172
2173        String getApproveButtonToolTipText(final JFileChooser fc) {
2174            return this.getApproveButtonToolTipText(fc, openButtonToolTipText);
2175        }
2176
2177        String getCancelButtonToolTipText(final JFileChooser fc) {
2178            return cancelOpenButtonToolTipText;
2179        }
2180    }
2181
2182    // used by open and custom panels for Directory only or files and directories
2183    abstract class DirOrAnyPanel extends FCSubpanel {
2184        void installPanel(final JFileChooser fc, final boolean controlButtonsAreShown) {
2185            fOpenButton.setVisible(false);
2186        }
2187
2188        JButton getDefaultButton(final JFileChooser fc) {
2189            return getApproveButton(fc);
2190        }
2191
2192        void updateButtonState(final JFileChooser fc, final File f) {
2193            // Button is disabled if there's nothing selected
2194            // Approve button is handled by the subclasses
2195            // getApproveButton(fc).setEnabled(f != null);
2196
2197            // The OpenDirectory button should be disabled if there's no directory selected
2198            // - we only check the first item
2199
2200            fOpenButton.setEnabled(false);
2201            setDefaultButtonForMode(fc);
2202        }
2203    }
2204
2205    // Open FILES_AND_DIRECTORIES or DIRECTORIES_ONLY
2206    /*
2207     NavServices Choose
2208     Approve button label = Choose/Custom
2209     Has OpenDirectory button
2210     Default button / double click = OpenDirectory
2211     No text field
2212     List - files are disabled in DIRECTORIES_ONLY
2213     */
2214    class OpenDirOrAnyPanel extends DirOrAnyPanel {
2215        void installPanel(final JFileChooser fc, final boolean controlButtonsAreShown) {
2216            super.installPanel(fc, controlButtonsAreShown);
2217            fTextfieldPanel.setVisible(false);
2218            fNewFolderButton.setVisible(false);
2219        }
2220
2221        // Default to the list
2222        JComponent getFocusComponent(final JFileChooser fc) {
2223            return fFileList;
2224        }
2225
2226        int getApproveButtonMnemonic(final JFileChooser fc) {
2227            return chooseButtonMnemonic;
2228        }
2229
2230        String getApproveButtonToolTipText(final JFileChooser fc) {
2231            String fallbackText;
2232            if (fc.getFileSelectionMode() == JFileChooser.DIRECTORIES_ONLY) fallbackText = chooseFolderButtonToolTipText;
2233            else fallbackText = chooseItemButtonToolTipText;
2234            return this.getApproveButtonToolTipText(fc, fallbackText);
2235        }
2236
2237        void updateButtonState(final JFileChooser fc, final File f) {
2238            // Button is disabled if there's nothing selected
2239            getApproveButton(fc).setEnabled(f != null);
2240            super.updateButtonState(fc, f);
2241        }
2242    }
2243
2244    // Custom FILES_AND_DIRECTORIES or DIRECTORIES_ONLY
2245    /*
2246     No NavServices equivalent
2247     Approve button label = user defined or Choose
2248     Has OpenDirectory button
2249     Default button / double click = OpenDirectory
2250     Has text field
2251     Has NewFolder button (by text field)
2252     List - files are disabled in DIRECTORIES_ONLY
2253     */
2254    class CustomDirOrAnyPanel extends DirOrAnyPanel {
2255        void installPanel(final JFileChooser fc, final boolean controlButtonsAreShown) {
2256            super.installPanel(fc, controlButtonsAreShown);
2257            fTextfieldPanel.setVisible(true);
2258            fNewFolderButton.setVisible(true);
2259        }
2260
2261        // If there's text, make a file and select it
2262        void approveSelection(final JFileChooser fc) {
2263            final File f = makeFile(fc, getFileName());
2264            if (f != null) {
2265                selectionInProgress = true;
2266                getFileChooser().setSelectedFile(f);
2267                selectionInProgress = false;
2268            }
2269            getFileChooser().approveSelection();
2270        }
2271
2272        void updateButtonState(final JFileChooser fc, final File f) {
2273            // Button is disabled if there's nothing selected
2274            getApproveButton(fc).setEnabled(f != null || textfieldIsValid());
2275            super.updateButtonState(fc, f);
2276        }
2277    }
2278
2279    // See FileRenderer - documents in Save dialogs draw disabled, so they shouldn't be selected
2280    @SuppressWarnings("serial") // Superclass is not serializable across versions
2281    class MacListSelectionModel extends DefaultListSelectionModel {
2282        AquaFileSystemModel fModel;
2283
2284        MacListSelectionModel(final AquaFileSystemModel model) {
2285            fModel = model;
2286        }
2287
2288        // Can the file be selected in this mode?
2289        // (files are visible even if they can't be selected)
2290        boolean isSelectableInListIndex(final int index) {
2291            final File file = (File)fModel.getValueAt(index, 0);
2292            return (file != null && isSelectableInList(file));
2293        }
2294
2295        // Make sure everything in the selection interval is valid
2296        void verifySelectionInterval(int index0, int index1, boolean isSetSelection) {
2297            if (index0 > index1) {
2298                final int tmp = index1;
2299                index1 = index0;
2300                index0 = tmp;
2301            }
2302            int start = index0;
2303            int end;
2304            do {
2305                // Find the first selectable file in the range
2306                for (; start <= index1; start++) {
2307                    if (isSelectableInListIndex(start)) break;
2308                }
2309                end = -1;
2310                // Find the last selectable file in the range
2311                for (int i = start; i <= index1; i++) {
2312                    if (!isSelectableInListIndex(i)) {
2313                        break;
2314                    }
2315                    end = i;
2316                }
2317                // Select the range
2318                if (end >= 0) {
2319                    // If setting the selection, do "set" the first time to clear the old one
2320                    // after that do "add" to extend it
2321                    if (isSetSelection) {
2322                        super.setSelectionInterval(start, end);
2323                        isSetSelection = false;
2324                    } else {
2325                        super.addSelectionInterval(start, end);
2326                    }
2327                    start = end + 1;
2328                } else {
2329                    break;
2330                }
2331            } while (start <= index1);
2332        }
2333
2334        public void setAnchorSelectionIndex(final int anchorIndex) {
2335            if (isSelectableInListIndex(anchorIndex)) super.setAnchorSelectionIndex(anchorIndex);
2336        }
2337
2338        public void setLeadSelectionIndex(final int leadIndex) {
2339            if (isSelectableInListIndex(leadIndex)) super.setLeadSelectionIndex(leadIndex);
2340        }
2341
2342        public void setSelectionInterval(final int index0, final int index1) {
2343            if (index0 == -1 || index1 == -1) { return; }
2344
2345            if ((getSelectionMode() == SINGLE_SELECTION) || (index0 == index1)) {
2346                if (isSelectableInListIndex(index1)) super.setSelectionInterval(index1, index1);
2347            } else {
2348                verifySelectionInterval(index0, index1, true);
2349            }
2350        }
2351
2352        public void addSelectionInterval(final int index0, final int index1) {
2353            if (index0 == -1 || index1 == -1) { return; }
2354
2355            if (index0 == index1) {
2356                if (isSelectableInListIndex(index1)) super.addSelectionInterval(index1, index1);
2357                return;
2358            }
2359
2360            if (getSelectionMode() != MULTIPLE_INTERVAL_SELECTION) {
2361                setSelectionInterval(index0, index1);
2362                return;
2363            }
2364
2365            verifySelectionInterval(index0, index1, false);
2366        }
2367    }
2368
2369    // Convenience, to translate from the JList directory view to the Mac-style JTable
2370    //   & minimize diffs between this and BasicFileChooserUI
2371    @SuppressWarnings("serial") // Superclass is not serializable across versions
2372    class JTableExtension extends JTable {
2373        public void setSelectedIndex(final int index) {
2374            getSelectionModel().setSelectionInterval(index, index);
2375        }
2376
2377        public void removeSelectedIndex(final int index) {
2378            getSelectionModel().removeSelectionInterval(index, index);
2379        }
2380
2381        public void ensureIndexIsVisible(final int index) {
2382            final Rectangle cellBounds = getCellRect(index, 0, false);
2383            if (cellBounds != null) {
2384                scrollRectToVisible(cellBounds);
2385            }
2386        }
2387
2388        public int locationToIndex(final Point location) {
2389            return rowAtPoint(location);
2390        }
2391    }
2392}
2393