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