JAL-4285 Better checkFiles processing. Better dialog.
[jalview.git] / src / jalview / gui / Desktop.java
index 585537e..ee24c25 100644 (file)
@@ -22,6 +22,7 @@ package jalview.gui;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
+import java.awt.Component;
 import java.awt.Dimension;
 import java.awt.FontMetrics;
 import java.awt.Graphics;
@@ -51,6 +52,7 @@ import java.awt.event.WindowEvent;
 import java.awt.geom.AffineTransform;
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
+import java.beans.PropertyVetoException;
 import java.io.File;
 import java.io.FileWriter;
 import java.io.IOException;
@@ -63,6 +65,7 @@ import java.util.Hashtable;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Vector;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -81,12 +84,16 @@ import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
 import javax.swing.JComponent;
 import javax.swing.JDesktopPane;
+import javax.swing.JFrame;
 import javax.swing.JInternalFrame;
 import javax.swing.JLabel;
 import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JPopupMenu;
 import javax.swing.JProgressBar;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
 import javax.swing.JTextField;
 import javax.swing.KeyStroke;
 import javax.swing.SwingUtilities;
@@ -100,9 +107,16 @@ import org.stackoverflowusers.file.WindowsShortcut;
 
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
+import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.bin.Cache;
 import jalview.bin.Jalview;
+import jalview.bin.Jalview.ExitCode;
+import jalview.datamodel.Alignment;
+import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
 import jalview.gui.ImageExporter.ImageWriterI;
+import jalview.gui.QuitHandler.QResponse;
 import jalview.io.BackupFiles;
 import jalview.io.DataSourceType;
 import jalview.io.FileFormat;
@@ -114,10 +128,9 @@ import jalview.io.FormatAdapter;
 import jalview.io.IdentifyFile;
 import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
+import jalview.io.exceptions.ImageOutputException;
 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;
@@ -190,6 +203,16 @@ public class Desktop extends jalview.jbgui.GDesktop
 
   public static HashMap<String, FileWriter> savingFiles = new HashMap<String, FileWriter>();
 
+  private static int DRAG_MODE = JDesktopPane.OUTLINE_DRAG_MODE;
+
+  public static void setLiveDragMode(boolean b)
+  {
+    DRAG_MODE = b ? JDesktopPane.LIVE_DRAG_MODE
+            : JDesktopPane.OUTLINE_DRAG_MODE;
+    if (desktop != null)
+      desktop.setDragMode(DRAG_MODE);
+  }
+
   private JalviewChangeSupport changeSupport = new JalviewChangeSupport();
 
   public static boolean nosplash = false;
@@ -419,9 +442,14 @@ public class Desktop extends jalview.jbgui.GDesktop
     {
       if (LaunchUtils.getJavaVersion() >= 11)
       {
-        jalview.bin.Console.info(
+        /*
+         * Send this message to stderr as the warning that follows (due to
+         * reflection) also goes to stderr.
+         */
+        jalview.bin.Console.errPrintln(
                 "Linux platform only! You may have the following warning next: \"WARNING: An illegal reflective access operation has occurred\"\nThis is expected and cannot be avoided, sorry about that.");
       }
+      final String awtAppClassName = "awtAppClassName";
       try
       {
         Toolkit xToolkit = Toolkit.getDefaultToolkit();
@@ -429,10 +457,10 @@ public class Desktop extends jalview.jbgui.GDesktop
         Field awtAppClassNameField = null;
 
         if (Arrays.stream(declaredFields)
-                .anyMatch(f -> f.getName().equals("awtAppClassName")))
+                .anyMatch(f -> f.getName().equals(awtAppClassName)))
         {
           awtAppClassNameField = xToolkit.getClass()
-                  .getDeclaredField("awtAppClassName");
+                  .getDeclaredField(awtAppClassName);
         }
 
         String title = ChannelProperties.getProperty("app_name");
@@ -443,28 +471,26 @@ public class Desktop extends jalview.jbgui.GDesktop
         }
         else
         {
-          jalview.bin.Console.debug("XToolkit: awtAppClassName not found");
+          jalview.bin.Console
+                  .debug("XToolkit: " + awtAppClassName + " not found");
         }
       } catch (Exception e)
       {
-        jalview.bin.Console.debug("Error setting awtAppClassName");
+        jalview.bin.Console.debug("Error setting " + awtAppClassName);
         jalview.bin.Console.trace(Cache.getStackTraceString(e));
       }
     }
 
     setIconImages(ChannelProperties.getIconList());
 
+    // override quit handling when GUI OS close [X] button pressed
     this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
     addWindowListener(new WindowAdapter()
     {
       @Override
       public void windowClosing(WindowEvent ev)
       {
-        QResponse qresponse = desktopQuit();
-        if (qresponse != QResponse.CANCEL_QUIT)
-        {
-          instance.dispose();
-        }
+        QuitHandler.QResponse ret = desktopQuit(true, true); // ui, disposeFlag
       }
     });
 
@@ -489,16 +515,20 @@ public class Desktop extends jalview.jbgui.GDesktop
     }
 
     getContentPane().add(desktop, BorderLayout.CENTER);
-    desktop.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
+    desktop.setDragMode(DRAG_MODE);
 
     // This line prevents Windows Look&Feel resizing all new windows to maximum
     // if previous window was maximised
     desktop.setDesktopManager(new MyDesktopManager(
-            (Platform.isWindowsAndNotJS() ? new DefaultDesktopManager()
-                    : Platform.isAMacAndNotJS()
-                            ? new AquaInternalFrameManager(
-                                    desktop.getDesktopManager())
-                            : desktop.getDesktopManager())));
+            Platform.isJS() ? desktop.getDesktopManager()
+                    : new DefaultDesktopManager()));
+    /*
+    (Platform.isWindowsAndNotJS() ? new DefaultDesktopManager()
+            : Platform.isAMacAndNotJS()
+                    ? new AquaInternalFrameManager(
+                            desktop.getDesktopManager())
+                    : desktop.getDesktopManager())));
+                    */
 
     Rectangle dims = getLastKnownDimensions("");
     if (dims != null)
@@ -513,6 +543,9 @@ public class Desktop extends jalview.jbgui.GDesktop
       setBounds(xPos, yPos, 900, 650);
     }
 
+    // start dialogue queue for single dialogues
+    startDialogQueue();
+
     if (!Platform.isJS())
     /**
      * Java only
@@ -546,15 +579,16 @@ public class Desktop extends jalview.jbgui.GDesktop
       }
 
       // Thread off a new instance of the file chooser - this reduces the time
-      // it
-      // takes to open it later on.
+      // it takes to open it later on.
       new Thread(new Runnable()
       {
         @Override
         public void run()
         {
           jalview.bin.Console.debug("Filechooser init thread started.");
-          String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
+          String fileFormat = FileLoader.getUseDefaultFileFormat()
+                  ? Cache.getProperty("DEFAULT_FILE_FORMAT")
+                  : null;
           JalviewFileChooser.forRead(Cache.getProperty("LAST_DIRECTORY"),
                   fileFormat);
           jalview.bin.Console.debug("Filechooser init thread finished.");
@@ -600,6 +634,13 @@ public class Desktop extends jalview.jbgui.GDesktop
       }
     });
     desktop.addMouseListener(ma);
+
+    if (Platform.isJS())
+    {
+      // used for jalviewjsTest
+      jalview.bin.Console.info("JALVIEWJS: CREATED DESKTOP");
+    }
+
   }
 
   /**
@@ -786,26 +827,64 @@ public class Desktop extends jalview.jbgui.GDesktop
 
   public void paste()
   {
-    try
+    // quick patch for JAL-4150 - needs some more work and test coverage
+    // TODO - unify below and AlignFrame.paste()
+    // TODO - write tests and fix AlignFrame.paste() which doesn't track if
+    // clipboard has come from a different alignment window than the one where
+    // paste has been called! JAL-4151
+
+    if (Desktop.jalviewClipboard != null)
     {
-      Clipboard c = Toolkit.getDefaultToolkit().getSystemClipboard();
-      Transferable contents = c.getContents(this);
+      // The clipboard was filled from within Jalview, we must use the
+      // sequences
+      // And dataset from the copied alignment
+      SequenceI[] newseq = (SequenceI[]) Desktop.jalviewClipboard[0];
+      // be doubly sure that we create *new* sequence objects.
+      SequenceI[] sequences = new SequenceI[newseq.length];
+      for (int i = 0; i < newseq.length; i++)
+      {
+        sequences[i] = new Sequence(newseq[i]);
+      }
+      Alignment alignment = new Alignment(sequences);
+      // dataset is inherited
+      alignment.setDataset((Alignment) Desktop.jalviewClipboard[1]);
+      AlignFrame af = new AlignFrame(alignment, AlignFrame.DEFAULT_WIDTH,
+              AlignFrame.DEFAULT_HEIGHT);
+      String newtitle = new String("Copied sequences");
 
-      if (contents != null)
+      if (Desktop.jalviewClipboard[2] != null)
       {
-        String file = (String) contents
-                .getTransferData(DataFlavor.stringFlavor);
+        HiddenColumns hc = (HiddenColumns) Desktop.jalviewClipboard[2];
+        af.viewport.setHiddenColumns(hc);
+      }
 
-        FileFormatI format = new IdentifyFile().identify(file,
-                DataSourceType.PASTE);
+      Desktop.addInternalFrame(af, newtitle, AlignFrame.DEFAULT_WIDTH,
+              AlignFrame.DEFAULT_HEIGHT);
 
-        new FileLoader().LoadFile(file, DataSourceType.PASTE, format);
+    }
+    else
+    {
+      try
+      {
+        Clipboard c = Toolkit.getDefaultToolkit().getSystemClipboard();
+        Transferable contents = c.getContents(this);
+
+        if (contents != null)
+        {
+          String file = (String) contents
+                  .getTransferData(DataFlavor.stringFlavor);
+
+          FileFormatI format = new IdentifyFile().identify(file,
+                  DataSourceType.PASTE);
 
+          new FileLoader().LoadFile(file, DataSourceType.PASTE, format);
+
+        }
+      } catch (Exception ex)
+      {
+        jalview.bin.Console.outPrintln(
+                "Unable to paste alignment from system clipboard:\n" + ex);
       }
-    } catch (Exception ex)
-    {
-      System.out.println(
-              "Unable to paste alignment from system clipboard:\n" + ex);
     }
   }
 
@@ -1008,7 +1087,38 @@ public class Desktop extends jalview.jbgui.GDesktop
 
     setKeyBindings(frame);
 
-    desktop.add(frame);
+    // Since the latest FlatLaf patch, we occasionally have problems showing
+    // structureViewer frames...
+    int tries = 3;
+    boolean shown = false;
+    Exception last = null;
+    do
+    {
+      try
+      {
+        desktop.add(frame);
+        shown = true;
+      } catch (IllegalArgumentException iaex)
+      {
+        last = iaex;
+        tries--;
+        jalview.bin.Console.info("Squashed IllegalArgument Exception ("
+                + tries + " left) for " + frame.getTitle(), iaex);
+        try
+        {
+          Thread.sleep(5);
+        } catch (InterruptedException iex)
+        {
+        }
+        ;
+      }
+    } while (!shown && tries > 0);
+    if (!shown)
+    {
+      jalview.bin.Console.error(
+              "Serious Problem whilst showing window " + frame.getTitle(),
+              last);
+    }
 
     windowMenu.add(menuItem);
 
@@ -1168,7 +1278,9 @@ public class Desktop extends jalview.jbgui.GDesktop
   @Override
   public void inputLocalFileMenuItem_actionPerformed(AlignViewport viewport)
   {
-    String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
+    String fileFormat = FileLoader.getUseDefaultFileFormat()
+            ? Cache.getProperty("DEFAULT_FILE_FORMAT")
+            : null;
     JalviewFileChooser chooser = JalviewFileChooser.forRead(
             Cache.getProperty("LAST_DIRECTORY"), fileFormat,
             BackupFiles.getEnabled());
@@ -1178,36 +1290,31 @@ public class Desktop extends jalview.jbgui.GDesktop
             MessageManager.getString("label.open_local_file"));
     chooser.setToolTipText(MessageManager.getString("action.open"));
 
-    chooser.setResponseHandler(0, new Runnable()
-    {
-      @Override
-      public void run()
-      {
-        File selectedFile = chooser.getSelectedFile();
-        Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
+    chooser.setResponseHandler(0, () -> {
+      File selectedFile = chooser.getSelectedFile();
+      Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
 
-        FileFormatI format = chooser.getSelectedFormat();
+      FileFormatI format = chooser.getSelectedFormat();
 
-        /*
-         * Call IdentifyFile to verify the file contains what its extension implies.
-         * Skip this step for dynamically added file formats, because IdentifyFile does
-         * not know how to recognise them.
-         */
-        if (FileFormats.getInstance().isIdentifiable(format))
+      /*
+       * Call IdentifyFile to verify the file contains what its extension implies.
+       * Skip this step for dynamically added file formats, because IdentifyFile does
+       * not know how to recognise them.
+       */
+      if (FileFormats.getInstance().isIdentifiable(format))
+      {
+        try
         {
-          try
-          {
-            format = new IdentifyFile().identify(selectedFile,
-                    DataSourceType.FILE);
-          } catch (FileFormatException e)
-          {
-            // format = null; //??
-          }
+          format = new IdentifyFile().identify(selectedFile,
+                  DataSourceType.FILE);
+        } catch (FileFormatException e)
+        {
+          // format = null; //??
         }
-
-        new FileLoader().LoadFile(viewport, selectedFile,
-                DataSourceType.FILE, format);
       }
+
+      new FileLoader().LoadFile(viewport, selectedFile, DataSourceType.FILE,
+              format);
     });
     chooser.showOpenDialog(this);
   }
@@ -1263,62 +1370,56 @@ public class Desktop extends jalview.jbgui.GDesktop
 
     Object[] options = new Object[] { MessageManager.getString("action.ok"),
         MessageManager.getString("action.cancel") };
-    Runnable action = new Runnable()
-    {
-      @Override
-      public void run()
-      {
-        @SuppressWarnings("unchecked")
-        String url = (history instanceof JTextField
-                ? ((JTextField) history).getText()
-                : ((JComboBox<String>) history).getEditor().getItem()
-                        .toString().trim());
+    Runnable action = () -> {
+      @SuppressWarnings("unchecked")
+      String url = (history instanceof JTextField
+              ? ((JTextField) history).getText()
+              : ((JComboBox<String>) history).getEditor().getItem()
+                      .toString().trim());
 
-        if (url.toLowerCase(Locale.ROOT).endsWith(".jar"))
+      if (url.toLowerCase(Locale.ROOT).endsWith(".jar"))
+      {
+        if (viewport != null)
         {
-          if (viewport != null)
-          {
-            new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
-                    FileFormat.Jalview);
-          }
-          else
-          {
-            new FileLoader().LoadFile(url, DataSourceType.URL,
-                    FileFormat.Jalview);
-          }
+          new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
+                  FileFormat.Jalview);
         }
         else
         {
-          FileFormatI format = null;
-          try
-          {
-            format = new IdentifyFile().identify(url, DataSourceType.URL);
-          } catch (FileFormatException e)
-          {
-            // TODO revise error handling, distinguish between
-            // URL not found and response not valid
-          }
-
-          if (format == null)
-          {
-            String msg = MessageManager
-                    .formatMessage("label.couldnt_locate", url);
-            JvOptionPane.showInternalMessageDialog(Desktop.desktop, msg,
-                    MessageManager.getString("label.url_not_found"),
-                    JvOptionPane.WARNING_MESSAGE);
+          new FileLoader().LoadFile(url, DataSourceType.URL,
+                  FileFormat.Jalview);
+        }
+      }
+      else
+      {
+        FileFormatI format = null;
+        try
+        {
+          format = new IdentifyFile().identify(url, DataSourceType.URL);
+        } catch (FileFormatException e)
+        {
+          // TODO revise error handling, distinguish between
+          // URL not found and response not valid
+        }
 
-            return;
-          }
+        if (format == null)
+        {
+          String msg = MessageManager.formatMessage("label.couldnt_locate",
+                  url);
+          JvOptionPane.showInternalMessageDialog(Desktop.desktop, msg,
+                  MessageManager.getString("label.url_not_found"),
+                  JvOptionPane.WARNING_MESSAGE);
+          return;
+        }
 
-          if (viewport != null)
-          {
-            new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
-                    format);
-          }
-          else
-          {
-            new FileLoader().LoadFile(url, DataSourceType.URL, format);
-          }
+        if (viewport != null)
+        {
+          new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
+                  format);
+        }
+        else
+        {
+          new FileLoader().LoadFile(url, DataSourceType.URL, format);
         }
       }
     };
