JAL-4125 Move confirmation of closing external viewer windows into the quit handler...
[jalview.git] / src / jalview / gui / QuitHandler.java
1 package jalview.gui;
2
3 import java.io.File;
4 import java.util.List;
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;
13
14 import javax.swing.JButton;
15 import javax.swing.JFrame;
16 import javax.swing.JOptionPane;
17 import javax.swing.JTextPane;
18
19 import com.formdev.flatlaf.extras.FlatDesktop;
20
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;
30
31 public class QuitHandler
32 {
33   private static final int MIN_WAIT_FOR_SAVE = 1000;
34
35   private static final int MAX_WAIT_FOR_SAVE = 20000;
36
37   private static boolean interactive = true;
38
39   public static enum QResponse
40   {
41     NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
42   };
43
44   private static ExecutorService executor = Executors.newFixedThreadPool(3);
45
46   public static QResponse setQuitHandler()
47   {
48     FlatDesktop.setQuitHandler(response -> {
49       Callable<Void> performQuit = () -> {
50         response.performQuit();
51         setResponse(QResponse.QUIT);
52         return null;
53       };
54       Callable<Void> performForceQuit = () -> {
55         response.performQuit();
56         setResponse(QResponse.FORCE_QUIT);
57         return null;
58       };
59       Callable<Void> cancelQuit = () -> {
60         response.cancelQuit();
61         // reset
62         setResponse(QResponse.NULL);
63         return null;
64       };
65       getQuitResponse(true, performQuit, performForceQuit, cancelQuit);
66     });
67
68     return gotQuitResponse();
69   }
70
71   private static QResponse gotQuitResponse = QResponse.NULL;
72
73   protected static QResponse setResponse(QResponse qresponse)
74   {
75     gotQuitResponse = qresponse;
76     return qresponse;
77   }
78
79   public static QResponse gotQuitResponse()
80   {
81     return gotQuitResponse;
82   }
83
84   public static final Callable<Void> defaultCancelQuit = () -> {
85     Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
86     // reset
87     setResponse(QResponse.CANCEL_QUIT);
88     return null;
89   };
90
91   public static final Callable<Void> defaultOkQuit = () -> {
92     Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
93     setResponse(QResponse.QUIT);
94     return null;
95   };
96
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!
102     return null;
103   };
104
105   public static QResponse getQuitResponse(boolean ui)
106   {
107     return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
108             defaultCancelQuit);
109   }
110
111   public static QResponse getQuitResponse(boolean ui, Callable<Void> okQuit,
112           Callable<Void> forceQuit, Callable<Void> cancelQuit)
113   {
114     QResponse got = gotQuitResponse();
115     if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
116     {
117       // quit has already been selected, continue with calling quit method
118       return got;
119     }
120
121     interactive = ui && !Platform.isHeadless();
122     // confirm quit if needed and wanted
123     boolean confirmQuit = true;
124
125     if (!interactive)
126     {
127       Console.debug("Non interactive quit -- not confirming");
128       confirmQuit = false;
129     }
130     else if (Jalview2XML.allSavedUpToDate())
131     {
132       Console.debug("Nothing changed -- not confirming quit");
133       confirmQuit = false;
134     }
135     else
136     {
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");
143     }
144     got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
145     setResponse(got);
146
147     if (confirmQuit)
148     {
149       setQuitDialog(JvOptionPane.newOptionDialog()
150               .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
151               .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit));
152       JvOptionPane qd = getQuitDialog();
153       qd.showDialogOnTopAsync(
154               new StringBuilder(
155                       MessageManager.getString("label.quit_jalview"))
156                       .append("\n")
157                       .append(MessageManager
158                               .getString("label.unsaved_changes"))
159                       .toString(),
160               MessageManager.getString("action.quit"),
161               JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
162               new Object[]
163               { MessageManager.getString("action.quit"),
164                   MessageManager.getString("action.cancel") },
165               MessageManager.getString("action.quit"), true);
166     }
167
168     got = gotQuitResponse();
169
170     // check for external viewer frames
171     if (got != QResponse.CANCEL_QUIT)
172     {
173       int count = Desktop.instance.structureViewersStillRunningCount();
174       if (count > 0)
175       {
176         String prompt = MessageManager
177                 .formatMessage(count == 1 ? "label.confirm_quit_viewer"
178                         : "label.confirm_quit_viewers");
179         String title = MessageManager.getString(
180                 count == 1 ? "label.close_viewer" : "label.close_viewers");
181         String cancelQuitText = MessageManager
182                 .getString("action.cancel_quit");
183         String[] buttonsText = { MessageManager.getString("action.yes"),
184             MessageManager.getString("action.no"), cancelQuitText };
185
186         int confirmResponse = JvOptionPane.showOptionDialog(
187                 Desktop.instance, prompt, title,
188                 JvOptionPane.YES_NO_CANCEL_OPTION,
189                 JvOptionPane.WARNING_MESSAGE, null, buttonsText,
190                 cancelQuit);
191
192         if (confirmResponse == JvOptionPane.CANCEL_OPTION)
193         {
194           // Cancel Quit
195           QuitHandler.setResponse(QResponse.CANCEL_QUIT);
196         }
197         else
198         {
199           // Close viewers/Leave viewers open
200           StructureViewerBase
201                   .setQuitClose(confirmResponse == JvOptionPane.YES_OPTION);
202         }
203       }
204
205     }
206
207     got = gotQuitResponse();
208
209     boolean wait = false;
210     if (got == QResponse.CANCEL_QUIT)
211     {
212       // reset
213       Console.debug("Cancelling quit.  Resetting response to NULL");
214       setResponse(QResponse.NULL);
215       // but return cancel
216       return QResponse.CANCEL_QUIT;
217     }
218     else if (got == QResponse.QUIT)
219     {
220       if (Cache.getDefault("WAIT_FOR_SAVE", true)
221               && BackupFiles.hasSavesInProgress())
222       {
223         waitQuit(interactive, okQuit, forceQuit, cancelQuit);
224         QResponse waitResponse = gotQuitResponse();
225         wait = waitResponse == QResponse.QUIT;
226       }
227     }
228
229     Callable<Void> next = null;
230     switch (gotQuitResponse())
231     {
232     case QUIT:
233       next = okQuit;
234       break;
235     case FORCE_QUIT: // not actually an option at this stage
236       next = forceQuit;
237       break;
238     default:
239       next = cancelQuit;
240       break;
241     }
242     try
243     {
244       executor.submit(next).get();
245       got = gotQuitResponse();
246     } catch (RejectedExecutionException e)
247     {
248       // QuitHander.abortQuit() probably called
249       // CANCEL_QUIT test will reset QuitHandler
250       Console.info("Quit aborted!");
251       got = QResponse.NULL;
252       setResponse(QResponse.NULL);
253     } catch (InterruptedException | ExecutionException e)
254     {
255       jalview.bin.Console
256               .debug("Exception during quit handling (final choice)", e);
257     }
258     setResponse(got);
259
260     if (quitCancelled())
261     {
262       // reset if cancelled
263       Console.debug("Quit cancelled");
264       setResponse(QResponse.NULL);
265       return QResponse.CANCEL_QUIT;
266     }
267     return gotQuitResponse();
268   }
269
270   private static QResponse waitQuit(boolean interactive,
271           Callable<Void> okQuit, Callable<Void> forceQuit,
272           Callable<Void> cancelQuit)
273   {
274     // check for saves in progress
275     if (!BackupFiles.hasSavesInProgress())
276       return QResponse.QUIT;
277
278     int size = 0;
279     AlignFrame[] afArray = Desktop.getAlignFrames();
280     if (!(afArray == null || afArray.length == 0))
281     {
282       for (int i = 0; i < afArray.length; i++)
283       {
284         AlignFrame af = afArray[i];
285         List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
286         for (AlignmentViewPanel avp : avpList)
287         {
288           AlignmentI a = avp.getAlignment();
289           List<SequenceI> sList = a.getSequences();
290           for (SequenceI s : sList)
291           {
292             size += s.getLength();
293           }
294         }
295       }
296     }
297     int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
298             Math.max(MIN_WAIT_FOR_SAVE, size / 2));
299     Console.debug("Set waitForSave to " + waitTime);
300
301     int iteration = 0;
302     boolean doIterations = true; // note iterations not used in the gui now,
303                                  // only one pass without the "Wait" button
304     while (doIterations && BackupFiles.hasSavesInProgress()
305             && iteration++ < (interactive ? 100 : 5))
306     {
307       // future that returns a Boolean when all files are saved
308       CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
309
310       // callback as each file finishes saving
311       for (CompletableFuture<Boolean> cf : BackupFiles
312               .savesInProgressCompletableFutures(false))
313       {
314         // if this is the last one then complete filesAllSaved
315         cf.whenComplete((ret, e) -> {
316           if (!BackupFiles.hasSavesInProgress())
317           {
318             filesAllSaved.complete(true);
319           }
320         });
321       }
322       try
323       {
324         filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
325       } catch (InterruptedException | ExecutionException e1)
326       {
327         Console.debug(
328                 "Exception whilst waiting for files to save before quit",
329                 e1);
330       } catch (TimeoutException e2)
331       {
332         // this Exception to be expected
333       }
334
335       if (interactive && BackupFiles.hasSavesInProgress())
336       {
337         boolean showForceQuit = iteration > 0; // iteration > 1 to not show
338                                                // force quit the first time
339         JFrame parent = new JFrame();
340         JButton[] buttons = { new JButton(), new JButton() };
341         JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
342         JTextPane messagePane = new JTextPane();
343         messagePane.setBackground(waitDialog.getBackground());
344         messagePane.setBorder(null);
345         messagePane.setText(waitingForSaveMessage());
346         // callback as each file finishes saving
347         for (CompletableFuture<Boolean> cf : BackupFiles
348                 .savesInProgressCompletableFutures(false))
349         {
350           cf.whenComplete((ret, e) -> {
351             if (BackupFiles.hasSavesInProgress())
352             {
353               // update the list of saving files as they save too
354               messagePane.setText(waitingForSaveMessage());
355             }
356             else
357             {
358               if (!(quitCancelled()))
359               {
360                 for (int i = 0; i < buttons.length; i++)
361                 {
362                   Console.debug("DISABLING BUTTON " + buttons[i].getText());
363                   buttons[i].setEnabled(false);
364                   buttons[i].setVisible(false);
365                 }
366                 // if this is the last one then close the dialog
367                 messagePane.setText(new StringBuilder()
368                         .append(MessageManager.getString("label.all_saved"))
369                         .append("\n")
370                         .append(MessageManager
371                                 .getString("label.quitting_bye"))
372                         .toString());
373                 messagePane.setEditable(false);
374                 try
375                 {
376                   Thread.sleep(1500);
377                 } catch (InterruptedException e1)
378                 {
379                 }
380                 parent.dispose();
381               }
382             }
383           });
384         }
385
386         String[] options;
387         int dialogType = -1;
388         if (showForceQuit)
389         {
390           options = new String[2];
391           options[0] = MessageManager.getString("action.force_quit");
392           options[1] = MessageManager.getString("action.cancel_quit");
393           dialogType = JOptionPane.YES_NO_OPTION;
394           waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
395                   .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
396         }
397         else
398         {
399           options = new String[1];
400           options[0] = MessageManager.getString("action.cancel_quit");
401           dialogType = JOptionPane.YES_OPTION;
402           waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
403         }
404         waitDialog.showDialogOnTopAsync(parent, messagePane,
405                 MessageManager.getString("label.wait_for_save"), dialogType,
406                 JOptionPane.WARNING_MESSAGE, null, options,
407                 MessageManager.getString("action.cancel_quit"), true,
408                 buttons);
409
410         parent.dispose();
411         final QResponse thisWaitResponse = gotQuitResponse();
412         switch (thisWaitResponse)
413         {
414         case QUIT: // wait -- do another iteration
415           break;
416         case FORCE_QUIT:
417           doIterations = false;
418           break;
419         case CANCEL_QUIT:
420           doIterations = false;
421           break;
422         case NULL: // already cancelled
423           doIterations = false;
424           break;
425         default:
426         }
427       } // end if interactive
428
429     } // end while wait iteration loop
430     return gotQuitResponse();
431   };
432
433   private static String waitingForSaveMessage()
434   {
435     StringBuilder messageSB = new StringBuilder();
436
437     messageSB.append(MessageManager.getString("label.save_in_progress"));
438     List<File> files = BackupFiles.savesInProgressFiles(false);
439     boolean any = files.size() > 0;
440     if (any)
441     {
442       for (File file : files)
443       {
444         messageSB.append("\n\u2022 ").append(file.getName());
445       }
446     }
447     else
448     {
449       messageSB.append(MessageManager.getString("label.unknown"));
450     }
451     messageSB.append("\n\n")
452             .append(MessageManager.getString("label.quit_after_saving"));
453     return messageSB.toString();
454   }
455
456   public static void abortQuit()
457   {
458     setResponse(QResponse.NULL);
459     // executor.shutdownNow();
460   }
461
462   private static JvOptionPane quitDialog = null;
463
464   private static void setQuitDialog(JvOptionPane qd)
465   {
466     quitDialog = qd;
467   }
468
469   private static JvOptionPane getQuitDialog()
470   {
471     return quitDialog;
472   }
473
474   public static boolean quitCancelled()
475   {
476     return QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
477             || QuitHandler.gotQuitResponse() == QResponse.NULL;
478   }
479
480   public static boolean quitting()
481   {
482     return QuitHandler.gotQuitResponse() == QResponse.QUIT
483             || QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT;
484   }
485 }