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    /**
052     * Creates a new {@code FileChooserManager}.
053     * @param open If true, "Open File" dialogs will be created. If false, "Save File" dialogs will be created.
054     * @param lastDirProperty The name of the property used to get the last directory. This directory is used to initialize the AbstractFileChooser.
055     *                        Then, if the user effectively chooses a file or a directory, this property will be updated to the directory path.
056     * @see #createFileChooser
057     */
058    public FileChooserManager(boolean open, String lastDirProperty) {
059        this(open, lastDirProperty, null);
060    }
061
062    /**
063     * Creates a new {@code FileChooserManager}.
064     * @param open If true, "Open File" dialogs will be created. If false, "Save File" dialogs will be created.
065     * @param lastDirProperty The name of the property used to get the last directory. This directory is used to initialize the AbstractFileChooser.
066     *                        Then, if the user effectively chooses a file or a directory, this property will be updated to the directory path.
067     * @param defaultDir The default directory used to initialize the AbstractFileChooser if the {@code lastDirProperty} property value is missing.
068     * @see #createFileChooser
069     */
070    public FileChooserManager(boolean open, String lastDirProperty, String defaultDir) {
071        this.open = open;
072        this.lastDirProperty = lastDirProperty == null || lastDirProperty.isEmpty() ? "lastDirectory" : lastDirProperty;
073        this.curDir = Main.pref.get(this.lastDirProperty).isEmpty() ?
074                (defaultDir == null || defaultDir.isEmpty() ? "." : defaultDir)
075                : Main.pref.get(this.lastDirProperty);
076    }
077
078    /**
079     * Replies the {@code AbstractFileChooser} that has been previously created.
080     * @return The {@code AbstractFileChooser} that has been previously created, or {@code null} if it has not been created yet.
081     * @see #createFileChooser
082     */
083    public final AbstractFileChooser getFileChooser() {
084        return fc;
085    }
086
087    /**
088     * Replies the initial directory used to construct the {@code AbstractFileChooser}.
089     * @return The initial directory used to construct the {@code AbstractFileChooser}.
090     */
091    public final String getInitialDirectory() {
092        return curDir;
093    }
094
095    /**
096     * Creates a new {@link AbstractFileChooser} with default settings. All files will be accepted.
097     * @return this
098     */
099    public final FileChooserManager createFileChooser() {
100        return doCreateFileChooser(false, null, null, null, null, JFileChooser.FILES_ONLY, false);
101    }
102
103    /**
104     * Creates a new {@link AbstractFileChooser} with given settings for a single {@code FileFilter}.
105     *
106     * @param multiple If true, makes the dialog allow multiple file selections
107     * @param title The string that goes in the dialog window's title bar
108     * @param filter The only file filter that will be proposed by the dialog
109     * @param selectionMode The selection mode that allows the user to:<br><ul>
110     *                      <li>just select files ({@code JFileChooser.FILES_ONLY})</li>
111     *                      <li>just select directories ({@code JFileChooser.DIRECTORIES_ONLY})</li>
112     *                      <li>select both files and directories ({@code JFileChooser.FILES_AND_DIRECTORIES})</li></ul>
113     * @return this
114     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, FileFilter, int, String)
115     */
116    public final FileChooserManager createFileChooser(boolean multiple, String title, FileFilter filter, int selectionMode) {
117        doCreateFileChooser(multiple, title, Collections.singleton(filter), filter, null, selectionMode, false);
118        getFileChooser().setAcceptAllFileFilterUsed(false);
119        return this;
120    }
121
122    /**
123     * Creates a new {@link AbstractFileChooser} with given settings for a collection of {@code FileFilter}s.
124     *
125     * @param multiple If true, makes the dialog allow multiple file selections
126     * @param title The string that goes in the dialog window's title bar
127     * @param filters The file filters that will be proposed by the dialog
128     * @param defaultFilter The file filter that will be selected by default
129     * @param selectionMode The selection mode that allows the user to:<br><ul>
130     *                      <li>just select files ({@code JFileChooser.FILES_ONLY})</li>
131     *                      <li>just select directories ({@code JFileChooser.DIRECTORIES_ONLY})</li>
132     *                      <li>select both files and directories ({@code JFileChooser.FILES_AND_DIRECTORIES})</li></ul>
133     * @return this
134     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, Collection, FileFilter, int, String)
135     */
136    public final FileChooserManager createFileChooser(boolean multiple, String title, Collection<? extends FileFilter> filters,
137            FileFilter defaultFilter, int selectionMode) {
138        return doCreateFileChooser(multiple, title, filters, defaultFilter, null, selectionMode, false);
139    }
140
141    /**
142     * Creates a new {@link AbstractFileChooser} with given settings for a file extension.
143     *
144     * @param multiple If true, makes the dialog allow multiple file selections
145     * @param title The string that goes in the dialog window's title bar
146     * @param extension The file extension that will be selected as the default file filter
147     * @param allTypes If true, all the files types known by JOSM will be proposed in the "file type" combobox.
148     *                 If false, only the file filters that include {@code extension} will be proposed
149     * @param selectionMode The selection mode that allows the user to:<br><ul>
150     *                      <li>just select files ({@code JFileChooser.FILES_ONLY})</li>
151     *                      <li>just select directories ({@code JFileChooser.DIRECTORIES_ONLY})</li>
152     *                      <li>select both files and directories ({@code JFileChooser.FILES_AND_DIRECTORIES})</li></ul>
153     * @return this
154     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, FileFilter, int, String)
155     */
156    public final FileChooserManager createFileChooser(boolean multiple, String title, String extension, boolean allTypes, int selectionMode) {
157        return doCreateFileChooser(multiple, title, null, null, extension, selectionMode, allTypes);
158    }
159
160    private final FileChooserManager doCreateFileChooser(boolean multiple, String title, Collection<? extends FileFilter> filters,
161            FileFilter defaultFilter, String extension, int selectionMode, boolean allTypes) {
162        File file = new File(curDir);
163        // Use native dialog is preference is set, unless an unsupported selection mode is specifically wanted
164        if (PROP_USE_NATIVE_FILE_DIALOG.get() && NativeFileChooser.supportsSelectionMode(selectionMode)) {
165            fc = new NativeFileChooser(file);
166        } else {
167            fc = new SwingFileChooser(file);
168        }
169
170        if (title != null) {
171            fc.setDialogTitle(title);
172        }
173
174        fc.setFileSelectionMode(selectionMode);
175        fc.setMultiSelectionEnabled(multiple);
176        fc.setAcceptAllFileFilterUsed(false);
177
178        if (filters != null) {
179            for (FileFilter filter : filters) {
180                fc.addChoosableFileFilter(filter);
181            }
182            if (defaultFilter != null) {
183                fc.setFileFilter(defaultFilter);
184            }
185        } else if (open) {
186            ExtensionFileFilter.applyChoosableImportFileFilters(fc, extension, allTypes);
187        } else {
188            ExtensionFileFilter.applyChoosableExportFileFilters(fc, extension, allTypes);
189        }
190        return this;
191    }
192
193    /**
194     * Opens the {@code AbstractFileChooser} that has been created. Nothing happens if it has not been created yet.
195     * @return the {@code AbstractFileChooser} if the user effectively choses a file or directory. {@code null} if the user cancelled the dialog.
196     */
197    public final AbstractFileChooser openFileChooser() {
198        return openFileChooser(null);
199    }
200
201    /**
202     * Opens the {@code AbstractFileChooser} that has been created and waits for the user to choose a file/directory, or cancel the dialog.<br>
203     * Nothing happens if the dialog has not been created yet.<br>
204     * When the user choses a file or directory, the {@code lastDirProperty} is updated to the chosen directory path.
205     *
206     * @param parent The Component used as the parent of the AbstractFileChooser. If null, uses {@code Main.parent}.
207     * @return the {@code AbstractFileChooser} if the user effectively choses a file or directory. {@code null} if the user cancelled the dialog.
208     */
209    public AbstractFileChooser openFileChooser(Component parent) {
210        if (fc != null) {
211            if (parent == null) {
212                parent = Main.parent;
213            }
214
215            int answer = open ? fc.showOpenDialog(parent) : fc.showSaveDialog(parent);
216            if (answer != JFileChooser.APPROVE_OPTION) {
217                return null;
218            }
219
220            if (!fc.getCurrentDirectory().getAbsolutePath().equals(curDir)) {
221                Main.pref.put(lastDirProperty, fc.getCurrentDirectory().getAbsolutePath());
222            }
223
224            if (!open) {
225                File file = fc.getSelectedFile();
226                if (!SaveActionBase.confirmOverwrite(file)) {
227                    return null;
228                }
229            }
230        }
231        return fc;
232    }
233}