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