@@ -1352,61 +1453,72 @@ public class Desktop extends jalview.jbgui.GDesktop
   /*
    * Check with user and saving files before actually quitting
    */
-  public QResponse desktopQuit()
+  public void desktopQuit()
   {
-    return desktopQuit(true);
+    desktopQuit(true, false);
   }
 
-  public QResponse desktopQuit(boolean ui)
+  public QuitHandler.QResponse desktopQuit(boolean ui, boolean disposeFlag)
   {
-    QuitHandler.QResponse qresponse = QuitHandler.getQuitResponse(ui);
+    final Runnable doDesktopQuit = () -> {
+      Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+      Cache.setProperty("SCREENGEOMETRY_WIDTH", screen.width + "");
+      Cache.setProperty("SCREENGEOMETRY_HEIGHT", screen.height + "");
+      storeLastKnownDimensions("", new Rectangle(getBounds().x,
+              getBounds().y, getWidth(), getHeight()));
 
-    if (qresponse == QResponse.CANCEL_QUIT)
-    {
-      return qresponse;
-    }
+      if (jconsole != null)
+      {
+        storeLastKnownDimensions("JAVA_CONSOLE_", jconsole.getBounds());
+        jconsole.stopConsole();
+      }
 
-    Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
-    Cache.setProperty("SCREENGEOMETRY_WIDTH", screen.width + "");
-    Cache.setProperty("SCREENGEOMETRY_HEIGHT", screen.height + "");
-    storeLastKnownDimensions("", new Rectangle(getBounds().x, getBounds().y,
-            getWidth(), getHeight()));
+      if (jvnews != null)
+      {
+        storeLastKnownDimensions("JALVIEW_RSS_WINDOW_", jvnews.getBounds());
+      }
 
-    if (jconsole != null)
-    {
-      storeLastKnownDimensions("JAVA_CONSOLE_", jconsole.getBounds());
-      jconsole.stopConsole();
-    }
-    if (jvnews != null)
-    {
-      storeLastKnownDimensions("JALVIEW_RSS_WINDOW_", jvnews.getBounds());
+      // Frames should all close automatically. Keeping external
+      // viewers open should already be decided by user.
+      closeAll_actionPerformed(null);
 
-    }
-    if (dialogExecutor != null)
-    {
-      dialogExecutor.shutdownNow();
-    }
-    closeAll_actionPerformed(null);
+      // check for aborted quit
+      if (QuitHandler.quitCancelled())
+      {
+        jalview.bin.Console.debug("Desktop aborting quit");
+        return;
+      }
 
-    if (groovyConsole != null)
-    {
-      // suppress a possible repeat prompt to save script
-      groovyConsole.setDirty(false);
-      groovyConsole.exit();
-    }
+      if (dialogExecutor != null)
+      {
+        dialogExecutor.shutdownNow();
+      }
 
-    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);
-    }
+      if (groovyConsole != null)
+      {
+        // suppress a possible repeat prompt to save script
+        groovyConsole.setDirty(false);
+        groovyConsole.exit();
+      }
+
+      if (QuitHandler.gotQuitResponse() == 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();
+      jalview.bin.Console.debug("Quit selected by user");
+      if (disposeFlag)
+      {
+        instance.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+        // instance.dispose();
+      }
+      instance.quit();
+    };
 
