Clover icon

Coverage Report

  1. Project Clover database Wed Sep 18 2024 02:54:09 BST
  2. Package jalview.gui

File QuitHandler.java

 

Coverage histogram

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

Code metrics

62
201
14
3
558
460
65
0.32
14.36
4.67
4.64

Classes

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