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