JAL-1988 JAL-3772 Non-blocking modal dialogs for unsaved changes and saving files...
[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
264     // future that returns a Boolean when all files are saved
265     CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
266
267     // callback as each file finishes saving
268     for (CompletableFuture<Boolean> cf : BackupFiles
269             .savesInProgressCompletableFutures())
270     {
271       // if this is the last one then complete filesAllSaved
272       cf.whenComplete((ret, e) -> {
273         if (!BackupFiles.hasSavesInProgress())
274         {
275           filesAllSaved.complete(true);
276         }
277       });
278     }
279
280     final int waitTimeFinal = waitTime;
281     // timeout the wait -- will result in another wait button when looped
282     CompletableFuture<Boolean> waitTimeout = CompletableFuture
283             .supplyAsync(() -> {
284               executor.submit(() -> {
285                 try
286                 {
287                   Thread.sleep(waitTimeFinal);
288                 } catch (InterruptedException e)
289                 {
290                   // probably interrupted by all files saving
291                 }
292               });
293               return true;
294             });
295     CompletableFuture<Object> waitForSave = CompletableFuture
296             .anyOf(waitTimeout, filesAllSaved);
297     Console.debug("##### WAITFORSAVE RUNNING");
298
299     QResponse waitResponse = QResponse.NULL;
300
301     int iteration = 0;
302     boolean doIterations = true;
303     while (doIterations && BackupFiles.hasSavesInProgress()
304             && iteration++ < (interactive ? 100 : 5))
305     {
306       try
307       {
308         waitForSave.get();
309       } catch (InterruptedException | ExecutionException e1)
310       {
311         Console.debug(
312                 "Exception whilst waiting for files to save before quitting",
313                 e1);
314       }
315       if (interactive)
316       {
317         Console.debug("********************About to make waitDialog");
318         JvOptionPane waitDialog = JvOptionPane.newOptionDialog()
319                 .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
320                 .setResponseHandler(JOptionPane.NO_OPTION, forceQuit)
321                 .setResponseHandler(JOptionPane.CANCEL_OPTION, cancelQuit);
322
323         // callback as each file finishes saving
324         for (CompletableFuture<Boolean> cf : BackupFiles
325                 .savesInProgressCompletableFutures())
326         {
327           // update the list of saving files as they save too
328           cf.thenRun(() -> {
329             waitDialog.setMessage(waitingForSaveMessage());
330           });
331           // if this is the last one then close the dialog
332           cf.whenComplete((ret, e) -> {
333             if (!BackupFiles.hasSavesInProgress())
334             {
335               // like a click on Wait button
336               Console.debug(
337                       "***** TRYING TO MAKE THE WAIT FOR SAVE DIALOG DISAPPEAR!");
338               waitDialog.setValue(JOptionPane.YES_OPTION);
339             }
340           });
341         }
342
343         waitDialog.showDialogOnTopAsync(waitingForSaveMessage(),
344                 MessageManager.getString("action.wait"),
345                 JOptionPane.YES_NO_CANCEL_OPTION,
346                 JOptionPane.WARNING_MESSAGE, null, new Object[]
347                 { MessageManager.getString("action.wait"),
348                     MessageManager.getString("action.force_quit"),
349                     MessageManager.getString("action.cancel_quit") },
350                 MessageManager.getString("action.wait"), true);
351         Console.debug("********************Finished waitDialog");
352
353         waitResponse = gotQuitResponse();
354         Console.debug("####### WAITFORSAVE SET: " + waitResponse);
355         switch (waitResponse)
356         {
357         case QUIT: // wait -- do another iteration
358           break;
359         case FORCE_QUIT:
360           doIterations = false;
361           break;
362         case CANCEL_QUIT:
363           doIterations = false;
364           break;
365         case NULL: // already cancelled
366           doIterations = false;
367           break;
368         default:
369         }
370       } // end if interactive
371
372     }
373     waitResponse = gotQuitResponse();
374
375     Console.debug("####### WAITFORSAVE RETURNING: " + waitResponse);
376     return waitResponse;
377   };
378
379   public static void okk()
380   {
381     /*
382     if (false)
383     {
384       if (false)
385       {
386     
387         waitLonger = JOptionPane.showOptionDialog(dialogParent,
388                 waitingForSaveMessage(),
389                 MessageManager.getString("action.wait"),
390                 JOptionPane.YES_NO_CANCEL_OPTION,
391                 JOptionPane.WARNING_MESSAGE, null, options, wait);
392       }
393       else
394       {
395         // non-interactive
396         waitLonger = iteration < NON_INTERACTIVE_WAIT_CYCLES
397                 ? JOptionPane.YES_OPTION
398                 : JOptionPane.NO_OPTION;
399       }
400     
401       if (waitLonger == JOptionPane.YES_OPTION) // "wait"
402       {
403         saving = !waitForSave(waitIncrement);
404       }
405       else if (waitLonger == JOptionPane.NO_OPTION) // "force
406       // quit"
407       {
408         // do a force quit
409         return setResponse(QResponse.FORCE_QUIT);
410       }
411       else if (waitLonger == JOptionPane.CANCEL_OPTION) // cancel quit
412       {
413         return setResponse(QResponse.CANCEL_QUIT);
414       }
415       else
416       {
417         // Most likely got here by user dismissing the dialog with the
418         // 'x'
419         // -- treat as a "Cancel"
420         return setResponse(QResponse.CANCEL_QUIT);
421       }
422     }
423     
424     // not sure how we got here, best be safe
425     return QResponse.CANCEL_QUIT;
426     */
427   };
428
429   private static int waitForceQuitCancelQuitOptionDialog(Object message,
430           String title)
431   {
432     JFrame dialogParent = new JFrame();
433     dialogParent.setAlwaysOnTop(true);
434     String wait = MessageManager.getString("action.wait");
435     Object[] options = { wait,
436         MessageManager.getString("action.force_quit"),
437         MessageManager.getString("action.cancel_quit") };
438
439     // BackupFiles.setWaitForSaveDialog(dialogParent);
440
441     int answer = JOptionPane.showOptionDialog(dialogParent, message, title,
442             JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE,
443             null, options, wait);
444
445     // BackupFiles.clearWaitForSaveDialog();
446
447     return answer;
448   }
449
450   private static String waitingForSaveMessage()
451   {
452     StringBuilder messageSB = new StringBuilder(
453             MessageManager.getString("label.save_in_progress"));
454     boolean any = false;
455     for (File file : BackupFiles.savesInProgressFiles())
456     {
457       messageSB.append("\n- ");
458       messageSB.append(file.getName());
459       any = true;
460     }
461     if (!any)
462     {
463       messageSB.append("\n");
464       messageSB.append(MessageManager.getString("label.unknown"));
465     }
466
467     return messageSB.toString();
468   }
469
470   private static Boolean waitForSave(long t)
471   {
472     boolean ret = false;
473     try
474     {
475       Console.debug("Wait for save to complete: " + t + "ms");
476       long c = 0;
477       int i = 100;
478       while (c < t)
479       {
480         Thread.sleep(i);
481         c += i;
482         ret = !BackupFiles.hasSavesInProgress();
483         if (ret)
484         {
485           Console.debug(
486                   "Save completed whilst waiting (" + c + "/" + t + "ms)");
487           return ret;
488         }
489         if (c % 1000 < i) // just gone over another second
490         {
491           Console.debug("...waiting (" + c + "/" + t + "ms]");
492         }
493       }
494     } catch (InterruptedException e)
495     {
496       Console.debug("Wait for save interrupted");
497     }
498     Console.debug("Save has " + (ret ? "" : "not ") + "completed");
499     return ret;
500   }
501
502 }