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}