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}