package jalview.jbgui;
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import com.formdev.flatlaf.extras.FlatDesktop;
+import jalview.api.AlignmentViewPanel;
+import jalview.bin.Cache;
+import jalview.bin.Console;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.gui.Desktop;
+import jalview.gui.JvOptionPane;
+import jalview.io.BackupFiles;
+import jalview.project.Jalview2XML;
import jalview.util.MessageManager;
+import jalview.util.Platform;
public class QuitHandler
{
- public static void setQuitHandler()
+ private static final int INITIAL_WAIT_FOR_SAVE = 3000;
+
+ private static final int NON_INTERACTIVE_WAIT_CYCLES = 2;
+
+ public static enum QResponse
+ {
+ NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
+ };
+
+ private static ExecutorService executor = Executors.newFixedThreadPool(3);
+
+ public static QResponse setQuitHandler()
{
FlatDesktop.setQuitHandler(response -> {
- boolean confirmQuit = jalview.bin.Cache
+ Callable<QResponse> performQuit = () -> {
+ response.performQuit();
+ return setResponse(QResponse.QUIT);
+ };
+ Callable<QResponse> performForceQuit = () -> {
+ response.performQuit();
+ return setResponse(QResponse.FORCE_QUIT);
+ };
+ Callable<QResponse> cancelQuit = () -> {
+ response.cancelQuit();
+ // reset
+ setResponse(QResponse.NULL);
+ // but return cancel
+ return QResponse.CANCEL_QUIT;
+ };
+ QResponse qresponse = getQuitResponse(true, performQuit,
+ performForceQuit, cancelQuit);
+ });
+
+ return gotQuitResponse();
+ }
+
+ private static QResponse gotQuitResponse = QResponse.NULL;
+
+ private static QResponse setResponse(QResponse qresponse)
+ {
+ gotQuitResponse = qresponse;
+ Console.debug("##### Setting gotQuitResponse to " + qresponse);
+ return qresponse;
+ }
+
+ public static QResponse gotQuitResponse()
+ {
+ return gotQuitResponse;
+ }
+
+ public static final Callable<QResponse> defaultCancelQuit = () -> {
+ Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
+ // reset
+ setResponse(QResponse.NULL);
+ // and return cancel
+ return QResponse.CANCEL_QUIT;
+ };
+
+ public static final Callable<QResponse> defaultOkQuit = () -> {
+ Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
+ return setResponse(QResponse.QUIT);
+ };
+
+ public static final Callable<QResponse> defaultForceQuit = () -> {
+ Console.debug("QuitHandler: (default) Quit action FORCED by user");
+ // note that shutdown hook will not be run
+ Runtime.getRuntime().halt(0);
+ return setResponse(QResponse.FORCE_QUIT); // this line never reached!
+ };
+
+ public static QResponse getQuitResponse(boolean ui)
+ {
+ return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
+ defaultCancelQuit);
+ }
+
+ private static boolean interactive = true;
+
+ public static QResponse getQuitResponse(boolean ui,
+ Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
+ Callable<QResponse> cancelQuit)
+ {
+ QResponse got = gotQuitResponse();
+ if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
+ {
+ // quit has already been selected, continue with calling quit method
+ Console.debug("##### getQuitResponse called. gotQuitResponse=" + got);
+ return got;
+ }
+
+ interactive = ui && !Platform.isHeadless();
+ // confirm quit if needed and wanted
+ boolean confirmQuit = true;
+
+ if (!interactive)
+ {
+ Console.debug("Non interactive quit -- not confirming");
+ confirmQuit = false;
+ }
+ else if (Jalview2XML.allSavedUpToDate())
+ {
+ Console.debug("Nothing changed -- not confirming quit");
+ confirmQuit = false;
+ }
+ else
+ {
+ confirmQuit = jalview.bin.Cache
.getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true);
- boolean canQuit = !confirmQuit;
- int n;
- if (confirmQuit)
+ Console.debug("Jalview property '"
+ + jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT
+ + "' is/defaults to " + confirmQuit + " -- "
+ + (confirmQuit ? "" : "not ") + "confirming quit");
+ }
+ got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
+ Console.debug("initial calculation, got=" + got);
+ setResponse(got);
+
+ if (confirmQuit)
+ {
+
+ Console.debug("********************ABOUT TO CONFIRM QUIT");
+ JvOptionPane quitDialog = JvOptionPane.newOptionDialog()
+ .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
+ .setResponseHandler(JOptionPane.NO_OPTION, defaultCancelQuit);
+ quitDialog.showDialogOnTopAsync(
+ new StringBuilder(
+ MessageManager.getString("label.quit_jalview"))
+ .append("\n")
+ .append(MessageManager
+ .getString("label.unsaved_changes"))
+ .toString(),
+ MessageManager.getString("action.quit"),
+ JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
+ new Object[]
+ { MessageManager.getString("action.quit"),
+ MessageManager.getString("action.cancel") },
+ MessageManager.getString("action.quit"), true);
+ }
+
+ got = gotQuitResponse();
+ Console.debug("first user response, got=" + got);
+ boolean wait = false;
+ if (got == QResponse.CANCEL_QUIT)
+ {
+ // reset
+ setResponse(QResponse.NULL);
+ // but return cancel
+ return QResponse.CANCEL_QUIT;
+ }
+ else if (got == QResponse.QUIT)
+ {
+ if (Cache.getDefault("WAIT_FOR_SAVE", true)
+ && BackupFiles.hasSavesInProgress())
+ {
+ /*
+ Future<QResponse> waitGot = executor.submit(waitQuitCall);
+ try
+ {
+ got = waitGot.get();
+ } catch (InterruptedException | ExecutionException e)
+ {
+ jalview.bin.Console.debug(
+ "Exception during quit handling (wait for save)", e);
+ }
+ */
+ QResponse waitResponse = waitQuit(interactive, okQuit, forceQuit,
+ cancelQuit);
+ wait = waitResponse == QResponse.QUIT;
+ }
+ }
+
+ Callable<QResponse> next = null;
+ switch (gotQuitResponse())
+ {
+ case QUIT:
+ Console.debug("### User selected QUIT");
+ next = okQuit;
+ break;
+ case FORCE_QUIT: // not actually an option at this stage
+ Console.debug("### User selected FORCE QUIT");
+ next = forceQuit;
+ break;
+ default:
+ Console.debug("### User selected CANCEL QUIT");
+ next = cancelQuit;
+ break;
+ }
+ try
+ {
+ got = executor.submit(next).get();
+ } catch (InterruptedException | ExecutionException e)
+ {
+ jalview.bin.Console
+ .debug("Exception during quit handling (final choice)", e);
+ }
+ jalview.bin.Console.debug("### nextResponse=" + got.toString());
+ setResponse(got);
+
+ return gotQuitResponse();
+ }
+
+ private static QResponse waitQuit(boolean interactive,
+ Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
+ Callable<QResponse> cancelQuit)
+ {
+ jalview.bin.Console.debug("#### waitQuit started");
+ // check for saves in progress
+ if (!BackupFiles.hasSavesInProgress())
+ return QResponse.QUIT;
+
+ int waitTime = INITIAL_WAIT_FOR_SAVE; // start with 3 second wait
+ AlignFrame[] afArray = Desktop.getAlignFrames();
+ if (!(afArray == null || afArray.length == 0))
+ {
+ int size = 0;
+ for (int i = 0; i < afArray.length; i++)
{
- // ensure Jalview window is brought to front for Quit confirmation
- // window to be visible
+ AlignFrame af = afArray[i];
+ List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
+ for (AlignmentViewPanel avp : avpList)
+ {
+ AlignmentI a = avp.getAlignment();
+ List<SequenceI> sList = a.getSequences();
+ for (SequenceI s : sList)
+ {
+ size += s.getLength();
+ }
+ }
+ }
+ waitTime = Math.max(waitTime, size / 2);
+ Console.debug("Set waitForSave to " + waitTime);
+ }
+ final int waitTimeFinal = waitTime;
+ QResponse waitResponse = QResponse.NULL;
- // this method of raising the Jalview window is broken in java
- // jalviewDesktop.setVisible(true);
- // jalviewDesktop.toFront();
+ // future that returns a Boolean when all files are saved
+ CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
- // a better hack which works instead
- JFrame dialogParent = new JFrame();
- dialogParent.setAlwaysOnTop(true);
+ // callback as each file finishes saving
+ for (CompletableFuture<Boolean> cf : BackupFiles
+ .savesInProgressCompletableFutures())
+ {
+ // if this is the last one then complete filesAllSaved
+ cf.whenComplete((ret, e) -> {
+ if (!BackupFiles.hasSavesInProgress())
+ {
+ filesAllSaved.complete(true);
+ }
+ });
+ }
- n = JOptionPane.showConfirmDialog(dialogParent,
- MessageManager.getString("label.quit_jalview"),
- MessageManager.getString("action.quit"),
- JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE,
- null);
+ // timeout the wait -- will result in another wait button when looped
+ CompletableFuture<Boolean> waitTimeout = CompletableFuture
+ .supplyAsync(() -> {
+ Console.debug("################# STARTING WAIT SLEEP");
+ try
+ {
+ Thread.sleep(waitTimeFinal);
+ } catch (InterruptedException e)
+ {
+ // probably interrupted by all files saving
+ }
+ return true;
+ });
+ CompletableFuture<Object> waitForSave = CompletableFuture
+ .anyOf(waitTimeout, filesAllSaved);
- dialogParent.setAlwaysOnTop(false);
- dialogParent.dispose();
+ int iteration = 0;
+ boolean doIterations = true;
+ while (doIterations && BackupFiles.hasSavesInProgress()
+ && iteration++ < (interactive ? 100 : 5))
+ {
+ try
+ {
+ waitForSave.copy().get();
+ } catch (InterruptedException | ExecutionException e1)
+ {
+ Console.debug(
+ "Exception whilst waiting for files to save before quit",
+ e1);
+ }
+ if (interactive && BackupFiles.hasSavesInProgress())
+ {
+ Console.debug("********************About to make waitDialog");
+ JFrame parent = new JFrame();
+ JvOptionPane waitDialog = JvOptionPane.newOptionDialog()
+ .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
+ .setResponseHandler(JOptionPane.NO_OPTION, forceQuit)
+ .setResponseHandler(JOptionPane.CANCEL_OPTION, cancelQuit);
+
+ // callback as each file finishes saving
+ for (CompletableFuture<Boolean> cf : BackupFiles
+ .savesInProgressCompletableFutures())
+ {
+ cf.whenComplete((ret, e) -> {
+ Console.debug("############# A FILE SAVED!");
+ // update the list of saving files as they save too
+ waitDialog.setMessage(waitingForSaveMessage());
+ waitDialog.setName("AAARGH!");
+ // if this is the last one then close the dialog
+ if (!BackupFiles.hasSavesInProgress())
+ {
+ // like a click on Wait button ???
+ Console.debug(
+ "***** TRYING TO MAKE THE WAIT FOR SAVE DIALOG DISAPPEAR!");
+ waitDialog.setValue(JOptionPane.YES_OPTION);
+ parent.dispose();
+ }
+ });
+ }
+
+ waitDialog.showDialogOnTopAsync(parent, waitingForSaveMessage(),
+ MessageManager.getString("action.wait"),
+ JOptionPane.YES_NO_CANCEL_OPTION,
+ JOptionPane.WARNING_MESSAGE, null, new Object[]
+ { MessageManager.getString("action.wait"),
+ MessageManager.getString("action.force_quit"),
+ MessageManager.getString("action.cancel_quit") },
+ MessageManager.getString("action.wait"), true);
+ Console.debug("********************Finished waitDialog");
+
+ final QResponse thisWaitResponse = gotQuitResponse();
+ Console.debug("####### WAITFORSAVE SET: " + thisWaitResponse);
+ switch (thisWaitResponse)
+ {
+ case QUIT: // wait -- do another iteration
+ break;
+ case FORCE_QUIT:
+ doIterations = false;
+ break;
+ case CANCEL_QUIT:
+ doIterations = false;
+ break;
+ case NULL: // already cancelled
+ doIterations = false;
+ break;
+ default:
+ }
+ } // end if interactive
+
+ } // end while wait iteration loop
+ waitResponse = gotQuitResponse();
+
+ Console.debug("####### WAITFORSAVE RETURNING: " + waitResponse);
+ return waitResponse;
+ };
+
+ public static void okk()
+ {
+ /*
+ if (false)
+ {
+ if (false)
+ {
+
+ waitLonger = JOptionPane.showOptionDialog(dialogParent,
+ waitingForSaveMessage(),
+ MessageManager.getString("action.wait"),
+ JOptionPane.YES_NO_CANCEL_OPTION,
+ JOptionPane.WARNING_MESSAGE, null, options, wait);
}
else
{
- n = JOptionPane.OK_OPTION;
+ // non-interactive
+ waitLonger = iteration < NON_INTERACTIVE_WAIT_CYCLES
+ ? JOptionPane.YES_OPTION
+ : JOptionPane.NO_OPTION;
}
- canQuit = (n == JOptionPane.OK_OPTION);
- if (canQuit)
+
+ if (waitLonger == JOptionPane.YES_OPTION) // "wait"
{
- response.performQuit();
+ saving = !waitForSave(waitIncrement);
+ }
+ else if (waitLonger == JOptionPane.NO_OPTION) // "force
+ // quit"
+ {
+ // do a force quit
+ return setResponse(QResponse.FORCE_QUIT);
+ }
+ else if (waitLonger == JOptionPane.CANCEL_OPTION) // cancel quit
+ {
+ return setResponse(QResponse.CANCEL_QUIT);
}
else
{
- response.cancelQuit();
+ // Most likely got here by user dismissing the dialog with the
+ // 'x'
+ // -- treat as a "Cancel"
+ return setResponse(QResponse.CANCEL_QUIT);
}
- });
+ }
+
+ // not sure how we got here, best be safe
+ return QResponse.CANCEL_QUIT;
+ */
+ };
+
+ private static int waitForceQuitCancelQuitOptionDialog(Object message,
+ String title)
+ {
+ JFrame dialogParent = new JFrame();
+ dialogParent.setAlwaysOnTop(true);
+ String wait = MessageManager.getString("action.wait");
+ Object[] options = { wait,
+ MessageManager.getString("action.force_quit"),
+ MessageManager.getString("action.cancel_quit") };
+
+ // BackupFiles.setWaitForSaveDialog(dialogParent);
+
+ int answer = JOptionPane.showOptionDialog(dialogParent, message, title,
+ JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE,
+ null, options, wait);
+
+ // BackupFiles.clearWaitForSaveDialog();
+
+ return answer;
+ }
+
+ private static String waitingForSaveMessage()
+ {
+ StringBuilder messageSB = new StringBuilder(
+ MessageManager.getString("label.save_in_progress"));
+ boolean any = false;
+ for (File file : BackupFiles.savesInProgressFiles())
+ {
+ messageSB.append("\n- ");
+ messageSB.append(file.getName());
+ any = true;
+ }
+ if (!any)
+ {
+ messageSB.append("\n");
+ messageSB.append(MessageManager.getString("label.unknown"));
+ }
+
+ return messageSB.toString();
+ }
+
+ private static Boolean waitForSave(long t)
+ {
+ boolean ret = false;
+ try
+ {
+ Console.debug("Wait for save to complete: " + t + "ms");
+ long c = 0;
+ int i = 100;
+ while (c < t)
+ {
+ Thread.sleep(i);
+ c += i;
+ ret = !BackupFiles.hasSavesInProgress();
+ if (ret)
+ {
+ Console.debug(
+ "Save completed whilst waiting (" + c + "/" + t + "ms)");
+ return ret;
+ }
+ if (c % 1000 < i) // just gone over another second
+ {
+ Console.debug("...waiting (" + c + "/" + t + "ms]");
+ }
+ }
+ } catch (InterruptedException e)
+ {
+ Console.debug("Wait for save interrupted");
+ }
+ Console.debug("Save has " + (ret ? "" : "not ") + "completed");
+ return ret;
}
}