From 072176426bbe5af5d6588b3a6eb6d5969fe1b01e Mon Sep 17 00:00:00 2001 From: Ben Soares Date: Fri, 23 Sep 2022 18:34:12 +0100 Subject: [PATCH] JAL-1988 JAL-3772 Quit confirmation dialog boxes with saving files check and wait --- resources/lang/Messages.properties | 4 +- resources/lang/Messages_es.properties | 4 +- src/jalview/bin/Jalview.java | 48 +++++-- src/jalview/gui/Desktop.java | 68 +++++++--- src/jalview/jbgui/GDesktop.java | 11 +- src/jalview/jbgui/QuitHandler.java | 233 +++++++++++++++++++++++++++------ 6 files changed, 301 insertions(+), 67 deletions(-) diff --git a/resources/lang/Messages.properties b/resources/lang/Messages.properties index 6125bb5..f280d66 100644 --- a/resources/lang/Messages.properties +++ b/resources/lang/Messages.properties @@ -34,7 +34,9 @@ action.save_project_as = Save Project as... action.quit = Quit action.force_quit = Force Quit label.quit_jalview = Quit Jalview? -label.save_in_progress = Some files are still saving. Force quit? +label.save_in_progress = Some files are still saving. +action.wait = Wait +action.cancel_quit = Cancel quit action.expand_views = Expand Views action.gather_views = Gather Views action.page_setup = Page Setup... diff --git a/resources/lang/Messages_es.properties b/resources/lang/Messages_es.properties index e375bad..ae5865d 100644 --- a/resources/lang/Messages_es.properties +++ b/resources/lang/Messages_es.properties @@ -34,7 +34,9 @@ action.save_project_as = Guardar proyecto como... action.quit = Salir action.force_quit = Forzar la salida label.quit_jalview = Salir de Jalview? -label.save_in_progress = Algunos archivos aún se están guardando. ¿Forzar la salida? +label.save_in_progress = Algunos archivos aún se están guardando. +action.wait = Espere +action.cancel_quit = Cancelar la salida action.expand_views = Expandir vistas action.gather_views = Capturar vistas action.page_setup = Configuración de la página diff --git a/src/jalview/bin/Jalview.java b/src/jalview/bin/Jalview.java index d1056bf..3d3e99f 100755 --- a/src/jalview/bin/Jalview.java +++ b/src/jalview/bin/Jalview.java @@ -73,6 +73,8 @@ import jalview.io.HtmlSvgOutput; import jalview.io.IdentifyFile; import jalview.io.NewickFile; import jalview.io.gff.SequenceOntologyFactory; +import jalview.jbgui.QuitHandler; +import jalview.jbgui.QuitHandler.QResponse; import jalview.schemes.ColourSchemeI; import jalview.schemes.ColourSchemeProperty; import jalview.util.ChannelProperties; @@ -271,6 +273,25 @@ public class Jalview if (!Platform.isJS()) { System.setSecurityManager(null); + + Runtime.getRuntime().addShutdownHook(new Thread() + { + public void run() + { + Console.debug("Running shutdown hook"); + if (QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT) + { + Console.debug("Checking for saving files"); + QuitHandler.getQuitResponse(false); + } + else + { + Console.debug("Nothing more to do"); + } + Console.debug("Exiting, bye!"); + // shutdownHook cannot be cancelled, JVM will now halt + } + }); } System.out @@ -279,6 +300,7 @@ public class Jalview System.out.println(System.getProperty("os.arch") + " " + System.getProperty("os.name") + " " + System.getProperty("os.version")); + String val = System.getProperty("sys.install4jVersion"); if (val != null) { @@ -299,10 +321,12 @@ public class Jalview Cache.loadBuildProperties(true); ArgsParser aparser = new ArgsParser(args); + boolean headless = false; String usrPropsFile = aparser.getValue("props"); - Cache.loadProperties(usrPropsFile); // must do this before + Cache.loadProperties(usrPropsFile); // must do this + // before if (usrPropsFile != null) { System.out.println( @@ -380,7 +404,9 @@ public class Jalview try { Console.initLogger(); - } catch (NoClassDefFoundError error) + } catch ( + + NoClassDefFoundError error) { error.printStackTrace(); System.out.println("\nEssential logging libraries not found." @@ -555,8 +581,11 @@ public class Jalview } String file = null, data = null; + FileFormatI format = null; + DataSourceType protocol = null; + FileLoader fileLoader = new FileLoader(!headless); String groovyscript = null; // script to execute after all loading is @@ -570,6 +599,7 @@ public class Jalview System.out.println("No files to open!"); System.exit(1); } + long progress = -1; // Finally, deal with the remaining input data. if (file != null) @@ -829,6 +859,7 @@ public class Jalview } } } + AlignFrame startUpAlframe = null; // We'll only open the default file if the desktop is visible. // And the user @@ -1365,19 +1396,12 @@ public class Jalview } /** - * Quit method delegates to Desktop.quit - unless running in headless mode - * when it just ends the JVM + * jalview.bin.Jalview.quit() will just run the non-GUI shutdownHook and exit */ public void quit() { - if (desktop != null) - { - desktop.quit(); - } - else - { - System.exit(0); - } + // System.exit will run the shutdownHook first + System.exit(0); } public static AlignFrame getCurrentAlignFrame() diff --git a/src/jalview/gui/Desktop.java b/src/jalview/gui/Desktop.java index 16603df..585537e 100644 --- a/src/jalview/gui/Desktop.java +++ b/src/jalview/gui/Desktop.java @@ -90,6 +90,7 @@ import javax.swing.JProgressBar; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; +import javax.swing.WindowConstants; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkEvent.EventType; import javax.swing.event.InternalFrameAdapter; @@ -115,6 +116,8 @@ import jalview.io.JalviewFileChooser; import jalview.io.JalviewFileView; import jalview.jbgui.GSplitFrame; import jalview.jbgui.GStructureViewer; +import jalview.jbgui.QuitHandler; +import jalview.jbgui.QuitHandler.QResponse; import jalview.project.Jalview2XML; import jalview.structure.StructureSelectionManager; import jalview.urls.IdOrgSettings; @@ -451,13 +454,17 @@ public class Desktop extends jalview.jbgui.GDesktop setIconImages(ChannelProperties.getIconList()); + this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { - @Override public void windowClosing(WindowEvent ev) { - quit(); + QResponse qresponse = desktopQuit(); + if (qresponse != QResponse.CANCEL_QUIT) + { + instance.dispose(); + } } }); @@ -571,15 +578,6 @@ public class Desktop extends jalview.jbgui.GDesktop this.setDropTarget(new java.awt.dnd.DropTarget(desktop, this)); - this.addWindowListener(new WindowAdapter() - { - @Override - public void windowClosing(WindowEvent evt) - { - quit(); - } - }); - MouseAdapter ma; this.addMouseListener(ma = new MouseAdapter() { @@ -1352,11 +1350,22 @@ public class Desktop extends jalview.jbgui.GDesktop } /* - * Exit the program + * Check with user and saving files before actually quitting */ - @Override - public void quit() + public QResponse desktopQuit() + { + return desktopQuit(true); + } + + public QResponse desktopQuit(boolean ui) { + QuitHandler.QResponse qresponse = QuitHandler.getQuitResponse(ui); + + if (qresponse == QResponse.CANCEL_QUIT) + { + return qresponse; + } + Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); Cache.setProperty("SCREENGEOMETRY_WIDTH", screen.width + ""); Cache.setProperty("SCREENGEOMETRY_HEIGHT", screen.height + ""); @@ -1385,6 +1394,30 @@ public class Desktop extends jalview.jbgui.GDesktop groovyConsole.setDirty(false); groovyConsole.exit(); } + + if (qresponse == QResponse.FORCE_QUIT) + { + // note that shutdown hook will not be run + jalview.bin.Console.debug("Force Quit selected by user"); + Runtime.getRuntime().halt(0); + } + + jalview.bin.Console.debug("Quit selected by user"); + quit(); + + // unlikely to reach here! + return QResponse.QUIT; + } + + /** + * Don't call this directly, use desktopQuit() above. Exits the program. + */ + @Override + public void quit() + { + // this will run the shutdownHook but QuitHandler.getQuitResponse() should + // not run a second time if gotQuitResponse flag has been set (i.e. user + // confirmed quit of some kind). System.exit(0); } @@ -2521,7 +2554,12 @@ public class Desktop extends jalview.jbgui.GDesktop @Override public void actionPerformed(ActionEvent e) { - quit(); + QResponse qresponse = desktopQuit(); + if (qresponse == QResponse.CANCEL_QUIT) + { + jalview.bin.Console + .debug("Desktop: Quit action cancelled by user"); + } } }); } diff --git a/src/jalview/jbgui/GDesktop.java b/src/jalview/jbgui/GDesktop.java index ca95222..7b4f477 100755 --- a/src/jalview/jbgui/GDesktop.java +++ b/src/jalview/jbgui/GDesktop.java @@ -32,7 +32,9 @@ import javax.swing.JMenuItem; import jalview.api.AlignmentViewPanel; import jalview.bin.Cache; +import jalview.gui.Desktop; import jalview.io.FileFormatException; +import jalview.jbgui.QuitHandler.QResponse; import jalview.util.MessageManager; import jalview.util.Platform; @@ -213,7 +215,14 @@ public class GDesktop extends JFrame @Override public void actionPerformed(ActionEvent e) { - quit(); + QResponse qresponse = Desktop.instance != null + ? Desktop.instance.desktopQuit() + : QResponse.QUIT; + if (qresponse == QResponse.CANCEL_QUIT) + { + jalview.bin.Console + .debug("GDesktop: Quit action cancelled by user"); + } } }); aboutMenuItem.setText(MessageManager.getString("label.about")); diff --git a/src/jalview/jbgui/QuitHandler.java b/src/jalview/jbgui/QuitHandler.java index 15cc37a..f7a30f3 100644 --- a/src/jalview/jbgui/QuitHandler.java +++ b/src/jalview/jbgui/QuitHandler.java @@ -1,45 +1,129 @@ package jalview.jbgui; import java.io.File; +import java.util.Date; import javax.swing.JFrame; import javax.swing.JOptionPane; import com.formdev.flatlaf.extras.FlatDesktop; +import jalview.bin.Console; import jalview.io.BackupFiles; import jalview.util.MessageManager; +import jalview.util.Platform; public class QuitHandler { + public static enum QResponse + { + QUIT, CANCEL_QUIT, FORCE_QUIT + }; + public static void setQuitHandler() { FlatDesktop.setQuitHandler(response -> { - // confirm quit if needed and wanted - boolean confirmQuit = jalview.bin.Cache - .getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true); - /* - if undostack is empty - confirmQuit = false - */ - int n = confirmQuit ? JOptionPane.CANCEL_OPTION - : JOptionPane.OK_OPTION; - - // if going to confirm, do it before the save in progress check to give - // the save time to finish! - if (confirmQuit) + QResponse qresponse = getQuitResponse(); + switch (qresponse) { - n = frameOnTop(MessageManager.getString("label.quit_jalview"), - MessageManager.getString("action.quit"), - JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); - + case QUIT: + response.performQuit(); + break; + case CANCEL_QUIT: + response.cancelQuit(); + break; + case FORCE_QUIT: + response.performQuit(); + break; + default: + response.cancelQuit(); } + }); + } + + private static QResponse gotQuitResponse = QResponse.CANCEL_QUIT; + + private static QResponse returnResponse(QResponse qresponse) + { + gotQuitResponse = qresponse; + return qresponse; + } - if (BackupFiles.hasSavesInProgress()) + public static QResponse gotQuitResponse() + { + return gotQuitResponse; + } + + public static QResponse getQuitResponse() + { + return getQuitResponse(true); + } + + public static QResponse getQuitResponse(boolean ui) + { + if (gotQuitResponse() != QResponse.CANCEL_QUIT) + { + return returnResponse(getQuitResponse()); + } + + boolean interactive = ui && !Platform.isHeadless(); + // confirm quit if needed and wanted + boolean confirmQuit = true; + + if (!interactive) + { + Console.debug("Non interactive quit -- not confirming"); + confirmQuit = false; + } + /* + else if (undostack is empty) { + Console.debug("Nothing changed -- not confirming quit"); + confirmQuit = false + } + */ + else + { + confirmQuit = jalview.bin.Cache + .getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true); + Console.debug("Jalview property '" + + jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT + + "' is/defaults to " + confirmQuit + " -- " + + (confirmQuit ? "" : "not ") + "confirming quit"); + } + + int answer = JOptionPane.OK_OPTION; + + // if going to confirm, do it before the save in progress check to give + // the save time to finish! + if (confirmQuit) + { + answer = frameOnTop(MessageManager.getString("label.quit_jalview"), + MessageManager.getString("action.quit"), + JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); + } + + if (answer == JOptionPane.CANCEL_OPTION) + { + Console.debug("QuitHandler: Quit action cancelled by user"); + return returnResponse(QResponse.CANCEL_QUIT); + } + + // check for saves in progress + int waitForSave = 5000; // MAKE THIS BETTER + int waitIncrement = 2000; + long startTime = new Date().getTime(); + boolean saving = BackupFiles.hasSavesInProgress(); + if (saving) + { + boolean waiting = (new Date().getTime() - startTime) < waitForSave; + while (saving && waiting) { - // sleep 1 - // ... + saving = !waitForSave(waitIncrement); + waiting = (new Date().getTime() - startTime) < waitForSave; + } + if (saving) // still saving after a wait + { StringBuilder messageSB = new StringBuilder( MessageManager.getString("label.save_in_progress")); for (File file : BackupFiles.savesInProgressFiles()) @@ -47,26 +131,53 @@ public class QuitHandler messageSB.append("\n"); messageSB.append(file.getName()); } - n = frameOnTop(messageSB.toString(), - MessageManager.getString("action.force_quit"), - JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); + int waitLonger = interactive ? JOptionPane.YES_OPTION + : JOptionPane.NO_OPTION; + while (saving && waitLonger == JOptionPane.YES_OPTION) + { + waitLonger = waitForceQuitCancelQuitOptionDialog( + messageSB.toString(), + MessageManager.getString("action.wait")); + if (waitLonger == JOptionPane.YES_OPTION) // wait + { + Console.debug("*** YES answer=" + waitLonger); + // do wait stuff + saving = !waitForSave(waitIncrement); + } + else if (waitLonger == JOptionPane.NO_OPTION) // force quit + { + Console.debug("*** NO answer=" + waitLonger); + // do a force quit + return returnResponse(QResponse.FORCE_QUIT); // shouldn't reach this + } + else if (waitLonger == JOptionPane.CANCEL_OPTION) // cancel quit + { + Console.debug("*** CANCEL answer=" + waitLonger); + return returnResponse(QResponse.CANCEL_QUIT); + } + else + { + Console.debug("**** Shouldn't have got here!"); + } + } } + } - boolean canQuit = (n == JOptionPane.OK_OPTION); - if (canQuit) - { - response.performQuit(); - } - else - { - response.cancelQuit(); - } - }); + // not cancelled and not saving + return returnResponse(QResponse.QUIT); } public static int frameOnTop(String label, String actionString, int JOPTIONPANE_OPTION, int JOPTIONPANE_MESSAGETYPE) { + return frameOnTop(new JFrame(), label, actionString, JOPTIONPANE_OPTION, + JOPTIONPANE_MESSAGETYPE); + } + + public static int frameOnTop(JFrame dialogParent, String label, + String actionString, int JOPTIONPANE_OPTION, + int JOPTIONPANE_MESSAGETYPE) + { // ensure Jalview window is brought to front for Quit confirmation // window to be visible @@ -76,16 +187,64 @@ public class QuitHandler // a better hack which works instead - JFrame dialogParent = new JFrame(); dialogParent.setAlwaysOnTop(true); - int n = JOptionPane.showConfirmDialog(dialogParent, label, actionString, - JOPTIONPANE_OPTION, JOPTIONPANE_MESSAGETYPE); + int answer = JOptionPane.showConfirmDialog(dialogParent, label, + actionString, JOPTIONPANE_OPTION, JOPTIONPANE_MESSAGETYPE); dialogParent.setAlwaysOnTop(false); dialogParent.dispose(); - return n; + return answer; + } + + private static int waitForceQuitCancelQuitOptionDialog(Object message, + String title) + { + JFrame dialogParent = new JFrame(); + dialogParent.setAlwaysOnTop(true); + String wait = MessageManager.getString("action.wait"); + Object[] options = { wait, + MessageManager.getString("action.force_quit"), + MessageManager.getString("action.cancel_quit") }; + + int answer = JOptionPane.showOptionDialog(dialogParent, message, title, + JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, + null, options, wait); + + return answer; + } + + private static boolean waitForSave(long t) + { + boolean ret = false; + try + { + Console.debug("Wait for save to complete: " + t + "ms"); + long c = 0; + int i = 100; + while (c < t) + { + Thread.sleep(i); + c += i; + ret = !BackupFiles.hasSavesInProgress(); + if (ret) + { + Console.debug( + "Save completed whilst waiting (" + c + "/" + t + "ms)"); + return ret; + } + if (c % 1000 < i) // just gone over another second + { + Console.debug("...waiting (" + c + "/" + t + "ms]"); + } + } + } catch (InterruptedException e) + { + Console.debug("Wait for save interrupted"); + } + Console.debug("Save has " + (ret ? "" : "not ") + "completed"); + return ret; } } -- 1.7.10.2