Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
QuitHandler | 52 | 182 | 65 | ||
QuitHandler.QResponse | 62 | 0 | 0 | ||
QuitHandler.Message | 67 | 0 | 0 |
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 | public static void setMessage(Message m) |
75 | { | |
76 | 452 | message = m; |
77 | } | |
78 | ||
79 | private static ExecutorService executor = Executors.newFixedThreadPool(3); | |
80 | ||
81 | 0 | public static void setQuitHandler() |
82 | { | |
83 | 0 | FlatDesktop.setQuitHandler(response -> { |
84 | 0 | flatlafResponse = response; |
85 | 0 | Desktop.instance.desktopQuit(); |
86 | }); | |
87 | } | |
88 | ||
89 | 99 | public static void startForceQuit() |
90 | { | |
91 | 99 | setResponse(QResponse.FORCE_QUIT); |
92 | } | |
93 | ||
94 | private static QResponse gotQuitResponse = QResponse.NULL; | |
95 | ||
96 | 131 | protected static QResponse setResponse(QResponse qresponse) |
97 | { | |
98 | 131 | gotQuitResponse = qresponse; |
99 | 131 | if ((qresponse == QResponse.CANCEL_QUIT || qresponse == QResponse.NULL) |
100 | && flatlafResponse != null) | |
101 | { | |
102 | 0 | flatlafResponse.cancelQuit(); |
103 | } | |
104 | 131 | return qresponse; |
105 | } | |
106 | ||
107 | 150 | public static QResponse gotQuitResponse() |
108 | { | |
109 | 150 | 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 | public static QResponse getQuitResponse(boolean ui) |
131 | { | |
132 | 6 | return getQuitResponse(ui, defaultOkQuit, defaultForceQuit, |
133 | defaultCancelQuit); | |
134 | } | |
135 | ||
136 | 7 | 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 | 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 | 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 | public static void abortQuit() |
496 | { | |
497 | 0 | setResponse(QResponse.NULL); |
498 | // executor.shutdownNow(); | |
499 | } | |
500 | ||
501 | private static JvOptionPane quitDialog = null; | |
502 | ||
503 | 1 | private static void setQuitDialog(JvOptionPane qd) |
504 | { | |
505 | 1 | quitDialog = qd; |
506 | } | |
507 | ||
508 | 1 | private static JvOptionPane getQuitDialog() |
509 | { | |
510 | 1 | return quitDialog; |
511 | } | |
512 | ||
513 | 9 | public static boolean quitCancelled() |
514 | { | |
515 | 9 | return QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT |
516 | || QuitHandler.gotQuitResponse() == QResponse.NULL; | |
517 | } | |
518 | ||
519 | 0 | public static boolean quitting() |
520 | { | |
521 | 0 | return QuitHandler.gotQuitResponse() == QResponse.QUIT |
522 | || QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT; | |
523 | } | |
524 | } |