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.JButton;
14 import javax.swing.JFrame;
15 import javax.swing.JOptionPane;
16 import javax.swing.JTextPane;
18 import com.formdev.flatlaf.extras.FlatDesktop;
20 import jalview.api.AlignmentViewPanel;
21 import jalview.bin.Cache;
22 import jalview.bin.Console;
23 import jalview.datamodel.AlignmentI;
24 import jalview.datamodel.SequenceI;
25 import jalview.gui.AlignFrame;
26 import jalview.gui.Desktop;
27 import jalview.gui.JvOptionPane;
28 import jalview.io.BackupFiles;
29 import jalview.project.Jalview2XML;
30 import jalview.util.MessageManager;
31 import jalview.util.Platform;
33 public class QuitHandler
35 private static final int MIN_WAIT_FOR_SAVE = 3000;
37 private static final int MAX_WAIT_FOR_SAVE = 20000;
39 private static final int NON_INTERACTIVE_WAIT_CYCLES = 2;
41 public static enum QResponse
43 NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
46 private static ExecutorService executor = Executors.newFixedThreadPool(3);
48 public static QResponse setQuitHandler()
50 FlatDesktop.setQuitHandler(response -> {
51 Callable<QResponse> performQuit = () -> {
52 response.performQuit();
53 return setResponse(QResponse.QUIT);
55 Callable<QResponse> performForceQuit = () -> {
56 response.performQuit();
57 return setResponse(QResponse.FORCE_QUIT);
59 Callable<QResponse> cancelQuit = () -> {
60 response.cancelQuit();
62 setResponse(QResponse.NULL);
64 return QResponse.CANCEL_QUIT;
66 QResponse qresponse = getQuitResponse(true, performQuit,
67 performForceQuit, cancelQuit);
70 return gotQuitResponse();
73 private static QResponse gotQuitResponse = QResponse.NULL;
75 private static QResponse setResponse(QResponse qresponse)
77 gotQuitResponse = qresponse;
81 public static QResponse gotQuitResponse()
83 return gotQuitResponse;
86 public static final Callable<QResponse> defaultCancelQuit = () -> {
87 Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
89 setResponse(QResponse.NULL);
91 return QResponse.CANCEL_QUIT;
94 public static final Callable<QResponse> defaultOkQuit = () -> {
95 Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
96 return setResponse(QResponse.QUIT);
99 public static final Callable<QResponse> defaultForceQuit = () -> {
100 Console.debug("QuitHandler: (default) Quit action FORCED by user");
101 // note that shutdown hook will not be run
102 Runtime.getRuntime().halt(0);
103 return setResponse(QResponse.FORCE_QUIT); // this line never reached!
106 public static QResponse getQuitResponse(boolean ui)
108 return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
112 private static boolean interactive = true;
114 public static QResponse getQuitResponse(boolean ui,
115 Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
116 Callable<QResponse> cancelQuit)
118 QResponse got = gotQuitResponse();
119 if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
121 // quit has already been selected, continue with calling quit method
125 interactive = ui && !Platform.isHeadless();
126 // confirm quit if needed and wanted
127 boolean confirmQuit = true;
131 Console.debug("Non interactive quit -- not confirming");
134 else if (Jalview2XML.allSavedUpToDate())
136 Console.debug("Nothing changed -- not confirming quit");
141 confirmQuit = jalview.bin.Cache
142 .getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true);
143 Console.debug("Jalview property '"
144 + jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT
145 + "' is/defaults to " + confirmQuit + " -- "
146 + (confirmQuit ? "" : "not ") + "confirming quit");
148 got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
149 Console.debug("initial calculation, got=" + got);
154 JvOptionPane.newOptionDialog()
155 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
156 .setResponseHandler(JOptionPane.NO_OPTION, defaultCancelQuit)
157 .showDialogOnTopAsync(
158 new StringBuilder(MessageManager
159 .getString("label.quit_jalview"))
161 .append(MessageManager.getString(
162 "label.unsaved_changes"))
164 MessageManager.getString("action.quit"),
165 JOptionPane.YES_NO_OPTION,
166 JOptionPane.QUESTION_MESSAGE, null, new Object[]
167 { MessageManager.getString("action.quit"),
168 MessageManager.getString("action.cancel") },
169 MessageManager.getString("action.quit"), true);
172 got = gotQuitResponse();
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())
198 case FORCE_QUIT: // not actually an option at this stage
207 got = executor.submit(next).get();
208 } catch (InterruptedException | ExecutionException e)
211 .debug("Exception during quit handling (final choice)", e);
215 return gotQuitResponse();
218 private static QResponse waitQuit(boolean interactive,
219 Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
220 Callable<QResponse> cancelQuit)
222 // check for saves in progress
223 if (!BackupFiles.hasSavesInProgress())
224 return QResponse.QUIT;
227 AlignFrame[] afArray = Desktop.getAlignFrames();
228 if (!(afArray == null || afArray.length == 0))
230 for (int i = 0; i < afArray.length; i++)
232 AlignFrame af = afArray[i];
233 List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
234 for (AlignmentViewPanel avp : avpList)
236 AlignmentI a = avp.getAlignment();
237 List<SequenceI> sList = a.getSequences();
238 for (SequenceI s : sList)
240 size += s.getLength();
245 int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
246 Math.max(MIN_WAIT_FOR_SAVE, size / 2));
247 Console.debug("Set waitForSave to " + waitTime);
248 QResponse waitResponse = QResponse.NULL;
251 boolean doIterations = true; // note iterations not used in the gui now,
252 // only one pass without the "Wait" button
253 while (doIterations && BackupFiles.hasSavesInProgress()
254 && iteration++ < (interactive ? 100 : 5))
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);
273 filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
274 } catch (InterruptedException | ExecutionException e1)
277 "Exception whilst waiting for files to save before quit",
279 } catch (TimeoutException e2)
281 // this Exception to be expected
284 if (interactive && BackupFiles.hasSavesInProgress())
286 boolean showForceQuit = iteration > 0; // iteration > 1 to not show
287 // force quit the first time
288 JFrame parent = new JFrame();
289 JButton[] buttons = { new JButton(), new JButton() };
290 JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
291 JTextPane messagePane = new JTextPane();
292 messagePane.setBackground(waitDialog.getBackground());
293 messagePane.setBorder(null);
294 messagePane.setText(waitingForSaveMessage());
295 // callback as each file finishes saving
296 for (CompletableFuture<Boolean> cf : BackupFiles
297 .savesInProgressCompletableFutures(false))
299 cf.whenComplete((ret, e) -> {
300 if (BackupFiles.hasSavesInProgress())
302 // update the list of saving files as they save too
303 messagePane.setText(waitingForSaveMessage());
307 if (!(QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
308 || QuitHandler.gotQuitResponse() == QResponse.NULL))
310 for (int i = 0; i < buttons.length; i++)
312 Console.debug("DISABLING BUTTON " + buttons[i].getText());
313 buttons[i].setEnabled(false);
314 buttons[i].setVisible(false);
316 // if this is the last one then close the dialog
317 messagePane.setText(new StringBuilder()
318 .append(MessageManager.getString("label.all_saved"))
320 .append(MessageManager
321 .getString("label.quitting_bye"))
323 messagePane.setEditable(false);
327 } catch (InterruptedException e1)
340 options = new String[2];
341 options[0] = MessageManager.getString("action.force_quit");
342 options[1] = MessageManager.getString("action.cancel_quit");
343 dialogType = JOptionPane.YES_NO_OPTION;
344 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
345 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
349 options = new String[1];
350 options[0] = MessageManager.getString("action.cancel_quit");
351 dialogType = JOptionPane.YES_OPTION;
352 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
354 waitDialog.showDialogOnTopAsync(parent, messagePane,
355 MessageManager.getString("label.wait_for_save"), dialogType,
356 JOptionPane.WARNING_MESSAGE, null, options,
357 MessageManager.getString("action.cancel_quit"), true,
361 final QResponse thisWaitResponse = gotQuitResponse();
362 switch (thisWaitResponse)
364 case QUIT: // wait -- do another iteration
367 doIterations = false;
370 doIterations = false;
372 case NULL: // already cancelled
373 doIterations = false;
377 } // end if interactive
379 } // end while wait iteration loop
380 waitResponse = gotQuitResponse();
385 private static String waitingForSaveMessage()
387 StringBuilder messageSB = new StringBuilder();
389 messageSB.append(MessageManager.getString("label.save_in_progress"));
390 List<File> files = BackupFiles.savesInProgressFiles(false);
391 boolean any = files.size() > 0;
394 for (File file : files)
396 messageSB.append("\n\u2022 ").append(file.getName());
401 messageSB.append(MessageManager.getString("label.unknown"));
403 messageSB.append("\n\n")
404 .append(MessageManager.getString("label.quit_after_saving"));
405 return messageSB.toString();