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();
169 boolean wait = false;
170 if (got == QResponse.CANCEL_QUIT)
173 Console.debug("Cancelling quit. Resetting response to NULL");
174 setResponse(QResponse.NULL);
176 return QResponse.CANCEL_QUIT;
178 else if (got == QResponse.QUIT)
180 if (Cache.getDefault("WAIT_FOR_SAVE", true)
181 && BackupFiles.hasSavesInProgress())
183 waitQuit(interactive, okQuit, forceQuit, cancelQuit);
184 QResponse waitResponse = gotQuitResponse();
185 wait = waitResponse == QResponse.QUIT;
189 Callable<Void> next = null;
190 switch (gotQuitResponse())
195 case FORCE_QUIT: // not actually an option at this stage
204 executor.submit(next).get();
205 got = gotQuitResponse();
206 } catch (RejectedExecutionException e)
208 // QuitHander.abortQuit() probably called
209 // CANCEL_QUIT test will reset QuitHandler
210 Console.info("Quit aborted!");
211 got = QResponse.NULL;
212 setResponse(QResponse.NULL);
213 } catch (InterruptedException | ExecutionException e)
216 .debug("Exception during quit handling (final choice)", e);
222 // reset if cancelled
223 Console.debug("Quit cancelled");
224 setResponse(QResponse.NULL);
225 return QResponse.CANCEL_QUIT;
227 return gotQuitResponse();
230 private static QResponse waitQuit(boolean interactive,
231 Callable<Void> okQuit, Callable<Void> forceQuit,
232 Callable<Void> cancelQuit)
234 // check for saves in progress
235 if (!BackupFiles.hasSavesInProgress())
236 return QResponse.QUIT;
239 AlignFrame[] afArray = Desktop.getAlignFrames();
240 if (!(afArray == null || afArray.length == 0))
242 for (int i = 0; i < afArray.length; i++)
244 AlignFrame af = afArray[i];
245 List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
246 for (AlignmentViewPanel avp : avpList)
248 AlignmentI a = avp.getAlignment();
249 List<SequenceI> sList = a.getSequences();
250 for (SequenceI s : sList)
252 size += s.getLength();
257 int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
258 Math.max(MIN_WAIT_FOR_SAVE, size / 2));
259 Console.debug("Set waitForSave to " + waitTime);
262 boolean doIterations = true; // note iterations not used in the gui now,
263 // only one pass without the "Wait" button
264 while (doIterations && BackupFiles.hasSavesInProgress()
265 && iteration++ < (interactive ? 100 : 5))
267 // future that returns a Boolean when all files are saved
268 CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
270 // callback as each file finishes saving
271 for (CompletableFuture<Boolean> cf : BackupFiles
272 .savesInProgressCompletableFutures(false))
274 // if this is the last one then complete filesAllSaved
275 cf.whenComplete((ret, e) -> {
276 if (!BackupFiles.hasSavesInProgress())
278 filesAllSaved.complete(true);
284 filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
285 } catch (InterruptedException | ExecutionException e1)
288 "Exception whilst waiting for files to save before quit",
290 } catch (TimeoutException e2)
292 // this Exception to be expected
295 if (interactive && BackupFiles.hasSavesInProgress())
297 boolean showForceQuit = iteration > 0; // iteration > 1 to not show
298 // force quit the first time
299 JFrame parent = new JFrame();
300 JButton[] buttons = { new JButton(), new JButton() };
301 JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
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())
313 // update the list of saving files as they save too
314 messagePane.setText(waitingForSaveMessage());
318 if (!(quitCancelled()))
320 for (int i = 0; i < buttons.length; i++)
322 Console.debug("DISABLING BUTTON " + buttons[i].getText());
323 buttons[i].setEnabled(false);
324 buttons[i].setVisible(false);
326 // if this is the last one then close the dialog
327 messagePane.setText(new StringBuilder()
328 .append(MessageManager.getString("label.all_saved"))
330 .append(MessageManager
331 .getString("label.quitting_bye"))
333 messagePane.setEditable(false);
337 } catch (InterruptedException e1)
350 options = new String[2];
351 options[0] = MessageManager.getString("action.force_quit");
352 options[1] = MessageManager.getString("action.cancel_quit");
353 dialogType = JOptionPane.YES_NO_OPTION;
354 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
355 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
359 options = new String[1];
360 options[0] = MessageManager.getString("action.cancel_quit");
361 dialogType = JOptionPane.YES_OPTION;
362 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
364 waitDialog.showDialogOnTopAsync(parent, messagePane,
365 MessageManager.getString("label.wait_for_save"), dialogType,
366 JOptionPane.WARNING_MESSAGE, null, options,
367 MessageManager.getString("action.cancel_quit"), true,
371 final QResponse thisWaitResponse = gotQuitResponse();
372 switch (thisWaitResponse)
374 case QUIT: // wait -- do another iteration
377 doIterations = false;
380 doIterations = false;
382 case NULL: // already cancelled
383 doIterations = false;
387 } // end if interactive
389 } // end while wait iteration loop
390 return gotQuitResponse();
393 private static String waitingForSaveMessage()
395 StringBuilder messageSB = new StringBuilder();
397 messageSB.append(MessageManager.getString("label.save_in_progress"));
398 List<File> files = BackupFiles.savesInProgressFiles(false);
399 boolean any = files.size() > 0;
402 for (File file : files)
404 messageSB.append("\n\u2022 ").append(file.getName());
409 messageSB.append(MessageManager.getString("label.unknown"));
411 messageSB.append("\n\n")
412 .append(MessageManager.getString("label.quit_after_saving"));
413 return messageSB.toString();
416 public static void abortQuit()
418 setResponse(QResponse.NULL);
419 // executor.shutdownNow();
422 private static JvOptionPane quitDialog = null;
424 private static void setQuitDialog(JvOptionPane qd)
429 private static JvOptionPane getQuitDialog()
434 public static boolean quitCancelled()
436 return QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
437 || QuitHandler.gotQuitResponse() == QResponse.NULL;
440 public static boolean quitting()
442 return QuitHandler.gotQuitResponse() == QResponse.QUIT
443 || QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT;