Clover icon

Coverage Report

  1. Project Clover database Mon Sep 2 2024 17:57:51 BST
  2. Package jalview.gui

File QuitHandler.java

 

Coverage histogram

../../img/srcFileCovDistChart8.png
21% of files have more coverage

Code metrics

62
182
14
3
524
430
65
0.36
13
4.67
4.64

Classes

Class Line # Actions
QuitHandler 52 182 65
0.736434173.6%
QuitHandler.QResponse 62 0 0
-1.0 -
QuitHandler.Message 67 0 0
-1.0 -
 

Contributing tests

This file is covered by 153 tests. .

Source view

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