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);
263 final int waitTimeFinal = waitTime;
264 QResponse waitResponse = QResponse.NULL;
266 // future that returns a Boolean when all files are saved
267 CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
269 // callback as each file finishes saving
270 for (CompletableFuture<Boolean> cf : BackupFiles
271 .savesInProgressCompletableFutures())
273 // if this is the last one then complete filesAllSaved
274 cf.whenComplete((ret, e) -> {
275 if (!BackupFiles.hasSavesInProgress())
277 filesAllSaved.complete(true);
282 // timeout the wait -- will result in another wait button when looped
283 CompletableFuture<Boolean> waitTimeout = CompletableFuture
285 Console.debug("################# STARTING WAIT SLEEP");
288 Thread.sleep(waitTimeFinal);
289 } catch (InterruptedException e)
291 // probably interrupted by all files saving
295 CompletableFuture<Object> waitForSave = CompletableFuture
296 .anyOf(waitTimeout, filesAllSaved);
299 boolean doIterations = true;
300 while (doIterations && BackupFiles.hasSavesInProgress()
301 && iteration++ < (interactive ? 100 : 5))
305 waitForSave.copy().get();
306 } catch (InterruptedException | ExecutionException e1)
309 "Exception whilst waiting for files to save before quit",
312 if (interactive && BackupFiles.hasSavesInProgress())
314 Console.debug("********************About to make waitDialog");
315 JFrame parent = new JFrame();
316 JvOptionPane waitDialog = JvOptionPane.newOptionDialog()
317 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
318 .setResponseHandler(JOptionPane.NO_OPTION, forceQuit)
319 .setResponseHandler(JOptionPane.CANCEL_OPTION, cancelQuit);
321 // callback as each file finishes saving
322 for (CompletableFuture<Boolean> cf : BackupFiles
323 .savesInProgressCompletableFutures())
325 cf.whenComplete((ret, e) -> {
326 Console.debug("############# A FILE SAVED!");
327 // update the list of saving files as they save too
328 waitDialog.setMessage(waitingForSaveMessage());
329 waitDialog.setName("AAARGH!");
330 // if this is the last one then close the dialog
331 if (!BackupFiles.hasSavesInProgress())
333 // like a click on Wait button ???
335 "***** TRYING TO MAKE THE WAIT FOR SAVE DIALOG DISAPPEAR!");
336 waitDialog.setValue(JOptionPane.YES_OPTION);
342 waitDialog.showDialogOnTopAsync(parent, waitingForSaveMessage(),
343 MessageManager.getString("action.wait"),
344 JOptionPane.YES_NO_CANCEL_OPTION,
345 JOptionPane.WARNING_MESSAGE, null, new Object[]
346 { MessageManager.getString("action.wait"),
347 MessageManager.getString("action.force_quit"),
348 MessageManager.getString("action.cancel_quit") },
349 MessageManager.getString("action.wait"), true);
350 Console.debug("********************Finished waitDialog");
352 final QResponse thisWaitResponse = gotQuitResponse();
353 Console.debug("####### WAITFORSAVE SET: " + thisWaitResponse);
354 switch (thisWaitResponse)
356 case QUIT: // wait -- do another iteration
359 doIterations = false;
362 doIterations = false;
364 case NULL: // already cancelled
365 doIterations = false;
369 } // end if interactive
371 } // end while wait iteration loop
372 waitResponse = gotQuitResponse();
374 Console.debug("####### WAITFORSAVE RETURNING: " + waitResponse);
378 public static void okk()
386 waitLonger = JOptionPane.showOptionDialog(dialogParent,
387 waitingForSaveMessage(),
388 MessageManager.getString("action.wait"),
389 JOptionPane.YES_NO_CANCEL_OPTION,
390 JOptionPane.WARNING_MESSAGE, null, options, wait);
395 waitLonger = iteration < NON_INTERACTIVE_WAIT_CYCLES
396 ? JOptionPane.YES_OPTION
397 : JOptionPane.NO_OPTION;
400 if (waitLonger == JOptionPane.YES_OPTION) // "wait"
402 saving = !waitForSave(waitIncrement);
404 else if (waitLonger == JOptionPane.NO_OPTION) // "force
408 return setResponse(QResponse.FORCE_QUIT);
410 else if (waitLonger == JOptionPane.CANCEL_OPTION) // cancel quit
412 return setResponse(QResponse.CANCEL_QUIT);
416 // Most likely got here by user dismissing the dialog with the
418 // -- treat as a "Cancel"
419 return setResponse(QResponse.CANCEL_QUIT);
423 // not sure how we got here, best be safe
424 return QResponse.CANCEL_QUIT;
428 private static int waitForceQuitCancelQuitOptionDialog(Object message,
431 JFrame dialogParent = new JFrame();
432 dialogParent.setAlwaysOnTop(true);
433 String wait = MessageManager.getString("action.wait");
434 Object[] options = { wait,
435 MessageManager.getString("action.force_quit"),
436 MessageManager.getString("action.cancel_quit") };
438 // BackupFiles.setWaitForSaveDialog(dialogParent);
440 int answer = JOptionPane.showOptionDialog(dialogParent, message, title,
441 JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE,
442 null, options, wait);
444 // BackupFiles.clearWaitForSaveDialog();
449 private static String waitingForSaveMessage()
451 StringBuilder messageSB = new StringBuilder(
452 MessageManager.getString("label.save_in_progress"));
454 for (File file : BackupFiles.savesInProgressFiles())
456 messageSB.append("\n- ");
457 messageSB.append(file.getName());
462 messageSB.append("\n");
463 messageSB.append(MessageManager.getString("label.unknown"));
466 return messageSB.toString();
469 private static Boolean waitForSave(long t)
474 Console.debug("Wait for save to complete: " + t + "ms");
481 ret = !BackupFiles.hasSavesInProgress();
485 "Save completed whilst waiting (" + c + "/" + t + "ms)");
488 if (c % 1000 < i) // just gone over another second
490 Console.debug("...waiting (" + c + "/" + t + "ms]");
493 } catch (InterruptedException e)
495 Console.debug("Wait for save interrupted");
497 Console.debug("Save has " + (ret ? "" : "not ") + "completed");