/* * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * Jalview is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ package jalview.gui; import java.io.File; import java.util.List; import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; 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 com.formdev.flatlaf.extras.FlatDesktop.QuitResponse; 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; private static QuitResponse flatlafResponse = null; 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 void setQuitHandler() { FlatDesktop.setQuitHandler(response -> { flatlafResponse = response; Desktop.instance.desktopQuit(); }); } public static void startForceQuit() { setResponse(QResponse.FORCE_QUIT); } private static QResponse gotQuitResponse = QResponse.NULL; protected static QResponse setResponse(QResponse qresponse) { gotQuitResponse = qresponse; if ((qresponse == QResponse.CANCEL_QUIT || qresponse == QResponse.NULL) && flatlafResponse != null) { flatlafResponse.cancelQuit(); } return qresponse; } public static QResponse gotQuitResponse() { return gotQuitResponse; } public static final Runnable defaultCancelQuit = () -> { Console.debug("QuitHandler: (default) Quit action CANCELLED by user"); // reset setResponse(QResponse.CANCEL_QUIT); }; public static final Runnable defaultOkQuit = () -> { Console.debug("QuitHandler: (default) Quit action CONFIRMED by user"); setResponse(QResponse.QUIT); }; public static final Runnable 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! }; public static QResponse getQuitResponse(boolean ui) { return getQuitResponse(ui, defaultOkQuit, defaultForceQuit, defaultCancelQuit); } public static QResponse getQuitResponse(boolean ui, Runnable okQuit, Runnable forceQuit, Runnable 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"); setQuitDialog(JvOptionPane.newOptionDialog() .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit) .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit)); JvOptionPane qd = getQuitDialog(); qd.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(); // check for external viewer frames if (got != QResponse.CANCEL_QUIT) { int count = Desktop.instance.structureViewersStillRunningCount(); if (count > 0) { String alwaysCloseExternalViewers = Cache .getDefault("ALWAYS_CLOSE_EXTERNAL_VIEWERS", "ask"); String prompt = MessageManager .formatMessage(count == 1 ? "label.confirm_quit_viewer" : "label.confirm_quit_viewers"); String title = MessageManager.getString( count == 1 ? "label.close_viewer" : "label.close_viewers"); String cancelQuitText = MessageManager .getString("action.cancel_quit"); String[] buttonsText = { MessageManager.getString("action.yes"), MessageManager.getString("action.no"), cancelQuitText }; int confirmResponse = -1; if (alwaysCloseExternalViewers == null || "ask".equals( alwaysCloseExternalViewers.toLowerCase(Locale.ROOT))) { confirmResponse = JvOptionPane.showOptionDialog(Desktop.instance, prompt, title, JvOptionPane.YES_NO_CANCEL_OPTION, JvOptionPane.WARNING_MESSAGE, null, buttonsText, cancelQuit); } else { confirmResponse = Cache .getDefault("ALWAYS_CLOSE_EXTERNAL_VIEWERS", false) ? JvOptionPane.YES_OPTION : JvOptionPane.NO_OPTION; } if (confirmResponse == JvOptionPane.CANCEL_OPTION) { // Cancel Quit QuitHandler.setResponse(QResponse.CANCEL_QUIT); } else { // Close viewers/Leave viewers open StructureViewerBase .setQuitClose(confirmResponse == JvOptionPane.YES_OPTION); } } } 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; } } Runnable 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 (RejectedExecutionException e) { // QuitHander.abortQuit() probably called // CANCEL_QUIT test will reset QuitHandler Console.info("Quit aborted!"); got = QResponse.NULL; setResponse(QResponse.NULL); } catch (InterruptedException | ExecutionException e) { jalview.bin.Console .debug("Exception during quit handling (final choice)", e); } setResponse(got); if (quitCancelled()) { // reset if cancelled Console.debug("Quit cancelled"); setResponse(QResponse.NULL); return QResponse.CANCEL_QUIT; } return gotQuitResponse(); } private static QResponse waitQuit(boolean interactive, Runnable okQuit, Runnable forceQuit, Runnable cancelQuit) { // check for saves in progress if (!BackupFiles.hasSavesInProgress()) return QResponse.QUIT; int size = 0; AlignFrame[] afArray = Desktop.getDesktopAlignFrames(); 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 (!(quitCancelled())) { 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.NULL); // executor.shutdownNow(); } private static JvOptionPane quitDialog = null; private static void setQuitDialog(JvOptionPane qd) { quitDialog = qd; } private static JvOptionPane getQuitDialog() { return quitDialog; } public static boolean quitCancelled() { return QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT || QuitHandler.gotQuitResponse() == QResponse.NULL; } public static boolean quitting() { return QuitHandler.gotQuitResponse() == QResponse.QUIT || QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT; } }