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 = 1000;
34 private static final int MAX_WAIT_FOR_SAVE = 20000;
36 private static boolean interactive = true;
38 public static enum QResponse
40 NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
43 public static enum Message
45 UNSAVED_CHANGES, UNSAVED_ALIGNMENTS
48 protected static Message message = Message.UNSAVED_CHANGES;
50 public static void setMessage(Message m)
55 private static ExecutorService executor = Executors.newFixedThreadPool(3);
57 public static QResponse setQuitHandler()
59 FlatDesktop.setQuitHandler(response -> {
60 Callable<Void> performQuit = () -> {
61 response.performQuit();
62 setResponse(QResponse.QUIT);
65 Callable<Void> performForceQuit = () -> {
66 response.performQuit();
67 setResponse(QResponse.FORCE_QUIT);
70 Callable<Void> cancelQuit = () -> {
71 response.cancelQuit();
73 setResponse(QResponse.NULL);
76 getQuitResponse(true, performQuit, performForceQuit, cancelQuit);
79 return gotQuitResponse();
82 private static QResponse gotQuitResponse = QResponse.NULL;
84 protected static QResponse setResponse(QResponse qresponse)
86 gotQuitResponse = qresponse;
90 public static QResponse gotQuitResponse()
92 return gotQuitResponse;
95 public static final Callable<Void> defaultCancelQuit = () -> {
96 Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
98 setResponse(QResponse.CANCEL_QUIT);
102 public static final Callable<Void> defaultOkQuit = () -> {
103 Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
104 setResponse(QResponse.QUIT);
108 public static final Callable<Void> defaultForceQuit = () -> {
109 Console.debug("QuitHandler: (default) Quit action FORCED by user");
110 // note that shutdown hook will not be run
111 Runtime.getRuntime().halt(0);
112 setResponse(QResponse.FORCE_QUIT); // this line never reached!
116 public static QResponse getQuitResponse(boolean ui)
118 return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
122 public static QResponse getQuitResponse(boolean ui, Callable<Void> okQuit,
123 Callable<Void> forceQuit, Callable<Void> cancelQuit)
125 QResponse got = gotQuitResponse();
126 if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
128 // quit has already been selected, continue with calling quit method
132 interactive = ui && !Platform.isHeadless();
133 // confirm quit if needed and wanted
134 boolean confirmQuit = true;
138 Console.debug("Non interactive quit -- not confirming");
141 else if (Jalview2XML.allSavedUpToDate())
143 Console.debug("Nothing changed -- not confirming quit");
148 confirmQuit = jalview.bin.Cache
149 .getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true);
150 Console.debug("Jalview property '"
151 + jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT
152 + "' is/defaults to " + confirmQuit + " -- "
153 + (confirmQuit ? "" : "not ") + "confirming quit");
155 got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
160 String messageString = MessageManager
161 .getString(message == Message.UNSAVED_ALIGNMENTS
162 ? "label.unsaved_alignments"
163 : "label.unsaved_changes");
164 JvOptionPane.newOptionDialog()
165 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
166 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit)
167 .showDialogOnTopAsync(
168 new StringBuilder(MessageManager
169 .getString("label.quit_jalview")).append("\n")
170 .append(messageString).toString(),
171 MessageManager.getString("action.quit"),
172 JOptionPane.YES_NO_OPTION,
173 JOptionPane.QUESTION_MESSAGE, null, new Object[]
174 { MessageManager.getString("action.quit"),
175 MessageManager.getString("action.cancel") },
176 MessageManager.getString("action.quit"), true);
179 got = gotQuitResponse();
180 boolean wait = false;
181 if (got == QResponse.CANCEL_QUIT)
184 Console.debug("Cancelling quit. Resetting response to NULL");
185 setResponse(QResponse.NULL);
187 return QResponse.CANCEL_QUIT;
189 else if (got == QResponse.QUIT)
191 if (Cache.getDefault("WAIT_FOR_SAVE", true)
192 && BackupFiles.hasSavesInProgress())
194 waitQuit(interactive, okQuit, forceQuit, cancelQuit);
195 QResponse waitResponse = gotQuitResponse();
196 wait = waitResponse == QResponse.QUIT;
200 Callable<Void> next = null;
201 switch (gotQuitResponse())
206 case FORCE_QUIT: // not actually an option at this stage
215 executor.submit(next).get();
216 got = gotQuitResponse();
217 } catch (InterruptedException | ExecutionException e)
220 .debug("Exception during quit handling (final choice)", e);
224 if (gotQuitResponse() == QResponse.CANCEL_QUIT)
226 // reset if cancelled
227 setResponse(QResponse.NULL);
228 return QResponse.CANCEL_QUIT;
230 return gotQuitResponse();
233 private static QResponse waitQuit(boolean interactive,
234 Callable<Void> okQuit, Callable<Void> forceQuit,
235 Callable<Void> cancelQuit)
237 // check for saves in progress
238 if (!BackupFiles.hasSavesInProgress())
239 return QResponse.QUIT;
242 AlignFrame[] afArray = Desktop.getAlignFrames();
243 if (!(afArray == null || afArray.length == 0))
245 for (int i = 0; i < afArray.length; i++)
247 AlignFrame af = afArray[i];
248 List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
249 for (AlignmentViewPanel avp : avpList)
251 AlignmentI a = avp.getAlignment();
252 List<SequenceI> sList = a.getSequences();
253 for (SequenceI s : sList)
255 size += s.getLength();
260 int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
261 Math.max(MIN_WAIT_FOR_SAVE, size / 2));
262 Console.debug("Set waitForSave to " + waitTime);
265 boolean doIterations = true; // note iterations not used in the gui now,
266 // only one pass without the "Wait" button
267 while (doIterations && BackupFiles.hasSavesInProgress()
268 && iteration++ < (interactive ? 100 : 5))
270 // future that returns a Boolean when all files are saved
271 CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
273 // callback as each file finishes saving
274 for (CompletableFuture<Boolean> cf : BackupFiles
275 .savesInProgressCompletableFutures(false))
277 // if this is the last one then complete filesAllSaved
278 cf.whenComplete((ret, e) -> {
279 if (!BackupFiles.hasSavesInProgress())
281 filesAllSaved.complete(true);
287 filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
288 } catch (InterruptedException | ExecutionException e1)
291 "Exception whilst waiting for files to save before quit",
293 } catch (TimeoutException e2)
295 // this Exception to be expected
298 if (interactive && BackupFiles.hasSavesInProgress())
300 boolean showForceQuit = iteration > 0; // iteration > 1 to not show
301 // force quit the first time
302 JFrame parent = new JFrame();
303 JButton[] buttons = { new JButton(), new JButton() };
304 JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
305 JTextPane messagePane = new JTextPane();
306 messagePane.setBackground(waitDialog.getBackground());
307 messagePane.setBorder(null);
308 messagePane.setText(waitingForSaveMessage());
309 // callback as each file finishes saving
310 for (CompletableFuture<Boolean> cf : BackupFiles
311 .savesInProgressCompletableFutures(false))
313 cf.whenComplete((ret, e) -> {
314 if (BackupFiles.hasSavesInProgress())
316 // update the list of saving files as they save too
317 messagePane.setText(waitingForSaveMessage());
321 if (!(QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
322 || QuitHandler.gotQuitResponse() == QResponse.NULL))
324 for (int i = 0; i < buttons.length; i++)
326 Console.debug("DISABLING BUTTON " + buttons[i].getText());
327 buttons[i].setEnabled(false);
328 buttons[i].setVisible(false);
330 // if this is the last one then close the dialog
331 messagePane.setText(new StringBuilder()
332 .append(MessageManager.getString("label.all_saved"))
334 .append(MessageManager
335 .getString("label.quitting_bye"))
337 messagePane.setEditable(false);
341 } catch (InterruptedException e1)
354 options = new String[2];
355 options[0] = MessageManager.getString("action.force_quit");
356 options[1] = MessageManager.getString("action.cancel_quit");
357 dialogType = JOptionPane.YES_NO_OPTION;
358 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
359 .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
363 options = new String[1];
364 options[0] = MessageManager.getString("action.cancel_quit");
365 dialogType = JOptionPane.YES_OPTION;
366 waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
368 waitDialog.showDialogOnTopAsync(parent, messagePane,
369 MessageManager.getString("label.wait_for_save"), dialogType,
370 JOptionPane.WARNING_MESSAGE, null, options,
371 MessageManager.getString("action.cancel_quit"), true,
375 final QResponse thisWaitResponse = gotQuitResponse();
376 switch (thisWaitResponse)
378 case QUIT: // wait -- do another iteration
381 doIterations = false;
384 doIterations = false;
386 case NULL: // already cancelled
387 doIterations = false;
391 } // end if interactive
393 } // end while wait iteration loop
394 return gotQuitResponse();
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\u2022 ").append(file.getName());
413 messageSB.append(MessageManager.getString("label.unknown"));
415 messageSB.append("\n\n")
416 .append(MessageManager.getString("label.quit_after_saving"));
417 return messageSB.toString();
420 public static void abortQuit()
422 setResponse(QResponse.CANCEL_QUIT);