7b5be98e31281a53810b19dcc0e1f8d7595ccce9
[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     setResponse(got);
150
151     if (confirmQuit)
152     {
153       JvOptionPane.newOptionDialog()
154               .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
155               .setResponseHandler(JOptionPane.NO_OPTION, defaultCancelQuit)
156               .showDialogOnTopAsync(
157                       new StringBuilder(MessageManager
158                               .getString("label.quit_jalview"))
159                                       .append("\n")
160                                       .append(MessageManager.getString(
161                                               "label.unsaved_changes"))
162                                       .toString(),
163                       MessageManager.getString("action.quit"),
164                       JOptionPane.YES_NO_OPTION,
165                       JOptionPane.QUESTION_MESSAGE, null, new Object[]
166                       { MessageManager.getString("action.quit"),
167                           MessageManager.getString("action.cancel") },
168                       MessageManager.getString("action.quit"), true);
169     }
170
171     got = gotQuitResponse();
172     boolean wait = false;
173     if (got == QResponse.CANCEL_QUIT)
174     {
175       // reset
176       setResponse(QResponse.NULL);
177       // but return cancel
178       return QResponse.CANCEL_QUIT;
179     }
180     else if (got == QResponse.QUIT)
181     {
182       if (Cache.getDefault("WAIT_FOR_SAVE", true)
183               && BackupFiles.hasSavesInProgress())
184       {
185         QResponse waitResponse = waitQuit(interactive, okQuit, forceQuit,
186                 cancelQuit);
187         wait = waitResponse == QResponse.QUIT;
188       }
189     }
190
191     Callable<QResponse> next = null;
192     switch (gotQuitResponse())
193     {
194     case QUIT:
195       next = okQuit;
196       break;
197     case FORCE_QUIT: // not actually an option at this stage
198       next = forceQuit;
199       break;
200     default:
201       next = cancelQuit;
202       break;
203     }
204     try
205     {
206       got = executor.submit(next).get();
207     } catch (InterruptedException | ExecutionException e)
208     {
209       jalview.bin.Console
210               .debug("Exception during quit handling (final choice)", e);
211     }
212     setResponse(got);
213
214     return gotQuitResponse();
215   }
216
217   private static QResponse waitQuit(boolean interactive,
218           Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
219           Callable<QResponse> cancelQuit)
220   {
221     // check for saves in progress
222     if (!BackupFiles.hasSavesInProgress())
223       return QResponse.QUIT;
224
225     int size = 0;
226     AlignFrame[] afArray = Desktop.getAlignFrames();
227     if (!(afArray == null || afArray.length == 0))
228     {
229       for (int i = 0; i < afArray.length; i++)
230       {
231         AlignFrame af = afArray[i];
232         List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
233         for (AlignmentViewPanel avp : avpList)
234         {
235           AlignmentI a = avp.getAlignment();
236           List<SequenceI> sList = a.getSequences();
237           for (SequenceI s : sList)
238           {
239             size += s.getLength();
240           }
241         }
242       }
243     }
244     int waitTime = Math.min(MAX_WAIT_FOR_SAVE,
245             Math.max(MIN_WAIT_FOR_SAVE, size / 2));
246     Console.debug("Set waitForSave to " + waitTime);
247     QResponse waitResponse = QResponse.NULL;
248
249     int iteration = 0;
250     boolean doIterations = true; // note iterations not used in the gui now,
251                                  // only one pass without the "Wait" button
252     while (doIterations && BackupFiles.hasSavesInProgress()
253             && iteration++ < (interactive ? 100 : 5))
254     {
255       // future that returns a Boolean when all files are saved
256       CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
257
258       // callback as each file finishes saving
259       for (CompletableFuture<Boolean> cf : BackupFiles
260               .savesInProgressCompletableFutures(false))
261       {
262         // if this is the last one then complete filesAllSaved
263         cf.whenComplete((ret, e) -> {
264           if (!BackupFiles.hasSavesInProgress())
265           {
266             filesAllSaved.complete(true);
267           }
268         });
269       }
270       try
271       {
272         filesAllSaved.get(waitTime, TimeUnit.MILLISECONDS);
273       } catch (InterruptedException | ExecutionException e1)
274       {
275         Console.debug(
276                 "Exception whilst waiting for files to save before quit",
277                 e1);
278       } catch (TimeoutException e2)
279       {
280         // this Exception to be expected
281       }
282
283       if (interactive && BackupFiles.hasSavesInProgress())
284       {
285         boolean showForceQuit = iteration > 0; // iteration > 1 to not show
286                                                // force quit the first time
287         JFrame parent = new JFrame();
288         JButton[] buttons = { new JButton(), new JButton() };
289         JvOptionPane waitDialog = JvOptionPane.newOptionDialog();
290         JTextPane messagePane = new JTextPane();
291         messagePane.setBackground(waitDialog.getBackground());
292         messagePane.setBorder(null);
293         messagePane.setText(waitingForSaveMessage());
294         // callback as each file finishes saving
295         for (CompletableFuture<Boolean> cf : BackupFiles
296                 .savesInProgressCompletableFutures(false))
297         {
298           cf.whenComplete((ret, e) -> {
299             if (BackupFiles.hasSavesInProgress())
300             {
301               // update the list of saving files as they save too
302               messagePane.setText(waitingForSaveMessage());
303             }
304             else
305             {
306               if (!(QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT
307                       || QuitHandler.gotQuitResponse() == QResponse.NULL))
308               {
309                 for (int i = 0; i < buttons.length; i++)
310                 {
311                   Console.debug("DISABLING BUTTON " + buttons[i].getText());
312                   buttons[i].setEnabled(false);
313                   buttons[i].setVisible(false);
314                 }
315                 // if this is the last one then close the dialog
316                 messagePane.setText(new StringBuilder()
317                         .append(MessageManager.getString("label.all_saved"))
318                         .append("\n")
319                         .append(MessageManager
320                                 .getString("label.quitting_bye"))
321                         .toString());
322                 messagePane.setEditable(false);
323                 try
324                 {
325                   Thread.sleep(1500);
326                 } catch (InterruptedException e1)
327                 {
328                 }
329                 parent.dispose();
330               }
331             }
332           });
333         }
334
335         String[] options;
336         int dialogType = -1;
337         if (showForceQuit)
338         {
339           options = new String[2];
340           options[0] = MessageManager.getString("action.force_quit");
341           options[1] = MessageManager.getString("action.cancel_quit");
342           dialogType = JOptionPane.YES_NO_OPTION;
343           waitDialog.setResponseHandler(JOptionPane.YES_OPTION, forceQuit)
344                   .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit);
345         }
346         else
347         {
348           options = new String[1];
349           options[0] = MessageManager.getString("action.cancel_quit");
350           dialogType = JOptionPane.YES_OPTION;
351           waitDialog.setResponseHandler(JOptionPane.YES_OPTION, cancelQuit);
352         }
353         waitDialog.showDialogOnTopAsync(parent, messagePane,
354                 MessageManager.getString("label.wait_for_save"), dialogType,
355                 JOptionPane.WARNING_MESSAGE, null, options,
356                 MessageManager.getString("action.cancel_quit"), true,
357                 buttons);
358
359         parent.dispose();
360         final QResponse thisWaitResponse = gotQuitResponse();
361         switch (thisWaitResponse)
362         {
363         case QUIT: // wait -- do another iteration
364           break;
365         case FORCE_QUIT:
366           doIterations = false;
367           break;
368         case CANCEL_QUIT:
369           doIterations = false;
370           break;
371         case NULL: // already cancelled
372           doIterations = false;
373           break;
374         default:
375         }
376       } // end if interactive
377
378     } // end while wait iteration loop
379     waitResponse = gotQuitResponse();
380
381     return waitResponse;
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 }