Merge branch 'develop' into features/JAL-518_justify_seqs_in_region
[jalview.git] / src / jalview / gui / Desktop.java
index 16603df..bbd4dae 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,27 +84,44 @@ 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.JTextPane;
 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;
 import javax.swing.event.InternalFrameEvent;
+import javax.swing.text.JTextComponent;
 
 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.bin.argparser.Arg;
+import jalview.bin.groovy.JalviewObject;
+import jalview.bin.groovy.JalviewObjectI;
+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;
@@ -113,6 +133,7 @@ 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.project.Jalview2XML;
@@ -139,7 +160,7 @@ import jalview.ws.utils.UrlDownloadClient;
  */
 public class Desktop extends jalview.jbgui.GDesktop
         implements DropTargetListener, ClipboardOwner, IProgressIndicator,
-        jalview.api.StructureSelectionManagerProvider
+        jalview.api.StructureSelectionManagerProvider, JalviewObjectI
 {
   private static final String CITATION;
   static
@@ -163,7 +184,7 @@ public class Desktop extends jalview.jbgui.GDesktop
             : "&nbsp;<img alt=\"University of Dundee shield\" src=\""
                     + uod_logo_url.toString() + "\">");
     sb.append(
-            "<br><br>For help, see <a href=\"https://www.jalview.org/faq\">www.jalview.org/faq</a> and join <a href=\"https://discourse.jalview.org\">discourse.jalview.org</a>");
+            "<br><br>For help, see <a href=\"https://www.jalview.org/help/faq\">www.jalview.org/faq</a> and join <a href=\"https://discourse.jalview.org\">discourse.jalview.org</a>");
     sb.append("<br><br>If  you use Jalview, please cite:"
             + "<br>Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)"
             + "<br>Jalview Version 2 - a multiple sequence alignment editor and analysis workbench"
@@ -187,6 +208,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;
@@ -416,9 +447,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();
@@ -426,10 +462,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");
@@ -440,30 +476,53 @@ 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)
       {
-        quit();
+        QuitHandler.QResponse ret = desktopQuit(true, true); // ui, disposeFlag
       }
     });
 
     boolean selmemusage = Cache.getDefault("SHOW_MEMUSAGE", false);
 
-    boolean showjconsole = Cache.getDefault("SHOW_JAVA_CONSOLE", false);
+    boolean showjconsole = Cache.getArgCacheDefault(Arg.JAVACONSOLE,
+            "SHOW_JAVA_CONSOLE", false);
+
+    // start dialogue queue for single dialogues
+    startDialogQueue();
+
+    if (!Platform.isJS())
+    /**
+     * Java only
+     * 
+     * @j2sIgnore
+     */
+    {
+      Desktop.instance.acquireDialogQueue();
+
+      jconsole = new Console(this);
+      jconsole.setHeader(Cache.getVersionDetailsForConsole());
+      showConsole(showjconsole);
+
+      Desktop.instance.releaseDialogQueue();
+    }
+
     desktop = new MyDesktopPane(selmemusage);
 
     showMemusage.setSelected(selmemusage);
@@ -482,16 +541,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,10 +576,6 @@ public class Desktop extends jalview.jbgui.GDesktop
      * @j2sIgnore
      */
     {
-      jconsole = new Console(this, showjconsole);
-      jconsole.setHeader(Cache.getVersionDetailsForConsole());
-      showConsole(showjconsole);
-
       showNews.setVisible(false);
 
       experimentalFeatures.setSelected(showExperimental());
@@ -539,15 +598,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.");
@@ -571,15 +631,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()
     {
@@ -602,6 +653,13 @@ public class Desktop extends jalview.jbgui.GDesktop
       }
     });
     desktop.addMouseListener(ma);
