JAL-4125 Additions to JvOptionPane and QuitHandler with StructureViewerBase handling...
[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   private static ExecutorService executor = Executors.newFixedThreadPool(3);
45
46   public static QResponse setQuitHandler()
47   {
48     FlatDesktop.setQuitHandler(response -> {
49       Callable<Void> performQuit = () -> {
50         response.performQuit();
51         setResponse(QResponse.QUIT);
52         return null;
53       };
54       Callable<Void> performForceQuit = () -> {
55         response.performQuit();
56         setResponse(QResponse.FORCE_QUIT);
57         return null;
58       };
59       Callable<Void> cancelQuit = () -> {
60         response.cancelQuit();
61         // reset
62         setResponse(QResponse.NULL);
63         return null;
64       };
65       getQuitResponse(true, performQuit, performForceQuit, cancelQuit);
66     });
67
68     return gotQuitResponse();
69   }
70
71   private static QResponse gotQuitResponse = QResponse.NULL;
72
73   protected static QResponse setResponse(QResponse qresponse)
74   {
75     gotQuitResponse = qresponse;
76     return qresponse;
77   }
78
79   public static QResponse gotQuitResponse()
80   {
81     return gotQuitResponse;
82   }
83
84   public static final Callable<Void> defaultCancelQuit = () -> {
85     Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
86     // reset
87     setResponse(QResponse.CANCEL_QUIT);
88     return null;
89   };
90
91   public static final Callable<Void> defaultOkQuit = () -> {
92     Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
93     setResponse(QResponse.QUIT);
94     return null;
95   };
96
97   public static final Callable<Void> defaultForceQuit = () -> {
98     Console.debug("QuitHandler: (default) Quit action FORCED by user");
99     // note that shutdown hook will not be run
100     Runtime.getRuntime().halt(0);
101     setResponse(QResponse.FORCE_QUIT); // this line never reached!
102     return null;
103   };
104
105   public static QResponse getQuitResponse(boolean ui)
106   {
107     return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
108             defaultCancelQuit);
109   }
110
111   public static QResponse getQuitResponse(boolean ui, Callable<Void> okQuit,
112           Callable<Void> forceQuit, Callable<Void> 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       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     setResponse(got);
146
147     if (confirmQuit)
148     {
149       setQuitDialog(JvOptionPane.newOptionDialog()
150               .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
151               .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit));
152       JvOptionPane qd = getQuitDialog();
153       qd.showDialogOnTopAsync(
154               new StringBuilder(
155                       MessageManager.getString("label.quit_jalview"))
156                       .append("\n")
157                       .append(MessageManager
158                               .getString("label.unsaved_changes"))
159                       .toString(),
160               MessageManager.getString("action.quit"),
161               JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
162               new Object[]
163               { MessageManager.getString("action.quit"),
164                   MessageManager.getString("action.cancel") },
165               MessageManager.getString("action.quit"), true);
166     }
167
168     got = gotQuitResponse();
169     boolean wait = false;
170     if (got == QResponse.CANCEL_QUIT)
171     {
172       // reset
173       Console.debug("Cancelling quit.  Resetting response to NULL");
174       setResponse(QResponse.NULL);
175       // but return cancel
176       return QResponse.CANCEL_QUIT;
177     }
178     else if (got == QResponse.QUIT)
179     {
180       if (Cache.getDefault("WAIT_FOR_SAVE", true)
181               && BackupFiles.hasSavesInProgress())
182       {
183         waitQuit(interactive, okQuit, forceQuit, cancelQuit);
184         QResponse waitResponse = gotQuitResponse();
185         wait = waitResponse == QResponse.QUIT;
186       }
187     }
188
189     Callable<Void> next = null;
190     switch (gotQuitResponse())
191     {
192     case QUIT:
193       next = okQuit;
194       break;
195     case FORCE_QUIT: // not actually an option at this stage
196       next = forceQuit;
197       break;
198     default:
199       next = cancelQuit;
200       break;
201     }
202     try
203     {
204       executor.submit(next).get();
205       got = gotQuitResponse();
206     } catch (RejectedExecutionException e)
207     {
208       // QuitHander.abortQuit() probably called
209       // CANCEL_QUIT test will reset QuitHandler
210       Console.info("Quit aborted!");
211       got = QResponse.NULL;
212       setResponse(QResponse.NULL);
213     } catch (InterruptedException | ExecutionException e)
214     {
215       jalview.bin.Console
216               .debug("Exception during quit handling (final choice)", e);
217     }
218     setResponse(got);
219
220     if (quitCancelled())
221     {
222       // reset if cancelled
223       Console.debug("Quit cancelled");
224       setResponse(QResponse.NULL);
225       return QResponse.CANCEL_QUIT;
226     }
227     return gotQuitResponse();
228   }
229
230   private static QResponse waitQuit(boolean interactive,
231           Callable<Void> okQuit, Callable<Void> forceQuit,
232           Callable<Void> cancelQuit)
233   {
234     // check for saves in progress
235     if (!BackupFiles.hasSavesInProgress())
236       return QResponse.QUIT;
237
238     int size = 0;
239     AlignFrame[] afArray = Desktop.getAlignFrames();
240     if (!(afArray == null || afArray.length == 0))
241     {
242       for (int i = 0; i < afArray.length; i++)
243       {
244         AlignFrame af = afArray[i];
245         List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
246         for (AlignmentViewPanel avp : avpList)
247         {
248           AlignmentI a = avp.getAlignment();
249           List<SequenceI> sList = a.getSequences();
250           for (SequenceI s : sList)
251           {
252             size += s.getLength();
253           }
254         }
255       }
256     }
257     int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
258             Math.max(MIN_WAIT_FOR_SAVE, size / 2));
259     Console.debug("Set waitForSave to " + waitTime);
260
261     int iteration = 0;
262     boolean doIterations = true; // note iterations not used in the gui now,
263                                  // only one pass without the "Wait" button
264     while (doIterations && BackupFiles.hasSavesInProgress()
265             && iteration++ < (interactive ? 100 : 5))
266     {
267       // future that returns a Boolean when all files are saved
268       CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
269
270       // callback as each file finishes saving
271       for (CompletableFuture<Boolean> cf : BackupFiles
272               .savesInProgressCompletableFutures(false))
273       {
274         // if this is the last one then complete filesAllSaved
275         cf.whenComplete((ret, e) -> {
276           if (!BackupFiles.hasSavesInProgress())
277           {
278             filesAllSaved.complete(true);
279           }
280         });
281       }
282       try
283       {
284         filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
285       } catch (InterruptedException | ExecutionException e1)
286       {
287         Console.debug(
288                 "Exception whilst waiting for files to save before quit",
289                 e1);
290       } catch (TimeoutException e2)
291       {
292         // this Exception to be expected
293       }
294
295       if (interactive && BackupFiles.hasSavesInProgress())
296       {
297         boolean showForceQuit = iteration > 0; // iteration > 1 to not show
298                                                // force quit the first time
299         JFrame parent = new JFrame();
300         JButton[] buttons = { new JButton(), new JButton() };
301         JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
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             {
313               // update the list of saving files as they save too
314               messagePane.setText(waitingForSaveMessage());
315             }
316             else
317             {
318               if (!(quitCancelled()))
319               {
320                 for (int i = 0; i < buttons.length; i++)
321                 {
322                   Console.debug("DISABLING BUTTON " + buttons[i].getText());
323                   buttons[i].setEnabled(false);
324                   buttons[i].setVisible(false);
325                 }
326                 // if this is the last one then close the dialog
327                 messagePane.setText(new StringBuilder()
328                         .append(MessageManager.getString("label.all_saved"))
329                         .append("\n")
330                         .append(MessageManager
331                                 .getString("label.quitting_bye"))
332                         .toString());
333                 messagePane.setEditable(false);
334                 try
335                 {
336                   Thread.sleep(1500);
337                 } catch (InterruptedException e1)
338                 {
339                 }
340                 parent.dispose();
341               }
342             }
343           });
344         }
345
346         String[] options;
347         int dialogType = -1;
348         if (showForceQuit)
349         {
350           options = new String[2];
351           options[0] = MessageManager.getString("action.force_quit");
352           options[1] = MessageManager.getString("action.cancel_quit");
353           dialogType = JOptionPane.YES_NO_OPTION;
354           waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
355                   .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
356         }
357         else
358         {
359           options = new String[1];
360           options[0] = MessageManager.getString("action.cancel_quit");
361           dialogType = JOptionPane.YES_OPTION;
362           waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
363         }
364         waitDialog.showDialogOnTopAsync(parent, messagePane,
365                 MessageManager.getString("label.wait_for_save"), dialogType,
366                 JOptionPane.WARNING_MESSAGE, null, options,
367                 MessageManager.getString("action.cancel_quit"), true,
368                 buttons);
369
370         parent.dispose();
371         final QResponse thisWaitResponse = gotQuitResponse();
372         switch (thisWaitResponse)
373         {
374         case QUIT: // wait -- do another iteration
375           break;
376         case FORCE_QUIT:
377           doIterations = false;
378           break;
379         case CANCEL_QUIT:
380           doIterations = false;
381           break;
382         case NULL: // already cancelled
383           doIterations = false;
384           break;
385         default:
386         }
387       } // end if interactive
388
389     } // end while wait iteration loop
390     return gotQuitResponse();
391   };
392
393   private static String waitingForSaveMessage()
394   {
395     StringBuilder messageSB = new StringBuilder();
396
397     messageSB.append(MessageManager.getString("label.save_in_progress"));
398     List<File> files = BackupFiles.savesInProgressFiles(false);
399     boolean any = files.size() > 0;
400     if (any)
401     {
402       for (File file : files)
403       {
404         messageSB.append("\n\u2022 ").append(file.getName());
405       }
406     }
407     else
408     {
409       messageSB.append(MessageManager.getString("label.unknown"));
410     }
411     messageSB.append("\n\n")
412             .append(MessageManager.getString("label.quit_after_saving"));
413     return messageSB.toString();
414   }
415
416   public static void abortQuit()
417   {
418     setResponse(QResponse.NULL);
419     // executor.shutdownNow();
420   }
421
422   private static JvOptionPane quitDialog = null;
423
424   private static void setQuitDialog(JvOptionPane qd)
425   {
426     quitDialog = qd;
427   }
428
429   private static JvOptionPane getQuitDialog()
430   {
431     return quitDialog;
432   }
433
434   public static boolean quitCancelled()
435   {
436     return QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
437             || QuitHandler.gotQuitResponse() == QResponse.NULL;
438   }
439
440   public static boolean quitting()
441   {
442     return QuitHandler.gotQuitResponse() == QResponse.QUIT
443             || QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT;
444   }
445 }