JAL-1988 JAL-3772 wait timer working first time but blocking desktop gui updates
[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
11 import javax.swing.JFrame;
12 import javax.swing.JOptionPane;
13
14 import com.formdev.flatlaf.extras.FlatDesktop;
15
16 import jalview.api.AlignmentViewPanel;
17 import jalview.bin.Cache;
18 import jalview.bin.Console;
19 import jalview.datamodel.AlignmentI;
20 import jalview.datamodel.SequenceI;
21 import jalview.gui.AlignFrame;
22 import jalview.gui.Desktop;
23 import jalview.gui.JvOptionPane;
24 import jalview.io.BackupFiles;
25 import jalview.project.Jalview2XML;
26 import jalview.util.MessageManager;
27 import jalview.util.Platform;
28
29 public class QuitHandler
30 {
31   private static final int INITIAL_WAIT_FOR_SAVE = 3000;
32
33   private static final int NON_INTERACTIVE_WAIT_CYCLES = 2;
34
35   public static enum QResponse
36   {
37     NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
38   };
39
40   private static ExecutorService executor = Executors.newFixedThreadPool(3);
41
42   public static QResponse setQuitHandler()
43   {
44     FlatDesktop.setQuitHandler(response -> {
45       Callable<QResponse> performQuit = () -> {
46         response.performQuit();
47         return setResponse(QResponse.QUIT);
48       };
49       Callable<QResponse> performForceQuit = () -> {
50         response.performQuit();
51         return setResponse(QResponse.FORCE_QUIT);
52       };
53       Callable<QResponse> cancelQuit = () -> {
54         response.cancelQuit();
55         // reset
56         setResponse(QResponse.NULL);
57         // but return cancel
58         return QResponse.CANCEL_QUIT;
59       };
60       QResponse qresponse = getQuitResponse(true, performQuit,
61               performForceQuit, cancelQuit);
62     });
63
64     return gotQuitResponse();
65   }
66
67   private static QResponse gotQuitResponse = QResponse.NULL;
68
69   private static QResponse setResponse(QResponse qresponse)
70   {
71     gotQuitResponse = qresponse;
72     Console.debug("##### Setting gotQuitResponse to " + qresponse);
73     return qresponse;
74   }
75
76   public static QResponse gotQuitResponse()
77   {
78     return gotQuitResponse;
79   }
80
81   public static final Callable<QResponse> defaultCancelQuit = () -> {
82     Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
83     // reset
84     setResponse(QResponse.NULL);
85     // and return cancel
86     return QResponse.CANCEL_QUIT;
87   };
88
89   public static final Callable<QResponse> defaultOkQuit = () -> {
90     Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
91     return setResponse(QResponse.QUIT);
92   };
93
94   public static final Callable<QResponse> defaultForceQuit = () -> {
95     Console.debug("QuitHandler: (default) Quit action FORCED by user");
96     // note that shutdown hook will not be run
97     Runtime.getRuntime().halt(0);
98     return setResponse(QResponse.FORCE_QUIT); // this line never reached!
99   };
100
101   public static QResponse getQuitResponse(boolean ui)
102   {
103     return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
104             defaultCancelQuit);
105   }
106
107   private static boolean interactive = true;
108
109   public static QResponse getQuitResponse(boolean ui,
110           Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
111           Callable<QResponse> cancelQuit)
112   {
113     QResponse got = gotQuitResponse();
114     if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
115     {
116       // quit has already been selected, continue with calling quit method
117       Console.debug("##### getQuitResponse called. gotQuitResponse=" + got);
118       return got;
119     }
120
121     interactive = ui && !Platform.isHeadless();
122     // confirm quit if needed and wanted
123     boolean confirmQuit = true;
124
125     if (!interactive)
126     {
127       Console.debug("Non interactive quit -- not confirming");
128       confirmQuit = false;
129     }
130     else if (Jalview2XML.allSavedUpToDate())
131     {
132       Console.debug("Nothing changed -- not confirming quit");
133       confirmQuit = false;
134     }
135     else
136     {
137       confirmQuit = jalview.bin.Cache
138               .getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true);
139       Console.debug("Jalview property '"
140               + jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT
141               + "' is/defaults to " + confirmQuit + " -- "
142               + (confirmQuit ? "" : "not ") + "confirming quit");
143     }
144     got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
145     Console.debug("initial calculation, got=" + got);
146     setResponse(got);
147
148     if (confirmQuit)
149     {
150
151       Console.debug("********************ABOUT TO CONFIRM QUIT");
152       JvOptionPane quitDialog = JvOptionPane.newOptionDialog()
153               .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
154               .setResponseHandler(JOptionPane.NO_OPTION, defaultCancelQuit);
155       quitDialog.showDialogOnTopAsync(
156               new StringBuilder(
157                       MessageManager.getString("label.quit_jalview"))
158                               .append("\n")
159                               .append(MessageManager
160                                       .getString("label.unsaved_changes"))
161                               .toString(),
162               MessageManager.getString("action.quit"),
163               JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
164               new Object[]
165               { MessageManager.getString("action.quit"),
166                   MessageManager.getString("action.cancel") },
167               MessageManager.getString("action.quit"), true);
168     }
169
170     got = gotQuitResponse();
171     Console.debug("first user response, got=" + got);
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         /*
186         Future<QResponse> waitGot = executor.submit(waitQuitCall);
187         try
188         {
189           got = waitGot.get();
190         } catch (InterruptedException | ExecutionException e)
191         {
192           jalview.bin.Console.debug(
193                   "Exception during quit handling (wait for save)", e);
194         }
195         */
196         QResponse waitResponse = waitQuit(interactive, okQuit, forceQuit,
197                 cancelQuit);
198         wait = waitResponse == QResponse.QUIT;
199       }
200     }
201
202     Callable<QResponse> next = null;
203     switch (gotQuitResponse())
204     {
205     case QUIT:
206       Console.debug("### User selected QUIT");
207       next = okQuit;
208       break;
209     case FORCE_QUIT: // not actually an option at this stage
210       Console.debug("### User selected FORCE QUIT");
211       next = forceQuit;
212       break;
213     default:
214       Console.debug("### User selected CANCEL QUIT");
215       next = cancelQuit;
216       break;
217     }
218     try
219     {
220       got = executor.submit(next).get();
221     } catch (InterruptedException | ExecutionException e)
222     {
223       jalview.bin.Console
224               .debug("Exception during quit handling (final choice)", e);
225     }
226     jalview.bin.Console.debug("### nextResponse=" + got.toString());
227     setResponse(got);
228
229     return gotQuitResponse();
230   }
231
232   private static QResponse waitQuit(boolean interactive,
233           Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
234           Callable<QResponse> cancelQuit)
235   {
236     jalview.bin.Console.debug("#### waitQuit started");
237     // check for saves in progress
238     if (!BackupFiles.hasSavesInProgress())
239       return QResponse.QUIT;
240
241     int waitTime = INITIAL_WAIT_FOR_SAVE; // start with 3 second wait
242     AlignFrame[] afArray = Desktop.getAlignFrames();
243     if (!(afArray == null || afArray.length == 0))
244     {
245       int size = 0;
246       for (int i = 0; i < afArray.length; i++)
247       {
248         AlignFrame af = afArray[i];
249         List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
250         for (AlignmentViewPanel avp : avpList)
251         {
252           AlignmentI a = avp.getAlignment();
253           List<SequenceI> sList = a.getSequences();
254           for (SequenceI s : sList)
255           {
256             size += s.getLength();
257           }
258         }
259       }
260       waitTime = Math.max(waitTime, size / 2);
261       Console.debug("Set waitForSave to " + waitTime);
262     }
263     final int waitTimeFinal = waitTime;
264     QResponse waitResponse = QResponse.NULL;
265
266     // future that returns a Boolean when all files are saved
267     CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
268
269     // callback as each file finishes saving
270     for (CompletableFuture<Boolean> cf : BackupFiles
271             .savesInProgressCompletableFutures())
272     {
273       // if this is the last one then complete filesAllSaved
274       cf.whenComplete((ret, e) -> {
275         if (!BackupFiles.hasSavesInProgress())
276         {
277           filesAllSaved.complete(true);
278         }
279       });
280     }
281
282     // timeout the wait -- will result in another wait button when looped
283     CompletableFuture<Boolean> waitTimeout = CompletableFuture
284             .supplyAsync(() -> {
285               Console.debug("################# STARTING WAIT SLEEP");
286               try
287               {
288                 Thread.sleep(waitTimeFinal);
289               } catch (InterruptedException e)
290               {
291                 // probably interrupted by all files saving
292               }
293               return true;
294             });
295     CompletableFuture<Object> waitForSave = CompletableFuture
296             .anyOf(waitTimeout, filesAllSaved);
297
298     int iteration = 0;
299     boolean doIterations = true;
300     while (doIterations && BackupFiles.hasSavesInProgress()
301             && iteration++ < (interactive ? 100 : 5))
302     {
303       try
304       {
305         waitForSave.copy().get();
306       } catch (InterruptedException | ExecutionException e1)
307       {
308         Console.debug(
309                 "Exception whilst waiting for files to save before quit",
310                 e1);
311       }
312       if (interactive && BackupFiles.hasSavesInProgress())
313       {
314         Console.debug("********************About to make waitDialog");
315         JFrame parent = new JFrame();
316         JvOptionPane waitDialog = JvOptionPane.newOptionDialog()
317                 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
318                 .setResponseHandler(JOptionPane.NO_OPTION, forceQuit)
319                 .setResponseHandler(JOptionPane.CANCEL_OPTION, cancelQuit);
320
321         // callback as each file finishes saving
322         for (CompletableFuture<Boolean> cf : BackupFiles
323                 .savesInProgressCompletableFutures())
324         {
325           cf.whenComplete((ret, e) -> {
326             Console.debug("############# A FILE SAVED!");
327             // update the list of saving files as they save too
328             waitDialog.setMessage(waitingForSaveMessage());
329             waitDialog.setName("AAARGH!");
330             // if this is the last one then close the dialog
331             if (!BackupFiles.hasSavesInProgress())
332             {
333               // like a click on Wait button ???
334               Console.debug(
335                       "***** TRYING TO MAKE THE WAIT FOR SAVE DIALOG DISAPPEAR!");
336               waitDialog.setValue(JOptionPane.YES_OPTION);
337               parent.dispose();
338             }
339           });
340         }
341
342         waitDialog.showDialogOnTopAsync(parent, waitingForSaveMessage(),
343                 MessageManager.getString("action.wait"),
344                 JOptionPane.YES_NO_CANCEL_OPTION,
345                 JOptionPane.WARNING_MESSAGE, null, new Object[]
346                 { MessageManager.getString("action.wait"),
347                     MessageManager.getString("action.force_quit"),
348                     MessageManager.getString("action.cancel_quit") },
349                 MessageManager.getString("action.wait"), true);
350         Console.debug("********************Finished waitDialog");
351
352         final QResponse thisWaitResponse = gotQuitResponse();
353         Console.debug("####### WAITFORSAVE SET: " + thisWaitResponse);
354         switch (thisWaitResponse)
355         {
356         case QUIT: // wait -- do another iteration
357           break;
358         case FORCE_QUIT:
359           doIterations = false;
360           break;
361         case CANCEL_QUIT:
362           doIterations = false;
363           break;
364         case NULL: // already cancelled
365           doIterations = false;
366           break;
367         default:
368         }
369       } // end if interactive
370
371     } // end while wait iteration loop
372     waitResponse = gotQuitResponse();
373
374     Console.debug("####### WAITFORSAVE RETURNING: " + waitResponse);
375     return waitResponse;
376   };
377
378   public static void okk()
379   {
380     /*
381     if (false)
382     {
383       if (false)
384       {
385     
386         waitLonger = JOptionPane.showOptionDialog(dialogParent,
387                 waitingForSaveMessage(),
388                 MessageManager.getString("action.wait"),
389                 JOptionPane.YES_NO_CANCEL_OPTION,
390                 JOptionPane.WARNING_MESSAGE, null, options, wait);
391       }
392       else
393       {
394         // non-interactive
395         waitLonger = iteration < NON_INTERACTIVE_WAIT_CYCLES
396                 ? JOptionPane.YES_OPTION
397                 : JOptionPane.NO_OPTION;
398       }
399     
400       if (waitLonger == JOptionPane.YES_OPTION) // "wait"
401       {
402         saving = !waitForSave(waitIncrement);
403       }
404       else if (waitLonger == JOptionPane.NO_OPTION) // "force
405       // quit"
406       {
407         // do a force quit
408         return setResponse(QResponse.FORCE_QUIT);
409       }
410       else if (waitLonger == JOptionPane.CANCEL_OPTION) // cancel quit
411       {
412         return setResponse(QResponse.CANCEL_QUIT);
413       }
414       else
415       {
416         // Most likely got here by user dismissing the dialog with the
417         // 'x'
418         // -- treat as a "Cancel"
419         return setResponse(QResponse.CANCEL_QUIT);
420       }
421     }
422     
423     // not sure how we got here, best be safe
424     return QResponse.CANCEL_QUIT;
425     */
426   };
427
428   private static int waitForceQuitCancelQuitOptionDialog(Object message,
429           String title)
430   {
431     JFrame dialogParent = new JFrame();
432     dialogParent.setAlwaysOnTop(true);
433     String wait = MessageManager.getString("action.wait");
434     Object[] options = { wait,
435         MessageManager.getString("action.force_quit"),
436         MessageManager.getString("action.cancel_quit") };
437
438     // BackupFiles.setWaitForSaveDialog(dialogParent);
439
440     int answer = JOptionPane.showOptionDialog(dialogParent, message, title,
441             JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE,
442             null, options, wait);
443
444     // BackupFiles.clearWaitForSaveDialog();
445
446     return answer;
447   }
448
449   private static String waitingForSaveMessage()
450   {
451     StringBuilder messageSB = new StringBuilder(
452             MessageManager.getString("label.save_in_progress"));
453     boolean any = false;
454     for (File file : BackupFiles.savesInProgressFiles())
455     {
456       messageSB.append("\n- ");
457       messageSB.append(file.getName());
458       any = true;
459     }
460     if (!any)
461     {
462       messageSB.append("\n");
463       messageSB.append(MessageManager.getString("label.unknown"));
464     }
465
466     return messageSB.toString();
467   }
468
469   private static Boolean waitForSave(long t)
470   {
471     boolean ret = false;
472     try
473     {
474       Console.debug("Wait for save to complete: " + t + "ms");
475       long c = 0;
476       int i = 100;
477       while (c < t)
478       {
479         Thread.sleep(i);
480         c += i;
481         ret = !BackupFiles.hasSavesInProgress();
482         if (ret)
483         {
484           Console.debug(
485                   "Save completed whilst waiting (" + c + "/" + t + "ms)");
486           return ret;
487         }
488         if (c % 1000 < i) // just gone over another second
489         {
490           Console.debug("...waiting (" + c + "/" + t + "ms]");
491         }
492       }
493     } catch (InterruptedException e)
494     {
495       Console.debug("Wait for save interrupted");
496     }
497     Console.debug("Save has " + (ret ? "" : "not ") + "completed");
498     return ret;
499   }
500
501 }