JAL-4305 Isolate and unify the Jalview object from all the gubbins in jalview.bin...
[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).toString(),
166               MessageManager.getString("action.quit"),
167               JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
168               new Object[]
169               { MessageManager.getString("action.quit"),
170                   MessageManager.getString("action.cancel") },
171               MessageManager.getString("action.quit"), true);
172     }
173
174     got = gotQuitResponse();
175
176     // check for external viewer frames
177     if (got != QResponse.CANCEL_QUIT)
178     {
179       int count = Desktop.instance.structureViewersStillRunningCount();
180       if (count > 0)
181       {
182         String alwaysCloseExternalViewers = Cache
183                 .getDefault("ALWAYS_CLOSE_EXTERNAL_VIEWERS", "ask");
184         String prompt = MessageManager
185                 .formatMessage(count == 1 ? "label.confirm_quit_viewer"
186                         : "label.confirm_quit_viewers");
187         String title = MessageManager.getString(
188                 count == 1 ? "label.close_viewer" : "label.close_viewers");
189         String cancelQuitText = MessageManager
190                 .getString("action.cancel_quit");
191         String[] buttonsText = { MessageManager.getString("action.yes"),
192             MessageManager.getString("action.no"), cancelQuitText };
193
194         int confirmResponse = -1;
195         if (alwaysCloseExternalViewers == null || "ask".equals(
196                 alwaysCloseExternalViewers.toLowerCase(Locale.ROOT)))
197         {
198           confirmResponse = JvOptionPane.showOptionDialog(Desktop.instance,
199                   prompt, title, JvOptionPane.YES_NO_CANCEL_OPTION,
200                   JvOptionPane.WARNING_MESSAGE, null, buttonsText,
201                   cancelQuit);
202         }
203         else
204         {
205           confirmResponse = Cache
206                   .getDefault("ALWAYS_CLOSE_EXTERNAL_VIEWERS", false)
207                           ? JvOptionPane.YES_OPTION
208                           : JvOptionPane.NO_OPTION;
209         }
210
211         if (confirmResponse == JvOptionPane.CANCEL_OPTION)
212         {
213           // Cancel Quit
214           QuitHandler.setResponse(QResponse.CANCEL_QUIT);
215         }
216         else
217         {
218           // Close viewers/Leave viewers open
219           StructureViewerBase
220                   .setQuitClose(confirmResponse == JvOptionPane.YES_OPTION);
221         }
222       }
223
224     }
225
226     got = gotQuitResponse();
227
228     boolean wait = false;
229     if (got == QResponse.CANCEL_QUIT)
230     {
231       // reset
232       Console.debug("Cancelling quit.  Resetting response to NULL");
233       setResponse(QResponse.NULL);
234       // but return cancel
235       return QResponse.CANCEL_QUIT;
236     }
237     else if (got == QResponse.QUIT)
238     {
239       if (Cache.getDefault("WAIT_FOR_SAVE", true)
240               && BackupFiles.hasSavesInProgress())
241       {
242         waitQuit(interactive, okQuit, forceQuit, cancelQuit);
243         QResponse waitResponse = gotQuitResponse();
244         wait = waitResponse == QResponse.QUIT;
245       }
246     }
247
248     Runnable next = null;
249     switch (gotQuitResponse())
250     {
251     case QUIT:
252       next = okQuit;
253       break;
254     case FORCE_QUIT: // not actually an option at this stage
255       next = forceQuit;
256       break;
257     default:
258       next = cancelQuit;
259       break;
260     }
261     try
262     {
263       executor.submit(next).get();
264       got = gotQuitResponse();
265     } catch (RejectedExecutionException e)
266     {
267       // QuitHander.abortQuit() probably called
268       // CANCEL_QUIT test will reset QuitHandler
269       Console.info("Quit aborted!");
270       got = QResponse.NULL;
271       setResponse(QResponse.NULL);
272     } catch (InterruptedException | ExecutionException e)
273     {
274       jalview.bin.Console
275               .debug("Exception during quit handling (final choice)", e);
276     }
277     setResponse(got);
278
279     if (quitCancelled())
280     {
281       // reset if cancelled
282       Console.debug("Quit cancelled");
283       setResponse(QResponse.NULL);
284       return QResponse.CANCEL_QUIT;
285     }
286     return gotQuitResponse();
287   }
288
289   private static QResponse waitQuit(boolean interactive, Runnable okQuit,
290           Runnable forceQuit, Runnable cancelQuit)
291   {
292     // check for saves in progress
293     if (!BackupFiles.hasSavesInProgress())
294       return QResponse.QUIT;
295
296     int size = 0;
297     AlignFrame[] afArray = Desktop.getDesktopAlignFrames();
298     if (!(afArray == null || afArray.length == 0))
299     {
300       for (int i = 0; i < afArray.length; i++)
301       {
302         AlignFrame af = afArray[i];
303         List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
304         for (AlignmentViewPanel avp : avpList)
305         {
306           AlignmentI a = avp.getAlignment();
307           List<SequenceI> sList = a.getSequences();
308           for (SequenceI s : sList)
309           {
310             size += s.getLength();
311           }
312         }
313       }
314     }
315     int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
316             Math.max(MIN_WAIT_FOR_SAVE, size / 2));
317     Console.debug("Set waitForSave to " + waitTime);
318
319     int iteration = 0;
320     boolean doIterations = true; // note iterations not used in the gui now,
321                                  // only one pass without the "Wait" button
322     while (doIterations && BackupFiles.hasSavesInProgress()
323             && iteration++ < (interactive ? 100 : 5))
324     {
325       // future that returns a Boolean when all files are saved
326       CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
327
328       // callback as each file finishes saving
329       for (CompletableFuture<Boolean> cf : BackupFiles
330               .savesInProgressCompletableFutures(false))
331       {
332         // if this is the last one then complete filesAllSaved
333         cf.whenComplete((ret, e) -> {
334           if (!BackupFiles.hasSavesInProgress())
335           {
336             filesAllSaved.complete(true);
337           }
338         });
339       }
340       try
341       {
342         filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
343       } catch (InterruptedException | ExecutionException e1)
344       {
345         Console.debug(
346                 "Exception whilst waiting for files to save before quit",
347                 e1);
348       } catch (TimeoutException e2)
349       {
350         // this Exception to be expected
351       }
352
353       if (interactive && BackupFiles.hasSavesInProgress())
354       {
355         boolean showForceQuit = iteration > 0; // iteration > 1 to not show
356                                                // force quit the first time
357         JFrame parent = new JFrame();
358         JButton[] buttons = { new JButton(), new JButton() };
359         JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
360         JTextPane messagePane = new JTextPane();
361         messagePane.setBackground(waitDialog.getBackground());
362         messagePane.setBorder(null);
363         messagePane.setText(waitingForSaveMessage());
364         // callback as each file finishes saving
365         for (CompletableFuture<Boolean> cf : BackupFiles
366                 .savesInProgressCompletableFutures(false))
367         {
368           cf.whenComplete((ret, e) -> {
369             if (BackupFiles.hasSavesInProgress())
370             {
371               // update the list of saving files as they save too
372               messagePane.setText(waitingForSaveMessage());
373             }
374             else
375             {
376               if (!(quitCancelled()))
377               {
378                 for (int i = 0; i < buttons.length; i++)
379                 {
380                   Console.debug("DISABLING BUTTON " + buttons[i].getText());
381                   buttons[i].setEnabled(false);
382                   buttons[i].setVisible(false);
383                 }
384                 // if this is the last one then close the dialog
385                 messagePane.setText(new StringBuilder()
386                         .append(MessageManager.getString("label.all_saved"))
387                         .append("\n")
388                         .append(MessageManager
389                                 .getString("label.quitting_bye"))
390                         .toString());
391                 messagePane.setEditable(false);
392                 try
393                 {
394                   Thread.sleep(1500);
395                 } catch (InterruptedException e1)
396                 {
397                 }
398                 parent.dispose();
399               }
400             }
401           });
402         }
403
404         String[] options;
405         int dialogType = -1;
406         if (showForceQuit)
407         {
408           options = new String[2];
409           options[0] = MessageManager.getString("action.force_quit");
410           options[1] = MessageManager.getString("action.cancel_quit");
411           dialogType = JOptionPane.YES_NO_OPTION;
412           waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
413                   .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
414         }
415         else
416         {
417           options = new String[1];
418           options[0] = MessageManager.getString("action.cancel_quit");
419           dialogType = JOptionPane.YES_OPTION;
420           waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
421         }
422         waitDialog.showDialogOnTopAsync(parent, messagePane,
423                 MessageManager.getString("label.wait_for_save"), dialogType,
424                 JOptionPane.WARNING_MESSAGE, null, options,
425                 MessageManager.getString("action.cancel_quit"), true,
426                 buttons);
427
428         parent.dispose();
429         final QResponse thisWaitResponse = gotQuitResponse();
430         switch (thisWaitResponse)
431         {
432         case QUIT: // wait -- do another iteration
433           break;
434         case FORCE_QUIT:
435           doIterations = false;
436           break;
437         case CANCEL_QUIT:
438           doIterations = false;
439           break;
440         case NULL: // already cancelled
441           doIterations = false;
442           break;
443         default:
444         }
445       } // end if interactive
446
447     } // end while wait iteration loop
448     return gotQuitResponse();
449   };
450
451   private static String waitingForSaveMessage()
452   {
453     StringBuilder messageSB = new StringBuilder();
454
455     messageSB.append(MessageManager.getString("label.save_in_progress"));
456     List<File> files = BackupFiles.savesInProgressFiles(false);
457     boolean any = files.size() > 0;
458     if (any)
459     {
460       for (File file : files)
461       {
462         messageSB.append("\n\u2022 ").append(file.getName());
463       }
464     }
465     else
466     {
467       messageSB.append(MessageManager.getString("label.unknown"));
468     }
469     messageSB.append("\n\n")
470             .append(MessageManager.getString("label.quit_after_saving"));
471     return messageSB.toString();
472   }
473
474   public static void abortQuit()
475   {
476     setResponse(QResponse.NULL);
477     // executor.shutdownNow();
478   }
479
480   private static JvOptionPane quitDialog = null;
481
482   private static void setQuitDialog(JvOptionPane qd)
483   {
484     quitDialog = qd;
485   }
486
487   private static JvOptionPane getQuitDialog()
488   {
489     return quitDialog;
490   }
491
492   public static boolean quitCancelled()
493   {
494     return QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
495             || QuitHandler.gotQuitResponse() == QResponse.NULL;
496   }
497
498   public static boolean quitting()
499   {
500     return QuitHandler.gotQuitResponse() == QResponse.QUIT
501             || QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT;
502   }
503 }