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).toString(),
166 MessageManager.getString("action.quit"),
167 JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
169 { MessageManager.getString("action.quit"),
170 MessageManager.getString("action.cancel") },
171 MessageManager.getString("action.quit"), true);
174 got = gotQuitResponse();
176 // check for external viewer frames
177 if (got != QResponse.CANCEL_QUIT)
179 int count = Desktop.instance.structureViewersStillRunningCount();
182 String alwaysCloseExternalViewers = Cache
183 .getDefault("ALWAYS_CLOSE_EXTERNAL_VIEWERS", "ask");
184 String prompt = MessageManager
185 .formatMessage(count == 1 ? "label.confirm_quit_viewer"
186 : "label.confirm_quit_viewers");
187 String title = MessageManager.getString(
188 count == 1 ? "label.close_viewer" : "label.close_viewers");
189 String cancelQuitText = MessageManager
190 .getString("action.cancel_quit");
191 String[] buttonsText = { MessageManager.getString("action.yes"),
192 MessageManager.getString("action.no"), cancelQuitText };
194 int confirmResponse = -1;
195 if (alwaysCloseExternalViewers == null || "ask".equals(
196 alwaysCloseExternalViewers.toLowerCase(Locale.ROOT)))
198 confirmResponse = JvOptionPane.showOptionDialog(Desktop.instance,
199 prompt, title, JvOptionPane.YES_NO_CANCEL_OPTION,
200 JvOptionPane.WARNING_MESSAGE, null, buttonsText,
205 confirmResponse = Cache
206 .getDefault("ALWAYS_CLOSE_EXTERNAL_VIEWERS", false)
207 ? JvOptionPane.YES_OPTION
208 : JvOptionPane.NO_OPTION;
211 if (confirmResponse == JvOptionPane.CANCEL_OPTION)
214 QuitHandler.setResponse(QResponse.CANCEL_QUIT);
218 // Close viewers/Leave viewers open
220 .setQuitClose(confirmResponse == JvOptionPane.YES_OPTION);
226 got = gotQuitResponse();
228 boolean wait = false;
229 if (got == QResponse.CANCEL_QUIT)
232 Console.debug("Cancelling quit. Resetting response to NULL");
233 setResponse(QResponse.NULL);
235 return QResponse.CANCEL_QUIT;
237 else if (got == QResponse.QUIT)
239 if (Cache.getDefault("WAIT_FOR_SAVE", true)
240 && BackupFiles.hasSavesInProgress())
242 waitQuit(interactive, okQuit, forceQuit, cancelQuit);
243 QResponse waitResponse = gotQuitResponse();
244 wait = waitResponse == QResponse.QUIT;
248 Runnable next = null;
249 switch (gotQuitResponse())
254 case FORCE_QUIT: // not actually an option at this stage
263 executor.submit(next).get();
264 got = gotQuitResponse();
265 } catch (RejectedExecutionException e)
267 // QuitHander.abortQuit() probably called
268 // CANCEL_QUIT test will reset QuitHandler
269 Console.info("Quit aborted!");
270 got = QResponse.NULL;
271 setResponse(QResponse.NULL);
272 } catch (InterruptedException | ExecutionException e)
275 .debug("Exception during quit handling (final choice)", e);
281 // reset if cancelled
282 Console.debug("Quit cancelled");
283 setResponse(QResponse.NULL);
284 return QResponse.CANCEL_QUIT;
286 return gotQuitResponse();
289 private static QResponse waitQuit(boolean interactive, Runnable okQuit,
290 Runnable forceQuit, Runnable cancelQuit)
292 // check for saves in progress
293 if (!BackupFiles.hasSavesInProgress())
294 return QResponse.QUIT;
297 AlignFrame[] afArray = Desktop.getAlignFrames();
298 if (!(afArray == null || afArray.length == 0))
300 for (int i = 0; i < afArray.length; i++)
302 AlignFrame af = afArray[i];
303 List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
304 for (AlignmentViewPanel avp : avpList)
306 AlignmentI a = avp.getAlignment();
307 List<SequenceI> sList = a.getSequences();
308 for (SequenceI s : sList)
310 size += s.getLength();
315 int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
316 Math.max(MIN_WAIT_FOR_SAVE, size / 2));
317 Console.debug("Set waitForSave to " + waitTime);
320 boolean doIterations = true; // note iterations not used in the gui now,
321 // only one pass without the "Wait" button
322 while (doIterations && BackupFiles.hasSavesInProgress()
323 && iteration++ < (interactive ? 100 : 5))
325 // future that returns a Boolean when all files are saved
326 CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
328 // callback as each file finishes saving
329 for (CompletableFuture<Boolean> cf : BackupFiles
330 .savesInProgressCompletableFutures(false))
332 // if this is the last one then complete filesAllSaved
333 cf.whenComplete((ret, e) -> {
334 if (!BackupFiles.hasSavesInProgress())
336 filesAllSaved.complete(true);
342 filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
343 } catch (InterruptedException | ExecutionException e1)
346 "Exception whilst waiting for files to save before quit",
348 } catch (TimeoutException e2)
350 // this Exception to be expected
353 if (interactive && BackupFiles.hasSavesInProgress())
355 boolean showForceQuit = iteration > 0; // iteration > 1 to not show
356 // force quit the first time
357 JFrame parent = new JFrame();
358 JButton[] buttons = { new JButton(), new JButton() };
359 JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
360 JTextPane messagePane = new JTextPane();
361 messagePane.setBackground(waitDialog.getBackground());
362 messagePane.setBorder(null);
363 messagePane.setText(waitingForSaveMessage());
364 // callback as each file finishes saving
365 for (CompletableFuture<Boolean> cf : BackupFiles
366 .savesInProgressCompletableFutures(false))
368 cf.whenComplete((ret, e) -> {
369 if (BackupFiles.hasSavesInProgress())
371 // update the list of saving files as they save too
372 messagePane.setText(waitingForSaveMessage());
376 if (!(quitCancelled()))
378 for (int i = 0; i < buttons.length; i++)
380 Console.debug("DISABLING BUTTON " + buttons[i].getText());
381 buttons[i].setEnabled(false);
382 buttons[i].setVisible(false);
384 // if this is the last one then close the dialog
385 messagePane.setText(new StringBuilder()
386 .append(MessageManager.getString("label.all_saved"))
388 .append(MessageManager
389 .getString("label.quitting_bye"))
391 messagePane.setEditable(false);
395 } catch (InterruptedException e1)
408 options = new String[2];
409 options[0] = MessageManager.getString("action.force_quit");
410 options[1] = MessageManager.getString("action.cancel_quit");
411 dialogType = JOptionPane.YES_NO_OPTION;
412 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
413 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
417 options = new String[1];
418 options[0] = MessageManager.getString("action.cancel_quit");
419 dialogType = JOptionPane.YES_OPTION;
420 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
422 waitDialog.showDialogOnTopAsync(parent, messagePane,
423 MessageManager.getString("label.wait_for_save"), dialogType,
424 JOptionPane.WARNING_MESSAGE, null, options,
425 MessageManager.getString("action.cancel_quit"), true,
429 final QResponse thisWaitResponse = gotQuitResponse();
430 switch (thisWaitResponse)
432 case QUIT: // wait -- do another iteration
435 doIterations = false;
438 doIterations = false;
440 case NULL: // already cancelled
441 doIterations = false;
445 } // end if interactive
447 } // end while wait iteration loop
448 return gotQuitResponse();
451 private static String waitingForSaveMessage()
453 StringBuilder messageSB = new StringBuilder();
455 messageSB.append(MessageManager.getString("label.save_in_progress"));
456 List<File> files = BackupFiles.savesInProgressFiles(false);
457 boolean any = files.size() > 0;
460 for (File file : files)
462 messageSB.append("\n\u2022 ").append(file.getName());
467 messageSB.append(MessageManager.getString("label.unknown"));
469 messageSB.append("\n\n")
470 .append(MessageManager.getString("label.quit_after_saving"));
471 return messageSB.toString();
474 public static void abortQuit()
476 setResponse(QResponse.NULL);
477 // executor.shutdownNow();
480 private static JvOptionPane quitDialog = null;
482 private static void setQuitDialog(JvOptionPane qd)
487 private static JvOptionPane getQuitDialog()
492 public static boolean quitCancelled()
494 return QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
495 || QuitHandler.gotQuitResponse() == QResponse.NULL;
498 public static boolean quitting()
500 return QuitHandler.gotQuitResponse() == QResponse.QUIT
501 || QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT;