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.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 MIN_WAIT_FOR_SAVE = 3000;
34 private static final int MAX_WAIT_FOR_SAVE = 20000;
36 private static final int NON_INTERACTIVE_WAIT_CYCLES = 2;
38 public static enum QResponse
40 NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
43 private static ExecutorService executor = Executors.newFixedThreadPool(3);
45 public static QResponse setQuitHandler()
47 FlatDesktop.setQuitHandler(response -> {
48 Callable<QResponse> performQuit = () -> {
49 response.performQuit();
50 return setResponse(QResponse.QUIT);
52 Callable<QResponse> performForceQuit = () -> {
53 response.performQuit();
54 return setResponse(QResponse.FORCE_QUIT);
56 Callable<QResponse> cancelQuit = () -> {
57 response.cancelQuit();
59 setResponse(QResponse.NULL);
61 return QResponse.CANCEL_QUIT;
63 QResponse qresponse = getQuitResponse(true, performQuit,
64 performForceQuit, cancelQuit);
67 return gotQuitResponse();
70 private static QResponse gotQuitResponse = QResponse.NULL;
72 private static QResponse setResponse(QResponse qresponse)
74 gotQuitResponse = qresponse;
78 public static QResponse gotQuitResponse()
80 return gotQuitResponse;
83 public static final Callable<QResponse> defaultCancelQuit = () -> {
84 Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
86 setResponse(QResponse.NULL);
88 return QResponse.CANCEL_QUIT;
91 public static final Callable<QResponse> defaultOkQuit = () -> {
92 Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
93 return setResponse(QResponse.QUIT);
96 public static final Callable<QResponse> defaultForceQuit = () -> {
97 Console.debug("QuitHandler: (default) Quit action FORCED by user");
98 // note that shutdown hook will not be run
99 Runtime.getRuntime().halt(0);
100 return setResponse(QResponse.FORCE_QUIT); // this line never reached!
103 public static QResponse getQuitResponse(boolean ui)
105 return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
109 private static boolean interactive = true;
111 public static QResponse getQuitResponse(boolean ui,
112 Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
113 Callable<QResponse> cancelQuit)
115 QResponse got = gotQuitResponse();
116 if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
118 // quit has already been selected, continue with calling quit method
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;
150 JvOptionPane.newOptionDialog()
151 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
152 .setResponseHandler(JOptionPane.NO_OPTION, defaultCancelQuit)
153 .showDialogOnTopAsync(
154 new StringBuilder(MessageManager
155 .getString("label.quit_jalview"))
157 .append(MessageManager.getString(
158 "label.unsaved_changes"))
160 MessageManager.getString("action.quit"),
161 JOptionPane.YES_NO_OPTION,
162 JOptionPane.QUESTION_MESSAGE, null, new Object[]
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 setResponse(QResponse.NULL);
175 return QResponse.CANCEL_QUIT;
177 else if (got == QResponse.QUIT)
179 if (Cache.getDefault("WAIT_FOR_SAVE", true)
180 && BackupFiles.hasSavesInProgress())
182 QResponse waitResponse = waitQuit(interactive, okQuit, forceQuit,
184 wait = waitResponse == QResponse.QUIT;
188 Callable<QResponse> next = null;
189 switch (gotQuitResponse())
194 case FORCE_QUIT: // not actually an option at this stage
203 got = executor.submit(next).get();
204 } catch (InterruptedException | ExecutionException e)
207 .debug("Exception during quit handling (final choice)", e);
211 return gotQuitResponse();
214 private static QResponse waitQuit(boolean interactive,
215 Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
216 Callable<QResponse> cancelQuit)
218 // check for saves in progress
219 if (!BackupFiles.hasSavesInProgress())
220 return QResponse.QUIT;
223 AlignFrame[] afArray = Desktop.getAlignFrames();
224 if (!(afArray == null || afArray.length == 0))
226 for (int i = 0; i < afArray.length; i++)
228 AlignFrame af = afArray[i];
229 List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
230 for (AlignmentViewPanel avp : avpList)
232 AlignmentI a = avp.getAlignment();
233 List<SequenceI> sList = a.getSequences();
234 for (SequenceI s : sList)
236 size += s.getLength();
241 int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
242 Math.max(MIN_WAIT_FOR_SAVE, size / 2));
243 Console.debug("Set waitForSave to " + waitTime);
244 QResponse waitResponse = QResponse.NULL;
247 boolean doIterations = true; // note iterations not used in the gui now,
248 // only one pass without the "Wait" button
249 while (doIterations && BackupFiles.hasSavesInProgress()
250 && iteration++ < (interactive ? 100 : 5))
252 // future that returns a Boolean when all files are saved
253 CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
255 // callback as each file finishes saving
256 for (CompletableFuture<Boolean> cf : BackupFiles
257 .savesInProgressCompletableFutures(false))
259 // if this is the last one then complete filesAllSaved
260 cf.whenComplete((ret, e) -> {
261 if (!BackupFiles.hasSavesInProgress())
263 filesAllSaved.complete(true);
269 filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
270 } catch (InterruptedException | ExecutionException e1)
273 "Exception whilst waiting for files to save before quit",
275 } catch (TimeoutException e2)
277 // this Exception to be expected
280 if (interactive && BackupFiles.hasSavesInProgress())
282 boolean showForceQuit = iteration > 0; // iteration > 1 to not show
283 // force quit the first time
284 JFrame parent = new JFrame();
285 JButton[] buttons = { new JButton(), new JButton() };
286 JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
287 JTextPane messagePane = new JTextPane();
288 messagePane.setBackground(waitDialog.getBackground());
289 messagePane.setBorder(null);
290 messagePane.setText(waitingForSaveMessage());
291 // callback as each file finishes saving
292 for (CompletableFuture<Boolean> cf : BackupFiles
293 .savesInProgressCompletableFutures(false))
295 cf.whenComplete((ret, e) -> {
296 if (BackupFiles.hasSavesInProgress())
298 // update the list of saving files as they save too
299 messagePane.setText(waitingForSaveMessage());
303 if (!(QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
304 || QuitHandler.gotQuitResponse() == QResponse.NULL))
306 for (int i = 0; i < buttons.length; i++)
308 Console.debug("DISABLING BUTTON " + buttons[i].getText());
309 buttons[i].setEnabled(false);
310 buttons[i].setVisible(false);
312 // if this is the last one then close the dialog
313 messagePane.setText(new StringBuilder()
314 .append(MessageManager.getString("label.all_saved"))
316 .append(MessageManager
317 .getString("label.quitting_bye"))
319 messagePane.setEditable(false);
323 } catch (InterruptedException e1)
336 options = new String[2];
337 options[0] = MessageManager.getString("action.force_quit");
338 options[1] = MessageManager.getString("action.cancel_quit");
339 dialogType = JOptionPane.YES_NO_OPTION;
340 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
341 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
345 options = new String[1];
346 options[0] = MessageManager.getString("action.cancel_quit");
347 dialogType = JOptionPane.YES_OPTION;
348 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
350 waitDialog.showDialogOnTopAsync(parent, messagePane,
351 MessageManager.getString("label.wait_for_save"), dialogType,
352 JOptionPane.WARNING_MESSAGE, null, options,
353 MessageManager.getString("action.cancel_quit"), true,
357 final QResponse thisWaitResponse = gotQuitResponse();
358 switch (thisWaitResponse)
360 case QUIT: // wait -- do another iteration
363 doIterations = false;
366 doIterations = false;
368 case NULL: // already cancelled
369 doIterations = false;
373 } // end if interactive
375 } // end while wait iteration loop
376 waitResponse = gotQuitResponse();
381 private static String waitingForSaveMessage()
383 StringBuilder messageSB = new StringBuilder();
385 messageSB.append(MessageManager.getString("label.save_in_progress"));
386 List<File> files = BackupFiles.savesInProgressFiles(false);
387 boolean any = files.size() > 0;
390 for (File file : files)
392 messageSB.append("\n\u2022 ").append(file.getName());
397 messageSB.append(MessageManager.getString("label.unknown"));
399 messageSB.append("\n\n")
400 .append(MessageManager.getString("label.quit_after_saving"));
401 return messageSB.toString();