2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
24 import java.util.List;
25 import java.util.Locale;
26 import java.util.concurrent.CompletableFuture;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.ExecutorService;
29 import java.util.concurrent.Executors;
30 import java.util.concurrent.RejectedExecutionException;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.TimeoutException;
34 import javax.swing.JButton;
35 import javax.swing.JFrame;
36 import javax.swing.JOptionPane;
37 import javax.swing.JTextPane;
39 import com.formdev.flatlaf.extras.FlatDesktop;
40 import com.formdev.flatlaf.extras.FlatDesktop.QuitResponse;
42 import jalview.api.AlignmentViewPanel;
43 import jalview.bin.Cache;
44 import jalview.bin.Console;
45 import jalview.datamodel.AlignmentI;
46 import jalview.datamodel.SequenceI;
47 import jalview.io.BackupFiles;
48 import jalview.project.Jalview2XML;
49 import jalview.util.MessageManager;
50 import jalview.util.Platform;
52 public class QuitHandler
54 private static final int MIN_WAIT_FOR_SAVE = 1000;
56 private static final int MAX_WAIT_FOR_SAVE = 20000;
58 private static boolean interactive = true;
60 private static QuitResponse flatlafResponse = null;
62 public static enum QResponse
64 NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
67 public static enum Message
69 UNSAVED_CHANGES, UNSAVED_ALIGNMENTS
72 protected static Message message = Message.UNSAVED_CHANGES;
74 public static void setMessage(Message m)
79 private static ExecutorService executor = Executors.newFixedThreadPool(3);
81 public static void setQuitHandler()
83 FlatDesktop.setQuitHandler(response -> {
84 flatlafResponse = response;
85 Desktop.instance.desktopQuit();
89 public static void startForceQuit()
91 setResponse(QResponse.FORCE_QUIT);
94 private static QResponse gotQuitResponse = QResponse.NULL;
96 protected static QResponse setResponse(QResponse qresponse)
98 gotQuitResponse = qresponse;
99 if ((qresponse == QResponse.CANCEL_QUIT || qresponse == QResponse.NULL)
100 && flatlafResponse != null)
102 flatlafResponse.cancelQuit();
107 public static QResponse gotQuitResponse()
109 return gotQuitResponse;
112 public static final Runnable defaultCancelQuit = () -> {
113 Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
115 setResponse(QResponse.CANCEL_QUIT);
118 public static final Runnable defaultOkQuit = () -> {
119 Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
120 setResponse(QResponse.QUIT);
123 public static final Runnable defaultForceQuit = () -> {
124 Console.debug("QuitHandler: (default) Quit action FORCED by user");
125 // note that shutdown hook will not be run
126 Runtime.getRuntime().halt(0);
127 setResponse(QResponse.FORCE_QUIT); // this line never reached!
130 public static QResponse getQuitResponse(boolean ui)
132 return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
136 public static QResponse getQuitResponse(boolean ui, Runnable okQuit,
137 Runnable forceQuit, Runnable cancelQuit)
139 QResponse got = gotQuitResponse();
140 if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
142 // quit has already been selected, continue with calling quit method
146 interactive = ui && !Platform.isHeadless();
147 // confirm quit if needed and wanted
148 boolean confirmQuit = true;
152 Console.debug("Non interactive quit -- not confirming");
155 else if (Jalview2XML.allSavedUpToDate())
157 Console.debug("Nothing changed -- not confirming quit");
162 confirmQuit = jalview.bin.Cache
163 .getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true);
164 Console.debug("Jalview property '"
165 + jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT
166 + "' is/defaults to " + confirmQuit + " -- "
167 + (confirmQuit ? "" : "not ") + "confirming quit");
169 got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
174 String messageString = MessageManager
175 .getString(message == Message.UNSAVED_ALIGNMENTS
176 ? "label.unsaved_alignments"
177 : "label.unsaved_changes");
178 setQuitDialog(JvOptionPane.newOptionDialog()
179 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
180 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit));
181 JvOptionPane qd = getQuitDialog();
182 qd.showDialogOnTopAsync(
184 MessageManager.getString("label.quit_jalview"))
185 .append("\n").append(messageString)
187 MessageManager.getString("action.quit"),
188 JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
190 { MessageManager.getString("action.quit"),
191 MessageManager.getString("action.cancel") },
192 MessageManager.getString("action.quit"), true);
195 got = gotQuitResponse();
197 // check for external viewer frames
198 if (got != QResponse.CANCEL_QUIT)
200 int count = Desktop.instance.structureViewersStillRunningCount();
203 String alwaysCloseExternalViewers = Cache
204 .getDefault("ALWAYS_CLOSE_EXTERNAL_VIEWERS", "ask");
205 String prompt = MessageManager
206 .formatMessage(count == 1 ? "label.confirm_quit_viewer"
207 : "label.confirm_quit_viewers");
208 String title = MessageManager.getString(
209 count == 1 ? "label.close_viewer" : "label.close_viewers");
210 String cancelQuitText = MessageManager
211 .getString("action.cancel_quit");
212 String[] buttonsText = { MessageManager.getString("action.yes"),
213 MessageManager.getString("action.no"), cancelQuitText };
215 int confirmResponse = -1;
216 if (alwaysCloseExternalViewers == null || "ask".equals(
217 alwaysCloseExternalViewers.toLowerCase(Locale.ROOT)))
219 confirmResponse = JvOptionPane.showOptionDialog(Desktop.instance,
220 prompt, title, JvOptionPane.YES_NO_CANCEL_OPTION,
221 JvOptionPane.WARNING_MESSAGE, null, buttonsText,
226 confirmResponse = Cache
227 .getDefault("ALWAYS_CLOSE_EXTERNAL_VIEWERS", false)
228 ? JvOptionPane.YES_OPTION
229 : JvOptionPane.NO_OPTION;
232 if (confirmResponse == JvOptionPane.CANCEL_OPTION)
235 QuitHandler.setResponse(QResponse.CANCEL_QUIT);
239 // Close viewers/Leave viewers open
241 .setQuitClose(confirmResponse == JvOptionPane.YES_OPTION);
247 got = gotQuitResponse();
249 boolean wait = false;
250 if (got == QResponse.CANCEL_QUIT)
253 Console.debug("Cancelling quit. Resetting response to NULL");
254 setResponse(QResponse.NULL);
256 return QResponse.CANCEL_QUIT;
258 else if (got == QResponse.QUIT)
260 if (Cache.getDefault("WAIT_FOR_SAVE", true)
261 && BackupFiles.hasSavesInProgress())
263 waitQuit(interactive, okQuit, forceQuit, cancelQuit);
264 QResponse waitResponse = gotQuitResponse();
265 wait = waitResponse == QResponse.QUIT;
269 Runnable next = null;
270 switch (gotQuitResponse())
275 case FORCE_QUIT: // not actually an option at this stage
284 executor.submit(next).get();
285 got = gotQuitResponse();
286 } catch (RejectedExecutionException e)
288 // QuitHander.abortQuit() probably called
289 // CANCEL_QUIT test will reset QuitHandler
290 Console.info("Quit aborted!");
291 got = QResponse.NULL;
292 setResponse(QResponse.NULL);
293 } catch (InterruptedException | ExecutionException e)
296 .debug("Exception during quit handling (final choice)", e);
302 // reset if cancelled
303 Console.debug("Quit cancelled");
304 setResponse(QResponse.NULL);
305 return QResponse.CANCEL_QUIT;
307 return gotQuitResponse();
310 private static QResponse waitQuit(boolean interactive, Runnable okQuit,
311 Runnable forceQuit, Runnable cancelQuit)
313 // check for saves in progress
314 if (!BackupFiles.hasSavesInProgress())
315 return QResponse.QUIT;
318 AlignFrame[] afArray = Desktop.getDesktopAlignFrames();
319 if (!(afArray == null || afArray.length == 0))
321 for (int i = 0; i < afArray.length; i++)
323 AlignFrame af = afArray[i];
324 List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
325 for (AlignmentViewPanel avp : avpList)
327 AlignmentI a = avp.getAlignment();
328 List<SequenceI> sList = a.getSequences();
329 for (SequenceI s : sList)
331 size += s.getLength();
336 int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
337 Math.max(MIN_WAIT_FOR_SAVE, size / 2));
338 Console.debug("Set waitForSave to " + waitTime);
341 boolean doIterations = true; // note iterations not used in the gui now,
342 // only one pass without the "Wait" button
343 while (doIterations && BackupFiles.hasSavesInProgress()
344 && iteration++ < (interactive ? 100 : 5))
346 // future that returns a Boolean when all files are saved
347 CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
349 // callback as each file finishes saving
350 for (CompletableFuture<Boolean> cf : BackupFiles
351 .savesInProgressCompletableFutures(false))
353 // if this is the last one then complete filesAllSaved
354 cf.whenComplete((ret, e) -> {
355 if (!BackupFiles.hasSavesInProgress())
357 filesAllSaved.complete(true);
363 filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
364 } catch (InterruptedException | ExecutionException e1)
367 "Exception whilst waiting for files to save before quit",
369 } catch (TimeoutException e2)
371 // this Exception to be expected
374 if (interactive && BackupFiles.hasSavesInProgress())
376 boolean showForceQuit = iteration > 0; // iteration > 1 to not show
377 // force quit the first time
378 JFrame parent = new JFrame();
379 JButton[] buttons = { new JButton(), new JButton() };
380 JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
381 JTextPane messagePane = new JTextPane();
382 messagePane.setBackground(waitDialog.getBackground());
383 messagePane.setBorder(null);
384 messagePane.setText(waitingForSaveMessage());
385 // callback as each file finishes saving
386 for (CompletableFuture<Boolean> cf : BackupFiles
387 .savesInProgressCompletableFutures(false))
389 cf.whenComplete((ret, e) -> {
390 if (BackupFiles.hasSavesInProgress())
392 // update the list of saving files as they save too
393 messagePane.setText(waitingForSaveMessage());
397 if (!(quitCancelled()))
399 for (int i = 0; i < buttons.length; i++)
401 Console.debug("DISABLING BUTTON " + buttons[i].getText());
402 buttons[i].setEnabled(false);
403 buttons[i].setVisible(false);
405 // if this is the last one then close the dialog
406 messagePane.setText(new StringBuilder()
407 .append(MessageManager.getString("label.all_saved"))
409 .append(MessageManager
410 .getString("label.quitting_bye"))
412 messagePane.setEditable(false);
416 } catch (InterruptedException e1)
429 options = new String[2];
430 options[0] = MessageManager.getString("action.force_quit");
431 options[1] = MessageManager.getString("action.cancel_quit");
432 dialogType = JOptionPane.YES_NO_OPTION;
433 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
434 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
438 options = new String[1];
439 options[0] = MessageManager.getString("action.cancel_quit");
440 dialogType = JOptionPane.YES_OPTION;
441 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
443 waitDialog.showDialogOnTopAsync(parent, messagePane,
444 MessageManager.getString("label.wait_for_save"), dialogType,
445 JOptionPane.WARNING_MESSAGE, null, options,
446 MessageManager.getString("action.cancel_quit"), true,
450 final QResponse thisWaitResponse = gotQuitResponse();
451 switch (thisWaitResponse)
453 case QUIT: // wait -- do another iteration
456 doIterations = false;
459 doIterations = false;
461 case NULL: // already cancelled
462 doIterations = false;
466 } // end if interactive
468 } // end while wait iteration loop
469 return gotQuitResponse();
472 private static String waitingForSaveMessage()
474 StringBuilder messageSB = new StringBuilder();
476 messageSB.append(MessageManager.getString("label.save_in_progress"));
477 List<File> files = BackupFiles.savesInProgressFiles(false);
478 boolean any = files.size() > 0;
481 for (File file : files)
483 messageSB.append("\n\u2022 ").append(file.getName());
488 messageSB.append(MessageManager.getString("label.unknown"));
490 messageSB.append("\n\n")
491 .append(MessageManager.getString("label.quit_after_saving"));
492 return messageSB.toString();
495 public static void abortQuit()
497 setResponse(QResponse.NULL);
498 // executor.shutdownNow();
501 private static JvOptionPane quitDialog = null;
503 private static void setQuitDialog(JvOptionPane qd)
508 private static JvOptionPane getQuitDialog()
513 public static boolean quitCancelled()
515 return QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
516 || QuitHandler.gotQuitResponse() == QResponse.NULL;
519 public static boolean quitting()
521 return QuitHandler.gotQuitResponse() == QResponse.QUIT
522 || QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT;