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;
13 import javax.swing.JTextPane;
15 import com.formdev.flatlaf.extras.FlatDesktop;
17 import jalview.api.AlignmentViewPanel;
18 import jalview.bin.Cache;
19 import jalview.bin.Console;
20 import jalview.datamodel.AlignmentI;
21 import jalview.datamodel.SequenceI;
22 import jalview.gui.AlignFrame;
23 import jalview.gui.Desktop;
24 import jalview.gui.JvOptionPane;
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 INITIAL_WAIT_FOR_SAVE = 3000;
34 private static final int NON_INTERACTIVE_WAIT_CYCLES = 2;
36 public static enum QResponse
38 NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
41 private static ExecutorService executor = Executors.newFixedThreadPool(3);
43 public static QResponse setQuitHandler()
45 FlatDesktop.setQuitHandler(response -> {
46 Callable<QResponse> performQuit = () -> {
47 response.performQuit();
48 return setResponse(QResponse.QUIT);
50 Callable<QResponse> performForceQuit = () -> {
51 response.performQuit();
52 return setResponse(QResponse.FORCE_QUIT);
54 Callable<QResponse> cancelQuit = () -> {
55 response.cancelQuit();
57 setResponse(QResponse.NULL);
59 return QResponse.CANCEL_QUIT;
61 QResponse qresponse = getQuitResponse(true, performQuit,
62 performForceQuit, cancelQuit);
65 return gotQuitResponse();
68 private static QResponse gotQuitResponse = QResponse.NULL;
70 private static QResponse setResponse(QResponse qresponse)
72 gotQuitResponse = qresponse;
73 Console.debug("##### Setting gotQuitResponse to " + qresponse);
77 public static QResponse gotQuitResponse()
79 return gotQuitResponse;
82 public static final Callable<QResponse> defaultCancelQuit = () -> {
83 Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
85 setResponse(QResponse.NULL);
87 return QResponse.CANCEL_QUIT;
90 public static final Callable<QResponse> defaultOkQuit = () -> {
91 Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
92 return setResponse(QResponse.QUIT);
95 public static final Callable<QResponse> defaultForceQuit = () -> {
96 Console.debug("QuitHandler: (default) Quit action FORCED by user");
97 // note that shutdown hook will not be run
98 Runtime.getRuntime().halt(0);
99 return setResponse(QResponse.FORCE_QUIT); // this line never reached!
102 public static QResponse getQuitResponse(boolean ui)
104 return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
108 private static boolean interactive = true;
110 public static QResponse getQuitResponse(boolean ui,
111 Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
112 Callable<QResponse> 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
118 Console.debug("##### getQuitResponse called. gotQuitResponse=" + got);
122 interactive = ui && !Platform.isHeadless();
123 // confirm quit if needed and wanted
124 boolean confirmQuit = true;
128 Console.debug("Non interactive quit -- not confirming");
131 else if (Jalview2XML.allSavedUpToDate())
133 Console.debug("Nothing changed -- not confirming quit");
138 confirmQuit = jalview.bin.Cache
139 .getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true);
140 Console.debug("Jalview property '"
141 + jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT
142 + "' is/defaults to " + confirmQuit + " -- "
143 + (confirmQuit ? "" : "not ") + "confirming quit");
145 got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
146 Console.debug("initial calculation, got=" + got);
152 Console.debug("********************ABOUT TO CONFIRM QUIT");
153 JvOptionPane quitDialog = JvOptionPane.newOptionDialog()
154 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
155 .setResponseHandler(JOptionPane.NO_OPTION, defaultCancelQuit);
156 quitDialog.showDialogOnTopAsync(
158 MessageManager.getString("label.quit_jalview"))
160 .append(MessageManager
161 .getString("label.unsaved_changes"))
163 MessageManager.getString("action.quit"),
164 JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
166 { MessageManager.getString("action.quit"),
167 MessageManager.getString("action.cancel") },
168 MessageManager.getString("action.quit"), true);
171 got = gotQuitResponse();
172 Console.debug("first user response, got=" + got);
173 boolean wait = false;
174 if (got == QResponse.CANCEL_QUIT)
177 setResponse(QResponse.NULL);
179 return QResponse.CANCEL_QUIT;
181 else if (got == QResponse.QUIT)
183 if (Cache.getDefault("WAIT_FOR_SAVE", true)
184 && BackupFiles.hasSavesInProgress())
186 QResponse waitResponse = waitQuit(interactive, okQuit, forceQuit,
188 wait = waitResponse == QResponse.QUIT;
192 Callable<QResponse> next = null;
193 switch (gotQuitResponse())
196 Console.debug("### User selected QUIT");
199 case FORCE_QUIT: // not actually an option at this stage
200 Console.debug("### User selected FORCE QUIT");
204 Console.debug("### User selected CANCEL QUIT");
210 got = executor.submit(next).get();
211 } catch (InterruptedException | ExecutionException e)
214 .debug("Exception during quit handling (final choice)", e);
216 jalview.bin.Console.debug("### nextResponse=" + got.toString());
219 return gotQuitResponse();
222 private static QResponse waitQuit(boolean interactive,
223 Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
224 Callable<QResponse> cancelQuit)
226 jalview.bin.Console.debug("#### waitQuit started");
227 // check for saves in progress
228 if (!BackupFiles.hasSavesInProgress())
229 return QResponse.QUIT;
231 int waitTime = INITIAL_WAIT_FOR_SAVE; // start with 3 second wait
232 AlignFrame[] afArray = Desktop.getAlignFrames();
233 if (!(afArray == null || afArray.length == 0))
236 for (int i = 0; i < afArray.length; i++)
238 AlignFrame af = afArray[i];
239 List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
240 for (AlignmentViewPanel avp : avpList)
242 AlignmentI a = avp.getAlignment();
243 List<SequenceI> sList = a.getSequences();
244 for (SequenceI s : sList)
246 size += s.getLength();
250 waitTime = Math.max(waitTime, size / 2);
251 Console.debug("Set waitForSave to " + waitTime);
253 final int waitTimeFinal = waitTime;
254 QResponse waitResponse = QResponse.NULL;
256 // future that returns a Boolean when all files are saved
257 CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
259 // callback as each file finishes saving
260 for (CompletableFuture<Boolean> cf : BackupFiles
261 .savesInProgressCompletableFutures(false))
263 // if this is the last one then complete filesAllSaved
264 cf.whenComplete((ret, e) -> {
265 if (!BackupFiles.hasSavesInProgress())
267 filesAllSaved.complete(true);
272 // timeout the wait -- will result in another wait button when looped
273 CompletableFuture<Boolean> waitTimeout = CompletableFuture
275 Console.debug("################# STARTING WAIT SLEEP");
278 Thread.sleep(waitTimeFinal);
279 } catch (InterruptedException e)
281 // probably interrupted by all files saving
285 CompletableFuture<Object> waitForSave = CompletableFuture
286 .anyOf(waitTimeout, filesAllSaved);
289 boolean doIterations = true;
290 while (doIterations && BackupFiles.hasSavesInProgress()
291 && iteration++ < (interactive ? 100 : 5))
295 waitForSave.copy().get();
296 } catch (InterruptedException | ExecutionException e1)
299 "Exception whilst waiting for files to save before quit",
302 if (interactive && BackupFiles.hasSavesInProgress())
304 Console.debug("********************About to make waitDialog");
305 JFrame parent = new JFrame();
306 JvOptionPane waitDialog = JvOptionPane.newOptionDialog()
307 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
308 .setResponseHandler(JOptionPane.NO_OPTION, forceQuit)
309 .setResponseHandler(JOptionPane.CANCEL_OPTION, cancelQuit);
311 JTextPane messagePane = new JTextPane();
312 messagePane.setBackground(waitDialog.getBackground());
313 messagePane.setBorder(null);
314 messagePane.setText(waitingForSaveMessage());
315 // callback as each file finishes saving
316 for (CompletableFuture<Boolean> cf : BackupFiles
317 .savesInProgressCompletableFutures(false))
319 cf.whenComplete((ret, e) -> {
320 Console.debug("############# A FILE SAVED!");
321 // update the list of saving files as they save too
322 messagePane.setText(waitingForSaveMessage());
323 // if this is the last one then close the dialog
324 if (!BackupFiles.hasSavesInProgress())
326 // like a click on Wait button
327 waitDialog.setValue(JOptionPane.YES_OPTION);
333 waitDialog.showDialogOnTopAsync(parent, messagePane,
334 MessageManager.getString("action.wait"),
335 JOptionPane.YES_NO_CANCEL_OPTION,
336 JOptionPane.WARNING_MESSAGE, null, new Object[]
337 { MessageManager.getString("action.wait"),
338 MessageManager.getString("action.force_quit"),
339 MessageManager.getString("action.cancel_quit") },
340 MessageManager.getString("action.wait"), true);
341 Console.debug("********************Finished waitDialog");
343 final QResponse thisWaitResponse = gotQuitResponse();
344 Console.debug("####### WAITFORSAVE SET: " + thisWaitResponse);
345 switch (thisWaitResponse)
347 case QUIT: // wait -- do another iteration
350 doIterations = false;
353 doIterations = false;
355 case NULL: // already cancelled
356 doIterations = false;
360 } // end if interactive
362 } // end while wait iteration loop
363 waitResponse = gotQuitResponse();
365 Console.debug("####### WAITFORSAVE RETURNING: " + waitResponse);
369 private static int waitForceQuitCancelQuitOptionDialog(Object message,
372 JFrame dialogParent = new JFrame();
373 dialogParent.setAlwaysOnTop(true);
374 String wait = MessageManager.getString("action.wait");
375 Object[] options = { wait,
376 MessageManager.getString("action.force_quit"),
377 MessageManager.getString("action.cancel_quit") };
379 int answer = JOptionPane.showOptionDialog(dialogParent, message, title,
380 JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE,
381 null, options, wait);
386 private static String waitingForSaveMessage()
388 StringBuilder messageSB = new StringBuilder();
390 List<File> files = BackupFiles.savesInProgressFiles(false);
391 boolean any = files.size() > 0;
394 messageSB.append(MessageManager.getString("label.save_in_progress"));
395 for (File file : files)
397 messageSB.append("\n- ").append(file.getName());
402 messageSB.append(MessageManager.getString("label.all_saved"))
404 .append(MessageManager.getString("label.quitting_bye"));
406 return messageSB.toString();
409 private static Boolean waitForSave(long t)
414 Console.debug("Wait for save to complete: " + t + "ms");
421 ret = !BackupFiles.hasSavesInProgress();
425 "Save completed whilst waiting (" + c + "/" + t + "ms)");
428 if (c % 1000 < i) // just gone over another second
430 Console.debug("...waiting (" + c + "/" + t + "ms]");
433 } catch (InterruptedException e)
435 Console.debug("Wait for save interrupted");
437 Console.debug("Save has " + (ret ? "" : "not ") + "completed");