package jalview.gui; 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 java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JTextPane; 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.io.BackupFiles; import jalview.project.Jalview2XML; import jalview.util.MessageManager; import jalview.util.Platform; public class QuitHandler { private static final int MIN_WAIT_FOR_SAVE = 1000; private static final int MAX_WAIT_FOR_SAVE = 20000; private static boolean interactive = true; public static enum QResponse { NULL, QUIT, CANCEL_QUIT, FORCE_QUIT }; public static enum Message { UNSAVED_CHANGES, UNSAVED_ALIGNMENTS }; protected static Message message = Message.UNSAVED_CHANGES; public static void setMessage(Message m) { message = m; } private static ExecutorService executor = Executors.newFixedThreadPool(3); public static QResponse setQuitHandler() { FlatDesktop.setQuitHandler(response -> { Callable performQuit = () -> { response.performQuit(); setResponse(QResponse.QUIT); return null; }; Callable performForceQuit = () -> { response.performQuit(); setResponse(QResponse.FORCE_QUIT); return null; }; Callable cancelQuit = () -> { response.cancelQuit(); // reset setResponse(QResponse.NULL); return null; }; getQuitResponse(true, performQuit, performForceQuit, cancelQuit); }); return gotQuitResponse(); } private static QResponse gotQuitResponse = QResponse.NULL; protected static QResponse setResponse(QResponse qresponse) { gotQuitResponse = 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.CANCEL_QUIT); return null; }; public static final Callable defaultOkQuit = () -> { Console.debug("QuitHandler: (default) Quit action CONFIRMED by user"); setResponse(QResponse.QUIT); return null; }; 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); setResponse(QResponse.FORCE_QUIT); // this line never reached! return null; }; public static QResponse getQuitResponse(boolean ui) { return getQuitResponse(ui, defaultOkQuit, defaultForceQuit, defaultCancelQuit); } 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 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; setResponse(got); if (confirmQuit) { String messageString = MessageManager .getString(message == Message.UNSAVED_ALIGNMENTS ? "label.unsaved_alignments" : "label.unsaved_changes"); JvOptionPane.newOptionDialog() .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit) .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit) .showDialogOnTopAsync( new StringBuilder(MessageManager .getString("label.quit_jalview")).append("\n") .append(messageString).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(); boolean wait = false; if (got == QResponse.CANCEL_QUIT) { // reset Console.debug("Cancelling quit. Resetting response to NULL"); setResponse(QResponse.NULL); // but return cancel return QResponse.CANCEL_QUIT; } else if (got == QResponse.QUIT) { if (Cache.getDefault("WAIT_FOR_SAVE", true) && BackupFiles.hasSavesInProgress()) { waitQuit(interactive, okQuit, forceQuit, cancelQuit); QResponse waitResponse = gotQuitResponse(); wait = waitResponse == QResponse.QUIT; } } Callable next = null; switch (gotQuitResponse()) { case QUIT: next = okQuit; break; case FORCE_QUIT: // not actually an option at this stage next = forceQuit; break; default: next = cancelQuit; break; } try { executor.submit(next).get(); got = gotQuitResponse(); } catch (InterruptedException | ExecutionException e) { jalview.bin.Console .debug("Exception during quit handling (final choice)", e); } setResponse(got); if (gotQuitResponse() == QResponse.CANCEL_QUIT) { // reset if cancelled setResponse(QResponse.NULL); return QResponse.CANCEL_QUIT; } return gotQuitResponse(); } private static QResponse waitQuit(boolean interactive, Callable okQuit, Callable forceQuit, Callable cancelQuit) { // check for saves in progress if (!BackupFiles.hasSavesInProgress()) return QResponse.QUIT; int size = 0; AlignFrame[] afArray = Desktop.getAlignFrames(); if (!(afArray == null || afArray.length == 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(); } } } } int waitTime = Math.min(MAX_WAIT_FOR_SAVE, Math.max(MIN_WAIT_FOR_SAVE, size / 2)); Console.debug("Set waitForSave to " + waitTime); int iteration = 0; boolean doIterations = true; // note iterations not used in the gui now, // only one pass without the "Wait" button while (doIterations && BackupFiles.hasSavesInProgress() && iteration++ < (interactive ? 100 : 5)) { // 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(false)) { // if this is the last one then complete filesAllSaved cf.whenComplete((ret, e) -> { if (!BackupFiles.hasSavesInProgress()) { filesAllSaved.complete(true); } }); } try { filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException e1) { Console.debug( "Exception whilst waiting for files to save before quit", e1); } catch (TimeoutException e2) { // this Exception to be expected } if (interactive && BackupFiles.hasSavesInProgress()) { boolean showForceQuit = iteration > 0; // iteration > 1 to not show // force quit the first time JFrame parent = new JFrame(); JButton[] buttons = { new JButton(), new JButton() }; JvOptionPane waitDialog = JvOptionPane.newOptionDialog(); JTextPane messagePane = new JTextPane(); messagePane.setBackground(waitDialog.getBackground()); messagePane.setBorder(null); messagePane.setText(waitingForSaveMessage()); // callback as each file finishes saving for (CompletableFuture cf : BackupFiles .savesInProgressCompletableFutures(false)) { cf.whenComplete((ret, e) -> { if (BackupFiles.hasSavesInProgress()) { // update the list of saving files as they save too messagePane.setText(waitingForSaveMessage()); } else { if (!(QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT || QuitHandler.gotQuitResponse() == QResponse.NULL)) { for (int i = 0; i < buttons.length; i++) { Console.debug("DISABLING BUTTON " + buttons[i].getText()); buttons[i].setEnabled(false); buttons[i].setVisible(false); } // if this is the last one then close the dialog messagePane.setText(new StringBuilder() .append(MessageManager.getString("label.all_saved")) .append("\n") .append(MessageManager .getString("label.quitting_bye")) .toString()); messagePane.setEditable(false); try { Thread.sleep(1500); } catch (InterruptedException e1) { } parent.dispose(); } } }); } String[] options; int dialogType = -1; if (showForceQuit) { options = new String[2]; options[0] = MessageManager.getString("action.force_quit"); options[1] = MessageManager.getString("action.cancel_quit"); dialogType = JOptionPane.YES_NO_OPTION; waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit) .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit); } else { options = new String[1]; options[0] = MessageManager.getString("action.cancel_quit"); dialogType = JOptionPane.YES_OPTION; waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit); } waitDialog.showDialogOnTopAsync(parent, messagePane, MessageManager.getString("label.wait_for_save"), dialogType, JOptionPane.WARNING_MESSAGE, null, options, MessageManager.getString("action.cancel_quit"), true, buttons); parent.dispose(); final QResponse thisWaitResponse = gotQuitResponse(); 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 return gotQuitResponse(); }; private static String waitingForSaveMessage() { StringBuilder messageSB = new StringBuilder(); messageSB.append(MessageManager.getString("label.save_in_progress")); List files = BackupFiles.savesInProgressFiles(false); boolean any = files.size() > 0; if (any) { for (File file : files) { messageSB.append("\n\u2022 ").append(file.getName()); } } else { messageSB.append(MessageManager.getString("label.unknown")); } messageSB.append("\n\n") .append(MessageManager.getString("label.quit_after_saving")); return messageSB.toString(); } public static void abortQuit() { setResponse(QResponse.CANCEL_QUIT); } }