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