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