001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2023
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 */
028
029package edu.wisc.ssec.mcidasv.util.nativepathchooser;
030
031import java.io.File;
032
033import java.util.concurrent.Callable;
034import java.util.concurrent.TimeUnit;
035import java.util.function.Function;
036import java.util.function.Supplier;
037
038import javafx.stage.DirectoryChooser;
039import javafx.stage.Window;
040
041// adapted from http://stackoverflow.com/a/30004156
042
043/**
044 * This class allows users to select a directory via the GUI.
045 */
046public class NativeDirectoryChooser {
047
048    private final Supplier<DirectoryChooser> directoryChooserFactory;
049
050    /**
051     * Constructs a new directory chooser that will use the provided factory.
052     * 
053     * The factory is accessed from the JavaFX event thread, so it should
054     * either be immutable or at least its state shouldn't be changed
055     * randomly while one of the dialog-showing method calls is in progress.
056     * 
057     * The factory should create and set up the chooser, for example,
058     * by setting extension filters. If there is no need to perform custom
059     * initialization of the chooser, DirectoryChooser::new could be passed as
060     * a factory.
061     * 
062     * Alternatively, the method parameter supplied to the
063     * {@link #showDialog(Function)} function can be used to provide custom
064     * initialization.
065     * 
066     * @param directoryChooserFactory Function used to construct new choosers.
067     */
068    public NativeDirectoryChooser(Supplier<DirectoryChooser> directoryChooserFactory) {
069        this.directoryChooserFactory = directoryChooserFactory;
070    }
071
072    /**
073     * Shows a {@link DirectoryChooser} dialog by calling the provided method.
074     * 
075     * <p>Waits for one second for the dialog-showing task to start in the
076     * JavaFX event thread, then throws an IllegalStateException if it
077     * didn't start.</p>
078     *
079     * @param <T> the return type of the method, usually File or List&lt;File&gt;
080     * @param method a function calling one of the dialog-showing methods
081     *
082     * @return whatever the method returns
083     *
084     * @see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit)
085     */
086    public <T> T showDialog(Function<DirectoryChooser, T> method) {
087        return showDialog(method, 1, TimeUnit.SECONDS);
088    }
089
090    /**
091     * Shows the {@link DirectoryChooser} dialog by calling the provided
092     * method. The dialog is created by the factory supplied to the
093     * constructor, then it is shown by calling the provided method on it,
094     * then the result is returned.
095     *
096     * <p>Everything happens in the right threads thanks to
097     * {@link SynchronousJFXCaller}. The task performed in the JavaFX thread
098     * consists of two steps: construct a chooser using the provided factory
099     * and invoke the provided method on it. Any exception thrown during these
100     * steps will be rethrown in the calling thread, which shouldn't
101     * normally happen unless the factory throws an unchecked exception.</p>
102     *
103     * <p>If the calling thread is interrupted during either the wait for
104     * the task to start or for its result, then null is returned and
105     * the Thread interrupted status is set.</p>
106     *
107     * @param <T> return type (usually File or List&lt;File&gt;)
108     * @param method a function that calls the desired FileChooser method
109     * @param timeout time to wait for Platform.runLater() to <em>start</em>
110     * the dialog-showing task (once started, it is allowed to run as long
111     * as needed)
112     * @param unit the time unit of the timeout argument
113     *
114     * @return whatever the method returns
115     *
116     * @throws IllegalStateException if Platform.runLater() fails to start
117     * the dialog-showing task within the given timeout
118     */
119    public <T> T showDialog(Function<DirectoryChooser, T> method,
120            long timeout, TimeUnit unit) {
121        Callable<T> task = () -> {
122            DirectoryChooser chooser = directoryChooserFactory.get();
123            return method.apply(chooser);
124        };
125        SynchronousJFXCaller<T> caller = new SynchronousJFXCaller<>(task);
126        try {
127            return caller.call(timeout, unit);
128        } catch (RuntimeException | Error ex) {
129            throw ex;
130        } catch (InterruptedException ex) {
131            Thread.currentThread().interrupt();
132            return null;
133        } catch (Exception ex) {
134            throw new AssertionError("Got unexpected checked exception from"
135                    + " SynchronousJFXCaller.call()", ex);
136        }
137    }
138
139    /**
140     * Shows a {@link DirectoryChooser} using
141     * {@link DirectoryChooser#showDialog(Window)}.
142     * 
143     * @see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit)
144     *
145     * @return Return value of {@code DirectoryChooser.showOpenDialog()}.
146     */
147    public File showOpenDialog() {
148        return showDialog(chooser -> chooser.showDialog(null));
149    }
150
151//    public static void main(String[] args) {
152//        javafx.embed.swing.JFXPanel dummy = new javafx.embed.swing.JFXPanel();
153//        Platform.setImplicitExit(false);
154//        try {
155//            SynchronousJFXDirectoryChooser chooser = new SynchronousJFXDirectoryChooser(() -> {
156//                DirectoryChooser ch = new DirectoryChooser();
157//                ch.setTitle("Choose any directory you wish");
158//                return ch;
159//            });
160//            File file = chooser.showOpenDialog();
161//            System.out.println(file);
162//            // this will throw an exception:
163//            chooser.showDialog(ch -> ch.showDialog(null), 1, TimeUnit.NANOSECONDS);
164//        } finally {
165//            Platform.exit();
166//        }
167//    }
168}