001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2017
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas
009 *
010 * All Rights Reserved
011 *
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.
014 *
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 *
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 *
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see http://www.gnu.org/licenses.
027 */
028package edu.wisc.ssec.mcidasv.util.nativepathchooser;
029
030import java.io.File;
031
032import java.util.concurrent.Callable;
033import java.util.concurrent.TimeUnit;
034import java.util.function.Function;
035import java.util.function.Supplier;
036
037import javafx.stage.DirectoryChooser;
038import javafx.stage.Window;
039
040// adapted from http://stackoverflow.com/a/30004156
041
042/**
043 * This class allows users to select a directory via the GUI.
044 */
045public class NativeDirectoryChooser {
046
047    private final Supplier<DirectoryChooser> directoryChooserFactory;
048
049    /**
050     * Constructs a new directory chooser that will use the provided factory.
051     * 
052     * The factory is accessed from the JavaFX event thread, so it should
053     * either be immutable or at least its state shouldn't be changed
054     * randomly while one of the dialog-showing method calls is in progress.
055     * 
056     * The factory should create and set up the chooser, for example,
057     * by setting extension filters. If there is no need to perform custom
058     * initialization of the chooser, DirectoryChooser::new could be passed as
059     * a factory.
060     * 
061     * Alternatively, the method parameter supplied to the
062     * {@link #showDialog(Function)} function can be used to provide custom
063     * initialization.
064     * 
065     * @param directoryChooserFactory Function used to construct new choosers.
066     */
067    public NativeDirectoryChooser(Supplier<DirectoryChooser> directoryChooserFactory) {
068        this.directoryChooserFactory = directoryChooserFactory;
069    }
070
071    /**
072     * Shows a {@link DirectoryChooser} dialog by calling the provided method.
073     * 
074     * <p>Waits for one second for the dialog-showing task to start in the
075     * JavaFX event thread, then throws an IllegalStateException if it
076     * didn't start.</p>
077     *
078     * @param <T> the return type of the method, usually File or List&lt;File&gt;
079     * @param method a function calling one of the dialog-showing methods
080     *
081     * @return whatever the method returns
082     *
083     * @see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit)
084     */
085    public <T> T showDialog(Function<DirectoryChooser, T> method) {
086        return showDialog(method, 1, TimeUnit.SECONDS);
087    }
088
089    /**
090     * Shows the {@link DirectoryChooser} dialog by calling the provided
091     * method. The dialog is created by the factory supplied to the
092     * constructor, then it is shown by calling the provided method on it,
093     * then the result is returned.
094     *
095     * <p>Everything happens in the right threads thanks to
096     * {@link SynchronousJFXCaller}. The task performed in the JavaFX thread
097     * consists of two steps: construct a chooser using the provided factory
098     * and invoke the provided method on it. Any exception thrown during these
099     * steps will be rethrown in the calling thread, which shouldn't
100     * normally happen unless the factory throws an unchecked exception.</p>
101     *
102     * <p>If the calling thread is interrupted during either the wait for
103     * the task to start or for its result, then null is returned and
104     * the Thread interrupted status is set.</p>
105     *
106     * @param <T> return type (usually File or List&lt;File&gt;)
107     * @param method a function that calls the desired FileChooser method
108     * @param timeout time to wait for Platform.runLater() to <em>start</em>
109     * the dialog-showing task (once started, it is allowed to run as long
110     * as needed)
111     * @param unit the time unit of the timeout argument
112     *
113     * @return whatever the method returns
114     *
115     * @throws IllegalStateException if Platform.runLater() fails to start
116     * the dialog-showing task within the given timeout
117     */
118    public <T> T showDialog(Function<DirectoryChooser, T> method,
119            long timeout, TimeUnit unit) {
120        Callable<T> task = () -> {
121            DirectoryChooser chooser = directoryChooserFactory.get();
122            return method.apply(chooser);
123        };
124        SynchronousJFXCaller<T> caller = new SynchronousJFXCaller<>(task);
125        try {
126            return caller.call(timeout, unit);
127        } catch (RuntimeException | Error ex) {
128            throw ex;
129        } catch (InterruptedException ex) {
130            Thread.currentThread().interrupt();
131            return null;
132        } catch (Exception ex) {
133            throw new AssertionError("Got unexpected checked exception from"
134                    + " SynchronousJFXCaller.call()", ex);
135        }
136    }
137
138    /**
139     * Shows a {@link DirectoryChooser} using
140     * {@link DirectoryChooser#showDialog(Window)}.
141     * 
142     * @see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit)
143     *
144     * @return Return value of {@code DirectoryChooser.showOpenDialog()}.
145     */
146    public File showOpenDialog() {
147        return showDialog(chooser -> chooser.showDialog(null));
148    }
149
150//    public static void main(String[] args) {
151//        javafx.embed.swing.JFXPanel dummy = new javafx.embed.swing.JFXPanel();
152//        Platform.setImplicitExit(false);
153//        try {
154//            SynchronousJFXDirectoryChooser chooser = new SynchronousJFXDirectoryChooser(() -> {
155//                DirectoryChooser ch = new DirectoryChooser();
156//                ch.setTitle("Choose any directory you wish");
157//                return ch;
158//            });
159//            File file = chooser.showOpenDialog();
160//            System.out.println(file);
161//            // this will throw an exception:
162//            chooser.showDialog(ch -> ch.showDialog(null), 1, TimeUnit.NANOSECONDS);
163//        } finally {
164//            Platform.exit();
165//        }
166//    }
167}