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