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;
11 import javax.swing.JFrame;
12 import javax.swing.JOptionPane;
14 import com.formdev.flatlaf.extras.FlatDesktop;
16 import jalview.api.AlignmentViewPanel;
17 import jalview.bin.Cache;
18 import jalview.bin.Console;
19 import jalview.datamodel.AlignmentI;
20 import jalview.datamodel.SequenceI;
21 import jalview.gui.AlignFrame;
22 import jalview.gui.Desktop;
23 import jalview.gui.JvOptionPane;
24 import jalview.io.BackupFiles;
25 import jalview.project.Jalview2XML;
26 import jalview.util.MessageManager;
27 import jalview.util.Platform;
29 public class QuitHandler
31 private static final int INITIAL_WAIT_FOR_SAVE = 3000;
33 private static final int NON_INTERACTIVE_WAIT_CYCLES = 2;
35 public static enum QResponse
37 NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
40 private static ExecutorService executor = Executors.newFixedThreadPool(3);
42 public static QResponse setQuitHandler()
44 FlatDesktop.setQuitHandler(response -> {
45 Callable<QResponse> performQuit = () -> {
46 response.performQuit();
47 return setResponse(QResponse.QUIT);
49 Callable<QResponse> performForceQuit = () -> {
50 response.performQuit();
51 return setResponse(QResponse.FORCE_QUIT);
53 Callable<QResponse> cancelQuit = () -> {
54 response.cancelQuit();
56 setResponse(QResponse.NULL);
58 return QResponse.CANCEL_QUIT;
60 QResponse qresponse = getQuitResponse(true, performQuit,
61 performForceQuit, cancelQuit);
64 return gotQuitResponse();
67 private static QResponse gotQuitResponse = QResponse.NULL;
69 private static QResponse setResponse(QResponse qresponse)
71 gotQuitResponse = qresponse;
72 Console.debug("##### Setting gotQuitResponse to " + qresponse);
76 public static QResponse gotQuitResponse()
78 return gotQuitResponse;
81 public static final Callable<QResponse> defaultCancelQuit = () -> {
82 Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
84 setResponse(QResponse.NULL);
86 return QResponse.CANCEL_QUIT;
89 public static final Callable<QResponse> defaultOkQuit = () -> {
90 Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
91 return setResponse(QResponse.QUIT);
94 public static final Callable<QResponse> defaultForceQuit = () -> {
95 Console.debug("QuitHandler: (default) Quit action FORCED by user");
96 // note that shutdown hook will not be run
97 Runtime.getRuntime().halt(0);
98 return setResponse(QResponse.FORCE_QUIT); // this line never reached!
101 public static QResponse getQuitResponse(boolean ui)
103 return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
107 private static boolean interactive = true;
109 public static QResponse getQuitResponse(boolean ui,
110 Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
111 Callable<QResponse> cancelQuit)
113 QResponse got = gotQuitResponse();
114 if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
116 // quit has already been selected, continue with calling quit method
117 Console.debug("##### getQuitResponse called. gotQuitResponse=" + got);
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;
145 Console.debug("initial calculation, got=" + got);
151 Console.debug("********************ABOUT TO CONFIRM QUIT");
152 JvOptionPane quitDialog = JvOptionPane.newOptionDialog()
153 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
154 .setResponseHandler(JOptionPane.NO_OPTION, defaultCancelQuit);
155 quitDialog.showDialogOnTopAsync(
157 MessageManager.getString("label.quit_jalview"))
159 .append(MessageManager
160 .getString("label.unsaved_changes"))
162 MessageManager.getString("action.quit"),
163 JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
165 { MessageManager.getString("action.quit"),
166 MessageManager.getString("action.cancel") },
167 MessageManager.getString("action.quit"), true);
170 got = gotQuitResponse();
171 Console.debug("first user response, got=" + got);
172 boolean wait = false;
173 if (got == QResponse.CANCEL_QUIT)
176 setResponse(QResponse.NULL);
178 return QResponse.CANCEL_QUIT;
180 else if (got == QResponse.QUIT)
182 if (Cache.getDefault("WAIT_FOR_SAVE", true)
183 && BackupFiles.hasSavesInProgress())
186 Future<QResponse> waitGot = executor.submit(waitQuitCall);
190 } catch (InterruptedException | ExecutionException e)
192 jalview.bin.Console.debug(
193 "Exception during quit handling (wait for save)", e);
196 QResponse waitResponse = waitQuit(interactive, okQuit, forceQuit,
198 wait = waitResponse == QResponse.QUIT;
202 Callable<QResponse> next = null;
203 switch (gotQuitResponse())
206 Console.debug("### User selected QUIT");
209 case FORCE_QUIT: // not actually an option at this stage
210 Console.debug("### User selected FORCE QUIT");
214 Console.debug("### User selected CANCEL QUIT");
220 got = executor.submit(next).get();
221 } catch (InterruptedException | ExecutionException e)
224 .debug("Exception during quit handling (final choice)", e);
226 jalview.bin.Console.debug("### nextResponse=" + got.toString());
229 return gotQuitResponse();
232 private static QResponse waitQuit(boolean interactive,
233 Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
234 Callable<QResponse> cancelQuit)
236 jalview.bin.Console.debug("#### waitQuit started");
237 // check for saves in progress
238 if (!BackupFiles.hasSavesInProgress())
239 return QResponse.QUIT;
241 int waitTime = INITIAL_WAIT_FOR_SAVE; // start with 3 second wait
242 AlignFrame[] afArray = Desktop.getAlignFrames();
243 if (!(afArray == null || afArray.length == 0))
246 for (int i = 0; i < afArray.length; i++)
248 AlignFrame af = afArray[i];
249 List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
250 for (AlignmentViewPanel avp : avpList)
252 AlignmentI a = avp.getAlignment();
253 List<SequenceI> sList = a.getSequences();
254 for (SequenceI s : sList)
256 size += s.getLength();
260 waitTime = Math.max(waitTime, size / 2);
261 Console.debug("Set waitForSave to " + waitTime);
264 // future that returns a Boolean when all files are saved
265 CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
267 // callback as each file finishes saving
268 for (CompletableFuture<Boolean> cf : BackupFiles
269 .savesInProgressCompletableFutures())
271 // if this is the last one then complete filesAllSaved
272 cf.whenComplete((ret, e) -> {
273 if (!BackupFiles.hasSavesInProgress())
275 filesAllSaved.complete(true);
280 final int waitTimeFinal = waitTime;
281 // timeout the wait -- will result in another wait button when looped
282 CompletableFuture<Boolean> waitTimeout = CompletableFuture
284 executor.submit(() -> {
287 Thread.sleep(waitTimeFinal);
288 } catch (InterruptedException e)
290 // probably interrupted by all files saving
295 CompletableFuture<Object> waitForSave = CompletableFuture
296 .anyOf(waitTimeout, filesAllSaved);
297 Console.debug("##### WAITFORSAVE RUNNING");
299 QResponse waitResponse = QResponse.NULL;
302 boolean doIterations = true;
303 while (doIterations && BackupFiles.hasSavesInProgress()
304 && iteration++ < (interactive ? 100 : 5))
309 } catch (InterruptedException | ExecutionException e1)
312 "Exception whilst waiting for files to save before quitting",
317 Console.debug("********************About to make waitDialog");
318 JvOptionPane waitDialog = JvOptionPane.newOptionDialog()
319 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
320 .setResponseHandler(JOptionPane.NO_OPTION, forceQuit)
321 .setResponseHandler(JOptionPane.CANCEL_OPTION, cancelQuit);
323 // callback as each file finishes saving
324 for (CompletableFuture<Boolean> cf : BackupFiles
325 .savesInProgressCompletableFutures())
327 // update the list of saving files as they save too
329 waitDialog.setMessage(waitingForSaveMessage());
331 // if this is the last one then close the dialog
332 cf.whenComplete((ret, e) -> {
333 if (!BackupFiles.hasSavesInProgress())
335 // like a click on Wait button
337 "***** TRYING TO MAKE THE WAIT FOR SAVE DIALOG DISAPPEAR!");
338 waitDialog.setValue(JOptionPane.YES_OPTION);
343 waitDialog.showDialogOnTopAsync(waitingForSaveMessage(),
344 MessageManager.getString("action.wait"),
345 JOptionPane.YES_NO_CANCEL_OPTION,
346 JOptionPane.WARNING_MESSAGE, null, new Object[]
347 { MessageManager.getString("action.wait"),
348 MessageManager.getString("action.force_quit"),
349 MessageManager.getString("action.cancel_quit") },
350 MessageManager.getString("action.wait"), true);
351 Console.debug("********************Finished waitDialog");
353 waitResponse = gotQuitResponse();
354 Console.debug("####### WAITFORSAVE SET: " + waitResponse);
355 switch (waitResponse)
357 case QUIT: // wait -- do another iteration
360 doIterations = false;
363 doIterations = false;
365 case NULL: // already cancelled
366 doIterations = false;
370 } // end if interactive
373 waitResponse = gotQuitResponse();
375 Console.debug("####### WAITFORSAVE RETURNING: " + waitResponse);
379 public static void okk()
387 waitLonger = JOptionPane.showOptionDialog(dialogParent,
388 waitingForSaveMessage(),
389 MessageManager.getString("action.wait"),
390 JOptionPane.YES_NO_CANCEL_OPTION,
391 JOptionPane.WARNING_MESSAGE, null, options, wait);
396 waitLonger = iteration < NON_INTERACTIVE_WAIT_CYCLES
397 ? JOptionPane.YES_OPTION
398 : JOptionPane.NO_OPTION;
401 if (waitLonger == JOptionPane.YES_OPTION) // "wait"
403 saving = !waitForSave(waitIncrement);
405 else if (waitLonger == JOptionPane.NO_OPTION) // "force
409 return setResponse(QResponse.FORCE_QUIT);
411 else if (waitLonger == JOptionPane.CANCEL_OPTION) // cancel quit
413 return setResponse(QResponse.CANCEL_QUIT);
417 // Most likely got here by user dismissing the dialog with the
419 // -- treat as a "Cancel"
420 return setResponse(QResponse.CANCEL_QUIT);
424 // not sure how we got here, best be safe
425 return QResponse.CANCEL_QUIT;
429 private static int waitForceQuitCancelQuitOptionDialog(Object message,
432 JFrame dialogParent = new JFrame();
433 dialogParent.setAlwaysOnTop(true);
434 String wait = MessageManager.getString("action.wait");
435 Object[] options = { wait,
436 MessageManager.getString("action.force_quit"),
437 MessageManager.getString("action.cancel_quit") };
439 // BackupFiles.setWaitForSaveDialog(dialogParent);
441 int answer = JOptionPane.showOptionDialog(dialogParent, message, title,
442 JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE,
443 null, options, wait);
445 // BackupFiles.clearWaitForSaveDialog();
450 private static String waitingForSaveMessage()
452 StringBuilder messageSB = new StringBuilder(
453 MessageManager.getString("label.save_in_progress"));
455 for (File file : BackupFiles.savesInProgressFiles())
457 messageSB.append("\n- ");
458 messageSB.append(file.getName());
463 messageSB.append("\n");
464 messageSB.append(MessageManager.getString("label.unknown"));
467 return messageSB.toString();
470 private static Boolean waitForSave(long t)
475 Console.debug("Wait for save to complete: " + t + "ms");
482 ret = !BackupFiles.hasSavesInProgress();
486 "Save completed whilst waiting (" + c + "/" + t + "ms)");
489 if (c % 1000 < i) // just gone over another second
491 Console.debug("...waiting (" + c + "/" + t + "ms]");
494 } catch (InterruptedException e)
496 Console.debug("Wait for save interrupted");
498 Console.debug("Save has " + (ret ? "" : "not ") + "completed");