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;
008import java.util.function.Predicate;
009
010import javax.swing.Action;
011import javax.swing.JFileChooser;
012import javax.swing.filechooser.FileFilter;
013
014import org.openstreetmap.josm.actions.DiskAccessAction;
015import org.openstreetmap.josm.actions.ExtensionFileFilter;
016import org.openstreetmap.josm.actions.SaveActionBase;
017import org.openstreetmap.josm.data.preferences.BooleanProperty;
018import org.openstreetmap.josm.gui.MainApplication;
019import org.openstreetmap.josm.spi.preferences.Config;
020import org.openstreetmap.josm.tools.PlatformManager;
021import org.openstreetmap.josm.tools.Utils;
022
023/**
024 * A chained utility class used to create and open {@link AbstractFileChooser} dialogs.<br>
025 * Use only this class if you need to control specifically your AbstractFileChooser dialog.<br>
026 * <p>
027 * A simpler usage is to call the {@link DiskAccessAction#createAndOpenFileChooser} methods.
028 *
029 * @since 5438 (creation)
030 * @since 7578 (rename)
031 */
032public class FileChooserManager {
033
034    /**
035     * Property to enable use of native file dialogs.
036     */
037    public static final BooleanProperty PROP_USE_NATIVE_FILE_DIALOG = new BooleanProperty("use.native.file.dialog",
038            // Native dialogs do not support file filters, so do not set them as default, except for OS X where they never worked
039            PlatformManager.isPlatformOsx());
040
041    /**
042     * Property to use the details view in file dialogs.
043     */
044    public static final BooleanProperty PROP_USE_DETAILS_VIEW_FILE_DIALOG = new BooleanProperty("use.details.view.file.dialog", false);
045
046    private final boolean open;
047    private final String lastDirProperty;
048    private final String curDir;
049
050    private boolean multiple;
051    private String title;
052    private Collection<? extends FileFilter> filters;
053    private FileFilter defaultFilter;
054    private int selectionMode = JFileChooser.FILES_ONLY;
055    private String extension;
056    private Predicate<ExtensionFileFilter> additionalTypes = ignore -> false;
057    private File file;
058
059    private AbstractFileChooser fc;
060
061    /**
062     * Creates a new {@code FileChooserManager} with default values.
063     * @see #createFileChooser
064     */
065    public FileChooserManager() {
066        this(false, null, null);
067    }
068
069    /**
070     * Creates a new {@code FileChooserManager}.
071     * @param open If true, "Open File" dialogs will be created. If false, "Save File" dialogs will be created.
072     * @see #createFileChooser
073     */
074    public FileChooserManager(boolean open) {
075        this(open, null);
076    }
077
078    // CHECKSTYLE.OFF: LineLength
079
080    /**
081     * Creates a new {@code FileChooserManager}.
082     * @param open If true, "Open File" dialogs will be created. If false, "Save File" dialogs will be created.
083     * @param lastDirProperty The name of the property used to get the last directory. This directory is used to initialize the AbstractFileChooser.
084     *                        Then, if the user effectively chooses a file or a directory, this property will be updated to the directory path.
085     * @see #createFileChooser
086     */
087    public FileChooserManager(boolean open, String lastDirProperty) {
088        this(open, lastDirProperty, null);
089    }
090
091    /**
092     * Creates a new {@code FileChooserManager}.
093     * @param open If true, "Open File" dialogs will be created. If false, "Save File" dialogs will be created.
094     * @param lastDirProperty The name of the property used to get the last directory. This directory is used to initialize the AbstractFileChooser.
095     *                        Then, if the user effectively chooses a file or a directory, this property will be updated to the directory path.
096     * @param defaultDir The default directory used to initialize the AbstractFileChooser if the {@code lastDirProperty} property value is missing.
097     * @see #createFileChooser
098     */
099    public FileChooserManager(boolean open, String lastDirProperty, String defaultDir) {
100        this.open = open;
101        this.lastDirProperty = Utils.isEmpty(lastDirProperty) ? "lastDirectory" : lastDirProperty;
102        this.curDir = Config.getPref().get(this.lastDirProperty).isEmpty() ?
103                Utils.isEmpty(defaultDir) ? "." : defaultDir
104                : Config.getPref().get(this.lastDirProperty);
105    }
106
107    // CHECKSTYLE.ON: LineLength
108
109    /**
110     * Replies the {@code AbstractFileChooser} that has been previously created.
111     * @return The {@code AbstractFileChooser} that has been previously created, or {@code null} if it has not been created yet.
112     * @see #createFileChooser
113     */
114    public final AbstractFileChooser getFileChooser() {
115        return fc;
116    }
117
118    /**
119     * Replies the initial directory used to construct the {@code AbstractFileChooser}.
120     * @return The initial directory used to construct the {@code AbstractFileChooser}.
121     */
122    public final String getInitialDirectory() {
123        return curDir;
124    }
125
126    /**
127     * Creates a new {@link AbstractFileChooser} with default settings. All files will be accepted.
128     * @return this
129     */
130    public final FileChooserManager createFileChooser() {
131        return doCreateFileChooser();
132    }
133
134    /**
135     * Creates a new {@link AbstractFileChooser} with given settings for a single {@code FileFilter}.
136     *
137     * @param multiple If true, makes the dialog allow multiple file selections
138     * @param title The string that goes in the dialog window's title bar
139     * @param filter The only file filter that will be proposed by the dialog
140     * @param selectionMode The selection mode that allows the user to:<br><ul>
141     *                      <li>just select files ({@code JFileChooser.FILES_ONLY})</li>
142     *                      <li>just select directories ({@code JFileChooser.DIRECTORIES_ONLY})</li>
143     *                      <li>select both files and directories ({@code JFileChooser.FILES_AND_DIRECTORIES})</li></ul>
144     * @return this
145     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, FileFilter, int, String)
146     */
147    public final FileChooserManager createFileChooser(boolean multiple, String title, FileFilter filter, int selectionMode) {
148        multiple(multiple);
149        title(title);
150        filters(Collections.singleton(filter));
151        defaultFilter(filter);
152        selectionMode(selectionMode);
153
154        doCreateFileChooser();
155        fc.setAcceptAllFileFilterUsed(false);
156        return this;
157    }
158
159    /**
160     * Creates a new {@link AbstractFileChooser} with given settings for a collection of {@code FileFilter}s.
161     *
162     * @param multiple If true, makes the dialog allow multiple file selections
163     * @param title The string that goes in the dialog window's title bar
164     * @param filters The file filters that will be proposed by the dialog
165     * @param defaultFilter The file filter that will be selected by default
166     * @param selectionMode The selection mode that allows the user to:<br><ul>
167     *                      <li>just select files ({@code JFileChooser.FILES_ONLY})</li>
168     *                      <li>just select directories ({@code JFileChooser.DIRECTORIES_ONLY})</li>
169     *                      <li>select both files and directories ({@code JFileChooser.FILES_AND_DIRECTORIES})</li></ul>
170     * @return this
171     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, Collection, FileFilter, int, String)
172     */
173    public final FileChooserManager createFileChooser(boolean multiple, String title, Collection<? extends FileFilter> filters,
174            FileFilter defaultFilter, int selectionMode) {
175        multiple(multiple);
176        title(title);
177        filters(filters);
178        defaultFilter(defaultFilter);
179        selectionMode(selectionMode);
180        return doCreateFileChooser();
181    }
182
183    /**
184     * Creates a new {@link AbstractFileChooser} with given settings for a file extension.
185     *
186     * @param multiple If true, makes the dialog allow multiple file selections
187     * @param title The string that goes in the dialog window's title bar
188     * @param extension The file extension that will be selected as the default file filter
189     * @param allTypes If true, all the files types known by JOSM will be proposed in the "file type" combobox.
190     *                 If false, only the file filters that include {@code extension} will be proposed
191     * @param selectionMode The selection mode that allows the user to:<br><ul>
192     *                      <li>just select files ({@code JFileChooser.FILES_ONLY})</li>
193     *                      <li>just select directories ({@code JFileChooser.DIRECTORIES_ONLY})</li>
194     *                      <li>select both files and directories ({@code JFileChooser.FILES_AND_DIRECTORIES})</li></ul>
195     * @return this
196     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, FileFilter, int, String)
197     */
198    public final FileChooserManager createFileChooser(boolean multiple, String title, String extension, boolean allTypes, int selectionMode) {
199        multiple(multiple);
200        title(title);
201        extension(extension);
202        allTypes(allTypes);
203        selectionMode(selectionMode);
204        return doCreateFileChooser();
205    }
206
207    /**
208     * Builder method to set {@code multiple} property.
209     * @param value If true, makes the dialog allow multiple file selections
210     * @return this
211     */
212    public FileChooserManager multiple(boolean value) {
213        multiple = value;
214        return this;
215    }
216
217    /**
218     * Builder method to set {@code title} property.
219     * @param value The string that goes in the dialog window's title bar
220     * @return this
221     */
222    public FileChooserManager title(String value) {
223        title = value;
224        return this;
225    }
226
227    /**
228     * Builder method to set {@code filters} property.
229     * @param value The file filters that will be proposed by the dialog
230     * @return this
231     */
232    public FileChooserManager filters(Collection<? extends FileFilter> value) {
233        filters = value;
234        return this;
235    }
236
237    /**
238     * Builder method to set {@code defaultFilter} property.
239     * @param value The file filter that will be selected by default
240     * @return this
241     */
242    public FileChooserManager defaultFilter(FileFilter value) {
243        defaultFilter = value;
244        return this;
245    }
246
247    /**
248     * Builder method to set {@code selectionMode} property.
249     * @param value The selection mode that allows the user to:<br><ul>
250     *                      <li>just select files ({@code JFileChooser.FILES_ONLY})</li>
251     *                      <li>just select directories ({@code JFileChooser.DIRECTORIES_ONLY})</li>
252     *                      <li>select both files and directories ({@code JFileChooser.FILES_AND_DIRECTORIES})</li></ul>
253     * @return this
254     */
255    public FileChooserManager selectionMode(int value) {
256        selectionMode = value;
257        return this;
258    }
259
260    /**
261     * Builder method to set {@code extension} property.
262     * @param value The file extension that will be selected as the default file filter
263     * @return this
264     */
265    public FileChooserManager extension(String value) {
266        extension = value;
267        return this;
268    }
269
270    /**
271     * Builder method to set {@code additionalTypes} property.
272     * @param value matching types will additionally be added to the "file type" combobox.
273     * @return this
274     */
275    public FileChooserManager additionalTypes(Predicate<ExtensionFileFilter> value) {
276        additionalTypes = value;
277        return this;
278    }
279
280    /**
281     * Builder method to set {@code allTypes} property.
282     * @param value If true, all the files types known by JOSM will be proposed in the "file type" combobox.
283     *              If false, only the file filters that include {@code extension} will be proposed
284     * @return this
285     */
286    public FileChooserManager allTypes(boolean value) {
287        additionalTypes = ignore -> value;
288        return this;
289    }
290
291    /**
292     * Builder method to set {@code file} property.
293     * @param value {@link File} object with default filename
294     * @return this
295     */
296    public FileChooserManager file(File value) {
297        file = value;
298        return this;
299    }
300
301    /**
302     * Builds {@code FileChooserManager} object using properties set by builder methods or default values.
303     * @return this
304     */
305    public FileChooserManager doCreateFileChooser() {
306        File f = new File(curDir);
307        // Use native dialog is preference is set, unless an unsupported selection mode is specifically wanted
308        if (PROP_USE_NATIVE_FILE_DIALOG.get() && NativeFileChooser.supportsSelectionMode(selectionMode)) {
309            fc = new NativeFileChooser(f);
310        } else {
311            fc = new SwingFileChooser(f);
312            if (PROP_USE_DETAILS_VIEW_FILE_DIALOG.get()) {
313                // See sun.swing.FilePane.ACTION_VIEW_DETAILS
314                Action details = fc.getActionMap().get("viewTypeDetails");
315                if (details != null) {
316                    details.actionPerformed(null);
317                }
318            }
319        }
320
321        if (title != null) {
322            fc.setDialogTitle(title);
323        }
324
325        fc.setFileSelectionMode(selectionMode);
326        fc.setMultiSelectionEnabled(multiple);
327        fc.setAcceptAllFileFilterUsed(false);
328        fc.setSelectedFile(this.file);
329
330        if (filters != null) {
331            for (FileFilter filter : filters) {
332                fc.addChoosableFileFilter(filter);
333            }
334            if (defaultFilter != null) {
335                fc.setFileFilter(defaultFilter);
336            }
337        } else if (open) {
338            ExtensionFileFilter.applyChoosableImportFileFilters(fc, extension, additionalTypes);
339        } else {
340            ExtensionFileFilter.applyChoosableExportFileFilters(fc, extension, additionalTypes);
341        }
342        return this;
343    }
344
345    /**
346     * Opens the {@code AbstractFileChooser} that has been created.
347     * @return the {@code AbstractFileChooser} if the user effectively chooses a file or directory.
348     * {@code null} if the user cancelled the dialog.
349     */
350    public final AbstractFileChooser openFileChooser() {
351        return openFileChooser(null);
352    }
353
354    /**
355     * Opens the {@code AbstractFileChooser} that has been created and waits for the user to choose a file/directory,
356     * or cancel the dialog.<br>
357     * When the user chooses a file or directory, the {@code lastDirProperty} is updated to the chosen directory path.
358     *
359     * @param parent The Component used as the parent of the AbstractFileChooser. If null,
360     *               uses {@code MainApplication.getMainFrame()}.
361     * @return the {@code AbstractFileChooser} if the user effectively chooses
362     * a file or directory.{@code null} if the user cancelled the dialog.
363     */
364    public AbstractFileChooser openFileChooser(Component parent) {
365        if (fc == null)
366            doCreateFileChooser();
367
368        if (parent == null) {
369            parent = MainApplication.getMainFrame();
370        }
371
372        int answer = open ? fc.showOpenDialog(parent) : fc.showSaveDialog(parent);
373        if (answer != JFileChooser.APPROVE_OPTION) {
374            return null;
375        }
376
377        if (!fc.getCurrentDirectory().getAbsolutePath().equals(curDir)) {
378            Config.getPref().put(lastDirProperty, fc.getCurrentDirectory().getAbsolutePath());
379        }
380
381        if (!open && !FileChooserManager.PROP_USE_NATIVE_FILE_DIALOG.get() &&
382            !SaveActionBase.confirmOverwrite(fc.getSelectedFile())) {
383            return null;
384        }
385        return fc;
386    }
387
388    /**
389     * Opens the file chooser dialog, then checks if filename has the given extension.
390     * If not, adds the extension and asks for overwrite if filename exists.
391     *
392     * @return the {@code File} or {@code null} if the user cancelled the dialog.
393     */
394    public File getFileForSave() {
395        return SaveActionBase.checkFileAndConfirmOverWrite(openFileChooser(), extension);
396    }
397}