+
+    if (Platform.isJS())
+    {
+      // used for jalviewjsTest
+      jalview.bin.Console.info("JALVIEWJS: CREATED DESKTOP");
+    }
+
   }
 
   /**
@@ -622,22 +680,7 @@ public class Desktop extends jalview.jbgui.GDesktop
     // configure services
     StructureSelectionManager ssm = StructureSelectionManager
             .getStructureSelectionManager(this);
-    if (Cache.getDefault(Preferences.ADD_SS_ANN, true))
-    {
-      ssm.setAddTempFacAnnot(
-              Cache.getDefault(Preferences.ADD_TEMPFACT_ANN, true));
-      ssm.setProcessSecondaryStructure(
-              Cache.getDefault(Preferences.STRUCT_FROM_PDB, true));
-      // JAL-3915 - RNAView is no longer an option so this has no effect
-      ssm.setSecStructServices(
-              Cache.getDefault(Preferences.USE_RNAVIEW, false));
-    }
-    else
-    {
-      ssm.setAddTempFacAnnot(false);
-      ssm.setProcessSecondaryStructure(false);
-      ssm.setSecStructServices(false);
-    }
+    StructureSelectionManager.doConfigureStructurePrefs(ssm);
   }
 
   public void checkForNews()
@@ -746,17 +789,17 @@ public class Desktop extends jalview.jbgui.GDesktop
         iw = (int) (iw * sw);
         iy = (int) (iy * sh);
         ih = (int) (ih * sh);
-        while (ix >= screenSize.width)
+        if (ix >= screenSize.width)
         {
           jalview.bin.Console.debug(
                   "Window geometry location recall error: shifting horizontal to within screenbounds.");
-          ix -= screenSize.width;
+          ix = ix % screenSize.width;
         }
-        while (iy >= screenSize.height)
+        if (iy >= screenSize.height)
         {
           jalview.bin.Console.debug(
                   "Window geometry location recall error: shifting vertical to within screenbounds.");
-          iy -= screenSize.height;
+          iy = iy % screenSize.height;
         }
         jalview.bin.Console.debug(
                 "Got last known dimensions for " + windowName + ": x:" + ix
@@ -788,26 +831,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)
+      {
+        HiddenColumns hc = (HiddenColumns) Desktop.jalviewClipboard[2];
+        af.viewport.setHiddenColumns(hc);
+      }
+
+      Desktop.addInternalFrame(af, newtitle, AlignFrame.DEFAULT_WIDTH,
+              AlignFrame.DEFAULT_HEIGHT);
+
+    }
+    else
+    {
+      try
       {
-        String file = (String) contents
-                .getTransferData(DataFlavor.stringFlavor);
+        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);
+          FileFormatI format = new IdentifyFile().identify(file,
+                  DataSourceType.PASTE);
 
-        new FileLoader().LoadFile(file, DataSourceType.PASTE, format);
+          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);
     }
   }
 
@@ -962,7 +1043,7 @@ public class Desktop extends jalview.jbgui.GDesktop
         {
           if (itf instanceof AlignFrame)
           {
-            Jalview.setCurrentAlignFrame((AlignFrame) itf);
+            Jalview.getInstance().setCurrentAlignFrame((AlignFrame) itf);
           }
           itf.requestFocus();
         }
@@ -1010,7 +1091,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);
 
@@ -1170,7 +1282,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());
@@ -1180,36 +1294,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);
   }
@@ -1265,62 +1374,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,10 +1455,23 @@ public class Desktop extends jalview.jbgui.GDesktop
   }
 
   /*
-   * Exit the program
+   * Check with user and saving files before actually quitting
    */
-  @Override
-  public void quit()
+  public void desktopQuit()
+  {
+    desktopQuit(true, false);
+  }
+
+  /**
+   * close everything, stash window geometries, and shut down all associated
+   * threads/workers
+   * 
+   * @param dispose
+   *          - sets the dispose on close flag - JVM may terminate when set
+   * @param terminateJvm
+   *          - quit with prejudice - stops the JVM.
+   */
+  public void quitTheDesktop(boolean dispose, boolean terminateJvm)
   {
     Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
     Cache.setProperty("SCREENGEOMETRY_WIDTH", screen.width + "");
@@ -1368,24 +1484,97 @@ public class Desktop extends jalview.jbgui.GDesktop
       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);
 
     if (groovyConsole != null)
     {
       // suppress a possible repeat prompt to save script
       groovyConsole.setDirty(false);
-      groovyConsole.exit();
+
+      // and tidy up
+      if (((Window) groovyConsole.getFrame()) != null
+              && ((Window) groovyConsole.getFrame()).isVisible())
+      {
+        // console is visible -- FIXME JAL-4327
+        groovyConsole.exit();
+      }
+      else
+      {
+        // console is not, so just let it dispose itself when we shutdown
+        // we don't call groovyConsole.exit() because it calls the shutdown
+        // handler with invokeAndWait() causing deadlock
+        groovyConsole = null;
+      }
+    }
+
+    if (terminateJvm)
+    {
+      // 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");
+    if (dispose)
+    {
+      instance.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+      // instance.dispose();
     }
-    System.exit(0);
+  }
+
+  public QuitHandler.QResponse desktopQuit(boolean ui, boolean disposeFlag)
+  {
+    final Runnable doDesktopQuit = () -> {
+
+      // FIRST !! check for aborted quit
+      if (QuitHandler.quitCancelled())
+      {
+        jalview.bin.Console
+                .debug("Quit was cancelled - Desktop aborting quit");
+        return;
+      }
+
+      // Proceed with quitting
+      quitTheDesktop(disposeFlag,
+              QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT);
+      // and exit the JVM
+      instance.quit();
+    };
+
+    return QuitHandler.getQuitResponse(ui, doDesktopQuit, doDesktopQuit,
+            QuitHandler.defaultCancelQuit);
+  }
+
+  /**
+   * Exits the program and the JVM.
+   * 
+   * Don't call this directly
+   * 
+   * - use desktopQuit() above to tidy up first.
+   * 
+   * - use closeDesktop() to shutdown Jalview without shutting down the JVM
+   * 
+   */
+  @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).
+    Jalview.exit("Desktop exiting.", ExitCode.OK);
   }
 
   private void storeLastKnownDimensions(String string, Rectangle jc)
@@ -1498,7 +1687,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());
     }
   }
 
@@ -1516,8 +1706,8 @@ public class Desktop extends jalview.jbgui.GDesktop
       {
       }
     }
-    Jalview.setCurrentAlignFrame(null);
-    System.out.println("ALL CLOSED");
+    Jalview.getInstance().setCurrentAlignFrame(null);
+    jalview.bin.Console.info("ALL CLOSED");
 
     /*
      * reset state of singleton objects as appropriate (clear down session state
@@ -1531,6 +1721,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)
   {
@@ -1713,7 +1919,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"));
 
@@ -1794,7 +2001,7 @@ public class Desktop extends jalview.jbgui.GDesktop
     saveState_actionPerformed(true);
   }
 
-  private void setProjectFile(File choice)
+  protected void setProjectFile(File choice)
   {
     this.projectFile = choice;
   }
@@ -1822,42 +2029,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);
@@ -1964,7 +2165,7 @@ public class Desktop extends jalview.jbgui.GDesktop
       return null;
     }
     List<AlignmentPanel> aps = new ArrayList<>();
-    AlignFrame[] frames = getAlignFrames();
+    AlignFrame[] frames = Desktop.getDesktopAlignFrames();
     if (frames == null)
     {
       return null;
@@ -2001,7 +2202,7 @@ public class Desktop extends jalview.jbgui.GDesktop
     List<AlignmentViewport> viewp = new ArrayList<>();
     if (desktop != null)
     {
-      AlignFrame[] frames = Desktop.getAlignFrames();
+      AlignFrame[] frames = Desktop.getDesktopAlignFrames();
 
       for (AlignFrame afr : frames)
       {
@@ -2362,12 +2563,12 @@ public class Desktop extends jalview.jbgui.GDesktop
    * 
    * @return an array of AlignFrame, or null if none found
    */
-  public static AlignFrame[] getAlignFrames()
+  @Override
+  public AlignFrame[] getAlignFrames()
   {
-    if (Jalview.isHeadlessMode())
+    if (desktop == null)
     {
-      // Desktop.desktop is null in headless mode
-      return new AlignFrame[] { Jalview.currentAlignFrame };
+      return null;
     }
 
     JInternalFrame[] frames = Desktop.desktop.getAllFrames();
@@ -2409,6 +2610,25 @@ public class Desktop extends jalview.jbgui.GDesktop
   }
 
   /**
+   * static version
+   */
+  public static AlignFrame[] getDesktopAlignFrames()
+  {
+    if (Jalview.isHeadlessMode())
+    {
+      // Desktop.desktop is null in headless mode
+      return Jalview.getInstance().getAlignFrames();
+    }
+
+    if (instance != null && desktop != null)
+    {
+      return instance.getAlignFrames();
+    }
+
+    return null;
+  }
+
+  /**
    * Returns an array of any AppJmol frames in the Desktop (or null if none).
    * 
    * @return
@@ -2450,7 +2670,7 @@ public class Desktop extends jalview.jbgui.GDesktop
       openGroovyConsole();
     } catch (Exception ex)
     {
-      jalview.bin.Console.error("Groovy Shell Creation failed.", ex);
+      jalview.bin.Console.error("Groovy Console creation failed.", ex);
       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 
               MessageManager.getString("label.couldnt_create_groovy_shell"),
@@ -2466,8 +2686,11 @@ public class Desktop extends jalview.jbgui.GDesktop
   {
     if (groovyConsole == null)
     {
-      groovyConsole = new groovy.ui.Console();
-      groovyConsole.setVariable("Jalview", this);
+      JalviewObjectI j = new JalviewObject(this);
+      groovyConsole = new groovy.console.ui.Console();
+      groovyConsole.setVariable(JalviewObjectI.jalviewObjectName, j);
+      groovyConsole.setVariable(JalviewObjectI.currentAlFrameName,
+              getCurrentAlignFrame());
       groovyConsole.run();
 
       /*
@@ -2521,7 +2744,7 @@ public class Desktop extends jalview.jbgui.GDesktop
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        quit();
+        desktopQuit();
       }
     });
   }
@@ -2540,7 +2763,7 @@ public class Desktop extends jalview.jbgui.GDesktop
      */
     groovyShell.setEnabled(!enabled);
 
