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