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