001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.widgets;
003
004import java.awt.Component;
005import java.io.File;
006import java.util.Collection;
007import java.util.Collections;
008
009import javax.swing.JFileChooser;
010import javax.swing.filechooser.FileFilter;
011
012import org.openstreetmap.josm.Main;
013import org.openstreetmap.josm.actions.DiskAccessAction;
014import org.openstreetmap.josm.actions.ExtensionFileFilter;
015import org.openstreetmap.josm.actions.SaveActionBase;
016import org.openstreetmap.josm.data.preferences.BooleanProperty;
017
018/**
019 * A chained utility class used to create and open {@link AbstractFileChooser} dialogs.<br>
020 * Use only this class if you need to control specifically your AbstractFileChooser dialog.<br>
021 * <p>
022 * A simpler usage is to call the {@link DiskAccessAction#createAndOpenFileChooser} methods.
023 *
024 * @since 5438 (creation)
025 * @since 7578 (rename)
026 */
027public class FileChooserManager {
028
029    /**
030     * Property to enable use of native file dialogs.
031     */
032    public static final BooleanProperty PROP_USE_NATIVE_FILE_DIALOG = new BooleanProperty("use.native.file.dialog",
033            // Native dialogs do not support file filters, so do not set them as default, except for OS X where they never worked
034            Main.isPlatformOsx());
035
036    private final boolean open;
037    private final String lastDirProperty;
038    private final String curDir;
039
040    private AbstractFileChooser fc;
041
042    /**
043     * Creates a new {@code FileChooserManager}.
044     * @param open If true, "Open File" dialogs will be created. If false, "Save File" dialogs will be created.
045     * @see #createFileChooser
046     */
047    public FileChooserManager(boolean open) {
048        this(open, null);
049    }
050
051    // CHECKSTYLE.OFF: LineLength
052
053    /**
054     * Creates a new {@code FileChooserManager}.
055     * @param open If true, "Open File" dialogs will be created. If false, "Save File" dialogs will be created.
056     * @param lastDirProperty The name of the property used to get the last directory. This directory is used to initialize the AbstractFileChooser.
057     *                        Then, if the user effectively chooses a file or a directory, this property will be updated to the directory path.
058     * @see #createFileChooser
059     */
060    public FileChooserManager(boolean open, String lastDirProperty) {
061        this(open, lastDirProperty, null);
062    }
063
064    /**
065     * Creates a new {@code FileChooserManager}.
066     * @param open If true, "Open File" dialogs will be created. If false, "Save File" dialogs will be created.
067     * @param lastDirProperty The name of the property used to get the last directory. This directory is used to initialize the AbstractFileChooser.
068     *                        Then, if the user effectively chooses a file or a directory, this property will be updated to the directory path.
069     * @param defaultDir The default directory used to initialize the AbstractFileChooser if the {@code lastDirProperty} property value is missing.
070     * @see #createFileChooser
071     */
072    public FileChooserManager(boolean open, String lastDirProperty, String defaultDir) {
073        this.open = open;
074        this.lastDirProperty = lastDirProperty == null || lastDirProperty.isEmpty() ? "lastDirectory" : lastDirProperty;
075        this.curDir = Main.pref.get(this.lastDirProperty).isEmpty() ?
076                defaultDir == null || defaultDir.isEmpty() ? "." : defaultDir
077                : Main.pref.get(this.lastDirProperty);
078    }
079
080    // CHECKSTYLE.ON: LineLength
081
082    /**
083     * Replies the {@code AbstractFileChooser} that has been previously created.
084     * @return The {@code AbstractFileChooser} that has been previously created, or {@code null} if it has not been created yet.
085     * @see #createFileChooser
086     */
087    public final AbstractFileChooser getFileChooser() {
088        return fc;
089    }
090
091    /**
092     * Replies the initial directory used to construct the {@code AbstractFileChooser}.
093     * @return The initial directory used to construct the {@code AbstractFileChooser}.
094     */
095    public final String getInitialDirectory() {
096        return curDir;
097    }
098
099    /**
100     * Creates a new {@link AbstractFileChooser} with default settings. All files will be accepted.
101     * @return this
102     */
103    public final FileChooserManager createFileChooser() {
104        return doCreateFileChooser(false, null, null, null, null, JFileChooser.FILES_ONLY, false);
105    }
106
107    /**
108     * Creates a new {@link AbstractFileChooser} with given settings for a single {@code FileFilter}.
109     *
110     * @param multiple If true, makes the dialog allow multiple file selections
111     * @param title The string that goes in the dialog window's title bar
112     * @param filter The only file filter that will be proposed by the dialog
113     * @param selectionMode The selection mode that allows the user to:<br><ul>
114     *                      <li>just select files ({@code JFileChooser.FILES_ONLY})</li>
115     *                      <li>just select directories ({@code JFileChooser.DIRECTORIES_ONLY})</li>
116     *                      <li>select both files and directories ({@code JFileChooser.FILES_AND_DIRECTORIES})</li></ul>
117     * @return this
118     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, FileFilter, int, String)
119     */
120    public final FileChooserManager createFileChooser(boolean multiple, String title, FileFilter filter, int selectionMode) {
121        doCreateFileChooser(multiple, title, Collections.singleton(filter), filter, null, selectionMode, false);
122        getFileChooser().setAcceptAllFileFilterUsed(false);
123        return this;
124    }
125
126    /**
127     * Creates a new {@link AbstractFileChooser} with given settings for a collection of {@code FileFilter}s.
128     *
129     * @param multiple If true, makes the dialog allow multiple file selections
130     * @param title The string that goes in the dialog window's title bar
131     * @param filters The file filters that will be proposed by the dialog
132     * @param defaultFilter The file filter that will be selected by default
133     * @param selectionMode The selection mode that allows the user to:<br><ul>
134     *                      <li>just select files ({@code JFileChooser.FILES_ONLY})</li>
135     *                      <li>just select directories ({@code JFileChooser.DIRECTORIES_ONLY})</li>
136     *                      <li>select both files and directories ({@code JFileChooser.FILES_AND_DIRECTORIES})</li></ul>
137     * @return this
138     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, Collection, FileFilter, int, String)
139     */
140    public final FileChooserManager createFileChooser(boolean multiple, String title, Collection<? extends FileFilter> filters,
141            FileFilter defaultFilter, int selectionMode) {
142        return doCreateFileChooser(multiple, title, filters, defaultFilter, null, selectionMode, false);
143    }
144
145    /**
146     * Creates a new {@link AbstractFileChooser} with given settings for a file extension.
147     *
148     * @param multiple If true, makes the dialog allow multiple file selections
149     * @param title The string that goes in the dialog window's title bar
150     * @param extension The file extension that will be selected as the default file filter
151     * @param allTypes If true, all the files types known by JOSM will be proposed in the "file type" combobox.
152     *                 If false, only the file filters that include {@code extension} will be proposed
153     * @param selectionMode The selection mode that allows the user to:<br><ul>
154     *                      <li>just select files ({@code JFileChooser.FILES_ONLY})</li>
155     *                      <li>just select directories ({@code JFileChooser.DIRECTORIES_ONLY})</li>
156     *                      <li>select both files and directories ({@code JFileChooser.FILES_AND_DIRECTORIES})</li></ul>
157     * @return this
158     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, FileFilter, int, String)
159     */
160    public final FileChooserManager createFileChooser(boolean multiple, String title, String extension, boolean allTypes, int selectionMode) {
161        return doCreateFileChooser(multiple, title, null, null, extension, selectionMode, allTypes);
162    }
163
164    private FileChooserManager doCreateFileChooser(boolean multiple, String title, Collection<? extends FileFilter> filters,
165            FileFilter defaultFilter, String extension, int selectionMode, boolean allTypes) {
166        File file = new File(curDir);
167        // Use native dialog is preference is set, unless an unsupported selection mode is specifically wanted
168        if (PROP_USE_NATIVE_FILE_DIALOG.get() && NativeFileChooser.supportsSelectionMode(selectionMode)) {
169            fc = new NativeFileChooser(file);
170        } else {
171            fc = new SwingFileChooser(file);
172        }
173
174        if (title != null) {
175            fc.setDialogTitle(title);
176        }
177
178        fc.setFileSelectionMode(selectionMode);
179        fc.setMultiSelectionEnabled(multiple);
180        fc.setAcceptAllFileFilterUsed(false);
181
182        if (filters != null) {
183            for (FileFilter filter : filters) {
184                fc.addChoosableFileFilter(filter);
185            }
186            if (defaultFilter != null) {
187                fc.setFileFilter(defaultFilter);
188            }
189        } else if (open) {
190            ExtensionFileFilter.applyChoosableImportFileFilters(fc, extension, allTypes);
191        } else {
192            ExtensionFileFilter.applyChoosableExportFileFilters(fc, extension, allTypes);
193        }
194        return this;
195    }
196
197    /**
198     * Opens the {@code AbstractFileChooser} that has been created. Nothing happens if it has not been created yet.
199     * @return the {@code AbstractFileChooser} if the user effectively choses a file or directory. {@code null} if the user cancelled the dialog.
200     */
201    public final AbstractFileChooser openFileChooser() {
202        return openFileChooser(null);
203    }
204
205    /**
206     * Opens the {@code AbstractFileChooser} that has been created and waits for the user to choose a file/directory, or cancel the dialog.<br>
207     * Nothing happens if the dialog has not been created yet.<br>
208     * When the user choses a file or directory, the {@code lastDirProperty} is updated to the chosen directory path.
209     *
210     * @param parent The Component used as the parent of the AbstractFileChooser. If null, uses {@code Main.parent}.
211     * @return the {@code AbstractFileChooser} if the user effectively choses a file or directory. {@code null} if the user cancelled the dialog.
212     */
213    public AbstractFileChooser openFileChooser(Component parent) {
214        if (fc != null) {
215            if (parent == null) {
216                parent = Main.parent;
217            }
218
219            int answer = open ? fc.showOpenDialog(parent) : fc.showSaveDialog(parent);
220            if (answer != JFileChooser.APPROVE_OPTION) {
221                return null;
222            }
223
224            if (!fc.getCurrentDirectory().getAbsolutePath().equals(curDir)) {
225                Main.pref.put(lastDirProperty, fc.getCurrentDirectory().getAbsolutePath());
226            }
227
228            if (!open) {
229                File file = fc.getSelectedFile();
230                if (!SaveActionBase.confirmOverwrite(file)) {
231                    return null;
232                }
233            }
234        }
235        return fc;
236    }
237}