5 import java.util.Locale;
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;
20 import com.formdev.flatlaf.extras.FlatDesktop.QuitResponse;
22 import jalview.api.AlignmentViewPanel;
23 import jalview.bin.Cache;
24 import jalview.bin.Console;
25 import jalview.datamodel.AlignmentI;
26 import jalview.datamodel.SequenceI;
27 import jalview.io.BackupFiles;
28 import jalview.project.Jalview2XML;
29 import jalview.util.MessageManager;
30 import jalview.util.Platform;
32 public class QuitHandler
34 private static final int MIN_WAIT_FOR_SAVE = 1000;
36 private static final int MAX_WAIT_FOR_SAVE = 20000;
38 private static boolean interactive = true;
40 private static QuitResponse flatlafResponse = null;
42 public static enum QResponse
44 NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
47 public static enum Message
49 UNSAVED_CHANGES, UNSAVED_ALIGNMENTS
52 protected static Message message = Message.UNSAVED_CHANGES;
54 public static void setMessage(Message m)
59 private static ExecutorService executor = Executors.newFixedThreadPool(3);
61 public static void setQuitHandler()
63 FlatDesktop.setQuitHandler(response -> {
64 flatlafResponse = response;
65 Desktop.instance.desktopQuit();
69 public static void startForceQuit()
71 setResponse(QResponse.FORCE_QUIT);
74 private static QResponse gotQuitResponse = QResponse.NULL;
76 protected static QResponse setResponse(QResponse qresponse)
78 gotQuitResponse = qresponse;
79 if ((qresponse == QResponse.CANCEL_QUIT || qresponse == QResponse.NULL)
80 && flatlafResponse != null)
82 flatlafResponse.cancelQuit();
87 public static QResponse gotQuitResponse()
89 return gotQuitResponse;
92 public static final Runnable defaultCancelQuit = () -> {
93 Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
95 setResponse(QResponse.CANCEL_QUIT);
98 public static final Runnable defaultOkQuit = () -> {
99 Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
100 setResponse(QResponse.QUIT);
103 public static final Runnable defaultForceQuit = () -> {
104 Console.debug("QuitHandler: (default) Quit action FORCED by user");
105 // note that shutdown hook will not be run
106 Runtime.getRuntime().halt(0);
107 setResponse(QResponse.FORCE_QUIT); // this line never reached!
110 public static QResponse getQuitResponse(boolean ui)
112 return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
116 public static QResponse getQuitResponse(boolean ui, Runnable okQuit,
117 Runnable forceQuit, Runnable cancelQuit)
119 QResponse got = gotQuitResponse();
120 if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
122 // quit has already been selected, continue with calling quit method
126 interactive = ui && !Platform.isHeadless();
127 // confirm quit if needed and wanted
128 boolean confirmQuit = true;
132 Console.debug("Non interactive quit -- not confirming");
135 else if (Jalview2XML.allSavedUpToDate())
137 Console.debug("Nothing changed -- not confirming quit");
142 confirmQuit = jalview.bin.Cache
143 .getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true);
144 Console.debug("Jalview property '"
145 + jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT
146 + "' is/defaults to " + confirmQuit + " -- "
147 + (confirmQuit ? "" : "not ") + "confirming quit");
149 got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
154 String messageString = MessageManager
155 .getString(message == Message.UNSAVED_ALIGNMENTS
156 ? "label.unsaved_alignments"
157 : "label.unsaved_changes");
158 setQuitDialog(JvOptionPane.newOptionDialog()
159 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
160 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit));
161 JvOptionPane qd = getQuitDialog();
162 qd.showDialogOnTopAsync(
164 MessageManager.getString("label.quit_jalview"))
165 .append("\n").append(messageString)
167 MessageManager.getString("action.quit"),
168 JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
170 { MessageManager.getString("action.quit"),
171 MessageManager.getString("action.cancel") },
172 MessageManager.getString("action.quit"), true);
175 got = gotQuitResponse();
177 // check for external viewer frames
178 if (got != QResponse.CANCEL_QUIT)
180 int count = Desktop.instance.structureViewersStillRunningCount();
183 String alwaysCloseExternalViewers = Cache
184 .getDefault("ALWAYS_CLOSE_EXTERNAL_VIEWERS", "ask");
185 String prompt = MessageManager
186 .formatMessage(count == 1 ? "label.confirm_quit_viewer"
187 : "label.confirm_quit_viewers");
188 String title = MessageManager.getString(
189 count == 1 ? "label.close_viewer" : "label.close_viewers");
190 String cancelQuitText = MessageManager
191 .getString("action.cancel_quit");
192 String[] buttonsText = { MessageManager.getString("action.yes"),
193 MessageManager.getString("action.no"), cancelQuitText };
195 int confirmResponse = -1;
196 if (alwaysCloseExternalViewers == null || "ask".equals(
197 alwaysCloseExternalViewers.toLowerCase(Locale.ROOT)))
199 confirmResponse = JvOptionPane.showOptionDialog(Desktop.instance,
200 prompt, title, JvOptionPane.YES_NO_CANCEL_OPTION,
201 JvOptionPane.WARNING_MESSAGE, null, buttonsText,
206 confirmResponse = Cache
207 .getDefault("ALWAYS_CLOSE_EXTERNAL_VIEWERS", false)
208 ? JvOptionPane.YES_OPTION
209 : JvOptionPane.NO_OPTION;
212 if (confirmResponse == JvOptionPane.CANCEL_OPTION)
215 QuitHandler.setResponse(QResponse.CANCEL_QUIT);
219 // Close viewers/Leave viewers open
221 .setQuitClose(confirmResponse == JvOptionPane.YES_OPTION);
227 got = gotQuitResponse();
229 boolean wait = false;
230 if (got == QResponse.CANCEL_QUIT)
233 Console.debug("Cancelling quit. Resetting response to NULL");
234 setResponse(QResponse.NULL);
236 return QResponse.CANCEL_QUIT;
238 else if (got == QResponse.QUIT)
240 if (Cache.getDefault("WAIT_FOR_SAVE", true)
241 && BackupFiles.hasSavesInProgress())
243 waitQuit(interactive, okQuit, forceQuit, cancelQuit);
244 QResponse waitResponse = gotQuitResponse();
245 wait = waitResponse == QResponse.QUIT;
249 Runnable next = null;
250 switch (gotQuitResponse())
255 case FORCE_QUIT: // not actually an option at this stage
264 executor.submit(next).get();
265 got = gotQuitResponse();
266 } catch (RejectedExecutionException e)
268 // QuitHander.abortQuit() probably called
269 // CANCEL_QUIT test will reset QuitHandler
270 Console.info("Quit aborted!");
271 got = QResponse.NULL;
272 setResponse(QResponse.NULL);
273 } catch (InterruptedException | ExecutionException e)
276 .debug("Exception during quit handling (final choice)", e);
282 // reset if cancelled
283 Console.debug("Quit cancelled");
284 setResponse(QResponse.NULL);
285 return QResponse.CANCEL_QUIT;
287 return gotQuitResponse();
290 private static QResponse waitQuit(boolean interactive, Runnable okQuit,
291 Runnable forceQuit, Runnable cancelQuit)
293 // check for saves in progress
294 if (!BackupFiles.hasSavesInProgress())
295 return QResponse.QUIT;
298 AlignFrame[] afArray = Desktop.getDesktopAlignFrames();
299 if (!(afArray == null || afArray.length == 0))
301 for (int i = 0; i < afArray.length; i++)
303 AlignFrame af = afArray[i];
304 List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
305 for (AlignmentViewPanel avp : avpList)
307 AlignmentI a = avp.getAlignment();
308 List<SequenceI> sList = a.getSequences();
309 for (SequenceI s : sList)
311 size += s.getLength();
316 int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
317 Math.max(MIN_WAIT_FOR_SAVE, size / 2));
318 Console.debug("Set waitForSave to " + waitTime);
321 boolean doIterations = true; // note iterations not used in the gui now,
322 // only one pass without the "Wait" button
323 while (doIterations && BackupFiles.hasSavesInProgress()
324 && iteration++ < (interactive ? 100 : 5))
326 // future that returns a Boolean when all files are saved
327 CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
329 // callback as each file finishes saving
330 for (CompletableFuture<Boolean> cf : BackupFiles
331 .savesInProgressCompletableFutures(false))
333 // if this is the last one then complete filesAllSaved
334 cf.whenComplete((ret, e) -> {
335 if (!BackupFiles.hasSavesInProgress())
337 filesAllSaved.complete(true);
343 filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
344 } catch (InterruptedException | ExecutionException e1)
347 "Exception whilst waiting for files to save before quit",
349 } catch (TimeoutException e2)
351 // this Exception to be expected
354 if (interactive && BackupFiles.hasSavesInProgress())
356 boolean showForceQuit = iteration > 0; // iteration > 1 to not show
357 // force quit the first time
358 JFrame parent = new JFrame();
359 JButton[] buttons = { new JButton(), new JButton() };
360 JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
361 JTextPane messagePane = new JTextPane();
362 messagePane.setBackground(waitDialog.getBackground());
363 messagePane.setBorder(null);
364 messagePane.setText(waitingForSaveMessage());
365 // callback as each file finishes saving
366 for (CompletableFuture<Boolean> cf : BackupFiles
367 .savesInProgressCompletableFutures(false))
369 cf.whenComplete((ret, e) -> {
370 if (BackupFiles.hasSavesInProgress())
372 // update the list of saving files as they save too
373 messagePane.setText(waitingForSaveMessage());
377 if (!(quitCancelled()))
379 for (int i = 0; i < buttons.length; i++)
381 Console.debug("DISABLING BUTTON " + buttons[i].getText());
382 buttons[i].setEnabled(false);
383 buttons[i].setVisible(false);
385 // if this is the last one then close the dialog
386 messagePane.setText(new StringBuilder()
387 .append(MessageManager.getString("label.all_saved"))
389 .append(MessageManager
390 .getString("label.quitting_bye"))
392 messagePane.setEditable(false);
396 } catch (InterruptedException e1)
409 options = new String[2];
410 options[0] = MessageManager.getString("action.force_quit");
411 options[1] = MessageManager.getString("action.cancel_quit");
412 dialogType = JOptionPane.YES_NO_OPTION;
413 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
414 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
418 options = new String[1];
419 options[0] = MessageManager.getString("action.cancel_quit");
420 dialogType = JOptionPane.YES_OPTION;
421 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
423 waitDialog.showDialogOnTopAsync(parent, messagePane,
424 MessageManager.getString("label.wait_for_save"), dialogType,
425 JOptionPane.WARNING_MESSAGE, null, options,
426 MessageManager.getString("action.cancel_quit"), true,
430 final QResponse thisWaitResponse = gotQuitResponse();
431 switch (thisWaitResponse)
433 case QUIT: // wait -- do another iteration
436 doIterations = false;
439 doIterations = false;
441 case NULL: // already cancelled
442 doIterations = false;
446 } // end if interactive
448 } // end while wait iteration loop
449 return gotQuitResponse();
452 private static String waitingForSaveMessage()
454 StringBuilder messageSB = new StringBuilder();
456 messageSB.append(MessageManager.getString("label.save_in_progress"));
457 List<File> files = BackupFiles.savesInProgressFiles(false);
458 boolean any = files.size() > 0;
461 for (File file : files)
463 messageSB.append("\n\u2022 ").append(file.getName());
468 messageSB.append(MessageManager.getString("label.unknown"));
470 messageSB.append("\n\n")
471 .append(MessageManager.getString("label.quit_after_saving"));
472 return messageSB.toString();
475 public static void abortQuit()
477 setResponse(QResponse.NULL);
478 // executor.shutdownNow();
481 private static JvOptionPane quitDialog = null;
483 private static void setQuitDialog(JvOptionPane qd)
488 private static JvOptionPane getQuitDialog()
493 public static boolean quitCancelled()
495 return QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
496 || QuitHandler.gotQuitResponse() == QResponse.NULL;
499 public static boolean quitting()
501 return QuitHandler.gotQuitResponse() == QResponse.QUIT
502 || QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT;