5 import java.util.concurrent.CompletableFuture;
6 import java.util.concurrent.ExecutionException;
7 import java.util.concurrent.ExecutorService;
8 import java.util.concurrent.Executors;
9 import java.util.concurrent.RejectedExecutionException;
10 import java.util.concurrent.TimeUnit;
11 import java.util.concurrent.TimeoutException;
13 import javax.swing.JButton;
14 import javax.swing.JFrame;
15 import javax.swing.JOptionPane;
16 import javax.swing.JTextPane;
18 import com.formdev.flatlaf.extras.FlatDesktop;
20 import jalview.api.AlignmentViewPanel;
21 import jalview.bin.Cache;
22 import jalview.bin.Console;
23 import jalview.datamodel.AlignmentI;
24 import jalview.datamodel.SequenceI;
25 import jalview.io.BackupFiles;
26 import jalview.project.Jalview2XML;
27 import jalview.util.MessageManager;
28 import jalview.util.Platform;
30 public class QuitHandler
32 private static final int MIN_WAIT_FOR_SAVE = 1000;
34 private static final int MAX_WAIT_FOR_SAVE = 20000;
36 private static boolean interactive = true;
38 public static enum QResponse
40 NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
43 public static enum Message
45 UNSAVED_CHANGES, UNSAVED_ALIGNMENTS
48 protected static Message message = Message.UNSAVED_CHANGES;
50 public static void setMessage(Message m)
55 private static ExecutorService executor = Executors.newFixedThreadPool(3);
57 public static QResponse setQuitHandler()
59 FlatDesktop.setQuitHandler(response -> {
60 Runnable performQuit = () -> {
61 response.performQuit();
62 setResponse(QResponse.QUIT);
64 Runnable performForceQuit = () -> {
65 response.performQuit();
66 setResponse(QResponse.FORCE_QUIT);
68 Runnable cancelQuit = () -> {
69 response.cancelQuit();
71 setResponse(QResponse.NULL);
73 getQuitResponse(true, performQuit, performForceQuit, cancelQuit);
76 return gotQuitResponse();
79 private static QResponse gotQuitResponse = QResponse.NULL;
81 protected static QResponse setResponse(QResponse qresponse)
83 gotQuitResponse = qresponse;
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 prompt = MessageManager
184 .formatMessage(count == 1 ? "label.confirm_quit_viewer"
185 : "label.confirm_quit_viewers");
186 String title = MessageManager.getString(
187 count == 1 ? "label.close_viewer" : "label.close_viewers");
188 String cancelQuitText = MessageManager
189 .getString("action.cancel_quit");
190 String[] buttonsText = { MessageManager.getString("action.yes"),
191 MessageManager.getString("action.no"), cancelQuitText };
193 int confirmResponse = JvOptionPane.showOptionDialog(
194 Desktop.instance, prompt, title,
195 JvOptionPane.YES_NO_CANCEL_OPTION,
196 JvOptionPane.WARNING_MESSAGE, null, buttonsText,
199 if (confirmResponse == JvOptionPane.CANCEL_OPTION)
202 QuitHandler.setResponse(QResponse.CANCEL_QUIT);
206 // Close viewers/Leave viewers open
208 .setQuitClose(confirmResponse == JvOptionPane.YES_OPTION);
214 got = gotQuitResponse();
216 boolean wait = false;
217 if (got == QResponse.CANCEL_QUIT)
220 Console.debug("Cancelling quit. Resetting response to NULL");
221 setResponse(QResponse.NULL);
223 return QResponse.CANCEL_QUIT;
225 else if (got == QResponse.QUIT)
227 if (Cache.getDefault("WAIT_FOR_SAVE", true)
228 && BackupFiles.hasSavesInProgress())
230 waitQuit(interactive, okQuit, forceQuit, cancelQuit);
231 QResponse waitResponse = gotQuitResponse();
232 wait = waitResponse == QResponse.QUIT;
236 Runnable next = null;
237 switch (gotQuitResponse())
242 case FORCE_QUIT: // not actually an option at this stage
251 executor.submit(next).get();
252 got = gotQuitResponse();
253 } catch (RejectedExecutionException e)
255 // QuitHander.abortQuit() probably called
256 // CANCEL_QUIT test will reset QuitHandler
257 Console.info("Quit aborted!");
258 got = QResponse.NULL;
259 setResponse(QResponse.NULL);
260 } catch (InterruptedException | ExecutionException e)
263 .debug("Exception during quit handling (final choice)", e);
269 // reset if cancelled
270 Console.debug("Quit cancelled");
271 setResponse(QResponse.NULL);
272 return QResponse.CANCEL_QUIT;
274 return gotQuitResponse();
277 private static QResponse waitQuit(boolean interactive, Runnable okQuit,
278 Runnable forceQuit, Runnable cancelQuit)
280 // check for saves in progress
281 if (!BackupFiles.hasSavesInProgress())
282 return QResponse.QUIT;
285 AlignFrame[] afArray = Desktop.getAlignFrames();
286 if (!(afArray == null || afArray.length == 0))
288 for (int i = 0; i < afArray.length; i++)
290 AlignFrame af = afArray[i];
291 List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
292 for (AlignmentViewPanel avp : avpList)
294 AlignmentI a = avp.getAlignment();
295 List<SequenceI> sList = a.getSequences();
296 for (SequenceI s : sList)
298 size += s.getLength();
303 int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
304 Math.max(MIN_WAIT_FOR_SAVE, size / 2));
305 Console.debug("Set waitForSave to " + waitTime);
308 boolean doIterations = true; // note iterations not used in the gui now,
309 // only one pass without the "Wait" button
310 while (doIterations && BackupFiles.hasSavesInProgress()
311 && iteration++ < (interactive ? 100 : 5))
313 // future that returns a Boolean when all files are saved
314 CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
316 // callback as each file finishes saving
317 for (CompletableFuture<Boolean> cf : BackupFiles
318 .savesInProgressCompletableFutures(false))
320 // if this is the last one then complete filesAllSaved
321 cf.whenComplete((ret, e) -> {
322 if (!BackupFiles.hasSavesInProgress())
324 filesAllSaved.complete(true);
330 filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
331 } catch (InterruptedException | ExecutionException e1)
334 "Exception whilst waiting for files to save before quit",
336 } catch (TimeoutException e2)
338 // this Exception to be expected
341 if (interactive && BackupFiles.hasSavesInProgress())
343 boolean showForceQuit = iteration > 0; // iteration > 1 to not show
344 // force quit the first time
345 JFrame parent = new JFrame();
346 JButton[] buttons = { new JButton(), new JButton() };
347 JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
348 JTextPane messagePane = new JTextPane();
349 messagePane.setBackground(waitDialog.getBackground());
350 messagePane.setBorder(null);
351 messagePane.setText(waitingForSaveMessage());
352 // callback as each file finishes saving
353 for (CompletableFuture<Boolean> cf : BackupFiles
354 .savesInProgressCompletableFutures(false))
356 cf.whenComplete((ret, e) -> {
357 if (BackupFiles.hasSavesInProgress())
359 // update the list of saving files as they save too
360 messagePane.setText(waitingForSaveMessage());
364 if (!(quitCancelled()))
366 for (int i = 0; i < buttons.length; i++)
368 Console.debug("DISABLING BUTTON " + buttons[i].getText());
369 buttons[i].setEnabled(false);
370 buttons[i].setVisible(false);
372 // if this is the last one then close the dialog
373 messagePane.setText(new StringBuilder()
374 .append(MessageManager.getString("label.all_saved"))
376 .append(MessageManager
377 .getString("label.quitting_bye"))
379 messagePane.setEditable(false);
383 } catch (InterruptedException e1)
396 options = new String[2];
397 options[0] = MessageManager.getString("action.force_quit");
398 options[1] = MessageManager.getString("action.cancel_quit");
399 dialogType = JOptionPane.YES_NO_OPTION;
400 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
401 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
405 options = new String[1];
406 options[0] = MessageManager.getString("action.cancel_quit");
407 dialogType = JOptionPane.YES_OPTION;
408 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
410 waitDialog.showDialogOnTopAsync(parent, messagePane,
411 MessageManager.getString("label.wait_for_save"), dialogType,
412 JOptionPane.WARNING_MESSAGE, null, options,
413 MessageManager.getString("action.cancel_quit"), true,
417 final QResponse thisWaitResponse = gotQuitResponse();
418 switch (thisWaitResponse)
420 case QUIT: // wait -- do another iteration
423 doIterations = false;
426 doIterations = false;
428 case NULL: // already cancelled
429 doIterations = false;
433 } // end if interactive
435 } // end while wait iteration loop
436 return gotQuitResponse();
439 private static String waitingForSaveMessage()
441 StringBuilder messageSB = new StringBuilder();
443 messageSB.append(MessageManager.getString("label.save_in_progress"));
444 List<File> files = BackupFiles.savesInProgressFiles(false);
445 boolean any = files.size() > 0;
448 for (File file : files)
450 messageSB.append("\n\u2022 ").append(file.getName());
455 messageSB.append(MessageManager.getString("label.unknown"));
457 messageSB.append("\n\n")
458 .append(MessageManager.getString("label.quit_after_saving"));
459 return messageSB.toString();
462 public static void abortQuit()
464 setResponse(QResponse.NULL);
465 // executor.shutdownNow();
468 private static JvOptionPane quitDialog = null;
470 private static void setQuitDialog(JvOptionPane qd)
475 private static JvOptionPane getQuitDialog()
480 public static boolean quitCancelled()
482 return QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
483 || QuitHandler.gotQuitResponse() == QResponse.NULL;
486 public static boolean quitting()
488 return QuitHandler.gotQuitResponse() == QResponse.QUIT
489 || QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT;