package org.limewire.ui.swing.util;
import java.awt.Component;
import java.awt.FileDialog;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
import javax.swing.filechooser.FileFilter;
import org.limewire.i18n.I18nMarker;
import org.limewire.ui.swing.components.FocusJOptionPane;
import org.limewire.ui.swing.components.LimeJFrame;
import org.limewire.ui.swing.settings.SwingUiSettings;
import org.limewire.util.CommonUtils;
import org.limewire.util.OSUtils;
/**
* This is a utility class that displays a file chooser dialog to the user.
*/
public final class FileChooser {
private FileChooser() {}
/**
* Returns the last directory that was used in a FileChooser.
* <p>
* If no last directory can be found, the users home directory is returned.
* If that cannot be found the current directory is returned.
*/
public static File getLastInputDirectory() {
File dir = SwingUiSettings.LAST_FILECHOOSER_DIRECTORY.get();
if(dir == null || dir.getPath().equals("") || !dir.exists() || !dir.isDirectory()) {
return getDefaultLastFileChooserDir();
}
else {
return dir;
}
}
/**
* Returns the default directory for the file chooser.
* Defaults to the users home directory if it exists,
* otherwise the current directory is used.
* <p>
* Logic is currently duplicated from ApplicationSettings getDefaultLastFileChooserDir.
*/
private static File getDefaultLastFileChooserDir() {
File defaultDirectory = CommonUtils.getUserHomeDir();
if(defaultDirectory == null || !defaultDirectory.exists()) {
defaultDirectory = CommonUtils.getCurrentDirectory();
}
return defaultDirectory;
}
public static File getInputDirectory(Component parent) {
return getInputDirectory(parent,
I18nMarker.marktr("Select Folder"),
I18nMarker.marktr("Select"),
null,
null);
}
public static File getInputDirectory(Component parent, FileFilter filter) {
return getInputDirectory(parent,
I18nMarker.marktr("Select Folder"),
I18nMarker.marktr("Select"),
null,
filter);
}
/**
* Same as <tt>getInputFile</tt> that takes no arguments, except this
* allows the caller to specify the parent component of the chooser as well
* as other options.
*
* @param parent the <tt>Component</tt> that should be the dialog's parent
* @param directory the directory to open the dialog to
*
* @return the selected <tt>File</tt> instance, or <tt>null</tt> if a
* file was not selected correctly
*/
public static File getInputDirectory(Component parent,
File directory) {
return getInputDirectory(parent,
I18nMarker.marktr("Select Folder"),
I18nMarker.marktr("Select"),
directory);
}
/**
* Same as <tt>getInputFile</tt> that takes no arguments, except this
* allows the caller to specify the parent component of the chooser as well
* as other options.
*
* @param parent the <tt>Component</tt> that should be the dialog's parent
* @param titleKey the key for the locale-specific string to use for the
* file dialog title
* @param approveKey the key for the locale-specific string to use for the
* approve button text
* @param directory the directory to open the dialog to
*
* @return the selected <tt>File</tt> instance, or <tt>null</tt> if a
* file was not selected correctly
*/
public static File getInputDirectory(Component parent, String titleKey,
String approveKey, File directory) {
return getInputDirectory(parent,
titleKey,
approveKey,
directory,
null);
}
/**
* Same as <tt>getInputFile</tt> that takes no arguments, except this
* allows the caller to specify the parent component of the chooser as well
* as other options.
*
* @param parent the <tt>Component</tt> that should be the dialog's parent
* @param titleKey the key for the locale-specific string to use for the
* file dialog title
* @param approveKey the key for the locale-specific string to use for the
* approve button text
* @param directory the directory to open the dialog to
* @param filter the <tt>FileFilter</tt> instance for customizing the
* files that are displayed -- if this is null, no filter is used
*
* @return the selected <tt>File</tt> instance, or <tt>null</tt> if a
* file was not selected correctly
*/
public static File getInputDirectory(Component parent, String titleKey,
String approveKey, File directory, FileFilter filter) {
List<File> dirs = getInput(parent, titleKey, approveKey, directory,
JFileChooser.DIRECTORIES_ONLY, JFileChooser.APPROVE_OPTION,
false, filter);
assert (dirs == null || dirs.size() <= 1)
: "selected more than one folder: " + dirs;
if (dirs != null && dirs.size() == 1) {
return dirs.get(0);
} else {
return null;
}
}
/**
* Same as <tt>getInputFile</tt> that takes no arguments, except this
* allows the caller to specify the parent component of the chooser.
*
* @param parent the <tt>Component</tt> that should be the dialog's parent
* @param titleKey the key for the locale-specific string to use for the
* file dialog title
* @param directory the directory to open the dialog to
* @param filter the <tt>FileFilter</tt> instance for customizing the
* files that are displayed -- if this is null, no filter is used
*
* @return the selected <tt>File</tt> instance, or <tt>null</tt> if a
* file was not selected correctly
*/
public static File getInputFile(Component parent, String titleKey, String approveKey,
File directory, FileFilter filter) {
List<File> files = getInput(parent, titleKey, approveKey, directory,
JFileChooser.FILES_ONLY, JFileChooser.APPROVE_OPTION, false,
filter);
assert (files == null || files.size() <= 1)
: "selected more than one directory: " + files;
if (files != null && files.size() == 1) {
return files.get(0);
} else {
return null;
}
}
/**
* The implementation that the other methods delegate to. This provides the
* caller with all available options for customizing the
* <tt>JFileChooser</tt> instance. If a <tt>FileDialog</tt> is displayed
* instead of a <tt>JFileChooser</tt> (on OS X, for example), most or all
* of these options have no effect.
*
* @param parent the <tt>Component</tt> that should be the dialog's parent
* @param titleKey the key for the locale-specific string to use for the
* file dialog title
* @param approveKey the key for the locale-specific string to use for the
* approve button text
* @param directory the directory to open the dialog to
* @param mode the "mode" to open the <tt>JFileChooser</tt> in from the
* <tt>JFileChooser</tt> class, such as
* <tt>JFileChooser.DIRECTORIES_ONLY</tt>
* @param option the option to look for in the return code, such as
* <tt>JFileChooser.APPROVE_OPTION</tt>
* @param allowMultiSelect true if the chooser allows multiple files to be
* chosen
* @param filter the <tt>FileFilter</tt> instance for customizing the
* files that are displayed -- if this is null, no filter is used
*
* @return the selected <tt>File</tt> instance, or <tt>null</tt> if a
* file was not selected correctly
*/
public static List<File> getInput(Component parent, String titleKey,
String approveKey,
File directory,
int mode,
int option,
boolean allowMultiSelect,
final FileFilter filter) {
if(!OSUtils.isMacOSX()) {
JFileChooser fileChooser = getDirectoryChooser(titleKey, approveKey, directory, mode, filter, false);
fileChooser.setMultiSelectionEnabled(allowMultiSelect);
boolean dispose = false;
if(parent == null) {
dispose = true;
parent = FocusJOptionPane.createFocusComponent();
}
try {
if(fileChooser.showOpenDialog(parent) != option)
return null;
} catch(NullPointerException npe) {
// ignore NPE. can't do anything with it ...
return null;
} finally {
if(dispose)
((JFrame)parent).dispose();
}
if(allowMultiSelect) {
File[] chosen = fileChooser.getSelectedFiles();
if(chosen.length > 0)
setLastInputDirectory(chosen[0]);
return Arrays.asList(chosen);
} else {
File chosen = fileChooser.getSelectedFile();
setLastInputDirectory(chosen);
return Collections.singletonList(chosen);
}
} else {
FileDialog dialog;
if(mode == JFileChooser.DIRECTORIES_ONLY) {
dialog = MacUtils.getFolderDialog(null);
}
else
dialog = new FileDialog(new LimeJFrame(), "");
dialog.setTitle(I18n.tr(titleKey));
if(filter != null) {
FilenameFilter f = new FilenameFilter() {
public boolean accept(File dir, String name) {
return filter.accept(new File(dir, name));
}
};
dialog.setFilenameFilter(f);
}
dialog.setVisible(true);
String dirStr = dialog.getDirectory();
String fileStr = dialog.getFile();
if((dirStr==null) || (fileStr==null))
return null;
setLastInputDirectory(new File(dirStr));
// if the filter didn't work, pretend that the person picked
// nothing
File f = new File(dirStr, fileStr);
if(filter != null && !filter.accept(f))
return null;
return Collections.singletonList(f);
}
}
/** Sets the last directory that was used for the FileChooser. */
private static void setLastInputDirectory(File file) {
if(file != null) {
if(!file.exists() || !file.isDirectory())
file = file.getParentFile();
if(file != null) {
if(file.exists() && file.isDirectory())
SwingUiSettings.LAST_FILECHOOSER_DIRECTORY.set(file);
}
}
}
/**
* Returns a new <tt>JFileChooser</tt> instance for selecting directories
* and with internationalized strings for the caption and the selection
* button.
*
* @param titleKey dialog title
* @param approveKey can be <code>null</code>
* @param directory can be <code>null</code>
* @param mode file selection mode
* @param filter can be <code>null</code>
* @param promptToOverwrite true if Save dialog prompts user to overwrite
* @return a new <tt>JFileChooser</tt> instance for selecting directories.
*/
private static JFileChooser getDirectoryChooser(String titleKey,
String approveKey, File directory, int mode, FileFilter filter,
boolean promptToOverwrite) {
JFileChooser chooser = null;
if (directory == null)
directory = getLastInputDirectory();
if(directory == null) {
chooser = createFileChooser(null, promptToOverwrite);
} else {
try {
chooser = createFileChooser(directory, promptToOverwrite);
} catch (NullPointerException e) {
// Workaround for JRE bug 4711700. A NullPointer is thrown
// sometimes on the first construction under XP look and feel,
// but construction succeeds on successive attempts.
try {
chooser = createFileChooser(directory, promptToOverwrite);
} catch (NullPointerException npe) {
// ok, now we use the metal file chooser, takes a long time to load
// but the user can still use the program
UIManager.getDefaults().put("FileChooserUI", "javax.swing.plaf.metal.MetalFileChooserUI");
chooser = createFileChooser(directory, promptToOverwrite);
}
} catch (ArrayIndexOutOfBoundsException ie) {
// workaround for Windows XP, not sure if second try succeeds
// then
chooser = createFileChooser(directory, promptToOverwrite);
} catch (RuntimeException re) {
// see: LWC-2690
// happens only on windows, try again and if it still happening
// us Metal L&F file chooser
if (re.getCause() instanceof IOException && OSUtils.isWindows()) {
// but construction succeeds on successive attempts.
try {
chooser = createFileChooser(directory, promptToOverwrite);
} catch (RuntimeException r) {
// ok, now we use the metal file chooser, takes a long time to load
// but the user can still use the program
UIManager.getDefaults().put("FileChooserUI", "javax.swing.plaf.metal.MetalFileChooserUI");
chooser = createFileChooser(directory, promptToOverwrite);
}
} else {
// rethrow if other OS or exception type
throw re;
}
}
}
if (filter != null) {
chooser.setFileFilter(filter);
} else {
if (mode == JFileChooser.DIRECTORIES_ONLY) {
chooser.setFileFilter(new FileFilter() {
@Override
public boolean accept(File file) {
return true;
}
@Override
public String getDescription() {
return I18n.tr("All Folders");
}
});
}
}
chooser.setFileSelectionMode(mode);
String title = I18n.tr(titleKey);
chooser.setDialogTitle(title);
if (approveKey != null) {
String approveButtonText = I18n.tr(approveKey);
chooser.setApproveButtonText(approveButtonText);
}
return chooser;
}
/**
* Creates a new file chooser with the specified current directory and
* <code>promptToOverwrite</code> behavior.
*/
private static JFileChooser createFileChooser(File currentDirectory, boolean promptToOverwrite) {
LimeFileChooser fileChooser = new LimeFileChooser(currentDirectory);
fileChooser.setPromptToOverwrite(promptToOverwrite);
return fileChooser;
}
/**
* Opens a dialog asking the user to choose a file which is used for
* saving to. If an existing file is selected, the user is automatically
* prompted to overwrite the file or cancel the selection.
*
* @param parent the parent component the dialog is centered on
* @param titleKey the key for the locale-specific string to use for the
* file dialog title
* @param suggestedFile the suggested file for saving
* @return the file or <code>null</code> when the user cancelled the
* dialog
*/
public static File getSaveAsFile(Component parent, String titleKey, File suggestedFile) {
return getSaveAsFile(parent, titleKey, suggestedFile, null);
}
/**
* Opens a dialog asking the user to choose a file which is used for
* saving to. If an existing file is selected, the user is automatically
* prompted to overwrite the file or cancel the selection.
*
* @param parent the parent component the dialog is centered on
* @param titleKey the key for the locale-specific string to use for the
* file dialog title
* @param suggestedFile the suggested file for saving
* @param the filter to use for what's shown.
* @return the file or <code>null</code> when the user cancelled the
* dialog
*/
public static File getSaveAsFile(Component parent, String titleKey,
File suggestedFile, final FileFilter filter) {
if (OSUtils.isMacOSX()) {
FileDialog dialog = new FileDialog(GuiUtils.getParentFrame(parent),
I18n.tr(titleKey),
FileDialog.SAVE);
dialog.setDirectory(suggestedFile.getParent());
dialog.setFile(suggestedFile.getName());
if (filter != null) {
FilenameFilter f = new FilenameFilter() {
public boolean accept(File dir, String name) {
return filter.accept(new File(dir, name));
}
};
dialog.setFilenameFilter(f);
}
dialog.setVisible(true);
String dir = dialog.getDirectory();
if(dir != null) {
setLastInputDirectory(new File(dir));
}
String file = dialog.getFile();
if ((dir != null) && (file != null)) {
File f = new File(dir, file);
if ((filter != null) && !filter.accept(f)) {
return null;
} else {
return f;
}
} else {
return null;
}
} else {
JFileChooser chooser = getDirectoryChooser(titleKey, null, null,
JFileChooser.FILES_ONLY, filter, true);
chooser.setSelectedFile(suggestedFile);
int ret = chooser.showSaveDialog(parent);
File file = chooser.getSelectedFile();
setLastInputDirectory(file);
return (ret != JFileChooser.APPROVE_OPTION) ? null : file;
}
}
/**
* An extension of JFileChooser that implements an option for file
* validation. When the Save dialog is displayed, the chooser may prompt
* the user to overwrite an existing file.
*/
private static class LimeFileChooser extends JFileChooser {
private boolean promptToOverwrite = false;
/**
* Constructs a LimeFileChooser using the user's default directory.
*/
public LimeFileChooser() {
super();
}
/**
* Constructs a LimeFileChooser using the specified current directory.
*/
public LimeFileChooser(File currentDirectory) {
super(currentDirectory);
}
/**
* Overrides the superclass method to validate the selected file before
* approving the selection.
*/
@Override
public void approveSelection() {
// Validate selection based on dialog type.
switch (getDialogType()) {
case SAVE_DIALOG:
if (promptToOverwrite) {
File selectedFile = getSelectedFile();
if ((selectedFile != null) && selectedFile.exists()) {
// Prompt user to overwrite existing file.
int answer = FocusJOptionPane.showConfirmDialog(this,
selectedFile.getPath() + "\n" +
I18n.tr("File already exists. Do you want to replace it?"),
getDialogTitle(), JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE);
// Exit if answer is not yes.
if (answer != JOptionPane.YES_OPTION) {
return;
}
}
}
default:
break;
}
// Call superclass method to close dialog and return value.
super.approveSelection();
}
/**
* Returns an indicator that determines whether the Save dialog prompts
* the user to overwrite an existing file.
*/
public boolean isPromptToOverwrite() {
return promptToOverwrite;
}
/**
* Sets an indicator that determines whether the Save dialog prompts
* the user to overwrite an existing file.
*/
public void setPromptToOverwrite(boolean promptToOverwrite) {
this.promptToOverwrite = promptToOverwrite;
}
}
}