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)
174 MessageManager.getString("action.quit"),
175 JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
177 { MessageManager.getString("action.quit"),
178 MessageManager.getString("action.cancel") },
179 MessageManager.getString("action.quit"), true);
182 got = gotQuitResponse();
184 // check for external viewer frames
185 if (got != QResponse.CANCEL_QUIT)
187 int count = Desktop.instance.structureViewersStillRunningCount();
190 String prompt = MessageManager
191 .formatMessage(count == 1 ? "label.confirm_quit_viewer"
192 : "label.confirm_quit_viewers");
193 String title = MessageManager.getString(
194 count == 1 ? "label.close_viewer" : "label.close_viewers");
195 String cancelQuitText = MessageManager
196 .getString("action.cancel_quit");
197 String[] buttonsText = { MessageManager.getString("action.yes"),
198 MessageManager.getString("action.no"), cancelQuitText };
200 int confirmResponse = JvOptionPane.showOptionDialog(
201 Desktop.instance, prompt, title,
202 JvOptionPane.YES_NO_CANCEL_OPTION,
203 JvOptionPane.WARNING_MESSAGE, null, buttonsText,
206 if (confirmResponse == JvOptionPane.CANCEL_OPTION)
209 QuitHandler.setResponse(QResponse.CANCEL_QUIT);
213 // Close viewers/Leave viewers open
215 .setQuitClose(confirmResponse == JvOptionPane.YES_OPTION);
221 got = gotQuitResponse();
223 boolean wait = false;
224 if (got == QResponse.CANCEL_QUIT)
227 Console.debug("Cancelling quit. Resetting response to NULL");
228 setResponse(QResponse.NULL);
230 return QResponse.CANCEL_QUIT;
232 else if (got == QResponse.QUIT)
234 if (Cache.getDefault("WAIT_FOR_SAVE", true)
235 && BackupFiles.hasSavesInProgress())
237 waitQuit(interactive, okQuit, forceQuit, cancelQuit);
238 QResponse waitResponse = gotQuitResponse();
239 wait = waitResponse == QResponse.QUIT;
243 Callable<Void> next = null;
244 switch (gotQuitResponse())
249 case FORCE_QUIT: // not actually an option at this stage
258 executor.submit(next).get();
259 got = gotQuitResponse();
260 } catch (RejectedExecutionException e)
262 // QuitHander.abortQuit() probably called
263 // CANCEL_QUIT test will reset QuitHandler
264 Console.info("Quit aborted!");
265 got = QResponse.NULL;
266 setResponse(QResponse.NULL);
267 } catch (InterruptedException | ExecutionException e)
270 .debug("Exception during quit handling (final choice)", e);
276 // reset if cancelled
277 Console.debug("Quit cancelled");
278 setResponse(QResponse.NULL);
279 return QResponse.CANCEL_QUIT;
281 return gotQuitResponse();
284 private static QResponse waitQuit(boolean interactive,
285 Callable<Void> okQuit, Callable<Void> forceQuit,
286 Callable<Void> cancelQuit)
288 // check for saves in progress
289 if (!BackupFiles.hasSavesInProgress())
290 return QResponse.QUIT;
293 AlignFrame[] afArray = Desktop.getAlignFrames();
294 if (!(afArray == null || afArray.length == 0))
296 for (int i = 0; i < afArray.length; i++)
298 AlignFrame af = afArray[i];
299 List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
300 for (AlignmentViewPanel avp : avpList)
302 AlignmentI a = avp.getAlignment();
303 List<SequenceI> sList = a.getSequences();
304 for (SequenceI s : sList)
306 size += s.getLength();
311 int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
312 Math.max(MIN_WAIT_FOR_SAVE, size / 2));
313 Console.debug("Set waitForSave to " + waitTime);
316 boolean doIterations = true; // note iterations not used in the gui now,
317 // only one pass without the "Wait" button
318 while (doIterations && BackupFiles.hasSavesInProgress()
319 && iteration++ < (interactive ? 100 : 5))
321 // future that returns a Boolean when all files are saved
322 CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
324 // callback as each file finishes saving
325 for (CompletableFuture<Boolean> cf : BackupFiles
326 .savesInProgressCompletableFutures(false))
328 // if this is the last one then complete filesAllSaved
329 cf.whenComplete((ret, e) -> {
330 if (!BackupFiles.hasSavesInProgress())
332 filesAllSaved.complete(true);
338 filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
339 } catch (InterruptedException | ExecutionException e1)
342 "Exception whilst waiting for files to save before quit",
344 } catch (TimeoutException e2)
346 // this Exception to be expected
349 if (interactive && BackupFiles.hasSavesInProgress())
351 boolean showForceQuit = iteration > 0; // iteration > 1 to not show
352 // force quit the first time
353 JFrame parent = new JFrame();
354 JButton[] buttons = { new JButton(), new JButton() };
355 JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
356 JTextPane messagePane = new JTextPane();
357 messagePane.setBackground(waitDialog.getBackground());
358 messagePane.setBorder(null);
359 messagePane.setText(waitingForSaveMessage());
360 // callback as each file finishes saving
361 for (CompletableFuture<Boolean> cf : BackupFiles
362 .savesInProgressCompletableFutures(false))
364 cf.whenComplete((ret, e) -> {
365 if (BackupFiles.hasSavesInProgress())
367 // update the list of saving files as they save too
368 messagePane.setText(waitingForSaveMessage());
372 if (!(quitCancelled()))
374 for (int i = 0; i < buttons.length; i++)
376 Console.debug("DISABLING BUTTON " + buttons[i].getText());
377 buttons[i].setEnabled(false);
378 buttons[i].setVisible(false);
380 // if this is the last one then close the dialog
381 messagePane.setText(new StringBuilder()
382 .append(MessageManager.getString("label.all_saved"))
384 .append(MessageManager
385 .getString("label.quitting_bye"))
387 messagePane.setEditable(false);
391 } catch (InterruptedException e1)
404 options = new String[2];
405 options[0] = MessageManager.getString("action.force_quit");
406 options[1] = MessageManager.getString("action.cancel_quit");
407 dialogType = JOptionPane.YES_NO_OPTION;
408 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
409 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
413 options = new String[1];
414 options[0] = MessageManager.getString("action.cancel_quit");
415 dialogType = JOptionPane.YES_OPTION;
416 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
418 waitDialog.showDialogOnTopAsync(parent, messagePane,
419 MessageManager.getString("label.wait_for_save"), dialogType,
420 JOptionPane.WARNING_MESSAGE, null, options,
421 MessageManager.getString("action.cancel_quit"), true,
425 final QResponse thisWaitResponse = gotQuitResponse();
426 switch (thisWaitResponse)
428 case QUIT: // wait -- do another iteration
431 doIterations = false;
434 doIterations = false;
436 case NULL: // already cancelled
437 doIterations = false;
441 } // end if interactive
443 } // end while wait iteration loop
444 return gotQuitResponse();
447 private static String waitingForSaveMessage()
449 StringBuilder messageSB = new StringBuilder();
451 messageSB.append(MessageManager.getString("label.save_in_progress"));
452 List<File> files = BackupFiles.savesInProgressFiles(false);
453 boolean any = files.size() > 0;
456 for (File file : files)
458 messageSB.append("\n\u2022 ").append(file.getName());
463 messageSB.append(MessageManager.getString("label.unknown"));
465 messageSB.append("\n\n")
466 .append(MessageManager.getString("label.quit_after_saving"));
467 return messageSB.toString();
470 public static void abortQuit()
472 setResponse(QResponse.NULL);
473 // executor.shutdownNow();
476 private static JvOptionPane quitDialog = null;
478 private static void setQuitDialog(JvOptionPane qd)
483 private static JvOptionPane getQuitDialog()
488 public static boolean quitCancelled()
490 return QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
491 || QuitHandler.gotQuitResponse() == QResponse.NULL;
494 public static boolean quitting()
496 return QuitHandler.gotQuitResponse() == QResponse.QUIT
497 || QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT;