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.TimeUnit;
11 import java.util.concurrent.TimeoutException;
13 import javax.swing.JFrame;
14 import javax.swing.JOptionPane;
15 import javax.swing.JTextPane;
17 import com.formdev.flatlaf.extras.FlatDesktop;
19 import jalview.api.AlignmentViewPanel;
20 import jalview.bin.Cache;
21 import jalview.bin.Console;
22 import jalview.datamodel.AlignmentI;
23 import jalview.datamodel.SequenceI;
24 import jalview.gui.AlignFrame;
25 import jalview.gui.Desktop;
26 import jalview.gui.JvOptionPane;
27 import jalview.io.BackupFiles;
28 import jalview.project.Jalview2XML;
29 import jalview.util.MessageManager;
30 import jalview.util.Platform;
32 public class QuitHandler
34 private static final int MIN_WAIT_FOR_SAVE = 5000;
36 private static final int MAX_WAIT_FOR_SAVE = 20000;
38 private static final int NON_INTERACTIVE_WAIT_CYCLES = 2;
40 public static enum QResponse
42 NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
45 private static ExecutorService executor = Executors.newFixedThreadPool(3);
47 public static QResponse setQuitHandler()
49 FlatDesktop.setQuitHandler(response -> {
50 Callable<QResponse> performQuit = () -> {
51 response.performQuit();
52 return setResponse(QResponse.QUIT);
54 Callable<QResponse> performForceQuit = () -> {
55 response.performQuit();
56 return setResponse(QResponse.FORCE_QUIT);
58 Callable<QResponse> cancelQuit = () -> {
59 response.cancelQuit();
61 setResponse(QResponse.NULL);
63 return QResponse.CANCEL_QUIT;
65 QResponse qresponse = getQuitResponse(true, performQuit,
66 performForceQuit, cancelQuit);
69 return gotQuitResponse();
72 private static QResponse gotQuitResponse = QResponse.NULL;
74 private static QResponse setResponse(QResponse qresponse)
76 gotQuitResponse = qresponse;
80 public static QResponse gotQuitResponse()
82 return gotQuitResponse;
85 public static final Callable<QResponse> defaultCancelQuit = () -> {
86 Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
88 setResponse(QResponse.NULL);
90 return QResponse.CANCEL_QUIT;
93 public static final Callable<QResponse> defaultOkQuit = () -> {
94 Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
95 return setResponse(QResponse.QUIT);
98 public static final Callable<QResponse> defaultForceQuit = () -> {
99 Console.debug("QuitHandler: (default) Quit action FORCED by user");
100 // note that shutdown hook will not be run
101 Runtime.getRuntime().halt(0);
102 return setResponse(QResponse.FORCE_QUIT); // this line never reached!
105 public static QResponse getQuitResponse(boolean ui)
107 return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
111 private static boolean interactive = true;
113 public static QResponse getQuitResponse(boolean ui,
114 Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
115 Callable<QResponse> cancelQuit)
117 QResponse got = gotQuitResponse();
118 if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
120 // quit has already been selected, continue with calling quit method
124 interactive = ui && !Platform.isHeadless();
125 // confirm quit if needed and wanted
126 boolean confirmQuit = true;
130 Console.debug("Non interactive quit -- not confirming");
133 else if (Jalview2XML.allSavedUpToDate())
135 Console.debug("Nothing changed -- not confirming quit");
140 confirmQuit = jalview.bin.Cache
141 .getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true);
142 Console.debug("Jalview property '"
143 + jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT
144 + "' is/defaults to " + confirmQuit + " -- "
145 + (confirmQuit ? "" : "not ") + "confirming quit");
147 got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
148 Console.debug("initial calculation, got=" + got);
153 JvOptionPane.newOptionDialog()
154 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
155 .setResponseHandler(JOptionPane.NO_OPTION, defaultCancelQuit)
156 .showDialogOnTopAsync(
157 new StringBuilder(MessageManager
158 .getString("label.quit_jalview"))
160 .append(MessageManager.getString(
161 "label.unsaved_changes"))
163 MessageManager.getString("action.quit"),
164 JOptionPane.YES_NO_OPTION,
165 JOptionPane.QUESTION_MESSAGE, null, new Object[]
166 { MessageManager.getString("action.quit"),
167 MessageManager.getString("action.cancel") },
168 MessageManager.getString("action.quit"), true);
171 got = gotQuitResponse();
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())
185 QResponse waitResponse = waitQuit(interactive, okQuit, forceQuit,
187 wait = waitResponse == QResponse.QUIT;
191 Callable<QResponse> next = null;
192 switch (gotQuitResponse())
197 case FORCE_QUIT: // not actually an option at this stage
206 got = executor.submit(next).get();
207 } catch (InterruptedException | ExecutionException e)
210 .debug("Exception during quit handling (final choice)", e);
214 return gotQuitResponse();
217 private static QResponse waitQuit(boolean interactive,
218 Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
219 Callable<QResponse> cancelQuit)
221 // check for saves in progress
222 if (!BackupFiles.hasSavesInProgress())
223 return QResponse.QUIT;
226 AlignFrame[] afArray = Desktop.getAlignFrames();
227 if (!(afArray == null || afArray.length == 0))
229 for (int i = 0; i < afArray.length; i++)
231 AlignFrame af = afArray[i];
232 List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
233 for (AlignmentViewPanel avp : avpList)
235 AlignmentI a = avp.getAlignment();
236 List<SequenceI> sList = a.getSequences();
237 for (SequenceI s : sList)
239 size += s.getLength();
244 int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
245 Math.max(MIN_WAIT_FOR_SAVE, size / 2));
246 Console.debug("Set waitForSave to " + waitTime);
247 QResponse waitResponse = QResponse.NULL;
250 boolean doIterations = true;
251 while (doIterations && BackupFiles.hasSavesInProgress()
252 && iteration++ < (interactive ? 100 : 5))
254 // future that returns a Boolean when all files are saved
255 CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
257 // callback as each file finishes saving
258 for (CompletableFuture<Boolean> cf : BackupFiles
259 .savesInProgressCompletableFutures(false))
261 // if this is the last one then complete filesAllSaved
262 cf.whenComplete((ret, e) -> {
263 if (!BackupFiles.hasSavesInProgress())
265 filesAllSaved.complete(true);
271 filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
272 } catch (InterruptedException | ExecutionException e1)
275 "Exception whilst waiting for files to save before quit",
277 } catch (TimeoutException e2)
279 // this Exception to be expected
282 if (interactive && BackupFiles.hasSavesInProgress())
284 boolean allowForceQuit = iteration > 0; // iteration > 1 to not show
285 // force quit the first time
286 JFrame parent = new JFrame();
287 JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
291 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
292 .setResponseHandler(JOptionPane.NO_OPTION, forceQuit)
293 .setResponseHandler(JOptionPane.CANCEL_OPTION,
299 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
300 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
302 JTextPane messagePane = new JTextPane();
303 messagePane.setBackground(waitDialog.getBackground());
304 messagePane.setBorder(null);
305 messagePane.setText(waitingForSaveMessage());
306 // callback as each file finishes saving
307 for (CompletableFuture<Boolean> cf : BackupFiles
308 .savesInProgressCompletableFutures(false))
310 cf.whenComplete((ret, e) -> {
311 if (BackupFiles.hasSavesInProgress())
312 // update the list of saving files as they save too
313 messagePane.setText(waitingForSaveMessage());
316 // if this is the last one then close the dialog
317 messagePane.setText(new StringBuilder()
318 .append(MessageManager.getString("label.all_saved"))
319 .append("\n").append(MessageManager
320 .getString("label.quitting_bye"))
324 Console.debug("WAITING FOR MESSAGE");
326 } catch (InterruptedException e1)
329 // like a click on Wait button
330 waitDialog.setValue(JOptionPane.YES_OPTION);
336 String[] options = new String[allowForceQuit ? 3 : 2];
339 options[0] = MessageManager.getString("action.wait");
340 options[1] = MessageManager.getString("action.force_quit");
341 options[2] = MessageManager.getString("action.cancel_quit");
345 options[0] = MessageManager.getString("action.wait");
346 options[1] = MessageManager.getString("action.cancel_quit");
348 waitDialog.showDialogOnTopAsync(parent, messagePane,
349 MessageManager.getString("action.wait"),
350 allowForceQuit ? JOptionPane.YES_NO_CANCEL_OPTION
351 : JOptionPane.YES_NO_OPTION,
352 JOptionPane.WARNING_MESSAGE, null, options,
353 MessageManager.getString("action.wait"), true);
356 final QResponse thisWaitResponse = gotQuitResponse();
357 switch (thisWaitResponse)
359 case QUIT: // wait -- do another iteration
362 doIterations = false;
365 doIterations = false;
367 case NULL: // already cancelled
368 doIterations = false;
372 } // end if interactive
374 } // end while wait iteration loop
375 waitResponse = gotQuitResponse();
380 private static int waitForceQuitCancelQuitOptionDialog(Object message,
383 JFrame dialogParent = new JFrame();
384 dialogParent.setAlwaysOnTop(true);
385 String wait = MessageManager.getString("action.wait");
386 Object[] options = { wait,
387 MessageManager.getString("action.force_quit"),
388 MessageManager.getString("action.cancel_quit") };
390 int answer = JOptionPane.showOptionDialog(dialogParent, message, title,
391 JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE,
392 null, options, wait);
397 private static String waitingForSaveMessage()
399 StringBuilder messageSB = new StringBuilder();
401 messageSB.append(MessageManager.getString("label.save_in_progress"));
402 List<File> files = BackupFiles.savesInProgressFiles(false);
403 boolean any = files.size() > 0;
406 for (File file : files)
408 messageSB.append("\n- ").append(file.getName());
413 messageSB.append(MessageManager.getString("label.unknown"));
415 return messageSB.toString();