001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2025 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 javax.swing.SwingUtilities; 032 033import javafx.application.Platform; 034 035import java.util.concurrent.Callable; 036import java.util.concurrent.CountDownLatch; 037import java.util.concurrent.ExecutionException; 038import java.util.concurrent.FutureTask; 039import java.util.concurrent.TimeUnit; 040import java.util.concurrent.atomic.AtomicBoolean; 041 042// taken from http://stackoverflow.com/a/30004156 043 044/** 045 * A utility class to execute a Callable synchronously 046 * on the JavaFX event thread. 047 * 048 * @param <T> Return type of the {@link Callable}. 049 */ 050public class SynchronousJFXCaller<T> { 051 private final Callable<T> callable; 052 053 /** 054 * Constructs a new caller that will execute the provided callable. 055 * 056 * The callable is accessed from the JavaFX event thread, so it should either 057 * be immutable or at least its state shouldn't be changed randomly while 058 * the call() method is in progress. 059 * 060 * @param callable Action to execute on the JFX event thread. 061 */ 062 public SynchronousJFXCaller(Callable<T> callable) { 063 this.callable = callable; 064 } 065 066 /** 067 * Executes the Callable. 068 * <p> 069 * A specialized task is run using Platform.runLater(). The calling thread 070 * then waits first for the task to start, then for it to return a result. 071 * Any exception thrown by the Callable will be rethrown in the calling 072 * thread. 073 * </p> 074 * @param startTimeout time to wait for Platform.runLater() to <em>start</em> 075 * the dialog-showing task 076 * @param startTimeoutUnit the time unit of the startTimeout argument 077 * @return whatever the Callable returns 078 * @throws IllegalStateException if Platform.runLater() fails to start 079 * the task within the given timeout 080 * @throws InterruptedException if the calling (this) thread is interrupted 081 * while waiting for the task to start or to get its result (note that the 082 * task will still run anyway and its result will be ignored) 083 */ 084 public T call(long startTimeout, TimeUnit startTimeoutUnit) 085 throws Exception { 086 final CountDownLatch taskStarted = new CountDownLatch(1); 087 // Can't use volatile boolean here because only finals can be accessed 088 // from closures like the lambda expression below. 089 final AtomicBoolean taskCancelled = new AtomicBoolean(false); 090 // disabling the modality emulation (for now?) 091 // a trick to emulate modality: 092 // final JDialog modalBlocker = new JDialog(); 093 // modalBlocker.setModal(true); 094 // modalBlocker.setModalityType(Dialog.ModalityType.DOCUMENT_MODAL); 095 // modalBlocker.setUndecorated(true); 096 // modalBlocker.setOpacity(0.0f); 097 // modalBlocker.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); 098 // final CountDownLatch modalityLatch = new CountDownLatch(1); 099 final FutureTask<T> task = new FutureTask<T>(() -> { 100 synchronized (taskStarted) { 101 if (taskCancelled.get()) { 102 return null; 103 } else { 104 taskStarted.countDown(); 105 } 106 } 107 try { 108 return callable.call(); 109 } finally { 110 // Wait until the Swing thread is blocked in setVisible(): 111 // modalityLatch.await(); 112 // and unblock it: 113 // SwingUtilities.invokeLater(() -> 114 // modalBlocker.setVisible(false)); 115 } 116 }); 117 Platform.runLater(task); 118 if (!taskStarted.await(startTimeout, startTimeoutUnit)) { 119 synchronized (taskStarted) { 120 // the last chance, it could have been started just now 121 if (!taskStarted.await(0, TimeUnit.MILLISECONDS)) { 122 // Can't use task.cancel() here because it would 123 // interrupt the JavaFX thread, which we don't own. 124 taskCancelled.set(true); 125 throw new IllegalStateException("JavaFX was shut down" 126 + " or is unresponsive"); 127 } 128 } 129 } 130 // a trick to notify the task AFTER we have been blocked 131 // in setVisible() 132 SwingUtilities.invokeLater(() -> { 133 // notify that we are ready to get the result: 134// modalityLatch.countDown(); 135 }); 136// modalBlocker.setVisible(true); // blocks 137// modalBlocker.dispose(); // release resources 138 try { 139 return task.get(); 140 } catch (ExecutionException ex) { 141 Throwable ec = ex.getCause(); 142 if (ec instanceof Exception) { 143 throw (Exception) ec; 144 } else if (ec instanceof Error) { 145 throw (Error) ec; 146 } else { 147 throw new AssertionError("Unexpected exception type", ec); 148 } 149 } 150 } 151 152}