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