-    AlignFrame[] alignFrames = getAlignFrames();
+    AlignFrame[] alignFrames = getDesktopAlignFrames();
     if (alignFrames != null)
     {
       for (AlignFrame af : alignFrames)
@@ -2851,7 +3074,16 @@ public class Desktop extends jalview.jbgui.GDesktop
    */
   public static void showUrl(final String url)
   {
-    showUrl(url, Desktop.instance);
+    if (url != null && !url.trim().equals(""))
+    {
+      jalview.bin.Console.info("Opening URL: " + url);
+      showUrl(url, Desktop.instance);
+    }
+    else
+    {
+      jalview.bin.Console.warn("Ignoring attempt to show an empty URL.");
+    }
+
   }
 
   /**
@@ -2937,7 +3169,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
@@ -2947,9 +3179,9 @@ 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;
+  private static groovy.console.ui.Console groovyConsole;
 
   /**
    * add another dialog thread to the queue
@@ -2965,12 +3197,7 @@ public class Desktop extends jalview.jbgui.GDesktop
       {
         if (dialogPause)
         {
-          try
-          {
-            block.acquire();
-          } catch (InterruptedException x)
-          {
-          }
+          acquireDialogQueue();
         }
         if (instance == null)
         {
@@ -2988,12 +3215,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;
   }
 
   /**
@@ -3028,7 +3284,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);
+    }
   }
 
   /**
@@ -3179,7 +3443,7 @@ public class Desktop extends jalview.jbgui.GDesktop
     myTopFrame.setDisplayedView(myTopFrame.alignPanel);
   }
 
-  public static groovy.ui.Console getGroovyConsole()
+  public static groovy.console.ui.Console getGroovyConsole()
   {
     return groovyConsole;
   }
@@ -3236,7 +3500,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");
           }
         }
@@ -3468,4 +3732,183 @@ public class Desktop extends jalview.jbgui.GDesktop
       jalview.bin.Console.debug(Cache.getStackTraceString(e));
     }
   }
+
+  /**
+   * closes the current instance window, but leaves the JVM running. Bypasses
+   * any shutdown prompts, but does not set window dispose on close in case JVM
+   * terminates.
+   */
+  public static void closeDesktop()
+  {
+    if (Desktop.instance != null)
+    {
+      Desktop us = Desktop.instance;
+      Desktop.instance.quitTheDesktop(false, false);
+      // call dispose in a separate thread - try to avoid indirect deadlocks
+      if (us != null)
+      {
+        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(title, message, null, button, type, scrollable, false,
+            modal, -1);
+  }
+
+  public void nonBlockingDialog(String title, String message,
+          String boxtext, String button, int type, boolean scrollable,
+          boolean html, boolean modal, int timeout)
+  {
+    nonBlockingDialog(32, 2, title, message, boxtext, button, type,
+            scrollable, html, modal, timeout);
+  }
+
+  public void nonBlockingDialog(int width, int height, String title,
+          String message, String boxtext, String button, int type,
+          boolean scrollable, boolean html, boolean modal, int timeout)
+  {
+    if (type < 0)
+    {
+      type = JvOptionPane.WARNING_MESSAGE;
+    }
+    JLabel jl = new JLabel(message);
+
+    JTextComponent jtc = null;
+    if (html)
+    {
+      JTextPane jtp = new JTextPane();
+      jtp.setContentType("text/html");
+      jtp.setEditable(false);
+      jtp.setAutoscrolls(true);
+      jtp.setText(boxtext);
+
+      jtc = jtp;
+    }
+    else
+    {
+      JTextArea jta = new JTextArea(height, width);
+      // jta.setLineWrap(true);
+      jta.setEditable(false);
+      jta.setWrapStyleWord(true);
+      jta.setAutoscrolls(true);
+      jta.setText(boxtext);
+
+      jtc = jta;
+    }
+
+    JScrollPane jsp = scrollable
+            ? new JScrollPane(jtc, 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
+      {
+        jtc.setAlignmentX(Component.LEFT_ALIGNMENT);
+        jp.add(jtc);
+      }
+    }
+
+    jvp.setResponseHandler(JOptionPane.YES_OPTION, () -> {
+    });
+    jvp.setTimeout(timeout);
+    JButton jb = new JButton(button);
+    jvp.showDialogOnTopAsync(this, jp, title, JOptionPane.YES_OPTION, type,
+            null, new Object[]
+            { button }, button, modal, new JButton[] { jb }, false);
+  }
+
+  @Override
+  public AlignFrame getCurrentAlignFrame()
+  {
+    return Jalview.getInstance().getCurrentAlignFrame();
+  }
 }