5 import java.util.concurrent.Callable;
6 import java.util.concurrent.CompletableFuture;
7 import java.util.concurrent.ExecutionException;
8 import java.util.concurrent.ExecutorService;
9 import java.util.concurrent.Executors;
10 import java.util.concurrent.RejectedExecutionException;
11 import java.util.concurrent.TimeUnit;
12 import java.util.concurrent.TimeoutException;
14 import javax.swing.JButton;
15 import javax.swing.JFrame;
16 import javax.swing.JOptionPane;
17 import javax.swing.JTextPane;
19 import com.formdev.flatlaf.extras.FlatDesktop;
21 import jalview.api.AlignmentViewPanel;
22 import jalview.bin.Cache;
23 import jalview.bin.Console;
24 import jalview.datamodel.AlignmentI;
25 import jalview.datamodel.SequenceI;
26 import jalview.io.BackupFiles;
27 import jalview.project.Jalview2XML;
28 import jalview.util.MessageManager;
29 import jalview.util.Platform;
31 public class QuitHandler
33 private static final int MIN_WAIT_FOR_SAVE = 1000;
35 private static final int MAX_WAIT_FOR_SAVE = 20000;
37 private static boolean interactive = true;
39 public static enum QResponse
41 NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
44 public static enum Message
46 UNSAVED_CHANGES, UNSAVED_ALIGNMENTS
49 protected static Message message = Message.UNSAVED_CHANGES;
51 public static void setMessage(Message m)
56 private static ExecutorService executor = Executors.newFixedThreadPool(3);
58 public static QResponse setQuitHandler()
60 FlatDesktop.setQuitHandler(response -> {
61 Callable<Void> performQuit = () -> {
62 response.performQuit();
63 setResponse(QResponse.QUIT);
66 Callable<Void> performForceQuit = () -> {
67 response.performQuit();
68 setResponse(QResponse.FORCE_QUIT);
71 Callable<Void> cancelQuit = () -> {
72 response.cancelQuit();
74 setResponse(QResponse.NULL);
77 getQuitResponse(true, performQuit, performForceQuit, cancelQuit);
80 return gotQuitResponse();
83 private static QResponse gotQuitResponse = QResponse.NULL;
85 protected static QResponse setResponse(QResponse qresponse)
87 gotQuitResponse = qresponse;
91 public static QResponse gotQuitResponse()
93 return gotQuitResponse;
96 public static final Callable<Void> defaultCancelQuit = () -> {
97 Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
99 setResponse(QResponse.CANCEL_QUIT);
103 public static final Callable<Void> defaultOkQuit = () -> {
104 Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
105 setResponse(QResponse.QUIT);
109 public static final Callable<Void> defaultForceQuit = () -> {
110 Console.debug("QuitHandler: (default) Quit action FORCED by user");
111 // note that shutdown hook will not be run
112 Runtime.getRuntime().halt(0);
113 setResponse(QResponse.FORCE_QUIT); // this line never reached!
117 public static QResponse getQuitResponse(boolean ui)
119 return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
123 public static QResponse getQuitResponse(boolean ui, Callable<Void> okQuit,
124 Callable<Void> forceQuit, Callable<Void> cancelQuit)
126 QResponse got = gotQuitResponse();
127 if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
129 // quit has already been selected, continue with calling quit method
133 interactive = ui && !Platform.isHeadless();
134 // confirm quit if needed and wanted
135 boolean confirmQuit = true;
139 Console.debug("Non interactive quit -- not confirming");
142 else if (Jalview2XML.allSavedUpToDate())
144 Console.debug("Nothing changed -- not confirming quit");
149 confirmQuit = jalview.bin.Cache
150 .getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true);
151 Console.debug("Jalview property '"
152 + jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT
153 + "' is/defaults to " + confirmQuit + " -- "
154 + (confirmQuit ? "" : "not ") + "confirming quit");
156 got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
161 String messageString = MessageManager
162 .getString(message == Message.UNSAVED_ALIGNMENTS
163 ? "label.unsaved_alignments"
164 : "label.unsaved_changes");
165 setQuitDialog(JvOptionPane.newOptionDialog()
166 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
167 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit));
168 JvOptionPane qd = getQuitDialog();
169 qd.showDialogOnTopAsync(
171 MessageManager.getString("label.quit_jalview"))
172 .append("\n").append(messageString).toString(),
173 MessageManager.getString("action.quit"),
174 JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
176 { MessageManager.getString("action.quit"),
177 MessageManager.getString("action.cancel") },
178 MessageManager.getString("action.quit"), true);
181 got = gotQuitResponse();
183 // check for external viewer frames
184 if (got != QResponse.CANCEL_QUIT)
186 int count = Desktop.instance.structureViewersStillRunningCount();
189 String prompt = MessageManager
190 .formatMessage(count == 1 ? "label.confirm_quit_viewer"
191 : "label.confirm_quit_viewers");
192 String title = MessageManager.getString(
193 count == 1 ? "label.close_viewer" : "label.close_viewers");
194 String cancelQuitText = MessageManager
195 .getString("action.cancel_quit");
196 String[] buttonsText = { MessageManager.getString("action.yes"),
197 MessageManager.getString("action.no"), cancelQuitText };
199 int confirmResponse = JvOptionPane.showOptionDialog(
200 Desktop.instance, prompt, title,
201 JvOptionPane.YES_NO_CANCEL_OPTION,
202 JvOptionPane.WARNING_MESSAGE, null, buttonsText,
205 if (confirmResponse == JvOptionPane.CANCEL_OPTION)
208 QuitHandler.setResponse(QResponse.CANCEL_QUIT);
212 // Close viewers/Leave viewers open
214 .setQuitClose(confirmResponse == JvOptionPane.YES_OPTION);
220 got = gotQuitResponse();
222 boolean wait = false;
223 if (got == QResponse.CANCEL_QUIT)
226 Console.debug("Cancelling quit. Resetting response to NULL");
227 setResponse(QResponse.NULL);
229 return QResponse.CANCEL_QUIT;
231 else if (got == QResponse.QUIT)
233 if (Cache.getDefault("WAIT_FOR_SAVE", true)
234 && BackupFiles.hasSavesInProgress())
236 waitQuit(interactive, okQuit, forceQuit, cancelQuit);
237 QResponse waitResponse = gotQuitResponse();
238 wait = waitResponse == QResponse.QUIT;
242 Callable<Void> next = null;
243 switch (gotQuitResponse())
248 case FORCE_QUIT: // not actually an option at this stage
257 executor.submit(next).get();
258 got = gotQuitResponse();
259 } catch (RejectedExecutionException e)
261 // QuitHander.abortQuit() probably called
262 // CANCEL_QUIT test will reset QuitHandler
263 Console.info("Quit aborted!");
264 got = QResponse.NULL;
265 setResponse(QResponse.NULL);
266 } catch (InterruptedException | ExecutionException e)
269 .debug("Exception during quit handling (final choice)", e);
275 // reset if cancelled
276 Console.debug("Quit cancelled");
277 setResponse(QResponse.NULL);
278 return QResponse.CANCEL_QUIT;
280 return gotQuitResponse();
283 private static QResponse waitQuit(boolean interactive,
284 Callable<Void> okQuit, Callable<Void> forceQuit,
285 Callable<Void> cancelQuit)
287 // check for saves in progress
288 if (!BackupFiles.hasSavesInProgress())
289 return QResponse.QUIT;
292 AlignFrame[] afArray = Desktop.getAlignFrames();
293 if (!(afArray == null || afArray.length == 0))
295 for (int i = 0; i < afArray.length; i++)
297 AlignFrame af = afArray[i];
298 List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
299 for (AlignmentViewPanel avp : avpList)
301 AlignmentI a = avp.getAlignment();
302 List<SequenceI> sList = a.getSequences();
303 for (SequenceI s : sList)
305 size += s.getLength();
310 int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
311 Math.max(MIN_WAIT_FOR_SAVE, size / 2));
312 Console.debug("Set waitForSave to " + waitTime);
315 boolean doIterations = true; // note iterations not used in the gui now,
316 // only one pass without the "Wait" button
317 while (doIterations && BackupFiles.hasSavesInProgress()
318 && iteration++ < (interactive ? 100 : 5))
320 // future that returns a Boolean when all files are saved
321 CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
323 // callback as each file finishes saving
324 for (CompletableFuture<Boolean> cf : BackupFiles
325 .savesInProgressCompletableFutures(false))
327 // if this is the last one then complete filesAllSaved
328 cf.whenComplete((ret, e) -> {
329 if (!BackupFiles.hasSavesInProgress())
331 filesAllSaved.complete(true);
337 filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
338 } catch (InterruptedException | ExecutionException e1)
341 "Exception whilst waiting for files to save before quit",
343 } catch (TimeoutException e2)
345 // this Exception to be expected
348 if (interactive && BackupFiles.hasSavesInProgress())
350 boolean showForceQuit = iteration > 0; // iteration > 1 to not show
351 // force quit the first time
352 JFrame parent = new JFrame();
353 JButton[] buttons = { new JButton(), new JButton() };
354 JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
355 JTextPane messagePane = new JTextPane();
356 messagePane.setBackground(waitDialog.getBackground());
357 messagePane.setBorder(null);
358 messagePane.setText(waitingForSaveMessage());
359 // callback as each file finishes saving
360 for (CompletableFuture<Boolean> cf : BackupFiles
361 .savesInProgressCompletableFutures(false))
363 cf.whenComplete((ret, e) -> {
364 if (BackupFiles.hasSavesInProgress())
366 // update the list of saving files as they save too
367 messagePane.setText(waitingForSaveMessage());
371 if (!(quitCancelled()))
373 for (int i = 0; i < buttons.length; i++)
375 Console.debug("DISABLING BUTTON " + buttons[i].getText());
376 buttons[i].setEnabled(false);
377 buttons[i].setVisible(false);
379 // if this is the last one then close the dialog
380 messagePane.setText(new StringBuilder()
381 .append(MessageManager.getString("label.all_saved"))
383 .append(MessageManager
384 .getString("label.quitting_bye"))
386 messagePane.setEditable(false);
390 } catch (InterruptedException e1)
403 options = new String[2];
404 options[0] = MessageManager.getString("action.force_quit");
405 options[1] = MessageManager.getString("action.cancel_quit");
406 dialogType = JOptionPane.YES_NO_OPTION;
407 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
408 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
412 options = new String[1];
413 options[0] = MessageManager.getString("action.cancel_quit");
414 dialogType = JOptionPane.YES_OPTION;
415 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
417 waitDialog.showDialogOnTopAsync(parent, messagePane,
418 MessageManager.getString("label.wait_for_save"), dialogType,
419 JOptionPane.WARNING_MESSAGE, null, options,
420 MessageManager.getString("action.cancel_quit"), true,
424 final QResponse thisWaitResponse = gotQuitResponse();
425 switch (thisWaitResponse)
427 case QUIT: // wait -- do another iteration
430 doIterations = false;
433 doIterations = false;
435 case NULL: // already cancelled
436 doIterations = false;
440 } // end if interactive
442 } // end while wait iteration loop
443 return gotQuitResponse();
446 private static String waitingForSaveMessage()
448 StringBuilder messageSB = new StringBuilder();
450 messageSB.append(MessageManager.getString("label.save_in_progress"));
451 List<File> files = BackupFiles.savesInProgressFiles(false);
452 boolean any = files.size() > 0;
455 for (File file : files)
457 messageSB.append("\n\u2022 ").append(file.getName());
462 messageSB.append(MessageManager.getString("label.unknown"));
464 messageSB.append("\n\n")
465 .append(MessageManager.getString("label.quit_after_saving"));
466 return messageSB.toString();
469 public static void abortQuit()
471 setResponse(QResponse.NULL);
472 // executor.shutdownNow();
475 private static JvOptionPane quitDialog = null;
477 private static void setQuitDialog(JvOptionPane qd)
482 private static JvOptionPane getQuitDialog()
487 public static boolean quitCancelled()
489 return QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
490 || QuitHandler.gotQuitResponse() == QResponse.NULL;
493 public static boolean quitting()
495 return QuitHandler.gotQuitResponse() == QResponse.QUIT
496 || QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT;