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