ef8ec551de90303cac28a56b30b109308ab10821
[jalview.git] / src / jalview / jbgui / QuitHandler.java
1 package jalview.jbgui;
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.TimeUnit;
11 import java.util.concurrent.TimeoutException;
12
13 import javax.swing.JFrame;
14 import javax.swing.JOptionPane;
15 import javax.swing.JTextPane;
16
17 import com.formdev.flatlaf.extras.FlatDesktop;
18
19 import jalview.api.AlignmentViewPanel;
20 import jalview.bin.Cache;
21 import jalview.bin.Console;
22 import jalview.datamodel.AlignmentI;
23 import jalview.datamodel.SequenceI;
24 import jalview.gui.AlignFrame;
25 import jalview.gui.Desktop;
26 import jalview.gui.JvOptionPane;
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 = 5000;
35
36   private static final int MAX_WAIT_FOR_SAVE = 20000;
37
38   private static final int NON_INTERACTIVE_WAIT_CYCLES = 2;
39
40   public static enum QResponse
41   {
42     NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
43   };
44
45   private static ExecutorService executor = Executors.newFixedThreadPool(3);
46
47   public static QResponse setQuitHandler()
48   {
49     FlatDesktop.setQuitHandler(response -> {
50       Callable<QResponse> performQuit = () -> {
51         response.performQuit();
52         return setResponse(QResponse.QUIT);
53       };
54       Callable<QResponse> performForceQuit = () -> {
55         response.performQuit();
56         return setResponse(QResponse.FORCE_QUIT);
57       };
58       Callable<QResponse> cancelQuit = () -> {
59         response.cancelQuit();
60         // reset
61         setResponse(QResponse.NULL);
62         // but return cancel
63         return QResponse.CANCEL_QUIT;
64       };
65       QResponse qresponse = getQuitResponse(true, performQuit,
66               performForceQuit, cancelQuit);
67     });
68
69     return gotQuitResponse();
70   }
71
72   private static QResponse gotQuitResponse = QResponse.NULL;
73
74   private static QResponse setResponse(QResponse qresponse)
75   {
76     gotQuitResponse = qresponse;
77     return qresponse;
78   }
79
80   public static QResponse gotQuitResponse()
81   {
82     return gotQuitResponse;
83   }
84
85   public static final Callable<QResponse> defaultCancelQuit = () -> {
86     Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
87     // reset
88     setResponse(QResponse.NULL);
89     // and return cancel
90     return QResponse.CANCEL_QUIT;
91   };
92
93   public static final Callable<QResponse> defaultOkQuit = () -> {
94     Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
95     return setResponse(QResponse.QUIT);
96   };
97
98   public static final Callable<QResponse> defaultForceQuit = () -> {
99     Console.debug("QuitHandler: (default) Quit action FORCED by user");
100     // note that shutdown hook will not be run
101     Runtime.getRuntime().halt(0);
102     return setResponse(QResponse.FORCE_QUIT); // this line never reached!
103   };
104
105   public static QResponse getQuitResponse(boolean ui)
106   {
107     return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
108             defaultCancelQuit);
109   }
110
111   private static boolean interactive = true;
112
113   public static QResponse getQuitResponse(boolean ui,
114           Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
115           Callable<QResponse> cancelQuit)
116   {
117     QResponse got = gotQuitResponse();
118     if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
119     {
120       // quit has already been selected, continue with calling quit method
121       return got;
122     }
123
124     interactive = ui && !Platform.isHeadless();
125     // confirm quit if needed and wanted
126     boolean confirmQuit = true;
127
128     if (!interactive)
129     {
130       Console.debug("Non interactive quit -- not confirming");
131       confirmQuit = false;
132     }
133     else if (Jalview2XML.allSavedUpToDate())
134     {
135       Console.debug("Nothing changed -- not confirming quit");
136       confirmQuit = false;
137     }
138     else
139     {
140       confirmQuit = jalview.bin.Cache
141               .getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true);
142       Console.debug("Jalview property '"
143               + jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT
144               + "' is/defaults to " + confirmQuit + " -- "
145               + (confirmQuit ? "" : "not ") + "confirming quit");
146     }
147     got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
148     Console.debug("initial calculation, got=" + got);
149     setResponse(got);
150
151     if (confirmQuit)
152     {
153       JvOptionPane.newOptionDialog()
154               .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
155               .setResponseHandler(JOptionPane.NO_OPTION, defaultCancelQuit)
156               .showDialogOnTopAsync(
157                       new StringBuilder(MessageManager
158                               .getString("label.quit_jalview"))
159                                       .append("\n")
160                                       .append(MessageManager.getString(
161                                               "label.unsaved_changes"))
162                                       .toString(),
163                       MessageManager.getString("action.quit"),
164                       JOptionPane.YES_NO_OPTION,
165                       JOptionPane.QUESTION_MESSAGE, null, new Object[]
166                       { MessageManager.getString("action.quit"),
167                           MessageManager.getString("action.cancel") },
168                       MessageManager.getString("action.quit"), true);
169     }
170
171     got = gotQuitResponse();
172     boolean wait = false;
173     if (got == QResponse.CANCEL_QUIT)
174     {
175       // reset
176       setResponse(QResponse.NULL);
177       // but return cancel
178       return QResponse.CANCEL_QUIT;
179     }
180     else if (got == QResponse.QUIT)
181     {
182       if (Cache.getDefault("WAIT_FOR_SAVE", true)
183               && BackupFiles.hasSavesInProgress())
184       {
185         QResponse waitResponse = waitQuit(interactive, okQuit, forceQuit,
186                 cancelQuit);
187         wait = waitResponse == QResponse.QUIT;
188       }
189     }
190
191     Callable<QResponse> next = null;
192     switch (gotQuitResponse())
193     {
194     case QUIT:
195       next = okQuit;
196       break;
197     case FORCE_QUIT: // not actually an option at this stage
198       next = forceQuit;
199       break;
200     default:
201       next = cancelQuit;
202       break;
203     }
204     try
205     {
206       got = executor.submit(next).get();
207     } catch (InterruptedException | ExecutionException e)
208     {
209       jalview.bin.Console
210               .debug("Exception during quit handling (final choice)", e);
211     }
212     setResponse(got);
213
214     return gotQuitResponse();
215   }
216
217   private static QResponse waitQuit(boolean interactive,
218           Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
219           Callable<QResponse> cancelQuit)
220   {
221     // check for saves in progress
222     if (!BackupFiles.hasSavesInProgress())
223       return QResponse.QUIT;
224
225     int size = 0;
226     AlignFrame[] afArray = Desktop.getAlignFrames();
227     if (!(afArray == null || afArray.length == 0))
228     {
229       for (int i = 0; i < afArray.length; i++)
230       {
231         AlignFrame af = afArray[i];
232         List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
233         for (AlignmentViewPanel avp : avpList)
234         {
235           AlignmentI a = avp.getAlignment();
236           List<SequenceI> sList = a.getSequences();
237           for (SequenceI s : sList)
238           {
239             size += s.getLength();
240           }
241         }
242       }
243     }
244     int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
245             Math.max(MIN_WAIT_FOR_SAVE, size / 2));
246     Console.debug("Set waitForSave to " + waitTime);
247     QResponse waitResponse = QResponse.NULL;
248
249     int iteration = 0;
250     boolean doIterations = true;
251     while (doIterations && BackupFiles.hasSavesInProgress()
252             && iteration++ < (interactive ? 100 : 5))
253     {
254       // future that returns a Boolean when all files are saved
255       CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
256
257       // callback as each file finishes saving
258       for (CompletableFuture<Boolean> cf : BackupFiles
259               .savesInProgressCompletableFutures(false))
260       {
261         // if this is the last one then complete filesAllSaved
262         cf.whenComplete((ret, e) -> {
263           if (!BackupFiles.hasSavesInProgress())
264           {
265             filesAllSaved.complete(true);
266           }
267         });
268       }
269       try
270       {
271         filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
272       } catch (InterruptedException | ExecutionException e1)
273       {
274         Console.debug(
275                 "Exception whilst waiting for files to save before quit",
276                 e1);
277       } catch (TimeoutException e2)
278       {
279         // this Exception to be expected
280       }
281
282       if (interactive && BackupFiles.hasSavesInProgress())
283       {
284         boolean allowForceQuit = iteration > 0; // iteration > 1 to not show
285                                                 // force quit the first time
286         JFrame parent = new JFrame();
287         JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
288         if (allowForceQuit)
289         {
290           waitDialog
291                   .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
292                   .setResponseHandler(JOptionPane.NO_OPTION, forceQuit)
293                   .setResponseHandler(JOptionPane.CANCEL_OPTION,
294                           cancelQuit);
295         }
296         else
297         {
298           waitDialog
299                   .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
300                   .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
301         }
302         JTextPane messagePane = new JTextPane();
303         messagePane.setBackground(waitDialog.getBackground());
304         messagePane.setBorder(null);
305         messagePane.setText(waitingForSaveMessage());
306         // callback as each file finishes saving
307         for (CompletableFuture<Boolean> cf : BackupFiles
308                 .savesInProgressCompletableFutures(false))
309         {
310           cf.whenComplete((ret, e) -> {
311             if (BackupFiles.hasSavesInProgress())
312               // update the list of saving files as they save too
313               messagePane.setText(waitingForSaveMessage());
314             else
315             {
316               // if this is the last one then close the dialog
317               messagePane.setText(new StringBuilder()
318                       .append(MessageManager.getString("label.all_saved"))
319                       .append("\n").append(MessageManager
320                               .getString("label.quitting_bye"))
321                       .toString());
322               try
323               {
324                 Console.debug("WAITING FOR MESSAGE");
325                 Thread.sleep(500);
326               } catch (InterruptedException e1)
327               {
328               }
329               // like a click on Wait button
330               waitDialog.setValue(JOptionPane.YES_OPTION);
331               parent.dispose();
332             }
333           });
334         }
335
336         String[] options = new String[allowForceQuit ? 3 : 2];
337         if (allowForceQuit)
338         {
339           options[0] = MessageManager.getString("action.wait");
340           options[1] = MessageManager.getString("action.force_quit");
341           options[2] = MessageManager.getString("action.cancel_quit");
342         }
343         else
344         {
345           options[0] = MessageManager.getString("action.wait");
346           options[1] = MessageManager.getString("action.cancel_quit");
347         }
348         waitDialog.showDialogOnTopAsync(parent, messagePane,
349                 MessageManager.getString("action.wait"),
350                 allowForceQuit ? JOptionPane.YES_NO_CANCEL_OPTION
351                         : JOptionPane.YES_NO_OPTION,
352                 JOptionPane.WARNING_MESSAGE, null, options,
353                 MessageManager.getString("action.wait"), true);
354
355         parent.dispose();
356         final QResponse thisWaitResponse = gotQuitResponse();
357         switch (thisWaitResponse)
358         {
359         case QUIT: // wait -- do another iteration
360           break;
361         case FORCE_QUIT:
362           doIterations = false;
363           break;
364         case CANCEL_QUIT:
365           doIterations = false;
366           break;
367         case NULL: // already cancelled
368           doIterations = false;
369           break;
370         default:
371         }
372       } // end if interactive
373
374     } // end while wait iteration loop
375     waitResponse = gotQuitResponse();
376
377     return waitResponse;
378   };
379
380   private static int waitForceQuitCancelQuitOptionDialog(Object message,
381           String title)
382   {
383     JFrame dialogParent = new JFrame();
384     dialogParent.setAlwaysOnTop(true);
385     String wait = MessageManager.getString("action.wait");
386     Object[] options = { wait,
387         MessageManager.getString("action.force_quit"),
388         MessageManager.getString("action.cancel_quit") };
389
390     int answer = JOptionPane.showOptionDialog(dialogParent, message, title,
391             JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE,
392             null, options, wait);
393
394     return answer;
395   }
396
397   private static String waitingForSaveMessage()
398   {
399     StringBuilder messageSB = new StringBuilder();
400
401     messageSB.append(MessageManager.getString("label.save_in_progress"));
402     List<File> files = BackupFiles.savesInProgressFiles(false);
403     boolean any = files.size() > 0;
404     if (any)
405     {
406       for (File file : files)
407       {
408         messageSB.append("\n- ").append(file.getName());
409       }
410     }
411     else
412     {
413       messageSB.append(MessageManager.getString("label.unknown"));
414     }
415     return messageSB.toString();
416   }
417
418 }