Merge branch 'develop' into bug/JAL-1988_JAL-3772_improved_quit_handling
authorBen Soares <b.soares@dundee.ac.uk>
Thu, 29 Sep 2022 14:55:25 +0000 (15:55 +0100)
committerBen Soares <b.soares@dundee.ac.uk>
Thu, 29 Sep 2022 14:55:25 +0000 (15:55 +0100)
12 files changed:
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/bin/Cache.java
src/jalview/bin/Jalview.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/Desktop.java
src/jalview/io/BackupFiles.java
src/jalview/jbgui/APQHandlers.java
src/jalview/jbgui/GDesktop.java
src/jalview/jbgui/QuitHandler.java [new file with mode: 0644]
src/jalview/project/Jalview2XML.java
src/jalview/viewmodel/AlignmentViewport.java

index 3843ddb..f196687 100644 (file)
@@ -32,7 +32,13 @@ action.load_project = Load Project
 action.save_project = Save Project
 action.save_project_as = Save Project as...
 action.quit = Quit
-label.quit_jalview = Quit Jalview?
+action.force_quit = Force Quit
+label.quit_jalview = Are you sure you want to quit Jalview?
+label.unsaved_changes = There are unsaved changes.
+label.save_in_progress = Some files are still saving.
+label.unknown = Unknown
+action.wait = Wait
+action.cancel_quit = Cancel quit
 action.expand_views = Expand Views
 action.gather_views = Gather Views
 action.page_setup = Page Setup...
index d0bfd65..5f49205 100644 (file)
@@ -32,7 +32,13 @@ action.load_project = Cargar proyecto
 action.save_project = Guardar proyecto
 action.save_project_as = Guardar proyecto como...
 action.quit = Salir
-label.quit_jalview = Salir de Jalview?
+action.force_quit = Forzar la salida
+label.quit_jalview = ¿Estás seguro de que quieres salir de Jalview?
+label.unsaved_changes = Hay cambios sin guardar.
+label.save_in_progress = Algunos archivos aún se están guardando.
+label.unknown = desconocido
+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
index 370a243..bb70c40 100755 (executable)
@@ -1190,6 +1190,7 @@ public class Cache
     sb.append("Java version: ");
     sb.append(System.getProperty("java.version"));
     sb.append("\n");
+    sb.append("Java platform: ");
     sb.append(System.getProperty("os.arch"));
     sb.append(" ");
     sb.append(System.getProperty("os.name"));
@@ -1210,17 +1211,19 @@ public class Cache
     sb.append(" (");
     sb.append(lafClass);
     sb.append(")\n");
+    appendIfNotNull(sb, "Channel: ",
+            ChannelProperties.getProperty("channel"), "\n", null);
     if (Console.isDebugEnabled()
             || !"release".equals(ChannelProperties.getProperty("channel")))
     {
-      appendIfNotNull(sb, "Channel: ",
-              ChannelProperties.getProperty("channel"), "\n", null);
       appendIfNotNull(sb, "Getdown appdir: ",
               System.getProperty("getdowninstanceappdir"), "\n", null);
       appendIfNotNull(sb, "Getdown appbase: ",
               System.getProperty("getdowninstanceappbase"), "\n", null);
       appendIfNotNull(sb, "Java home: ", System.getProperty("java.home"),
               "\n", "unknown");
+      appendIfNotNull(sb, "Preferences file: ", propertiesFile, "\n",
+              "unknown");
     }
     return sb.toString();
   }
@@ -1401,10 +1404,11 @@ public class Cache
                 if (customProxySet &&
                 // we have a username but no password for the scheme being
                 // requested
-                (protocol.equalsIgnoreCase("http")
-                        && (httpUser != null && httpUser.length() > 0
-                                && (httpPassword == null
-                                        || httpPassword.length == 0)))
+                        (protocol.equalsIgnoreCase("http")
+                                && (httpUser != null
+                                        && httpUser.length() > 0
+                                        && (httpPassword == null
+                                                || httpPassword.length == 0)))
                         || (protocol.equalsIgnoreCase("https")
                                 && (httpsUser != null
                                         && httpsUser.length() > 0
index d1056bf..3d3e99f 100755 (executable)
@@ -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()
index e24cbea..8a6907c 100644 (file)
@@ -20,8 +20,6 @@
  */
 package jalview.gui;
 
-import java.util.Locale;
-
 import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Component;
@@ -59,6 +57,7 @@ import java.util.Deque;
 import java.util.Enumeration;
 import java.util.Hashtable;
 import java.util.List;
+import java.util.Locale;
 import java.util.Vector;
 
 import javax.swing.ButtonGroup;
@@ -1259,6 +1258,12 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       lastSaveSuccessful = new Jalview2XML().saveAlignment(this, file,
               shortName);
 
+      Console.debug("lastSaveSuccessful=" + lastSaveSuccessful);
+      if (lastSaveSuccessful)
+      {
+        this.getViewport().setSavedUpToDate(true);
+      }
+
       statusBar.setText(MessageManager.formatMessage(
               "label.successfully_saved_to_file_in_format", new Object[]
               { file, format }));
@@ -1356,6 +1361,12 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
                             + (lastSaveSuccessful ? "" : "un")
                             + "successfully");
           }
+
+          Console.debug("lastSaveSuccessful=" + lastSaveSuccessful);
+          if (lastSaveSuccessful)
+          {
+            AlignFrame.this.getViewport().setSavedUpToDate(true);
+          }
         }
       }
     };
index 16603df..585537e 100644 (file)
@@ -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");
+        }
       }
     });
   }
index 2039d3c..529e1b2 100644 (file)
@@ -29,6 +29,7 @@ import java.nio.file.StandardCopyOption;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
 
@@ -105,6 +106,52 @@ public class BackupFiles
 
   private static final String oldTempFileSuffix = "_oldfile_tobedeleted";
 
+  private static List<BackupFiles> savesInProgress = new ArrayList<>();
+
+  private boolean addSaveInProgress()
+  {
+    if (savesInProgress.contains(this))
+    {
+      return false;
+    }
+    else
+    {
+      savesInProgress.add(this);
+      return true;
+    }
+  }
+
+  private boolean removeSaveInProgress()
+  {
+    if (savesInProgress.contains(this))
+    {
+      // remove all occurrences
+      while (savesInProgress.remove(this))
+      {
+      }
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  public static boolean hasSavesInProgress()
+  {
+    return savesInProgress.size() > 0;
+  }
+
+  public static List<File> savesInProgressFiles()
+  {
+    List<File> files = new ArrayList<>();
+    for (BackupFiles bfile : savesInProgress)
+    {
+      files.add(bfile.getFile());
+    }
+    return files;
+  }
+
   public BackupFiles(String filename)
   {
     this(new File(filename));
@@ -116,6 +163,10 @@ public class BackupFiles
   {
     classInit();
     this.file = file;
+
+    // add this file from the save in progress stack
+    addSaveInProgress();
+
     BackupFilesPresetEntry bfpe = BackupFilesPresetEntry
             .getSavedBackupEntry();
     this.suffix = bfpe.suffix;
@@ -819,6 +870,9 @@ public class BackupFiles
       tidyUpFiles();
     }
 
+    // remove this file from the save in progress stack
+    removeSaveInProgress();
+
     return rename;
   }
 
@@ -890,6 +944,11 @@ public class BackupFiles
     return ret;
   }
 
+  public File getFile()
+  {
+    return file;
+  }
+
   public static boolean moveFileToFile(File oldFile, File newFile)
   {
     Console.initLogger();
index 1a7e971..12ce1ba 100644 (file)
  */
 package jalview.jbgui;
 
-import javax.swing.JFrame;
-import javax.swing.JOptionPane;
-
 import com.formdev.flatlaf.extras.FlatDesktop;
 import com.formdev.flatlaf.extras.FlatDesktop.Action;
 
-import jalview.util.MessageManager;
 import jalview.util.Platform;
 
 public class APQHandlers
@@ -59,47 +55,7 @@ public class APQHandlers
     }
     if (FlatDesktop.isSupported(Action.APP_QUIT_HANDLER))
     {
-      FlatDesktop.setQuitHandler(response -> {
-        boolean confirmQuit = jalview.bin.Cache.getDefault(
-                jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true);
-        boolean canQuit = !confirmQuit;
-        int n;
-        if (confirmQuit)
-        {
-          // ensure Jalview window is brought to front for Quit confirmation
-          // window to be visible
-
-          // this method of raising the Jalview window is broken in java
-          // jalviewDesktop.setVisible(true);
-          // jalviewDesktop.toFront();
-
-          // a better hack which works instead
-          JFrame dialogParent = new JFrame();
-          dialogParent.setAlwaysOnTop(true);
-
-          n = JOptionPane.showConfirmDialog(dialogParent,
-                  MessageManager.getString("label.quit_jalview"),
-                  MessageManager.getString("action.quit"),
-                  JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE,
-                  null);
-
-          dialogParent.setAlwaysOnTop(false);
-          dialogParent.dispose();
-        }
-        else
-        {
-          n = JOptionPane.OK_OPTION;
-        }
-        canQuit = (n == JOptionPane.OK_OPTION);
-        if (canQuit)
-        {
-          response.performQuit();
-        }
-        else
-        {
-          response.cancelQuit();
-        }
-      });
+      QuitHandler.setQuitHandler();
       setQuit = true;
     }
     // if we got to here, no exceptions occurred when we set the handlers.
index ca95222..7b4f477 100755 (executable)
@@ -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
new file mode 100644 (file)
index 0000000..6a95db7
--- /dev/null
@@ -0,0 +1,293 @@
+package jalview.jbgui;
+
+import java.io.File;
+import java.util.Date;
+import java.util.List;
+
+import javax.swing.JFrame;
+import javax.swing.JOptionPane;
+
+import com.formdev.flatlaf.extras.FlatDesktop;
+
+import jalview.api.AlignmentViewPanel;
+import jalview.bin.Console;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.gui.Desktop;
+import jalview.io.BackupFiles;
+import jalview.project.Jalview2XML;
+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 -> {
+      QResponse qresponse = getQuitResponse();
+      switch (qresponse)
+      {
+      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;
+  }
+
+  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 (Jalview2XML.allSavedUpToDate())
+    {
+      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(
+              new StringBuilder(
+                      MessageManager.getString("label.quit_jalview"))
+                              .append(" ")
+                              .append(MessageManager
+                                      .getString("label.unsaved_changes"))
+                              .toString(),
+              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 = 1000; // MAKE THIS BETTER
+    AlignFrame[] afArray = Desktop.getAlignFrames();
+    if (afArray == null || afArray.length == 0)
+    {
+      // no change
+    }
+    else
+    {
+      int size = 0;
+      for (int i = 0; i < afArray.length; i++)
+      {
+        AlignFrame af = afArray[i];
+        List<AlignmentViewPanel> avpList = (List<AlignmentViewPanel>) af
+                .getAlignPanels();
+        for (AlignmentViewPanel avp : avpList)
+        {
+          AlignmentI a = avp.getAlignment();
+          List<SequenceI> sList = a.getSequences();
+          for (SequenceI s : sList)
+          {
+            size += s.getLength();
+          }
+        }
+      }
+      waitForSave = size;
+      Console.debug("Set waitForSave to " + waitForSave);
+    }
+    int waitIncrement = 3000;
+    long startTime = new Date().getTime();
+    boolean saving = BackupFiles.hasSavesInProgress();
+    if (saving)
+    {
+      boolean waiting = (new Date().getTime() - startTime) < waitForSave;
+      while (saving && waiting)
+      {
+        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"));
+        messageSB.append(":");
+        boolean any = false;
+        for (File file : BackupFiles.savesInProgressFiles())
+        {
+          messageSB.append("\n- ");
+          messageSB.append(file.getName());
+          any = true;
+        }
+        if (!any)
+        {
+          messageSB.append("\n");
+          messageSB.append(MessageManager.getString("label.unknown"));
+        }
+        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
+          {
+            // do wait stuff
+            saving = !waitForSave(waitIncrement);
+          }
+          else if (waitLonger == JOptionPane.NO_OPTION) // force quit
+          {
+            // do a force quit
+            return returnResponse(QResponse.FORCE_QUIT); // shouldn't reach this
+          }
+          else if (waitLonger == JOptionPane.CANCEL_OPTION) // cancel quit
+          {
+            return returnResponse(QResponse.CANCEL_QUIT);
+          }
+          else
+          {
+            Console.debug("**** Shouldn't have got here!");
+          }
+        }
+      }
+    }
+
+    // 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
+
+    // this method of raising the Jalview window is broken in java
+    // jalviewDesktop.setVisible(true);
+    // jalviewDesktop.toFront();
+
+    // a better hack which works instead
+
+    dialogParent.setAlwaysOnTop(true);
+
+    int answer = JOptionPane.showConfirmDialog(dialogParent, label,
+            actionString, JOPTIONPANE_OPTION, JOPTIONPANE_MESSAGETYPE);
+
+    dialogParent.setAlwaysOnTop(false);
+    dialogParent.dispose();
+
+    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;
+  }
+
+}
index d4b2c04..351fc04 100644 (file)
@@ -229,6 +229,11 @@ public class Jalview2XML
   private static final String UTF_8 = "UTF-8";
 
   /**
+   * used in decision if quit confirmation should be issued
+   */
+  private static boolean stateSavedUpToDate = false;
+
+  /**
    * prefix for recovering datasets for alignments with multiple views where
    * non-existent dataset IDs were written for some views
    */
@@ -616,6 +621,27 @@ public class Jalview2XML
   {
     AlignFrame[] frames = Desktop.getAlignFrames();
 
+    setStateSavedUpToDate(true);
+
+    if (Cache.getDefault("DEBUG_DELAY_SAVE", false))
+    {
+      int n = 20;
+      int i = 0;
+      while (i < n)
+      {
+        Console.debug("***** debugging save sleep " + i + "/" + n);
+        try
+        {
+          Thread.sleep(1000);
+        } catch (InterruptedException e)
+        {
+          // TODO Auto-generated catch block
+          e.printStackTrace();
+        }
+        i++;
+      }
+    }
+
     if (frames == null)
     {
       return;
@@ -763,6 +789,25 @@ public class Jalview2XML
       FileOutputStream fos = new FileOutputStream(
               doBackup ? backupfiles.getTempFilePath() : jarFile);
 
+      if (Cache.getDefault("DEBUG_DELAY_SAVE", false))
+      {
+        int n = 20;
+        int i = 0;
+        while (i < n)
+        {
+          Console.debug("***** debugging save sleep " + i + "/" + n);
+          try
+          {
+            Thread.sleep(1000);
+          } catch (InterruptedException e)
+          {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+          }
+          i++;
+        }
+      }
+
       JarOutputStream jout = new JarOutputStream(fos);
       List<AlignFrame> frames = new ArrayList<>();
 
@@ -6537,4 +6582,33 @@ public class Jalview2XML
 
     return colour;
   }
+
+  public static void setStateSavedUpToDate(boolean s)
+  {
+    Console.debug("Setting overall stateSavedUpToDate to " + s);
+    stateSavedUpToDate = s;
+  }
+
+  public static boolean stateSavedUpToDate()
+  {
+    Console.debug("Returning overall stateSavedUpToDate value: "
+            + stateSavedUpToDate);
+    return stateSavedUpToDate;
+  }
+
+  public static boolean allSavedUpToDate()
+  {
+    if (stateSavedUpToDate()) // nothing happened since last project save
+      return true;
+
+    AlignFrame[] frames = Desktop.getAlignFrames();
+    for (int i = 0; i < frames.length; i++)
+    {
+      if (frames[i] == null)
+        continue;
+      if (!frames[i].getViewport().savedUpToDate())
+        return false; // at least one alignment is not individually saved
+    }
+    return true;
+  }
 }
index 08af2ec..975b292 100644 (file)
  */
 package jalview.viewmodel;
 
+import java.awt.Color;
+import java.beans.PropertyChangeSupport;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
 import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
 import jalview.analysis.Conservation;
 import jalview.analysis.TreeModel;
@@ -29,6 +41,7 @@ import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 import jalview.api.FeaturesDisplayedI;
 import jalview.api.ViewStyleI;
+import jalview.bin.Console;
 import jalview.commands.CommandI;
 import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.AlignmentAnnotation;
@@ -45,6 +58,7 @@ import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.project.Jalview2XML;
 import jalview.renderer.ResidueShader;
 import jalview.renderer.ResidueShaderI;
 import jalview.schemes.ColourSchemeI;
@@ -61,18 +75,6 @@ import jalview.workers.ComplementConsensusThread;
 import jalview.workers.ConsensusThread;
 import jalview.workers.StrucConsensusThread;
 
-import java.awt.Color;
-import java.beans.PropertyChangeSupport;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.BitSet;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
 /**
  * base class holding visualization and analysis attributes and common logic for
  * an active alignment view displayed in the GUI
@@ -100,6 +102,11 @@ public abstract class AlignmentViewport
   protected Deque<CommandI> redoList = new ArrayDeque<>();
 
   /**
+   * used to determine if quit should be confirmed
+   */
+  private boolean savedUpToDate = true;
+
+  /**
    * alignment displayed in the viewport. Please use get/setter
    */
   protected AlignmentI alignment;
@@ -2614,6 +2621,8 @@ public abstract class AlignmentViewport
     {
       this.historyList.push(command);
       broadcastCommand(command, false);
+      setSavedUpToDate(false);
+      Jalview2XML.setStateSavedUpToDate(false);
     }
   }
 
@@ -3096,4 +3105,18 @@ public abstract class AlignmentViewport
     return (alignment.getHiddenColumns().getVisContigsIterator(start, end,
             false));
   }
+
+  public void setSavedUpToDate(boolean s)
+  {
+    Console.debug(
+            "Setting " + this.getViewId() + " setSavedUpToDate to " + s);
+    savedUpToDate = s;
+  }
+
+  public boolean savedUpToDate()
+  {
+    Console.debug("Returning " + this.getViewId() + " savedUpToDate value: "
+            + savedUpToDate);
+    return savedUpToDate;
+  }
 }