-    // unlikely to reach here!
-    return QResponse.QUIT;
+    return QuitHandler.getQuitResponse(ui, doDesktopQuit, doDesktopQuit,
+            QuitHandler.defaultCancelQuit);
   }
 
   /**
@@ -1418,7 +1530,7 @@ public class Desktop extends jalview.jbgui.GDesktop
     // 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);
+    Jalview.exit("Desktop exiting.", ExitCode.OK);
   }
 
   private void storeLastKnownDimensions(String string, Rectangle jc)
@@ -1531,7 +1643,8 @@ public class Desktop extends jalview.jbgui.GDesktop
       }
     } catch (Exception ex)
     {
-      System.err.println("Error opening help: " + ex.getMessage());
+      jalview.bin.Console
+              .errPrintln("Error opening help: " + ex.getMessage());
     }
   }
 
@@ -1550,7 +1663,7 @@ public class Desktop extends jalview.jbgui.GDesktop
       }
     }
     Jalview.setCurrentAlignFrame(null);
-    System.out.println("ALL CLOSED");
+    jalview.bin.Console.info("ALL CLOSED");
 
     /*
      * reset state of singleton objects as appropriate (clear down session state
@@ -1564,6 +1677,22 @@ public class Desktop extends jalview.jbgui.GDesktop
     }
   }
 
+  public int structureViewersStillRunningCount()
+  {
+    int count = 0;
+    JInternalFrame[] frames = desktop.getAllFrames();
+    for (int i = 0; i < frames.length; i++)
+    {
+      if (frames[i] != null
+              && frames[i] instanceof JalviewStructureDisplayI)
+      {
+        if (((JalviewStructureDisplayI) frames[i]).stillRunning())
+          count++;
+      }
+    }
+    return count;
+  }
+
   @Override
   public void raiseRelated_actionPerformed(ActionEvent e)
   {
@@ -1746,7 +1875,8 @@ public class Desktop extends jalview.jbgui.GDesktop
     boolean autoSave = projectFile != null && !saveAs
             && BackupFiles.getEnabled();
 
-    // System.out.println("autoSave="+autoSave+", projectFile='"+projectFile+"',
+    // jalview.bin.Console.outPrintln("autoSave="+autoSave+",
+    // projectFile='"+projectFile+"',
     // saveAs="+saveAs+", Backups
     // "+(BackupFiles.getEnabled()?"enabled":"disabled"));
 
@@ -1827,7 +1957,7 @@ public class Desktop extends jalview.jbgui.GDesktop
     saveState_actionPerformed(true);
   }
 
-  private void setProjectFile(File choice)
+  protected void setProjectFile(File choice)
   {
     this.projectFile = choice;
   }
@@ -1855,42 +1985,36 @@ public class Desktop extends jalview.jbgui.GDesktop
     // allowBackupFiles
     chooser.setFileView(new JalviewFileView());
     chooser.setDialogTitle(MessageManager.getString("label.restore_state"));
-    chooser.setResponseHandler(0, new Runnable()
-    {
-      @Override
-      public void run()
+    chooser.setResponseHandler(0, () -> {
+      File selectedFile = chooser.getSelectedFile();
+      setProjectFile(selectedFile);
+      String choice = selectedFile.getAbsolutePath();
+      Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
+      new Thread(new Runnable()
       {
-        File selectedFile = chooser.getSelectedFile();
-        setProjectFile(selectedFile);
-        String choice = selectedFile.getAbsolutePath();
-        Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
-        new Thread(new Runnable()
+        @Override
+        public void run()
         {
-          @Override
-          public void run()
+          try
           {
-            try
-            {
-              new Jalview2XML().loadJalviewAlign(selectedFile);
-            } catch (OutOfMemoryError oom)
-            {
-              new OOMWarning("Whilst loading project from " + choice, oom);
-            } catch (Exception ex)
-            {
-              jalview.bin.Console.error(
-                      "Problems whilst loading project from " + choice, ex);
-              JvOptionPane.showMessageDialog(Desktop.desktop,
-                      MessageManager.formatMessage(
-                              "label.error_whilst_loading_project_from",
-                              new Object[]
-                              { choice }),
-                      MessageManager
-                              .getString("label.couldnt_load_project"),
-                      JvOptionPane.WARNING_MESSAGE);
-            }
+            new Jalview2XML().loadJalviewAlign(selectedFile);
+          } catch (OutOfMemoryError oom)
+          {
+            new OOMWarning("Whilst loading project from " + choice, oom);
+          } catch (Exception ex)
+          {
+            jalview.bin.Console.error(
+                    "Problems whilst loading project from " + choice, ex);
+            JvOptionPane.showMessageDialog(Desktop.desktop,
+                    MessageManager.formatMessage(
+                            "label.error_whilst_loading_project_from",
+                            new Object[]
+                            { choice }),
+                    MessageManager.getString("label.couldnt_load_project"),
+                    JvOptionPane.WARNING_MESSAGE);
           }
-        }, "Project Loader").start();
-      }
+        }
+      }, "Project Loader").start();
     });
 
     chooser.showOpenDialog(this);
@@ -2554,12 +2678,7 @@ public class Desktop extends jalview.jbgui.GDesktop
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        QResponse qresponse = desktopQuit();
-        if (qresponse == QResponse.CANCEL_QUIT)
-        {
-          jalview.bin.Console
-                  .debug("Desktop: Quit action cancelled by user");
-        }
+        desktopQuit();
       }
     });
   }
@@ -2975,7 +3094,7 @@ public class Desktop extends jalview.jbgui.GDesktop
   /**
    * single thread that handles display of dialogs to user.
    */
