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