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