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