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 boolean multiple;
041    private String title;
042    private Collection<? extends FileFilter> filters;
043    private FileFilter defaultFilter;
044    private int selectionMode = JFileChooser.FILES_ONLY;
045    private String extension;
046    private boolean allTypes;
047    private File file;
048
049    private AbstractFileChooser fc;
050
051    /**
052     * Creates a new {@code FileChooserManager} with default values.
053     * @see #createFileChooser
054     */
055    public FileChooserManager() {
056        this(false, null, null);
057    }
058
059    /**
060     * Creates a new {@code FileChooserManager}.
061     * @param open If true, "Open File" dialogs will be created. If false, "Save File" dialogs will be created.
062     * @see #createFileChooser
063     */
064    public FileChooserManager(boolean open) {
065        this(open, null);
066    }
067
068    // CHECKSTYLE.OFF: LineLength
069
070    /**
071     * Creates a new {@code FileChooserManager}.
072     * @param open If true, "Open File" dialogs will be created. If false, "Save File" dialogs will be created.
073     * @param lastDirProperty The name of the property used to get the last directory. This directory is used to initialize the AbstractFileChooser.
074     *                        Then, if the user effectively chooses a file or a directory, this property will be updated to the directory path.
075     * @see #createFileChooser
076     */
077    public FileChooserManager(boolean open, String lastDirProperty) {
078        this(open, lastDirProperty, null);
079    }
080
081    /**
082     * Creates a new {@code FileChooserManager}.
083     * @param open If true, "Open File" dialogs will be created. If false, "Save File" dialogs will be created.
084     * @param lastDirProperty The name of the property used to get the last directory. This directory is used to initialize the AbstractFileChooser.
085     *                        Then, if the user effectively chooses a file or a directory, this property will be updated to the directory path.
086     * @param defaultDir The default directory used to initialize the AbstractFileChooser if the {@code lastDirProperty} property value is missing.
087     * @see #createFileChooser
088     */
089    public FileChooserManager(boolean open, String lastDirProperty, String defaultDir) {
090        this.open = open;
091        this.lastDirProperty = lastDirProperty == null || lastDirProperty.isEmpty() ? "lastDirectory" : lastDirProperty;
092        this.curDir = Main.pref.get(this.lastDirProperty).isEmpty() ?
093                defaultDir == null || defaultDir.isEmpty() ? "." : defaultDir
094                : Main.pref.get(this.lastDirProperty);
095    }
096
097    // CHECKSTYLE.ON: LineLength
098
099    /**
100     * Replies the {@code AbstractFileChooser} that has been previously created.
101     * @return The {@code AbstractFileChooser} that has been previously created, or {@code null} if it has not been created yet.
102     * @see #createFileChooser
103     */
104    public final AbstractFileChooser getFileChooser() {
105        return fc;
106    }
107
108    /**
109     * Replies the initial directory used to construct the {@code AbstractFileChooser}.
110     * @return The initial directory used to construct the {@code AbstractFileChooser}.
111     */
112    public final String getInitialDirectory() {
113        return curDir;
114    }
115
116    /**
117     * Creates a new {@link AbstractFileChooser} with default settings. All files will be accepted.
118     * @return this
119     */
120    public final FileChooserManager createFileChooser() {
121        return doCreateFileChooser();
122    }
123
124    /**
125     * Creates a new {@link AbstractFileChooser} with given settings for a single {@code FileFilter}.
126     *
127     * @param multiple If true, makes the dialog allow multiple file selections
128     * @param title The string that goes in the dialog window's title bar
129     * @param filter The only file filter that will be proposed by the dialog
130     * @param selectionMode The selection mode that allows the user to:<br><ul>
131     *                      <li>just select files ({@code JFileChooser.FILES_ONLY})</li>
132     *                      <li>just select directories ({@code JFileChooser.DIRECTORIES_ONLY})</li>
133     *                      <li>select both files and directories ({@code JFileChooser.FILES_AND_DIRECTORIES})</li></ul>
134     * @return this
135     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, FileFilter, int, String)
136     */
137    public final FileChooserManager createFileChooser(boolean multiple, String title, FileFilter filter, int selectionMode) {
138        multiple(multiple);
139        title(title);
140        filters(Collections.singleton(filter));
141        defaultFilter(filter);
142        selectionMode(selectionMode);
143
144        doCreateFileChooser();
145        fc.setAcceptAllFileFilterUsed(false);
146        return this;
147    }
148
149    /**
150     * Creates a new {@link AbstractFileChooser} with given settings for a collection of {@code FileFilter}s.
151     *
152     * @param multiple If true, makes the dialog allow multiple file selections
153     * @param title The string that goes in the dialog window's title bar
154     * @param filters The file filters that will be proposed by the dialog
155     * @param defaultFilter The file filter that will be selected by default
156     * @param selectionMode The selection mode that allows the user to:<br><ul>
157     *                      <li>just select files ({@code JFileChooser.FILES_ONLY})</li>
158     *                      <li>just select directories ({@code JFileChooser.DIRECTORIES_ONLY})</li>
159     *                      <li>select both files and directories ({@code JFileChooser.FILES_AND_DIRECTORIES})</li></ul>
160     * @return this
161     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, Collection, FileFilter, int, String)
162     */
163    public final FileChooserManager createFileChooser(boolean multiple, String title, Collection<? extends FileFilter> filters,
164            FileFilter defaultFilter, int selectionMode) {
165        multiple(multiple);
166        title(title);
167        filters(filters);
168        defaultFilter(defaultFilter);
169        selectionMode(selectionMode);
170        return doCreateFileChooser();
171    }
172
173    /**
174     * Creates a new {@link AbstractFileChooser} with given settings for a file extension.
175     *
176     * @param multiple If true, makes the dialog allow multiple file selections
177     * @param title The string that goes in the dialog window's title bar
178     * @param extension The file extension that will be selected as the default file filter
179     * @param allTypes If true, all the files types known by JOSM will be proposed in the "file type" combobox.
180     *                 If false, only the file filters that include {@code extension} will be proposed
181     * @param selectionMode The selection mode that allows the user to:<br><ul>
182     *                      <li>just select files ({@code JFileChooser.FILES_ONLY})</li>
183     *                      <li>just select directories ({@code JFileChooser.DIRECTORIES_ONLY})</li>
184     *                      <li>select both files and directories ({@code JFileChooser.FILES_AND_DIRECTORIES})</li></ul>
185     * @return this
186     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, FileFilter, int, String)
187     */
188    public final FileChooserManager createFileChooser(boolean multiple, String title, String extension, boolean allTypes, int selectionMode) {
189        multiple(multiple);
190        title(title);
191        extension(extension);
192        allTypes(allTypes);
193        selectionMode(selectionMode);
194        return doCreateFileChooser();
195    }
196
197    /**
198     * Builder method to set {@code multiple} property.
199     * @param value If true, makes the dialog allow multiple file selections
200     * @return this
201     */
202    public FileChooserManager multiple(boolean value) {
203        multiple = value;
204        return this;
205    }
206
207    /**
208     * Builder method to set {@code title} property.
209     * @param value The string that goes in the dialog window's title bar
210     * @return this
211     */
212     public FileChooserManager title(String value) {
213        title = value;
214        return this;
215    }
216
217    /**
218     * Builder method to set {@code filters} property.
219     * @param value The file filters that will be proposed by the dialog
220     * @return this
221     */
222    public FileChooserManager filters(Collection<? extends FileFilter> value) {
223        filters = value;
224        return this;
225    }
226
227    /**
228     * Builder method to set {@code defaultFilter} property.
229     * @param value The file filter that will be selected by default
230     * @return this
231     */
232    public FileChooserManager defaultFilter(FileFilter value) {
233        defaultFilter = value;
234        return this;
235    }
236
237    /**
238     * Builder method to set {@code selectionMode} property.
239     * @param value The selection mode that allows the user to:<br><ul>
240     *                      <li>just select files ({@code JFileChooser.FILES_ONLY})</li>
241     *                      <li>just select directories ({@code JFileChooser.DIRECTORIES_ONLY})</li>
242     *                      <li>select both files and directories ({@code JFileChooser.FILES_AND_DIRECTORIES})</li></ul>
243     * @return this
244     */
245    public FileChooserManager selectionMode(int value) {
246        selectionMode = value;
247        return this;
248    }
249
250    /**
251     * Builder method to set {@code extension} property.
252     * @param value The file extension that will be selected as the default file filter
253     * @return this
254     */
255    public FileChooserManager extension(String value) {
256        extension = value;
257        return this;
258    }
259
260    /**
261     * Builder method to set {@code allTypes} property.
262     * @param value If true, all the files types known by JOSM will be proposed in the "file type" combobox.
263     *              If false, only the file filters that include {@code extension} will be proposed
264     * @return this
265     */
266    public FileChooserManager allTypes(boolean value) {
267        allTypes = value;
268        return this;
269    }
270
271    /**
272     * Builder method to set {@code file} property.
273     * @param value {@link File} object with default filename
274     * @return this
275     */
276    public FileChooserManager file(File value) {
277        file = value;
278        return this;
279    }
280
281    /**
282     * Builds {@code FileChooserManager} object using properties set by builder methods or default values.
283     * @return this
284     */
285    public FileChooserManager doCreateFileChooser() {
286        File f = new File(curDir);
287        // Use native dialog is preference is set, unless an unsupported selection mode is specifically wanted
288        if (PROP_USE_NATIVE_FILE_DIALOG.get() && NativeFileChooser.supportsSelectionMode(selectionMode)) {
289            fc = new NativeFileChooser(f);
290        } else {
291            fc = new SwingFileChooser(f);
292        }
293
294        if (title != null) {
295            fc.setDialogTitle(title);
296        }
297
298        fc.setFileSelectionMode(selectionMode);
299        fc.setMultiSelectionEnabled(multiple);
300        fc.setAcceptAllFileFilterUsed(false);
301        fc.setSelectedFile(this.file);
302
303        if (filters != null) {
304            for (FileFilter filter : filters) {
305                fc.addChoosableFileFilter(filter);
306            }
307            if (defaultFilter != null) {
308                fc.setFileFilter(defaultFilter);
309            }
310        } else if (open) {
311            ExtensionFileFilter.applyChoosableImportFileFilters(fc, extension, allTypes);
312        } else {
313            ExtensionFileFilter.applyChoosableExportFileFilters(fc, extension, allTypes);
314        }
315        return this;
316    }
317
318    /**
319     * Opens the {@code AbstractFileChooser} that has been created.
320     * @return the {@code AbstractFileChooser} if the user effectively choses a file or directory. {@code null} if the user cancelled the dialog.
321     */
322    public final AbstractFileChooser openFileChooser() {
323        return openFileChooser(null);
324    }
325
326    /**
327     * Opens the {@code AbstractFileChooser} that has been created and waits for the user to choose a file/directory, or cancel the dialog.<br>
328     * When the user choses a file or directory, the {@code lastDirProperty} is updated to the chosen directory path.
329     *
330     * @param parent The Component used as the parent of the AbstractFileChooser. If null, uses {@code Main.parent}.
331     * @return the {@code AbstractFileChooser} if the user effectively choses a file or directory. {@code null} if the user cancelled the dialog.
332     */
333    public AbstractFileChooser openFileChooser(Component parent) {
334        if (fc == null)
335            doCreateFileChooser();
336
337        if (parent == null) {
338            parent = Main.parent;
339        }
340
341        int answer = open ? fc.showOpenDialog(parent) : fc.showSaveDialog(parent);
342        if (answer != JFileChooser.APPROVE_OPTION) {
343            return null;
344        }
345
346        if (!fc.getCurrentDirectory().getAbsolutePath().equals(curDir)) {
347            Main.pref.put(lastDirProperty, fc.getCurrentDirectory().getAbsolutePath());
348        }
349
350        if (!open && !FileChooserManager.PROP_USE_NATIVE_FILE_DIALOG.get() &&
351            !SaveActionBase.confirmOverwrite(fc.getSelectedFile())) {
352            return null;
353        }
354        return fc;
355    }
356
357    /**
358     * Opens the file chooser dialog, then checks if filename has the given extension.
359     * If not, adds the extension and asks for overwrite if filename exists.
360     *
361     * @return the {@code File} or {@code null} if the user cancelled the dialog.
362     */
363    public File getFileForSave() {
364        return SaveActionBase.checkFileAndConfirmOverWrite(openFileChooser(), extension);
365    }
366}