I think everyone can agree that JFileChooser is really poop. So I was looking for an alternative and found out that JavaFX has got a great FileChooser class. So now the obvious question: how can I embed that neat FileChooser into my Swing application?
Needless to say, I did some research before I posted this, and this is what I found so far: link to a Reddit post.
The code of that JavaFXFileDialog class is very interesting, but it does not close when I exit my application (JavaFX seems to continue running in the background). Also I am missing some fields I can pass to the FileChooser like the path to set default directory. And I don't like how it is static.
I am grateful for any input.
Creating a FileChooser In order to use the JavaFX FileChooser dialog you must first create a FileChooser instance. Here is an example of creating a JavaFX FileChooser dialog: FileChooser fileChooser = new FileChooser();
FileChooser class is a part of JavaFX. It is used to invoke file open dialogs for selecting a single file (showOpenDialog), file open dialogs for selecting multiple files (showOpenMultipleDialog) and file save dialogs (showSaveDialog). FileChooser class inherits Object class.
The code of that dialog has multiple problems besides the ones you mention. For example, it doesn't handle the situation when JavaFX platform shuts down right after isJavaFXStillUsable()
is called, but before the call to Platform.runLater()
, which will still make it hang forever. I don't like that huge synchronized
block either, although there don't seem to be any real problems with that. I also don't get why "the stupid synchronization object had to be a field" - each invocation of chooseFileWithJavaFXDialog()
is independent of each other, so it could just as well use a local final lock (even that array would do fine).
The right way to make JVM exit correctly is to call Platform.exit()
when you are shutting down your application (perhaps in windowClosed() of your main window). You need to do this manually because the chooser class has no idea whether you need JavaFX any more or not, and there is no way to restart it once it has been shut down.
That code inspired me to develop a utility class for calling just about any code in the JavaFX event thread, and get the result back to the calling thread, handling various exceptions and JavaFX states nicely:
/**
* A utility class to execute a Callable synchronously
* on the JavaFX event thread.
*
* @param <T> the return type of the callable
*/
public class SynchronousJFXCaller<T> {
private final Callable<T> callable;
/**
* Constructs a new caller that will execute the provided callable.
*
* The callable is accessed from the JavaFX event thread, so it should either
* be immutable or at least its state shouldn't be changed randomly while
* the call() method is in progress.
*
* @param callable the action to execute on the JFX event thread
*/
public SynchronousJFXCaller(Callable<T> callable) {
this.callable = callable;
}
/**
* Executes the Callable.
* <p>
* A specialized task is run using Platform.runLater(). The calling thread
* then waits first for the task to start, then for it to return a result.
* Any exception thrown by the Callable will be rethrown in the calling
* thread.
* </p>
* @param startTimeout time to wait for Platform.runLater() to <em>start</em>
* the dialog-showing task
* @param startTimeoutUnit the time unit of the startTimeout argument
* @return whatever the Callable returns
* @throws IllegalStateException if Platform.runLater() fails to start
* the task within the given timeout
* @throws InterruptedException if the calling (this) thread is interrupted
* while waiting for the task to start or to get its result (note that the
* task will still run anyway and its result will be ignored)
*/
public T call(long startTimeout, TimeUnit startTimeoutUnit)
throws Exception {
final CountDownLatch taskStarted = new CountDownLatch(1);
// Can't use volatile boolean here because only finals can be accessed
// from closures like the lambda expression below.
final AtomicBoolean taskCancelled = new AtomicBoolean(false);
// a trick to emulate modality:
final JDialog modalBlocker = new JDialog();
modalBlocker.setModal(true);
modalBlocker.setUndecorated(true);
modalBlocker.setOpacity(0.0f);
modalBlocker.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
final CountDownLatch modalityLatch = new CountDownLatch(1);
final FutureTask<T> task = new FutureTask<T>(() -> {
synchronized (taskStarted) {
if (taskCancelled.get()) {
return null;
} else {
taskStarted.countDown();
}
}
try {
return callable.call();
} finally {
// Wait until the Swing thread is blocked in setVisible():
modalityLatch.await();
// and unblock it:
SwingUtilities.invokeLater(() ->
modalBlocker.setVisible(false));
}
});
Platform.runLater(task);
if (!taskStarted.await(startTimeout, startTimeoutUnit)) {
synchronized (taskStarted) {
// the last chance, it could have been started just now
if (!taskStarted.await(0, TimeUnit.MILLISECONDS)) {
// Can't use task.cancel() here because it would
// interrupt the JavaFX thread, which we don't own.
taskCancelled.set(true);
throw new IllegalStateException("JavaFX was shut down"
+ " or is unresponsive");
}
}
}
// a trick to notify the task AFTER we have been blocked
// in setVisible()
SwingUtilities.invokeLater(() -> {
// notify that we are ready to get the result:
modalityLatch.countDown();
});
modalBlocker.setVisible(true); // blocks
modalBlocker.dispose(); // release resources
try {
return task.get();
} catch (ExecutionException ex) {
Throwable ec = ex.getCause();
if (ec instanceof Exception) {
throw (Exception) ec;
} else if (ec instanceof Error) {
throw (Error) ec;
} else {
throw new AssertionError("Unexpected exception type", ec);
}
}
}
}
The only part that worries me is that modality trick. It could very well work
without it (just remove any code that references modalBlocker
and modalityHatch
), but then the Swing part of the application won't just stop responding to the user input (which is what we need), but also will freeze,
stopping updates, progress bars and so on, which is not so nice. What worries me about this particular trick is that the invisible dialog may be not so invisible in some L&Fs, or cause other unwanted glitches.
I deliberately didn't include any initialization or shutdown code because I believe it doesn't belong there. I would just do new JFXPanel()
in main()
and Platform.exit()
wherever I perform other shutdown tasks.
Using this class, calling for a FileChooser is easy:
/**
* A utility class that summons JavaFX FileChooser from the Swing EDT.
* (Or anywhere else for that matter.) JavaFX should be initialized prior to
* using this class (e. g. by creating a JFXPanel instance). It is also
* recommended to call Platform.setImplicitExit(false) after initialization
* to ensure that JavaFX platform keeps running. Don't forget to call
* Platform.exit() when shutting down the application, to ensure that
* the JavaFX threads don't prevent JVM exit.
*/
public class SynchronousJFXFileChooser {
private final Supplier<FileChooser> fileChooserFactory;
/**
* Constructs a new file chooser that will use the provided factory.
*
* The factory is accessed from the JavaFX event thread, so it should either
* be immutable or at least its state shouldn't be changed randomly while
* one of the dialog-showing method calls is in progress.
*
* The factory should create and set up the chooser, for example,
* by setting extension filters. If there is no need to perform custom
* initialization of the chooser, FileChooser::new could be passed as
* a factory.
*
* Alternatively, the method parameter supplied to the showDialog()
* function can be used to provide custom initialization.
*
* @param fileChooserFactory the function used to construct new choosers
*/
public SynchronousJFXFileChooser(Supplier<FileChooser> fileChooserFactory) {
this.fileChooserFactory = fileChooserFactory;
}
/**
* Shows the FileChooser dialog by calling the provided method.
*
* Waits for one second for the dialog-showing task to start in the JavaFX
* event thread, then throws an IllegalStateException if it didn't start.
*
* @see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit)
* @param <T> the return type of the method, usually File or List<File>
* @param method a function calling one of the dialog-showing methods
* @return whatever the method returns
*/
public <T> T showDialog(Function<FileChooser, T> method) {
return showDialog(method, 1, TimeUnit.SECONDS);
}
/**
* Shows the FileChooser dialog by calling the provided method. The dialog
* is created by the factory supplied to the constructor, then it is shown
* by calling the provided method on it, then the result is returned.
* <p>
* Everything happens in the right threads thanks to
* {@link SynchronousJFXCaller}. The task performed in the JavaFX thread
* consists of two steps: construct a chooser using the provided factory
* and invoke the provided method on it. Any exception thrown during these
* steps will be rethrown in the calling thread, which shouldn't
* normally happen unless the factory throws an unchecked exception.
* </p>
* <p>
* If the calling thread is interrupted during either the wait for
* the task to start or for its result, then null is returned and
* the Thread interrupted status is set.
* </p>
* @param <T> return type (usually File or List<File>)
* @param method a function that calls the desired FileChooser method
* @param timeout time to wait for Platform.runLater() to <em>start</em>
* the dialog-showing task (once started, it is allowed to run as long
* as needed)
* @param unit the time unit of the timeout argument
* @return whatever the method returns
* @throws IllegalStateException if Platform.runLater() fails to start
* the dialog-showing task within the given timeout
*/
public <T> T showDialog(Function<FileChooser, T> method,
long timeout, TimeUnit unit) {
Callable<T> task = () -> {
FileChooser chooser = fileChooserFactory.get();
return method.apply(chooser);
};
SynchronousJFXCaller<T> caller = new SynchronousJFXCaller<>(task);
try {
return caller.call(timeout, unit);
} catch (RuntimeException | Error ex) {
throw ex;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return null;
} catch (Exception ex) {
throw new AssertionError("Got unexpected checked exception from"
+ " SynchronousJFXCaller.call()", ex);
}
}
/**
* Shows a FileChooser using FileChooser.showOpenDialog().
*
* @see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit)
* @return the return value of FileChooser.showOpenDialog()
*/
public File showOpenDialog() {
return showDialog(chooser -> chooser.showOpenDialog(null));
}
/**
* Shows a FileChooser using FileChooser.showSaveDialog().
*
* @see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit)
* @return the return value of FileChooser.showSaveDialog()
*/
public File showSaveDialog() {
return showDialog(chooser -> chooser.showSaveDialog(null));
}
/**
* Shows a FileChooser using FileChooser.showOpenMultipleDialog().
*
* @see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit)
* @return the return value of FileChooser.showOpenMultipleDialog()
*/
public List<File> showOpenMultipleDialog() {
return showDialog(chooser -> chooser.showOpenMultipleDialog(null));
}
public static void main(String[] args) {
javafx.embed.swing.JFXPanel dummy = new javafx.embed.swing.JFXPanel();
Platform.setImplicitExit(false);
try {
SynchronousJFXFileChooser chooser = new SynchronousJFXFileChooser(() -> {
FileChooser ch = new FileChooser();
ch.setTitle("Open any file you wish");
return ch;
});
File file = chooser.showOpenDialog();
System.out.println(file);
// this will throw an exception:
chooser.showDialog(ch -> ch.showOpenDialog(null), 1, TimeUnit.NANOSECONDS);
} finally {
Platform.exit();
}
}
}
Using this class, you may either initialize your chooser in the factory method, or, if you need to perform different initialization for each call, you could pass a custom method to showDialog()
instead:
System.out.println(chooser.showDialog(ch -> {
ch.setInitialDirectory(new File(System.getProperty("user.home")));
return ch.showOpenDialog(null);
}));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With