-  ExecutorService dialogExecutor = Executors.newSingleThreadExecutor();
+  ExecutorService dialogExecutor = Executors.newFixedThreadPool(3);
 
   /**
    * flag indicating if dialogExecutor should try to acquire a permit
@@ -2985,7 +3104,7 @@ public class Desktop extends jalview.jbgui.GDesktop
   /**
    * pause the queue
    */
-  private java.util.concurrent.Semaphore block = new Semaphore(0);
+  private Semaphore block = new Semaphore(0);
 
   private static groovy.ui.Console groovyConsole;
 
@@ -3003,12 +3122,7 @@ public class Desktop extends jalview.jbgui.GDesktop
       {
         if (dialogPause)
         {
-          try
-          {
-            block.acquire();
-          } catch (InterruptedException x)
-          {
-          }
+          acquireDialogQueue();
         }
         if (instance == null)
         {
@@ -3026,12 +3140,41 @@ public class Desktop extends jalview.jbgui.GDesktop
     });
   }
 
+  private boolean dialogQueueStarted = false;
+
   public void startDialogQueue()
   {
+    if (dialogQueueStarted)
+    {
+      return;
+    }
     // set the flag so we don't pause waiting for another permit and semaphore
     // the current task to begin
-    dialogPause = false;
+    releaseDialogQueue();
+    dialogQueueStarted = true;
+  }
+
+  public void acquireDialogQueue()
+  {
+    try
+    {
+      block.acquire();
+      dialogPause = true;
+    } catch (InterruptedException e)
+    {
+      jalview.bin.Console.debug("Interruption when acquiring DialogueQueue",
+              e);
+    }
+  }
+
+  public void releaseDialogQueue()
+  {
+    if (!dialogPause)
+    {
+      return;
+    }
     block.release();
+    dialogPause = false;
   }
 
   /**
@@ -3066,7 +3209,15 @@ public class Desktop extends jalview.jbgui.GDesktop
     String title = "View of desktop";
     ImageExporter exporter = new ImageExporter(writer, null, TYPE.EPS,
             title);
-    exporter.doExport(of, this, width, height, title);
+    try
+    {
+      exporter.doExport(of, this, width, height, title);
+    } catch (ImageOutputException ioex)
+    {
+      jalview.bin.Console.error(
+              "Unexpected error whilst writing Jalview desktop snapshot as EPS",
+              ioex);
+    }
   }
 
   /**
@@ -3274,7 +3425,7 @@ public class Desktop extends jalview.jbgui.GDesktop
         {
           if (Platform.isAMacAndNotJS())
           {
-            System.err.println(
+            jalview.bin.Console.errPrintln(
                     "Please ignore plist error - occurs due to problem with java 8 on OSX");
           }
         }
@@ -3506,4 +3657,149 @@ public class Desktop extends jalview.jbgui.GDesktop
       jalview.bin.Console.debug(Cache.getStackTraceString(e));
     }
   }
+
+  /**
+   * closes the current instance window, disposes and forgets about it.
+   */
+  public static void closeDesktop()
+  {
+    if (Desktop.instance != null)
+    {
+      Desktop.instance.closeAll_actionPerformed(null);
+      Desktop.instance.setVisible(false);
+      Desktop us = Desktop.instance;
+      Desktop.instance = null;
+      // call dispose in a separate thread - try to avoid indirect deadlocks
+      new Thread(new Runnable()
+      {
+        @Override
+        public void run()
+        {
+          ExecutorService dex = us.dialogExecutor;
+          if (dex != null)
+          {
+            dex.shutdownNow();
+            us.dialogExecutor = null;
+            us.block.drainPermits();
+          }
+          us.dispose();
+        }
+      }).start();
+    }
+  }
+
+  /**
+   * checks if any progress bars are being displayed in any of the windows
+   * managed by the desktop
+   * 
+   * @return
+   */
+  public boolean operationsAreInProgress()
+  {
+    JInternalFrame[] frames = getAllFrames();
+    for (JInternalFrame frame : frames)
+    {
+      if (frame instanceof IProgressIndicator)
+      {
+        if (((IProgressIndicator) frame).operationInProgress())
+        {
+          return true;
+        }
+      }
+    }
+    return operationInProgress();
+  }
+
+  /**
+   * keep track of modal JvOptionPanes open as modal dialogs for AlignFrames.
+   * The way the modal JInternalFrame is made means it cannot be a child of an
+   * AlignFrame, so closing the AlignFrame might leave the modal open :(
+   */
+  private static Map<AlignFrame, JInternalFrame> alignFrameModalMap = new HashMap<>();
+
+  protected static void addModal(AlignFrame af, JInternalFrame jif)
+  {
+    alignFrameModalMap.put(af, jif);
+  }
+
+  protected static void closeModal(AlignFrame af)
+  {
+    if (!alignFrameModalMap.containsKey(af))
+    {
+      return;
+    }
+    JInternalFrame jif = alignFrameModalMap.get(af);
+    if (jif != null)
+    {
+      try
+      {
+        jif.setClosed(true);
+      } catch (PropertyVetoException e)
+      {
+        e.printStackTrace();
+      }
+    }
+    alignFrameModalMap.remove(af);
+  }
+
+  public void nonBlockingDialog(String title, String message, String button,
+          int type, boolean scrollable, boolean modal)
+  {
+    nonBlockingDialog(32, 2, title, message, null, button, type, scrollable,
+            modal);
+  }
+
+  public void nonBlockingDialog(int width, int height, String title,
+          String message, String boxtext, String button, int type,
+          boolean scrollable, boolean modal)
+  {
+    if (type < 0)
+    {
+      type = JvOptionPane.WARNING_MESSAGE;
+    }
+    JLabel jl = new JLabel(message);
+
+    JTextArea jta = new JTextArea(height, width);
+    // jta.setLineWrap(true);
+    jta.setEditable(false);
+    jta.setWrapStyleWord(true);
+    jta.setAutoscrolls(true);
+    jta.setText(boxtext);
+
+    JScrollPane jsp = scrollable
+            ? new JScrollPane(jta, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
+                    JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED)
+            : null;
+
+    JvOptionPane jvp = JvOptionPane.newOptionDialog(this);
+
+    JPanel jp = new JPanel();
+    jp.setLayout(new BoxLayout(jp, BoxLayout.Y_AXIS));
+
+    if (message != null)
+    {
+      jl.setAlignmentX(Component.LEFT_ALIGNMENT);
+      jp.add(jl);
+    }
+    if (boxtext != null)
+    {
+      if (scrollable)
+      {
+        jsp.setAlignmentX(Component.LEFT_ALIGNMENT);
+        jp.add(jsp);
+      }
+      else
+      {
+        jta.setAlignmentX(Component.LEFT_ALIGNMENT);
+        jp.add(jta);
+      }
+    }
+
+    jvp.setResponseHandler(JOptionPane.YES_OPTION, () -> {
+    });
+    jvp.showDialogOnTopAsync(this, jp, title, JOptionPane.YES_OPTION, type,
+            null, new Object[]
+            { button }, button, modal, null, false);
+  }
+
 }