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 private static ExecutorService executor = Executors.newFixedThreadPool(3);
46 public static QResponse setQuitHandler()
48 FlatDesktop.setQuitHandler(response -> {
49 Callable<Void> performQuit = () -> {
50 response.performQuit();
51 setResponse(QResponse.QUIT);
54 Callable<Void> performForceQuit = () -> {
55 response.performQuit();
56 setResponse(QResponse.FORCE_QUIT);
59 Callable<Void> cancelQuit = () -> {
60 response.cancelQuit();
62 setResponse(QResponse.NULL);
65 getQuitResponse(true, performQuit, performForceQuit, cancelQuit);
68 return gotQuitResponse();
71 private static QResponse gotQuitResponse = QResponse.NULL;
73 protected static QResponse setResponse(QResponse qresponse)
75 gotQuitResponse = qresponse;
79 public static QResponse gotQuitResponse()
81 return gotQuitResponse;
84 public static final Callable<Void> defaultCancelQuit = () -> {
85 Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
87 setResponse(QResponse.CANCEL_QUIT);
91 public static final Callable<Void> defaultOkQuit = () -> {
92 Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
93 setResponse(QResponse.QUIT);
97 public static final Callable<Void> defaultForceQuit = () -> {
98 Console.debug("QuitHandler: (default) Quit action FORCED by user");
99 // note that shutdown hook will not be run
100 Runtime.getRuntime().halt(0);
101 setResponse(QResponse.FORCE_QUIT); // this line never reached!
105 public static QResponse getQuitResponse(boolean ui)
107 return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
111 public static QResponse getQuitResponse(boolean ui, Callable<Void> okQuit,
112 Callable<Void> forceQuit, Callable<Void> cancelQuit)
114 QResponse got = gotQuitResponse();
115 if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
117 // quit has already been selected, continue with calling quit method
121 interactive = ui && !Platform.isHeadless();
122 // confirm quit if needed and wanted
123 boolean confirmQuit = true;
127 Console.debug("Non interactive quit -- not confirming");
130 else if (Jalview2XML.allSavedUpToDate())
132 Console.debug("Nothing changed -- not confirming quit");
137 confirmQuit = jalview.bin.Cache
138 .getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true);
139 Console.debug("Jalview property '"
140 + jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT
141 + "' is/defaults to " + confirmQuit + " -- "
142 + (confirmQuit ? "" : "not ") + "confirming quit");
144 got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
149 setQuitDialog(JvOptionPane.newOptionDialog()
150 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
151 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit));
152 JvOptionPane qd = getQuitDialog();
153 qd.showDialogOnTopAsync(
155 MessageManager.getString("label.quit_jalview"))
157 .append(MessageManager
158 .getString("label.unsaved_changes"))
160 MessageManager.getString("action.quit"),
161 JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
163 { MessageManager.getString("action.quit"),
164 MessageManager.getString("action.cancel") },
165 MessageManager.getString("action.quit"), true);
168 got = gotQuitResponse();
170 // check for external viewer frames
171 if (got != QResponse.CANCEL_QUIT)
173 int count = Desktop.instance.structureViewersStillRunningCount();
176 String prompt = MessageManager
177 .formatMessage(count == 1 ? "label.confirm_quit_viewer"
178 : "label.confirm_quit_viewers");
179 String title = MessageManager.getString(
180 count == 1 ? "label.close_viewer" : "label.close_viewers");
181 String cancelQuitText = MessageManager
182 .getString("action.cancel_quit");
183 String[] buttonsText = { MessageManager.getString("action.yes"),
184 MessageManager.getString("action.no"), cancelQuitText };
186 int confirmResponse = JvOptionPane.showOptionDialog(
187 Desktop.instance, prompt, title,
188 JvOptionPane.YES_NO_CANCEL_OPTION,
189 JvOptionPane.WARNING_MESSAGE, null, buttonsText,
192 if (confirmResponse == JvOptionPane.CANCEL_OPTION)
195 QuitHandler.setResponse(QResponse.CANCEL_QUIT);
199 // Close viewers/Leave viewers open
201 .setQuitClose(confirmResponse == JvOptionPane.YES_OPTION);
207 got = gotQuitResponse();
209 boolean wait = false;
210 if (got == QResponse.CANCEL_QUIT)
213 Console.debug("Cancelling quit. Resetting response to NULL");
214 setResponse(QResponse.NULL);
216 return QResponse.CANCEL_QUIT;
218 else if (got == QResponse.QUIT)
220 if (Cache.getDefault("WAIT_FOR_SAVE", true)
221 && BackupFiles.hasSavesInProgress())
223 waitQuit(interactive, okQuit, forceQuit, cancelQuit);
224 QResponse waitResponse = gotQuitResponse();
225 wait = waitResponse == QResponse.QUIT;
229 Callable<Void> next = null;
230 switch (gotQuitResponse())
235 case FORCE_QUIT: // not actually an option at this stage
244 executor.submit(next).get();
245 got = gotQuitResponse();
246 } catch (RejectedExecutionException e)
248 // QuitHander.abortQuit() probably called
249 // CANCEL_QUIT test will reset QuitHandler
250 Console.info("Quit aborted!");
251 got = QResponse.NULL;
252 setResponse(QResponse.NULL);
253 } catch (InterruptedException | ExecutionException e)
256 .debug("Exception during quit handling (final choice)", e);
262 // reset if cancelled
263 Console.debug("Quit cancelled");
264 setResponse(QResponse.NULL);
265 return QResponse.CANCEL_QUIT;
267 return gotQuitResponse();
270 private static QResponse waitQuit(boolean interactive,
271 Callable<Void> okQuit, Callable<Void> forceQuit,
272 Callable<Void> cancelQuit)
274 // check for saves in progress
275 if (!BackupFiles.hasSavesInProgress())
276 return QResponse.QUIT;
279 AlignFrame[] afArray = Desktop.getAlignFrames();
280 if (!(afArray == null || afArray.length == 0))
282 for (int i = 0; i < afArray.length; i++)
284 AlignFrame af = afArray[i];
285 List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
286 for (AlignmentViewPanel avp : avpList)
288 AlignmentI a = avp.getAlignment();
289 List<SequenceI> sList = a.getSequences();
290 for (SequenceI s : sList)
292 size += s.getLength();
297 int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
298 Math.max(MIN_WAIT_FOR_SAVE, size / 2));
299 Console.debug("Set waitForSave to " + waitTime);
302 boolean doIterations = true; // note iterations not used in the gui now,
303 // only one pass without the "Wait" button
304 while (doIterations && BackupFiles.hasSavesInProgress()
305 && iteration++ < (interactive ? 100 : 5))
307 // future that returns a Boolean when all files are saved
308 CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
310 // callback as each file finishes saving
311 for (CompletableFuture<Boolean> cf : BackupFiles
312 .savesInProgressCompletableFutures(false))
314 // if this is the last one then complete filesAllSaved
315 cf.whenComplete((ret, e) -> {
316 if (!BackupFiles.hasSavesInProgress())
318 filesAllSaved.complete(true);
324 filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
325 } catch (InterruptedException | ExecutionException e1)
328 "Exception whilst waiting for files to save before quit",
330 } catch (TimeoutException e2)
332 // this Exception to be expected
335 if (interactive && BackupFiles.hasSavesInProgress())
337 boolean showForceQuit = iteration > 0; // iteration > 1 to not show
338 // force quit the first time
339 JFrame parent = new JFrame();
340 JButton[] buttons = { new JButton(), new JButton() };
341 JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
342 JTextPane messagePane = new JTextPane();
343 messagePane.setBackground(waitDialog.getBackground());
344 messagePane.setBorder(null);
345 messagePane.setText(waitingForSaveMessage());
346 // callback as each file finishes saving
347 for (CompletableFuture<Boolean> cf : BackupFiles
348 .savesInProgressCompletableFutures(false))
350 cf.whenComplete((ret, e) -> {
351 if (BackupFiles.hasSavesInProgress())
353 // update the list of saving files as they save too
354 messagePane.setText(waitingForSaveMessage());
358 if (!(quitCancelled()))
360 for (int i = 0; i < buttons.length; i++)
362 Console.debug("DISABLING BUTTON " + buttons[i].getText());
363 buttons[i].setEnabled(false);
364 buttons[i].setVisible(false);
366 // if this is the last one then close the dialog
367 messagePane.setText(new StringBuilder()
368 .append(MessageManager.getString("label.all_saved"))
370 .append(MessageManager
371 .getString("label.quitting_bye"))
373 messagePane.setEditable(false);
377 } catch (InterruptedException e1)
390 options = new String[2];
391 options[0] = MessageManager.getString("action.force_quit");
392 options[1] = MessageManager.getString("action.cancel_quit");
393 dialogType = JOptionPane.YES_NO_OPTION;
394 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
395 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
399 options = new String[1];
400 options[0] = MessageManager.getString("action.cancel_quit");
401 dialogType = JOptionPane.YES_OPTION;
402 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
404 waitDialog.showDialogOnTopAsync(parent, messagePane,
405 MessageManager.getString("label.wait_for_save"), dialogType,
406 JOptionPane.WARNING_MESSAGE, null, options,
407 MessageManager.getString("action.cancel_quit"), true,
411 final QResponse thisWaitResponse = gotQuitResponse();
412 switch (thisWaitResponse)
414 case QUIT: // wait -- do another iteration
417 doIterations = false;
420 doIterations = false;
422 case NULL: // already cancelled
423 doIterations = false;
427 } // end if interactive
429 } // end while wait iteration loop
430 return gotQuitResponse();
433 private static String waitingForSaveMessage()
435 StringBuilder messageSB = new StringBuilder();
437 messageSB.append(MessageManager.getString("label.save_in_progress"));
438 List<File> files = BackupFiles.savesInProgressFiles(false);
439 boolean any = files.size() > 0;
442 for (File file : files)
444 messageSB.append("\n\u2022 ").append(file.getName());
449 messageSB.append(MessageManager.getString("label.unknown"));
451 messageSB.append("\n\n")
452 .append(MessageManager.getString("label.quit_after_saving"));
453 return messageSB.toString();
456 public static void abortQuit()
458 setResponse(QResponse.NULL);
459 // executor.shutdownNow();
462 private static JvOptionPane quitDialog = null;
464 private static void setQuitDialog(JvOptionPane qd)
469 private static JvOptionPane getQuitDialog()
474 public static boolean quitCancelled()
476 return QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
477 || QuitHandler.gotQuitResponse() == QResponse.NULL;
480 public static boolean quitting()
482 return QuitHandler.gotQuitResponse() == QResponse.QUIT
483 || QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT;