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 { 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 -> { Callable performQuit = () -> { response.performQuit(); return setResponse(QResponse.QUIT); }; Callable performForceQuit = () -> { response.performQuit(); return setResponse(QResponse.FORCE_QUIT); }; Callable 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 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 defaultOkQuit = () -> { Console.debug("QuitHandler: (default) Quit action CONFIRMED by user"); return setResponse(QResponse.QUIT); }; public static final Callable 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 okQuit, Callable forceQuit, Callable 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); 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 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 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 okQuit, Callable forceQuit, Callable 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++) { AlignFrame af = afArray[i]; List avpList = af.getAlignPanels(); for (AlignmentViewPanel avp : avpList) { AlignmentI a = avp.getAlignment(); List 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; // future that returns a Boolean when all files are saved CompletableFuture filesAllSaved = new CompletableFuture<>(); // callback as each file finishes saving for (CompletableFuture cf : BackupFiles .savesInProgressCompletableFutures()) { // if this is the last one then complete filesAllSaved cf.whenComplete((ret, e) -> { if (!BackupFiles.hasSavesInProgress()) { filesAllSaved.complete(true); } }); } // timeout the wait -- will result in another wait button when looped CompletableFuture waitTimeout = CompletableFuture .supplyAsync(() -> { Console.debug("################# STARTING WAIT SLEEP"); try { Thread.sleep(waitTimeFinal); } catch (InterruptedException e) { // probably interrupted by all files saving } return true; }); CompletableFuture waitForSave = CompletableFuture .anyOf(waitTimeout, filesAllSaved); 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 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 { // non-interactive waitLonger = iteration < NON_INTERACTIVE_WAIT_CYCLES ? JOptionPane.YES_OPTION : JOptionPane.NO_OPTION; } if (waitLonger == JOptionPane.YES_OPTION) // "wait" { 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 { // 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; } }