e90a2d5f07913a4ad0bc05ed7dd0943e3a8eb5c7
[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.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.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 MIN_WAIT_FOR_SAVE = 3000;
33
34   private static final int MAX_WAIT_FOR_SAVE = 20000;
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<Void> performQuit = () -> {
47         response.performQuit();
48         setResponse(QResponse.QUIT);
49         return null;
50       };
51       Callable<Void> performForceQuit = () -> {
52         response.performQuit();
53         setResponse(QResponse.FORCE_QUIT);
54         return null;
55       };
56       Callable<Void> cancelQuit = () -> {
57         response.cancelQuit();
58         // reset
59         setResponse(QResponse.NULL);
60         return null;
61       };
62       getQuitResponse(true, performQuit, 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     return qresponse;
74   }
75
76   public static QResponse gotQuitResponse()
77   {
78     return gotQuitResponse;
79   }
80
81   public static final Callable<Void> defaultCancelQuit = () -> {
82     Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
83     // reset
84     setResponse(QResponse.NULL);
85     return null;
86   };
87
88   public static final Callable<Void> defaultOkQuit = () -> {
89     Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
90     setResponse(QResponse.QUIT);
91     return null;
92   };
93
94   public static final Callable<Void> 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     setResponse(QResponse.FORCE_QUIT); // this line never reached!
99     return null;
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, Callable<Void> okQuit,
111           Callable<Void> forceQuit, Callable<Void> 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       return got;
118     }
119
120     interactive = ui && !Platform.isHeadless();
121     // confirm quit if needed and wanted
122     boolean confirmQuit = true;
123
124     if (!interactive)
125     {
126       Console.debug("Non interactive quit -- not confirming");
127       confirmQuit = false;
128     }
129     else if (Jalview2XML.allSavedUpToDate())
130     {
131       Console.debug("Nothing changed -- not confirming quit");
132       confirmQuit = false;
133     }
134     else
135     {
136       confirmQuit = jalview.bin.Cache
137               .getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true);
138       Console.debug("Jalview property '"
139               + jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT
140               + "' is/defaults to " + confirmQuit + " -- "
141               + (confirmQuit ? "" : "not ") + "confirming quit");
142     }
143     got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
144     setResponse(got);
145
146     if (confirmQuit)
147     {
148       JvOptionPane.newOptionDialog()
149               .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
150               .setResponseHandler(JOptionPane.NO_OPTION, defaultCancelQuit)
151               .showDialogOnTopAsync(
152                       new StringBuilder(MessageManager
153                               .getString("label.quit_jalview"))
154                                       .append("\n")
155                                       .append(MessageManager.getString(
156                                               "label.unsaved_changes"))
157                                       .toString(),
158                       MessageManager.getString("action.quit"),
159                       JOptionPane.YES_NO_OPTION,
160                       JOptionPane.QUESTION_MESSAGE, null, new Object[]
161                       { MessageManager.getString("action.quit"),
162                           MessageManager.getString("action.cancel") },
163                       MessageManager.getString("action.quit"), true);
164     }
165
166     got = gotQuitResponse();
167     boolean wait = false;
168     if (got == QResponse.CANCEL_QUIT)
169     {
170       // reset
171       setResponse(QResponse.NULL);
172       // but return cancel
173       return QResponse.CANCEL_QUIT;
174     }
175     else if (got == QResponse.QUIT)
176     {
177       if (Cache.getDefault("WAIT_FOR_SAVE", true)
178               && BackupFiles.hasSavesInProgress())
179       {
180         waitQuit(interactive, okQuit, forceQuit, cancelQuit);
181         QResponse waitResponse = gotQuitResponse();
182         wait = waitResponse == QResponse.QUIT;
183       }
184     }
185
186     Callable<Void> next = null;
187     switch (gotQuitResponse())
188     {
189     case QUIT:
190       next = okQuit;
191       break;
192     case FORCE_QUIT: // not actually an option at this stage
193       next = forceQuit;
194       break;
195     default:
196       next = cancelQuit;
197       break;
198     }
199     try
200     {
201       executor.submit(next).get();
202       got = gotQuitResponse();
203     } catch (InterruptedException | ExecutionException e)
204     {
205       jalview.bin.Console
206               .debug("Exception during quit handling (final choice)", e);
207     }
208     setResponse(got);
209
210     if (gotQuitResponse() == QResponse.CANCEL_QUIT)
211     {
212       // reset if cancelled
213       setResponse(QResponse.NULL);
214       return QResponse.CANCEL_QUIT;
215     }
216     return gotQuitResponse();
217   }
218
219   private static QResponse waitQuit(boolean interactive,
220           Callable<Void> okQuit, Callable<Void> forceQuit,
221           Callable<Void> cancelQuit)
222   {
223     // check for saves in progress
224     if (!BackupFiles.hasSavesInProgress())
225       return QResponse.QUIT;
226
227     int size = 0;
228     AlignFrame[] afArray = Desktop.getAlignFrames();
229     if (!(afArray == null || afArray.length == 0))
230     {
231       for (int i = 0; i < afArray.length; i++)
232       {
233         AlignFrame af = afArray[i];
234         List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
235         for (AlignmentViewPanel avp : avpList)
236         {
237           AlignmentI a = avp.getAlignment();
238           List<SequenceI> sList = a.getSequences();
239           for (SequenceI s : sList)
240           {
241             size += s.getLength();
242           }
243         }
244       }
245     }
246     int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
247             Math.max(MIN_WAIT_FOR_SAVE, size / 2));
248     Console.debug("Set waitForSave to " + waitTime);
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     return gotQuitResponse();
381   };
382
383   private static String waitingForSaveMessage()
384   {
385     StringBuilder messageSB = new StringBuilder();
386
387     messageSB.append(MessageManager.getString("label.save_in_progress"));
388     List<File> files = BackupFiles.savesInProgressFiles(false);
389     boolean any = files.size() > 0;
390     if (any)
391     {
392       for (File file : files)
393       {
394         messageSB.append("\n\u2022 ").append(file.getName());
395       }
396     }
397     else
398     {
399       messageSB.append(MessageManager.getString("label.unknown"));
400     }
401     messageSB.append("\n\n")
402             .append(MessageManager.getString("label.quit_after_saving"));
403     return messageSB.toString();
404   }
405
406   public static void abortQuit()
407   {
408     setResponse(QResponse.CANCEL_QUIT);
409   }
410 }