Merge branch 'Jalview-JS/develop' into merge_js_develop
authorJim Procter <jprocter@issues.jalview.org>
Mon, 14 Dec 2020 19:58:34 +0000 (19:58 +0000)
committerJim Procter <jprocter@issues.jalview.org>
Mon, 14 Dec 2020 20:28:39 +0000 (20:28 +0000)
also patched new code from JAL-3690 refactorings

57 files changed:
1  2 
src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java
src/jalview/analysis/AlignmentSorter.java
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/AlignViewport.java
src/jalview/appletgui/AnnotationPanel.java
src/jalview/appletgui/TitledPanel.java
src/jalview/bin/Jalview.java
src/jalview/datamodel/Alignment.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/AnnotationLabels.java
src/jalview/gui/CalculationChooser.java
src/jalview/gui/Desktop.java
src/jalview/gui/JvSwingUtils.java
src/jalview/gui/OverviewPanel.java
src/jalview/gui/PCAPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/Preferences.java
src/jalview/gui/SplitFrame.java
src/jalview/gui/StructureChooser.java
src/jalview/gui/WebserviceInfo.java
src/jalview/gui/WsJobParameters.java
src/jalview/gui/WsParamSetManager.java
src/jalview/gui/WsPreferences.java
src/jalview/io/CountReader.java
src/jalview/io/FileFormat.java
src/jalview/io/FileLoader.java
src/jalview/io/IdentifyFile.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/io/StockholmFile.java
src/jalview/jbgui/GAlignFrame.java
src/jalview/jbgui/GPreferences.java
src/jalview/project/Jalview2XML.java
src/jalview/util/Platform.java
src/jalview/util/StringUtils.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/workers/AlignCalcManager.java
src/jalview/ws/jws1/Discoverer.java
src/jalview/ws/jws1/MsaWSClient.java
src/jalview/ws/jws1/SeqSearchWSClient.java
src/jalview/ws/jws2/JabaWsParamTest.java
src/jalview/ws/jws2/Jws2Discoverer.java
src/jalview/ws/jws2/MsaWSClient.java
src/jalview/ws/jws2/SeqAnnotationServiceCalcWorker.java
src/jalview/ws/jws2/SequenceAnnotationWSClient.java
src/jalview/ws/jws2/jabaws2/Jws2Instance.java
src/jalview/ws/jws2/jabaws2/Jws2InstanceFactory.java
src/jalview/ws/rest/RestClient.java
test/jalview/datamodel/SequenceTest.java
test/jalview/gui/AlignFrameTest.java
test/jalview/gui/AlignViewportTest.java
test/jalview/gui/PairwiseAlignmentPanelTest.java
test/jalview/gui/PopupMenuTest.java
test/jalview/hmmer/HMMERTest.java
test/jalview/io/FileFormatsTest.java
test/jalview/project/Jalview2xmlTests.java
test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java

@@@ -34,7 -34,7 +34,8 @@@ package ext.edu.ucsf.rbvi.strucviz2
  
  import jalview.bin.Cache;
  import jalview.gui.Preferences;
 +import jalview.util.FileUtils;
+ import jalview.util.Platform;
  
  import java.io.File;
  import java.io.IOException;
@@@ -57,39 -84,35 +84,39 @@@ public class AlignmentSorter implement
     * todo: refactor searches to follow a basic pattern: (search property, last
     * search state, current sort direction)
     */
-   static boolean sortIdAscending = true;
+   boolean sortIdAscending = true;
  
-   static int lastGroupHash = 0;
+   int lastGroupHash = 0;
  
-   static boolean sortGroupAscending = true;
+   boolean sortGroupAscending = true;
  
-   static AlignmentOrder lastOrder = null;
+   AlignmentOrder lastOrder = null;
  
-   static boolean sortOrderAscending = true;
+   boolean sortOrderAscending = true;
  
-   static TreeModel lastTree = null;
+   TreeModel lastTree = null;
  
-   static boolean sortTreeAscending = true;
+   boolean sortTreeAscending = true;
  
-   /*
+   /**
     * last Annotation Label used for sort by Annotation score
     */
-   private static String lastSortByAnnotation;
+   private String lastSortByAnnotation;
  
-   /*
-    * string hash of last arguments to sortByFeature
-    * (sort order toggles if this is unchanged between sorts)
+   /**
+    * string hash of last arguments to sortByFeature (sort order toggles if this
+    * is unchanged between sorts)
     */
-   private static String sortByFeatureCriteria;
+   private String sortByFeatureCriteria;
  
-   private static boolean sortByFeatureAscending = true;
+   private boolean sortByFeatureAscending = true;
  
-   private static boolean sortLengthAscending;
+   private boolean sortLengthAscending;
  
 +  private static boolean sortEValueAscending;
 +
 +  private static boolean sortBitScoreAscending;
 +
    /**
     * Sorts sequences in the alignment by Percentage Identity with the given
     * reference sequence, sorting the highest identity to the top
Simple merge
   */
  package jalview.appletgui;
  
 +import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.Annotation;
 +import jalview.datamodel.SequenceI;
 +import jalview.renderer.AnnotationRenderer;
 +import jalview.renderer.AwtRenderPanelI;
 +import jalview.schemes.ResidueProperties;
 +import jalview.util.Comparison;
 +import jalview.util.MessageManager;
++import jalview.util.Platform;
 +import jalview.viewmodel.ViewportListenerI;
 +import jalview.viewmodel.ViewportRanges;
 +
  import java.awt.Color;
  import java.awt.Dimension;
  import java.awt.Font;
@@@ -23,7 -23,6 +23,8 @@@ package jalview.appletgui
  import java.awt.Graphics;
  import java.awt.Insets;
  import java.awt.Panel;
++import java.awt.event.WindowAdapter;
++import java.awt.event.WindowEvent;
  
  public class TitledPanel extends Panel
  {
@@@ -437,13 -460,9 +460,14 @@@ public class Jalview implements Applica
        {
          System.out.println("Error setting Taskbar: " + t.getMessage());
        }
        desktop.setVisible(true);
 -
 +      if (Platform.isJS())
 +        Cache.setProperty("SHOW_JWS2_SERVICES", "false");
-       desktop.startServiceDiscovery();
-       if (!Platform.isJS())
++      if (allowServices)
++      {
++        desktop.startServiceDiscovery();
++      }
+       if (!isJS)
        /**
         * Java only
         * 
        }
        else
        {
-         setCurrentAlignFrame(af);
-         data = aparser.getValue("colour", true);
-         if (data != null)
-         {
-           data.replaceAll("%20", " ");
-           ColourSchemeI cs = ColourSchemeProperty.getColourScheme(
-                   af.getViewport(), af.getViewport().getAlignment(), data);
-           if (cs != null)
-           {
-             System.out.println(
-                     "CMD [-color " + data + "] executed successfully!");
-           }
-           af.changeColour(cs);
-         }
  
-         // Must maintain ability to use the groups flag
-         data = aparser.getValue("groups", true);
-         if (data != null)
-         {
-           af.parseFeaturesFile(data,
-                   AppletFormatAdapter.checkProtocol(data));
-           // System.out.println("Added " + data);
-           System.out.println(
-                   "CMD groups[-" + data + "]  executed successfully!");
-         }
-         data = aparser.getValue("features", true);
-         if (data != null)
-         {
-           af.parseFeaturesFile(data,
-                   AppletFormatAdapter.checkProtocol(data));
-           // System.out.println("Added " + data);
-           System.out.println(
-                   "CMD [-features " + data + "]  executed successfully!");
-         }
-         data = aparser.getValue("annotations", true);
-         if (data != null)
-         {
-           af.loadJalviewDataFile(data, null, null, null);
-           // System.out.println("Added " + data);
-           System.out.println(
-                   "CMD [-annotations " + data + "] executed successfully!");
-         }
-         // set or clear the sortbytree flag.
-         if (aparser.contains("sortbytree"))
+         // JalviewLite interface for JavaScript allows second file open
+         String file2 = aparser.getValue(ArgsParser.OPEN2, true);
+         if (file2 != null)
          {
-           af.getViewport().setSortByTree(true);
-           if (af.getViewport().getSortByTree())
+           protocol = AppletFormatAdapter.checkProtocol(file2);
+           try
            {
-             System.out.println("CMD [-sortbytree] executed successfully!");
-           }
-         }
-         if (aparser.contains("no-annotation"))
-         {
-           af.getViewport().setShowAnnotation(false);
-           if (!af.getViewport().isShowAnnotation())
+             format = new IdentifyFile().identify(file2, protocol);
+           } catch (FileFormatException e1)
            {
-             System.out.println("CMD no-annotation executed successfully!");
+             // TODO ?
            }
-         }
-         if (aparser.contains("nosortbytree"))
-         {
-           af.getViewport().setSortByTree(false);
-           if (!af.getViewport().getSortByTree())
+           AlignFrame af2 = new FileLoader(!headless)
+                   .LoadFileWaitTillLoaded(file2, protocol, format);
+           if (af2 == null)
            {
-             System.out
-                     .println("CMD [-nosortbytree] executed successfully!");
+             System.out.println("error");
            }
-         }
-         data = aparser.getValue("tree", true);
-         if (data != null)
-         {
-           try
+           else
            {
+             AlignViewport.openLinkedAlignmentAs(af,
+                     af.getViewport().getAlignment(),
+                     af2.getViewport().getAlignment(), "",
+                     AlignViewport.SPLIT_FRAME);
              System.out.println(
-                     "CMD [-tree " + data + "] executed successfully!");
-             NewickFile nf = new NewickFile(data,
-                     AppletFormatAdapter.checkProtocol(data));
-             af.getViewport()
-                     .setCurrentTree(af.showNewickTree(nf, data).getTree());
-           } catch (IOException ex)
-           {
-             System.err.println("Couldn't add tree " + data);
-             ex.printStackTrace(System.err);
+                     "CMD [-open2 " + file2 + "] executed successfully!");
            }
          }
-         // TODO - load PDB structure(s) to alignment JAL-629
-         // (associate with identical sequence in alignment, or a specified
-         // sequence)
-         if (groovyscript != null)
+         setCurrentAlignFrame(af);
+         setFrameDependentProperties(aparser, af);
+         
+         if (isJS)
          {
-           // Execute the groovy script after we've done all the rendering stuff
-           // and before any images or figures are generated.
-           System.out.println("Executing script " + groovyscript);
-           executeGroovyScript(groovyscript, af);
-           System.out.println("CMD groovy[" + groovyscript
-                   + "] executed successfully!");
-           groovyscript = null;
+           jsApp.initFromParams(af);
          }
-         String imageName = "unnamed.png";
-         while (aparser.getSize() > 1)
+         else
+         /**
+          * Java only
+          * 
+          * @j2sIgnore
+          */
          {
-           String outputFormat = aparser.nextValue();
-           file = aparser.nextValue();
-           if (outputFormat.equalsIgnoreCase("png"))
-           {
-             af.createPNG(new File(file));
-             imageName = (new File(file)).getName();
-             System.out.println("Creating PNG image: " + file);
-             continue;
-           }
-           else if (outputFormat.equalsIgnoreCase("svg"))
-           {
-             File imageFile = new File(file);
-             imageName = imageFile.getName();
-             af.createSVG(imageFile);
-             System.out.println("Creating SVG image: " + file);
-             continue;
-           }
-           else if (outputFormat.equalsIgnoreCase("html"))
-           {
-             File imageFile = new File(file);
-             imageName = imageFile.getName();
-             HtmlSvgOutput htmlSVG = new HtmlSvgOutput(af.alignPanel);
-             htmlSVG.exportHTML(file);
-             System.out.println("Creating HTML image: " + file);
-             continue;
-           }
-           else if (outputFormat.equalsIgnoreCase("biojsmsa"))
+           if (groovyscript != null)
            {
-             if (file == null)
-             {
-               System.err.println("The output html file must not be null");
-               return;
-             }
-             try
-             {
-               BioJsHTMLOutput.refreshVersionInfo(
-                       BioJsHTMLOutput.BJS_TEMPLATES_LOCAL_DIRECTORY);
-             } catch (URISyntaxException e)
-             {
-               e.printStackTrace();
-             }
-             BioJsHTMLOutput bjs = new BioJsHTMLOutput(af.alignPanel);
-             bjs.exportHTML(file);
-             System.out
-                     .println("Creating BioJS MSA Viwer HTML file: " + file);
-             continue;
-           }
-           else if (outputFormat.equalsIgnoreCase("imgMap"))
-           {
-             af.createImageMap(new File(file), imageName);
-             System.out.println("Creating image map: " + file);
-             continue;
-           }
-           else if (outputFormat.equalsIgnoreCase("eps"))
-           {
-             File outputFile = new File(file);
-             System.out.println(
-                     "Creating EPS file: " + outputFile.getAbsolutePath());
-             af.createEPS(outputFile);
-             continue;
-           }
-           af.saveAlignment(file, format);
-           if (af.isSaveAlignmentSuccessful())
-           {
-             System.out.println("Written alignment in " + format
-                     + " format to " + file);
-           }
-           else
-           {
-             System.out.println("Error writing file " + file + " in "
-                     + format + " format!!");
+             // Execute the groovy script after we've done all the rendering
+             // stuff
+             // and before any images or figures are generated.
+             System.out.println("Executing script " + groovyscript);
+             executeGroovyScript(groovyscript, af);
+             System.out.println("CMD groovy[" + groovyscript
+                     + "] executed successfully!");
+             groovyscript = null;
            }
          }
-         while (aparser.getSize() > 0)
-         {
-           System.out.println("Unknown arg: " + aparser.nextValue());
+         if (!isJS || !isStartup) {
+           createOutputFiles(aparser, format);
          }
        }
 +      if (headless)
 +      {
 +        af.getViewport().getCalcManager().shutdown();
 +      }
      }
-     AlignFrame startUpAlframe = null;
-     // We'll only open the default file if the desktop is visible.
-     // And the user
-     // ////////////////////
+     // extract groovy arguments before anything else.
+     // Once all other stuff is done, execute any groovy scripts (in order)
+     if (!isJS && groovyscript != null)
+     {
+       if (Cache.groovyJarsPresent())
+       {
+         System.out.println("Executing script " + groovyscript);
+         executeGroovyScript(groovyscript, af);
+       }
+       else
+       {
+         System.err.println(
+                 "Sorry. Groovy Support is not available, so ignoring the provided groovy script "
+                         + groovyscript);
+       }
+     }
  
-     if (!Platform.isJS() && !headless && file == null
-             && Cache.getDefault("SHOW_STARTUP_FILE", true))
-     /**
-      * Java only
-      * 
-      * @j2sIgnore
-      */
+     // and finally, turn off batch mode indicator - if the desktop still exists
+     if (desktop != null)
      {
-       file = Cache.getDefault("STARTUP_FILE",
-               Cache.getDefault("www.jalview.org",
-                       "http://www.jalview.org")
-                       + "/examples/exampleFile_2_7.jar");
-       if (file.equals(
-               "http://www.jalview.org/examples/exampleFile_2_3.jar"))
+       if (progress != -1)
        {
-         // hardwire upgrade of the startup file
-         file.replace("_2_3.jar", "_2_7.jar");
-         // and remove the stale setting
-         Cache.removeProperty("STARTUP_FILE");
+         desktop.setProgressBar(null, progress);
        }
+       desktop.setInBatchMode(false);
+     }
+     
+     if (jsApp != null) {
+       jsApp.callInitCallback();
+     }
+   }
+   
+   /**
+    * Set general display parameters irrespective of file loading or headlessness.
+    * 
+    * @param aparser
+    */
+   private void setDisplayParameters(ArgsParser aparser)
+   {
+     if (aparser.contains(ArgsParser.NOMENUBAR))
+     {
+       noMenuBar = true;
+       System.out.println("CMD [nomenu] executed successfully!");
+     }
+     if (aparser.contains(ArgsParser.NOSTATUS))
+     {
+       noStatus = true;
+       System.out.println("CMD [nostatus] executed successfully!");
+     }
+     if (aparser.contains(ArgsParser.NOANNOTATION)
+             || aparser.contains(ArgsParser.NOANNOTATION2))
+     {
+       noAnnotation = true;
+       System.out.println("CMD no-annotation executed successfully!");
+     }
+     if (aparser.contains(ArgsParser.NOCALCULATION))
+     {
+       noCalculation = true;
+       System.out.println("CMD [nocalculation] executed successfully!");
+     }
+   }
+   private void setFrameDependentProperties(ArgsParser aparser,
+           AlignFrame af)
+   {
+     String data = aparser.getValue(ArgsParser.COLOUR, true);
+     if (data != null)
+     {
+       data.replaceAll("%20", " ");
  
-       protocol = DataSourceType.FILE;
+       ColourSchemeI cs = ColourSchemeProperty.getColourScheme(
+               af.getViewport(), af.getViewport().getAlignment(), data);
  
-       if (file.indexOf("http:") > -1)
+       if (cs != null)
        {
-         protocol = DataSourceType.URL;
+         System.out.println(
+                 "CMD [-color " + data + "] executed successfully!");
        }
+       af.changeColour(cs);
+     }
+     // Must maintain ability to use the groups flag
+     data = aparser.getValue(ArgsParser.GROUPS, true);
+     if (data != null)
+     {
+       af.parseFeaturesFile(data,
+               AppletFormatAdapter.checkProtocol(data));
+       // System.out.println("Added " + data);
+       System.out.println(
+               "CMD groups[-" + data + "]  executed successfully!");
+     }
+     data = aparser.getValue(ArgsParser.FEATURES, true);
+     if (data != null)
+     {
+       af.parseFeaturesFile(data,
+               AppletFormatAdapter.checkProtocol(data));
+       // System.out.println("Added " + data);
+       System.out.println(
+               "CMD [-features " + data + "]  executed successfully!");
+     }
+     data = aparser.getValue(ArgsParser.ANNOTATIONS, true);
+     if (data != null)
+     {
+       af.loadJalviewDataFile(data, null, null, null);
+       // System.out.println("Added " + data);
+       System.out.println(
+               "CMD [-annotations " + data + "] executed successfully!");
+     }
+     // JavaScript feature
  
-       if (file.endsWith(".jar"))
+     if (aparser.contains(ArgsParser.SHOWOVERVIEW))
+     {
+       af.overviewMenuItem_actionPerformed(null);
+       System.out.println("CMD [showoverview] executed successfully!");
+     }
+     // set or clear the sortbytree flag.
+     if (aparser.contains(ArgsParser.SORTBYTREE))
+     {
+       af.getViewport().setSortByTree(true);
+       if (af.getViewport().getSortByTree())
        {
-         format = FileFormat.Jalview;
+         System.out.println("CMD [-sortbytree] executed successfully!");
        }
-       else
+     }
+     boolean doUpdateAnnotation = false;
+     /**
+      * we do this earlier in JalviewJS because of a complication with
+      * SHOWOVERVIEW
+      * 
+      * For now, just fixing this in JalviewJS.
+      *
+      * 
+      * @j2sIgnore
+      * 
+      */
+     {
+       if (noAnnotation)
        {
-         try
-         {
-           format = new IdentifyFile().identify(file, protocol);
-         } catch (FileFormatException e)
+         af.getViewport().setShowAnnotation(false);
+         if (!af.getViewport().isShowAnnotation())
          {
-           // TODO what?
+           doUpdateAnnotation = true;
          }
        }
-       startUpAlframe = fileLoader.LoadFileWaitTillLoaded(file, protocol,
-               format);
-       // extract groovy arguments before anything else.
      }
  
-     // Once all other stuff is done, execute any groovy scripts (in order)
-     if (groovyscript != null)
+     if (aparser.contains(ArgsParser.NOSORTBYTREE))
      {
-       if (Cache.groovyJarsPresent())
+       af.getViewport().setSortByTree(false);
+       if (!af.getViewport().getSortByTree())
        {
-         System.out.println("Executing script " + groovyscript);
-         executeGroovyScript(groovyscript, startUpAlframe);
+         doUpdateAnnotation = true;
+         System.out
+                 .println("CMD [-nosortbytree] executed successfully!");
        }
-       else
+     }
+     if (doUpdateAnnotation)
+     { // BH 2019.07.24
+       af.setMenusForViewport();
+       af.alignPanel.updateLayout();
+     }
+     data = aparser.getValue(ArgsParser.TREE, true);
+     if (data != null)
+     {
+       try
        {
-         System.err.println(
-                 "Sorry. Groovy Support is not available, so ignoring the provided groovy script "
-                         + groovyscript);
+         NewickFile nf = new NewickFile(data,
+                 AppletFormatAdapter.checkProtocol(data));
+         af.getViewport()
+                 .setCurrentTree(af.showNewickTree(nf, data).getTree());
+         System.out.println(
+                 "CMD [-tree " + data + "] executed successfully!");
+       } catch (IOException ex)
+       {
+         System.err.println("Couldn't add tree " + data);
+         ex.printStackTrace(System.err);
        }
      }
-     // and finally, turn off batch mode indicator - if the desktop still exists
-     if (desktop != null)
+     // TODO - load PDB structure(s) to alignment JAL-629
+     // (associate with identical sequence in alignment, or a specified
+     // sequence)
+   }
+   /**
+    * Writes an output file for each format (if any) specified in the
+    * command-line arguments. Supported formats are currently
+    * <ul>
+    * <li>png</li>
+    * <li>svg</li>
+    * <li>html</li>
+    * <li>biojsmsa</li>
+    * <li>imgMap</li>
+    * <li>eps</li>
+    * </ul>
+    * A format parameter should be followed by a parameter specifying the output
+    * file name. {@code imgMap} parameters should follow those for the
+    * corresponding alignment image output.
+    * 
+    * @param aparser
+    * @param format
+    */
+   private void createOutputFiles(ArgsParser aparser, FileFormatI format)
+   {
+     AlignFrame af = currentAlignFrame;
+     while (aparser.getSize() >= 2)
      {
-       if (progress != -1)
+       String outputFormat = aparser.nextValue();
+       File imageFile;
+       String fname;
+       switch (outputFormat.toLowerCase())
        {
-         desktop.setProgressBar(null, progress);
+       case "png":
+         imageFile = new File(aparser.nextValue());
+         af.createPNG(imageFile);
+         System.out.println(
+                 "Creating PNG image: " + imageFile.getAbsolutePath());
+         continue;
+       case "svg":
+         imageFile = new File(aparser.nextValue());
+         af.createSVG(imageFile);
+         System.out.println(
+                 "Creating SVG image: " + imageFile.getAbsolutePath());
+         continue;
+       case "eps":
+         imageFile = new File(aparser.nextValue());
+         System.out.println(
+                 "Creating EPS file: " + imageFile.getAbsolutePath());
+         af.createEPS(imageFile);
+         continue;
+       case "biojsmsa":
+         fname = new File(aparser.nextValue()).getAbsolutePath();
+         try
+         {
+           BioJsHTMLOutput.refreshVersionInfo(
+                   BioJsHTMLOutput.BJS_TEMPLATES_LOCAL_DIRECTORY);
+         } catch (URISyntaxException e)
+         {
+           e.printStackTrace();
+         }
+         BioJsHTMLOutput bjs = new BioJsHTMLOutput(af.alignPanel);
+         bjs.exportHTML(fname);
+         System.out.println("Creating BioJS MSA Viwer HTML file: " + fname);
+         continue;
+       case "html":
+         fname = new File(aparser.nextValue()).getAbsolutePath();
+         HtmlSvgOutput htmlSVG = new HtmlSvgOutput(af.alignPanel);
+         htmlSVG.exportHTML(fname);
+         System.out.println("Creating HTML image: " + fname);
+         continue;
+       case "imgmap":
+         imageFile = new File(aparser.nextValue());
+         af.alignPanel.makePNGImageMap(imageFile, "unnamed.png");
+         System.out.println(
+                 "Creating image map: " + imageFile.getAbsolutePath());
+         continue;
+       default:
+         // fall through - try to parse as an alignment data export format
+         FileFormatI outFormat = null;
+         try
+         {
+           outFormat = FileFormats.getInstance().forName(outputFormat);
+         } catch (Exception formatP)
+         {
+         }
+         if (outFormat == null)
+         {
+           System.out.println("Couldn't parse " + outputFormat
+                   + " as a valid Jalview format string.");
+           continue;
+         }
+         if (!outFormat.isWritable())
+         {
+           System.out.println(
+                   "This version of Jalview does not support alignment export as "
+                           + outputFormat);
+           continue;
+         }
+         // record file as it was passed to Jalview so it is recognisable to the CLI
+         // caller
+         String file;
+         fname = new File(file = aparser.nextValue()).getAbsolutePath();
+         // JBPNote - yuck - really wish we did have a bean returned from this which gave
+         // success/fail like before !
+         af.saveAlignment(fname, outFormat);
+         if (!af.isSaveAlignmentSuccessful())
+         {
+           System.out.println("Written alignment in " + outputFormat
+                   + " format to " + file);
+           continue;
+         }
+         else
+         {
+           System.out.println("Error writing file " + file + " in "
+                   + outputFormat + " format!!");
+         }
        }
-       desktop.setInBatchMode(false);
+     }
+     // ??? Should report - 'ignoring' extra args here...
+     while (aparser.getSize() > 0)
+     {
+       System.out.println("Ignoring extra argument: " + aparser.nextValue());
      }
    }
  
Simple merge
@@@ -157,24 -98,65 +105,82 @@@ import jalview.viewmodel.AlignmentViewp
  import jalview.viewmodel.ViewportRanges;
  import jalview.ws.DBRefFetcher;
  import jalview.ws.DBRefFetcher.FetchFinishedListenerI;
 +import jalview.ws.ServiceChangeListener;
 +import jalview.ws.WSDiscovererI;
 +import jalview.ws.api.ServiceWithParameters;
  import jalview.ws.jws1.Discoverer;
  import jalview.ws.jws2.Jws2Discoverer;
 -import jalview.ws.jws2.jabaws2.Jws2Instance;
 +import jalview.ws.jws2.PreferredServiceRegistry;
 +import jalview.ws.params.ArgumentI;
 +import jalview.ws.params.ParamDatastoreI;
 +import jalview.ws.params.WsParamSetI;
  import jalview.ws.seqfetcher.DbSourceProxy;
 +import jalview.ws.slivkaws.SlivkaWSDiscoverer;
 +import java.io.IOException;
 +import java.util.HashSet;
 +import java.util.Set;
 +
 +import javax.swing.JFileChooser;
 +import javax.swing.JOptionPane;
  
+ import java.awt.BorderLayout;
+ import java.awt.Color;
+ import java.awt.Component;
+ import java.awt.Dimension;
+ import java.awt.Rectangle;
+ import java.awt.Toolkit;
+ import java.awt.datatransfer.Clipboard;
+ import java.awt.datatransfer.DataFlavor;
+ import java.awt.datatransfer.StringSelection;
+ import java.awt.datatransfer.Transferable;
+ import java.awt.dnd.DnDConstants;
+ import java.awt.dnd.DropTargetDragEvent;
+ import java.awt.dnd.DropTargetDropEvent;
+ import java.awt.dnd.DropTargetEvent;
+ import java.awt.dnd.DropTargetListener;
+ import java.awt.event.ActionEvent;
+ import java.awt.event.ActionListener;
+ import java.awt.event.FocusAdapter;
+ import java.awt.event.FocusEvent;
+ import java.awt.event.ItemEvent;
+ import java.awt.event.ItemListener;
+ import java.awt.event.KeyAdapter;
+ import java.awt.event.KeyEvent;
+ import java.awt.event.MouseEvent;
+ import java.awt.print.PageFormat;
+ import java.awt.print.PrinterJob;
+ import java.beans.PropertyChangeEvent;
++import java.beans.PropertyChangeListener;
+ import java.io.File;
+ import java.io.FileWriter;
+ import java.io.PrintWriter;
+ import java.net.URL;
+ import java.util.ArrayList;
+ import java.util.Arrays;
++import java.util.Collection;
+ import java.util.Deque;
+ import java.util.Enumeration;
+ import java.util.Hashtable;
+ import java.util.List;
+ import java.util.Vector;
+ import javax.swing.ButtonGroup;
+ import javax.swing.JCheckBoxMenuItem;
+ import javax.swing.JComponent;
+ import javax.swing.JEditorPane;
+ import javax.swing.JInternalFrame;
+ import javax.swing.JLabel;
+ import javax.swing.JLayeredPane;
+ import javax.swing.JMenu;
+ import javax.swing.JMenuItem;
+ import javax.swing.JPanel;
+ import javax.swing.JScrollPane;
+ import javax.swing.SwingUtilities;
++import javax.swing.event.InternalFrameAdapter;
++import javax.swing.event.InternalFrameEvent;
+ import ext.vamsas.ServiceHandle;
  /**
   * DOCUMENT ME!
   * 
   * @version $Revision$
   */
  @SuppressWarnings("serial")
 -public class AlignFrame extends GAlignFrame implements DropTargetListener,
 -        IProgressIndicator, AlignViewControllerGuiI, ColourChangeListener
 +public class AlignFrame extends GAlignFrame
 +        implements DropTargetListener, IProgressIndicator,
 +        AlignViewControllerGuiI, ColourChangeListener, ServiceChangeListener
  {
+   public static int frameCount;
    public static final int DEFAULT_WIDTH = 700;
  
    public static final int DEFAULT_HEIGHT = 500;
     */
    String fileName = null;
  
 +  /**
 +   * TODO: remove reference to 'FileObject' in AlignFrame - not correct mapping
 +   */
    File fileObject;
  
+   private int id;
+   private DataSourceType protocol ;
    /**
     * Creates a new AlignFrame object with specific width and height.
     * 
     * initalise the alignframe from the underlying viewport data and the
     * configurations
     */
    void init()
    {
 -
+     boolean newPanel = (alignPanel == null);
+     viewport.setShowAutocalculatedAbove(isShowAutoCalculatedAbove());
+     if (newPanel)
+     {
+       if (Platform.isJS())
+       {
+         // need to set this up front if NOANNOTATION is
+         // used in conjunction with SHOWOVERVIEW.
+         // I have not determined if this is appropriate for
+         // Jalview/Java, as it means we are setting this flag
+         // for all subsequent AlignFrames. For now, at least,
+         // I am setting it to be JalviewJS-only.
+         boolean showAnnotation = Jalview.getInstance().getShowAnnotation();
+         viewport.setShowAnnotation(showAnnotation);
+       }
+       alignPanel = new AlignmentPanel(this, viewport);
+     }
+     addAlignmentPanel(alignPanel, newPanel);
      // setBackground(Color.white); // BH 2019
  
      if (!Jalview.isHeadlessMode())
      });
      buildColourMenu();
  
-     if (Desktop.desktop != null)
+     if (Desktop.getDesktopPane() != null)
      {
        this.setDropTarget(new java.awt.dnd.DropTarget(this, this));
 +      addServiceListeners();
        if (!Platform.isJS())
        {
 -        addServiceListeners();
        }
        setGUINucleotide();
      }
        {
          ap.av.getAlignment().padGaps();
        }
-       ap.av.updateConservation(ap);
-       ap.av.updateConsensus(ap);
-       ap.av.updateStrucConsensus(ap);
-       ap.av.initInformationWorker(ap);
+       if (Jalview.getInstance().getStartCalculations())
+       {
+         ap.av.updateConservation(ap);
+         ap.av.updateConsensus(ap);
+         ap.av.updateStrucConsensus(ap);
++        ap.av.initInformationWorker(ap);
+       }
      }
    }
  
    /* Set up intrinsic listeners for dynamically generated GUI bits. */
    private void addServiceListeners()
    {
 -    final java.beans.PropertyChangeListener thisListener;
 -    Desktop.getInstance().addJalviewPropertyChangeListener("services",
 -            thisListener = new java.beans.PropertyChangeListener()
 -            {
 -
 -              @Override
 -              public void propertyChange(PropertyChangeEvent evt)
 -              {
 -                // // System.out.println("Discoverer property change.");
 -                // if (evt.getPropertyName().equals("services"))
 -                {
 -                  SwingUtilities.invokeLater(new Runnable()
 -                  {
 -
 -                    @Override
 -                    public void run()
 -                    {
 -                      System.err.println(
 -                              "Rebuild WS Menu for service change");
 -                      BuildWebServiceMenu();
 -                    }
 -
 -                  });
 -                }
 -              }
 -            });
 -    addInternalFrameListener(new javax.swing.event.InternalFrameAdapter()
 +    if (Cache.getDefault("SHOW_SLIVKA_SERVICES", true))
      {
 -
 +      WSDiscovererI discoverer = SlivkaWSDiscoverer.getInstance();
 +      discoverer.addServiceChangeListener(this);
 +    }
 +    if (Cache.getDefault("SHOW_JWS2_SERVICES", true))
 +    {
-       WSDiscovererI discoverer = Jws2Discoverer.getDiscoverer();
++      WSDiscovererI discoverer = Jws2Discoverer.getInstance();
 +      discoverer.addServiceChangeListener(this);
 +    }
 +    // legacy event listener for compatibility with jws1
 +    PropertyChangeListener legacyListener = (changeEvent) -> {
 +      buildWebServicesMenu();
 +    };
-     Desktop.instance.addJalviewPropertyChangeListener("services",legacyListener);
++    Desktop.getInstance().addJalviewPropertyChangeListener("services",legacyListener);
 +    
 +    addInternalFrameListener(new InternalFrameAdapter() {
        @Override
 -      public void internalFrameClosed(
 -              javax.swing.event.InternalFrameEvent evt)
 -      {
 -        // System.out.println("deregistering discoverer listener");
 -        Desktop.getInstance().removeJalviewPropertyChangeListener(
 -                "services", thisListener);
 +      public void internalFrameClosed(InternalFrameEvent e) {
 +        System.out.println("deregistering discoverer listener");
 +        SlivkaWSDiscoverer.getInstance().removeServiceChangeListener(AlignFrame.this);
-         Jws2Discoverer.getDiscoverer().removeServiceChangeListener(AlignFrame.this);
-         Desktop.instance.removeJalviewPropertyChangeListener("services", legacyListener);
++        Jws2Discoverer.getInstance().removeServiceChangeListener(AlignFrame.this);
++        Desktop.getInstance().removeJalviewPropertyChangeListener("services", legacyListener);
          closeMenuItem_actionPerformed(true);
        }
      });
    }
  
    @Override
 +  public void hmmBuild_actionPerformed(boolean withDefaults)
 +  {
 +    if (!alignmentIsSufficient(1))
 +    {
 +      return;
 +    }
 +
 +    /*
 +     * get default parameters, and optionally show a dialog
 +     * to allow them to be modified
 +     */
 +    ParamDatastoreI store = HMMERParamStore.forBuild(viewport);
 +    List<ArgumentI> args = store.getServiceParameters();
 +
 +    if (!withDefaults)
 +    {
 +      WsParamSetI set = new HMMERPreset();
 +      WsJobParameters params = new WsJobParameters(store, set, args);
 +      if (params.showRunDialog())
 +      {
 +        args = params.getJobParams();
 +      }
 +      else
 +      {
 +        return; // user cancelled
 +      }
 +    }
 +    new Thread(new HMMBuild(this, args)).start();
 +  }
 +
 +  @Override
 +  public void hmmAlign_actionPerformed(boolean withDefaults)
 +  {
 +    if (!(checkForHMM() && alignmentIsSufficient(2)))
 +    {
 +      return;
 +    }
 +
 +    /*
 +     * get default parameters, and optionally show a dialog
 +     * to allow them to be modified
 +     */
 +    ParamDatastoreI store = HMMERParamStore.forAlign(viewport);
 +    List<ArgumentI> args = store.getServiceParameters();
 +
 +    if (!withDefaults)
 +    {
 +      WsParamSetI set = new HMMERPreset();
 +      WsJobParameters params = new WsJobParameters(store, set, args);
 +      if (params.showRunDialog())
 +      {
 +        args = params.getJobParams();
 +      }
 +      else
 +      {
 +        return; // user cancelled
 +      }
 +    }
 +    new Thread(new HMMAlign(this, args)).start();
 +  }
 +
 +  @Override
 +  public void hmmSearch_actionPerformed(boolean withDefaults)
 +  {
 +    if (!checkForHMM())
 +    {
 +      return;
 +    }
 +
 +    /*
 +     * get default parameters, and (if requested) show 
 +     * dialog to allow modification
 +     */
 +    ParamDatastoreI store = HMMERParamStore.forSearch(viewport);
 +    List<ArgumentI> args = store.getServiceParameters();
 +
 +    if (!withDefaults)
 +    {
 +      WsParamSetI set = new HMMERPreset();
 +      WsJobParameters params = new WsJobParameters(store, set, args);
 +      if (params.showRunDialog())
 +      {
 +        args = params.getJobParams();
 +      }
 +      else
 +      {
 +        return; // user cancelled
 +      }
 +    }
 +    new Thread(new HMMSearch(this, args)).start();
 +    alignPanel.repaint();
 +  }
 +
 +  @Override
 +  public void jackhmmer_actionPerformed(boolean withDefaults)
 +  {
 +
 +    /*
 +     * get default parameters, and (if requested) show 
 +     * dialog to allow modification
 +     */
 +
 +    ParamDatastoreI store = HMMERParamStore.forJackhmmer(viewport);
 +    List<ArgumentI> args = store.getServiceParameters();
 +
 +    if (!withDefaults)
 +    {
 +      WsParamSetI set = new HMMERPreset();
 +      WsJobParameters params = new WsJobParameters(store, set, args);
 +      if (params.showRunDialog())
 +      {
 +        args = params.getJobParams();
 +      }
 +      else
 +      {
 +        return; // user cancelled
 +      }
 +    }
 +    new Thread(new JackHMMER(this, args)).start();
 +    alignPanel.repaint();
 +
 +  }
 +
 +  /**
 +   * Checks if the alignment has at least one hidden Markov model, if not shows
 +   * a dialog advising to run hmmbuild or load an HMM profile
 +   * 
 +   * @return
 +   */
 +  private boolean checkForHMM()
 +  {
 +    if (viewport.getAlignment().getHmmSequences().isEmpty())
 +    {
 +      JOptionPane.showMessageDialog(this,
 +              MessageManager.getString("warn.no_hmm"));
 +      return false;
 +    }
 +    return true;
 +  }
 +
 +  @Override
 +  protected void filterByEValue_actionPerformed()
 +  {
 +    viewport.filterByEvalue(inputDouble("Enter E-Value Cutoff"));
 +  }
 +
 +  @Override
 +  protected void filterByScore_actionPerformed()
 +  {
 +    viewport.filterByScore(inputDouble("Enter Bit Score Threshold"));
 +  }
 +
 +  private double inputDouble(String message)
 +  {
 +    String str = null;
 +    Double d = null;
 +    while (d == null || d <= 0)
 +    {
 +      str = JOptionPane.showInputDialog(this.alignPanel, message);
 +      try
 +      {
 +        d = Double.valueOf(str);
 +      } catch (NumberFormatException e)
 +      {
 +      }
 +    }
 +    return d;
 +  }
 +
 +  /**
 +   * Checks if the alignment contains the required number of sequences.
 +   * 
 +   * @param required
 +   * @return
 +   */
 +  public boolean alignmentIsSufficient(int required)
 +  {
 +    if (getViewport().getSequenceSelection().length < required)
 +    {
 +      JOptionPane.showMessageDialog(this,
 +              MessageManager.getString("label.not_enough_sequences"));
 +      return false;
 +    }
 +    return true;
 +  }
 +
 +  /**
 +   * Opens a file browser and adds the selected file, if in Fasta, Stockholm or
 +   * Pfam format, to the list held under preference key "HMMSEARCH_DBS" (as a
 +   * comma-separated list)
 +   */
 +  @Override
 +  public void addDatabase_actionPerformed() throws IOException
 +  {
 +    if (Cache.getProperty(Preferences.HMMSEARCH_DBS) == null)
 +    {
 +      Cache.setProperty(Preferences.HMMSEARCH_DBS, "");
 +    }
 +
 +    String path = openFileChooser(false);
 +    if (path != null && new File(path).exists())
 +    {
 +      IdentifyFile identifier = new IdentifyFile();
 +      FileFormatI format = identifier.identify(path, DataSourceType.FILE);
 +      if (format == FileFormat.Fasta || format == FileFormat.Stockholm
 +              || format == FileFormat.Pfam)
 +      {
 +        String currentDbPaths = Cache
 +                .getProperty(Preferences.HMMSEARCH_DBS);
 +        currentDbPaths += Preferences.COMMA + path;
 +        Cache.setProperty(Preferences.HMMSEARCH_DBS, currentDbPaths);
 +      }
 +      else
 +      {
 +        JOptionPane.showMessageDialog(this,
 +                MessageManager.getString("warn.invalid_format"));
 +      }
 +    }
 +  }
 +
 +  /**
 +   * Opens a file chooser, optionally restricted to selecting folders
 +   * (directories) only. Answers the path to the selected file or folder, or
 +   * null if none is chosen.
 +   * 
 +   * @param
 +   * @return
 +   */
 +  protected String openFileChooser(boolean forFolder)
 +  {
 +    // TODO duplicates GPreferences method - relocate to JalviewFileChooser?
 +    String choice = null;
 +    JFileChooser chooser = new JFileChooser();
 +    if (forFolder)
 +    {
 +      chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
 +    }
 +    chooser.setDialogTitle(
 +            MessageManager.getString("label.open_local_file"));
 +    chooser.setToolTipText(MessageManager.getString("action.open"));
 +
 +    int value = chooser.showOpenDialog(this);
 +
 +    if (value == JFileChooser.APPROVE_OPTION)
 +    {
 +      choice = chooser.getSelectedFile().getPath();
 +    }
 +    return choice;
 +  }
 +
 +  @Override
    public void reload_actionPerformed(ActionEvent e)
    {
-     if (fileName != null)
+     if (fileName == null && fileObject == null)
+     {
+       return;
+     }
+     // TODO: JAL-1108 - ensure all associated frames are closed regardless of
+     // originating file's format
+     // TODO: work out how to recover feature settings for correct view(s) when
+     // file is reloaded.
+     if (FileFormat.Jalview.equals(currentFileFormat))
      {
-       // TODO: JAL-1108 - ensure all associated frames are closed regardless of
-       // originating file's format
-       // TODO: work out how to recover feature settings for correct view(s) when
-       // file is reloaded.
-       if (FileFormat.Jalview.equals(currentFileFormat))
+       JInternalFrame[] frames = Desktop.getDesktopPane().getAllFrames();
+       for (int i = 0; i < frames.length; i++)
        {
-         JInternalFrame[] frames = Desktop.desktop.getAllFrames();
-         for (int i = 0; i < frames.length; i++)
+         if (frames[i] instanceof AlignFrame && frames[i] != this
+                 && ((AlignFrame) frames[i]).fileName != null
+                 && ((AlignFrame) frames[i]).fileName.equals(fileName))
          {
-           if (frames[i] instanceof AlignFrame && frames[i] != this
-                   && ((AlignFrame) frames[i]).fileName != null
-                   && ((AlignFrame) frames[i]).fileName.equals(fileName))
+           try
+           {
+             frames[i].setSelected(true);
+             Desktop.getInstance().closeAssociatedWindows();
+           } catch (java.beans.PropertyVetoException ex)
            {
-             try
-             {
-               frames[i].setSelected(true);
-               Desktop.instance.closeAssociatedWindows();
-             } catch (java.beans.PropertyVetoException ex)
-             {
-             }
            }
          }
-         Desktop.instance.closeAssociatedWindows();
  
-         FileLoader loader = new FileLoader();
-         DataSourceType protocol = fileName.startsWith("http:")
-                 ? DataSourceType.URL
-                 : DataSourceType.FILE;
-         loader.LoadFile(viewport, fileName, protocol, currentFileFormat);
        }
-       else
-       {
-         Rectangle bounds = this.getBounds();
+       Desktop.getInstance().closeAssociatedWindows();
  
-         FileLoader loader = new FileLoader();
+       FileLoader loader = new FileLoader();
+ //      DataSourceType protocol = fileName.startsWith("http:")
+ //              ? DataSourceType.URL
+ //              : DataSourceType.FILE;
+         loader.LoadFile(viewport, (fileObject == null ? fileName : fileObject), protocol, currentFileFormat);
+     }
+     else
+     {
+       Rectangle bounds = this.getBounds();
  
-         AlignFrame newframe = null;
+       FileLoader loader = new FileLoader();
  
-         if (fileObject == null)
-         {
+       AlignFrame newframe = null;
  
-           DataSourceType protocol = (fileName.startsWith("http:")
-                   ? DataSourceType.URL
-                   : DataSourceType.FILE);
-           newframe = loader.LoadFileWaitTillLoaded(fileName, protocol,
-                   currentFileFormat);
-         }
-         else
-         {
-           newframe = loader.LoadFileWaitTillLoaded(fileObject,
-                   DataSourceType.FILE, currentFileFormat);
-         }
+       if (fileObject == null)
+       {
+         newframe = loader.LoadFileWaitTillLoaded(fileName, protocol,
+                 currentFileFormat);
+       }
+       else
+       {
+         newframe = loader.LoadFileWaitTillLoaded(fileObject,
+                 DataSourceType.FILE, currentFileFormat);
+       }
  
-         newframe.setBounds(bounds);
-         if (featureSettings != null && featureSettings.isShowing())
+       newframe.setBounds(bounds);
+       if (featureSettings != null && featureSettings.isShowing())
+       {
+         final Rectangle fspos = featureSettings.frame.getBounds();
+         // TODO: need a 'show feature settings' function that takes bounds -
+         // need to refactor Desktop.addFrame
+         newframe.featureSettings_actionPerformed(null);
+         final FeatureSettings nfs = newframe.featureSettings;
+         SwingUtilities.invokeLater(new Runnable()
          {
-           final Rectangle fspos = featureSettings.frame.getBounds();
-           // TODO: need a 'show feature settings' function that takes bounds -
-           // need to refactor Desktop.addFrame
-           newframe.featureSettings_actionPerformed(null);
-           final FeatureSettings nfs = newframe.featureSettings;
-           SwingUtilities.invokeLater(new Runnable()
+           @Override
+           public void run()
            {
-             @Override
-             public void run()
-             {
-               nfs.frame.setBounds(fspos);
-             }
-           });
-           this.featureSettings.close();
-           this.featureSettings = null;
-         }
-         this.closeMenuItem_actionPerformed(true);
+             nfs.frame.setBounds(fspos);
+           }
+         });
+         this.featureSettings.close();
+         this.featureSettings = null;
        }
+       this.closeMenuItem_actionPerformed(true);
      }
    }
  
    @Override
     * 
     * @param e
     *          DOCUMENT ME!
 +   * @throws InterruptedException
 +   * @throws IOException
     */
    @Override
    protected void pasteNew_actionPerformed(ActionEvent e)
 +          throws IOException, InterruptedException
    {
      paste(true);
    }
     * 
     * @param e
     *          DOCUMENT ME!
 +   * @throws InterruptedException
 +   * @throws IOException
     */
    @Override
    protected void pasteThis_actionPerformed(ActionEvent e)
 +          throws IOException, InterruptedException
    {
      paste(false);
    }
      return tp;
    }
  
 -  private boolean buildingMenu = false;
 -
    /**
 -   * Generates menu items and listener event actions for web service clients
 -   * 
 +   * Schedule the web services menu rebuild to the event dispatch thread.
     */
 -
 -  public void BuildWebServiceMenu()
 +  public void buildWebServicesMenu()
    {
 -    while (buildingMenu)
 -    {
 -      try
 +    SwingUtilities.invokeLater(() -> {
 +      Cache.log.info("Rebuiling WS menu");
 +      webService.removeAll();
 +      if (Cache.getDefault("SHOW_SLIVKA_SERVICES", true))
        {
 -        System.err.println("Waiting for building menu to finish.");
 -        Thread.sleep(10);
 -      } catch (Exception e)
 +        Cache.log.info("Building web service menu for slivka");
 +        SlivkaWSDiscoverer discoverer = SlivkaWSDiscoverer.getInstance();
 +        JMenu submenu = new JMenu("Slivka");
 +        buildWebServicesMenu(discoverer, submenu);
 +        webService.add(submenu);
 +      }
 +      if (Cache.getDefault("SHOW_JWS2_SERVICES", true))
        {
-         WSDiscovererI jws2servs = Jws2Discoverer.getDiscoverer();
++        WSDiscovererI jws2servs = Jws2Discoverer.getInstance();
 +        JMenu submenu = new JMenu("JABAWS");
 +        buildLegacyWebServicesMenu(submenu);
 +        buildWebServicesMenu(jws2servs, submenu);
 +        webService.add(submenu);
        }
 -    }
 -    final AlignFrame me = this;
 -    buildingMenu = true;
 -    new Thread(new Runnable()
 -    {
 +    });
 +  }
  
 -      @Override
 -      public void run()
 +  private void buildLegacyWebServicesMenu(JMenu menu)
 +  {
 +    JMenu secstrmenu = new JMenu("Secondary Structure Prediction");
-     if (Discoverer.services != null && Discoverer.services.size() > 0) 
++    if (Discoverer.getServices() != null && Discoverer.getServices().size() > 0) 
 +    {
-       var secstrpred = Discoverer.services.get("SecStrPred");
++      var secstrpred = Discoverer.getServices().get("SecStrPred");
 +      if (secstrpred != null) 
        {
 -        final List<JMenuItem> legacyItems = new ArrayList<>();
 -        try
 -        {
 -          // System.err.println("Building ws menu again "
 -          // + Thread.currentThread());
 -          // TODO: add support for context dependent disabling of services based
 -          // on
 -          // alignment and current selection
 -          // TODO: add additional serviceHandle parameter to specify abstract
 -          // handler
 -          // class independently of AbstractName
 -          // TODO: add in rediscovery GUI function to restart discoverer
 -          // TODO: group services by location as well as function and/or
 -          // introduce
 -          // object broker mechanism.
 -          final Vector<JMenu> wsmenu = new Vector<>();
 -          final IProgressIndicator af = me;
 -
 -          /*
 -           * do not i18n these strings - they are hard-coded in class
 -           * compbio.data.msa.Category, Jws2Discoverer.isRecalculable() and
 -           * SequenceAnnotationWSClient.initSequenceAnnotationWSClient()
 -           */
 -          final JMenu msawsmenu = new JMenu("Alignment");
 -          final JMenu secstrmenu = new JMenu(
 -                  "Secondary Structure Prediction");
 -          final JMenu seqsrchmenu = new JMenu("Sequence Database Search");
 -          final JMenu analymenu = new JMenu("Analysis");
 -          final JMenu dismenu = new JMenu("Protein Disorder");
 -          // JAL-940 - only show secondary structure prediction services from
 -          // the legacy server
 -          Hashtable<String, Vector<ServiceHandle>> ds = Discoverer
 -                  .getInstance().getServices();
 -          if (// Cache.getDefault("SHOW_JWS1_SERVICES", true)
 -              // &&
 -          ds != null && (ds.size() > 0))
 -          {
 -            // TODO: refactor to allow list of AbstractName/Handler bindings to
 -            // be
 -            // stored or retrieved from elsewhere
 -            // No MSAWS used any more:
 -            // Vector msaws = null; // (Vector)
 -            // Discoverer.services.get("MsaWS");
 -            Vector<ServiceHandle> secstrpr = ds.get("SecStrPred");
 -            if (secstrpr != null)
 -            {
 -              // Add any secondary structure prediction services
 -              for (int i = 0, j = secstrpr.size(); i < j; i++)
 -              {
 -                final ext.vamsas.ServiceHandle sh = secstrpr.get(i);
 -                jalview.ws.WSMenuEntryProviderI impl = jalview.ws.jws1.Discoverer
 -                        .getServiceClient(sh);
 -                int p = secstrmenu.getItemCount();
 -                impl.attachWSMenuEntry(secstrmenu, me);
 -                int q = secstrmenu.getItemCount();
 -                for (int litm = p; litm < q; litm++)
 -                {
 -                  legacyItems.add(secstrmenu.getItem(litm));
 -                }
 -              }
 -            }
 -          }
 -
 -          // Add all submenus in the order they should appear on the web
 -          // services menu
 -          wsmenu.add(msawsmenu);
 -          wsmenu.add(secstrmenu);
 -          wsmenu.add(dismenu);
 -          wsmenu.add(analymenu);
 -          // No search services yet
 -          // wsmenu.add(seqsrchmenu);
 -
 -          javax.swing.SwingUtilities.invokeLater(new Runnable()
 -          {
 -
 -            @Override
 -            public void run()
 -            {
 -              try
 -              {
 -                webService.removeAll();
 -                // first, add discovered services onto the webservices menu
 -                if (wsmenu.size() > 0)
 -                {
 -                  for (int i = 0, j = wsmenu.size(); i < j; i++)
 -                  {
 -                    webService.add(wsmenu.get(i));
 -                  }
 -                }
 -                else
 -                {
 -                  webService.add(me.webServiceNoServices);
 -                }
 -                // TODO: move into separate menu builder class.
 -                // boolean new_sspred = false;
 -                if (Cache.getDefault("SHOW_JWS2_SERVICES", true))
 -                {
 -                  Jws2Discoverer jws2servs = Jws2Discoverer.getInstance();
 -                  if (jws2servs != null)
 -                  {
 -                    if (jws2servs.hasServices())
 -                    {
 -                      jws2servs.attachWSMenuEntry(webService, me);
 -                      for (Jws2Instance sv : jws2servs.getServices())
 -                      {
 -                        if (sv.description.toLowerCase().contains("jpred"))
 -                        {
 -                          for (JMenuItem jmi : legacyItems)
 -                          {
 -                            jmi.setVisible(false);
 -                          }
 -                        }
 -                      }
 -
 -                    }
 -                    if (jws2servs.isRunning())
 -                    {
 -                      JMenuItem tm = new JMenuItem(
 -                              "Still discovering JABA Services");
 -                      tm.setEnabled(false);
 -                      webService.add(tm);
 -                    }
 -                  }
 -                }
 -                build_urlServiceMenu(me.webService);
 -                build_fetchdbmenu(webService);
 -                for (JMenu item : wsmenu)
 -                {
 -                  if (item.getItemCount() == 0)
 -                  {
 -                    item.setEnabled(false);
 -                  }
 -                  else
 -                  {
 -                    item.setEnabled(true);
 -                  }
 -                }
 -              } catch (Exception e)
 -              {
 -                Cache.log.debug(
 -                        "Exception during web service menu building process.",
 -                        e);
 -              }
 -            }
 -          });
 -        } catch (Exception e)
 +        for (ext.vamsas.ServiceHandle sh : secstrpred) 
          {
 +          var menuProvider = Discoverer.getServiceClient(sh);
 +          menuProvider.attachWSMenuEntry(secstrmenu, this);
          }
 -        buildingMenu = false;
        }
 -    }).start();
 +    }
 +    menu.add(secstrmenu);
 +  }
  
 +  /**
 +   * Constructs the web services menu for the given discoverer under the
 +   * specified menu. This method must be called on the EDT
 +   * 
 +   * @param discoverer
 +   *          the discoverer used to build the menu
 +   * @param menu
 +   *          parent component which the elements will be attached to
 +   */
 +  private void buildWebServicesMenu(WSDiscovererI discoverer, JMenu menu)
 +  {
 +    if (discoverer.hasServices())
 +    {
 +      PreferredServiceRegistry.getRegistry().populateWSMenuEntry(
 +              discoverer.getServices(), sv -> buildWebServicesMenu(), menu,
 +              this, null);
 +    }
 +    if (discoverer.isRunning())
 +    {
 +      JMenuItem item = new JMenuItem("Service discovery in progress.");
 +      item.setEnabled(false);
 +      menu.add(item);
 +    }
 +    else if (!discoverer.hasServices())
 +    {
 +      JMenuItem item = new JMenuItem("No services available.");
 +      item.setEnabled(false);
 +      menu.add(item);
 +    }
    }
  
    /**
     * 
     * @param file
     *          either a filename or a URL string.
 +   * @throws InterruptedException
 +   * @throws IOException
     */
    public void loadJalviewDataFile(Object file, DataSourceType sourceType,
            FileFormatI format, SequenceI assocSeq)
    {
@@@ -587,9 -588,9 +595,9 @@@ f   *
    public StructureSelectionManager getStructureSelectionManager()
    {
      return StructureSelectionManager
-             .getStructureSelectionManager(Desktop.instance);
+             .getStructureSelectionManager(Desktop.getInstance());
    }
 -
 +  
    @Override
    public boolean isNormaliseSequenceLogo()
    {
@@@ -63,6 -47,21 +47,22 @@@ import javax.swing.JPopupMenu
  import javax.swing.SwingUtilities;
  import javax.swing.ToolTipManager;
  
+ import jalview.analysis.AlignSeq;
+ import jalview.analysis.AlignmentUtils;
+ import jalview.datamodel.Alignment;
+ import jalview.datamodel.AlignmentAnnotation;
+ import jalview.datamodel.Annotation;
+ import jalview.datamodel.HiddenColumns;
+ import jalview.datamodel.Sequence;
+ import jalview.datamodel.SequenceGroup;
+ import jalview.datamodel.SequenceI;
+ import jalview.io.FileFormat;
+ import jalview.io.FormatAdapter;
+ import jalview.util.Comparison;
+ import jalview.util.MessageManager;
+ import jalview.util.Platform;
++import jalview.workers.InformationThread;
  /**
   * The panel that holds the labels for alignment annotations, providing
   * tooltips, context menus, drag to reorder rows, and drag to adjust panel
@@@ -561,14 -660,19 +660,18 @@@ public class CalculationChooser extend
    }
  
    /**
-    * Open a new PCA panel on the desktop
+    * public static method for JalviewJS API
     * 
+    * @param af
     * @param modelName
     * @param params
+    * @return the PCAPanel, or null if number of sequences selected is
+    *         inappropriate
     */
-   protected void openPcaPanel(String modelName, SimilarityParamsI params)
+   public static Object openPcaPanel(AlignFrame af, String modelName,
+           SimilarityParamsI params)
    {
 -
 -    AlignViewport viewport = af.getViewport();
 +    AlignViewportI viewport = af.getViewport();
  
      /*
       * gui validation shouldn't allow insufficient sequences here, but leave
@@@ -120,10 -121,8 +124,9 @@@ import jalview.util.BrowserLauncher
  import jalview.util.ImageMaker.TYPE;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
- import jalview.util.ShortcutKeyMaskExWrapper;
  import jalview.util.UrlConstants;
  import jalview.viewmodel.AlignmentViewport;
 +import jalview.ws.WSDiscovererI;
  import jalview.ws.params.ParamManager;
  import jalview.ws.utils.UrlDownloadClient;
  
@@@ -2669,11 -2711,12 +2732,11 @@@ public class Desktop extends GDeskto
  
      if (Cache.getDefault("SHOW_JWS2_SERVICES", true))
      {
-       tasks.add(jalview.ws.jws2.Jws2Discoverer.getDiscoverer().startDiscoverer());
 -      t2 = jalview.ws.jws2.Jws2Discoverer.getInstance()
 -              .startDiscoverer(changeSupport);
++      tasks.add(jalview.ws.jws2.Jws2Discoverer.getInstance().startDiscoverer());
      }
 -    Thread t3 = null;
 +    if (Cache.getDefault("SHOW_SLIVKA_SERVICES", true))
      {
 -      // TODO: do rest service discovery
 +      tasks.add(jalview.ws.slivkaws.SlivkaWSDiscoverer.getInstance().startDiscoverer());
      }
      if (blocking)
      {
    {
      if (evt.getNewValue() == null || evt.getNewValue() instanceof Vector)
      {
 -      final String ermsg = jalview.ws.jws2.Jws2Discoverer.getInstance()
 -              .getErrorMessages();
 +      final WSDiscovererI discoverer = jalview.ws.jws2.Jws2Discoverer
-           .getDiscoverer();
++          .getInstance();
 +      final String ermsg = discoverer.getErrorMessages();
++      // CONFLICT:ALT:?     final String ermsg = jalview.ws.jws2.Jws2Discoverer.getInstance()
        if (ermsg != null)
        {
          if (Cache.getDefault("SHOW_WSDISCOVERY_ERRORS", true))
   */
  package jalview.gui;
  
- import jalview.util.MessageManager;
 -import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Component;
 +import java.awt.Container;
  import java.awt.Font;
 -import java.awt.GridLayout;
 -import java.awt.Rectangle;
  import java.awt.event.ActionListener;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
Simple merge
Simple merge
Simple merge
@@@ -78,7 -50,29 +53,32 @@@ import javax.swing.table.TableColumn
  import javax.swing.table.TableModel;
  import javax.swing.table.TableRowSorter;
  
++import jalview.hmmer.HmmerCommand;
++import jalview.util.FileUtils;
++
  import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
+ import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
+ import jalview.bin.Cache;
+ import jalview.gui.Help.HelpId;
+ import jalview.gui.StructureViewer.ViewerType;
+ import jalview.io.BackupFiles;
+ import jalview.io.BackupFilesPresetEntry;
+ import jalview.io.FileFormatI;
+ import jalview.io.JalviewFileChooser;
+ import jalview.io.JalviewFileView;
+ import jalview.jbgui.GPreferences;
+ import jalview.jbgui.GSequenceLink;
+ import jalview.schemes.ColourSchemeI;
+ import jalview.schemes.ColourSchemes;
+ import jalview.schemes.ResidueColourScheme;
+ import jalview.urls.UrlLinkTableModel;
+ import jalview.urls.api.UrlProviderFactoryI;
+ import jalview.urls.api.UrlProviderI;
+ import jalview.urls.desktop.DesktopUrlProviderFactory;
+ import jalview.util.MessageManager;
+ import jalview.util.Platform;
+ import jalview.util.UrlConstants;
+ import jalview.ws.sifts.SiftsSettings;
  
  /**
   * DOCUMENT ME!
   */
  public class Preferences extends GPreferences
  {
 +  // suggested list delimiter character
 +  public static final String COMMA = ",";
 +
 +  public static final String HMMSEARCH_SEQCOUNT = "HMMSEARCH_SEQCOUNT";
 +
 +  public static final String HMMINFO_GLOBAL_BACKGROUND = "HMMINFO_GLOBAL_BACKGROUND";
 +
 +  public static final String HMMALIGN_TRIM_TERMINI = "HMMALIGN_TRIM_TERMINI";
+   
+   public static final String ADD_SS_ANN = "ADD_SS_ANN";
  
-   public static final String ENABLE_SPLIT_FRAME = "ENABLE_SPLIT_FRAME";
+   public static final String ADD_TEMPFACT_ANN = "ADD_TEMPFACT_ANN";
  
-   public static final String SCALE_PROTEIN_TO_CDNA = "SCALE_PROTEIN_TO_CDNA";
+   public static final String ALLOW_UNPUBLISHED_PDB_QUERYING = "ALLOW_UNPUBLISHED_PDB_QUERYING";
+   public static final String ANNOTATIONCOLOUR_MAX = "ANNOTATIONCOLOUR_MAX";
+   public static final String ANNOTATIONCOLOUR_MIN = "ANNOTATIONCOLOUR_MIN";
+   public static final String ANTI_ALIAS = "ANTI_ALIAS";
+   public static final String AUTO_CALC_CONSENSUS = "AUTO_CALC_CONSENSUS";
+   public static final String AUTOASSOCIATE_PDBANDSEQS = "AUTOASSOCIATE_PDBANDSEQS";
+   public static final String BLOSUM62_PCA_FOR_NUCLEOTIDE = "BLOSUM62_PCA_FOR_NUCLEOTIDE";
+   public static final String CENTRE_COLUMN_LABELS = "CENTRE_COLUMN_LABELS";
+   public static final String CHIMERA_PATH = "CHIMERA_PATH";
+   public static final String DBREFFETCH_USEPICR = "DBREFFETCH_USEPICR";
  
    public static final String DEFAULT_COLOUR = "DEFAULT_COLOUR";
  
+   public static final String DEFAULT_COLOUR_NUC = "DEFAULT_COLOUR_NUC";
    public static final String DEFAULT_COLOUR_PROT = "DEFAULT_COLOUR_PROT";
  
-   public static final String DEFAULT_COLOUR_NUC = "DEFAULT_COLOUR_NUC";
+   public static final String ENABLE_SPLIT_FRAME = "ENABLE_SPLIT_FRAME";
  
-   public static final String ADD_TEMPFACT_ANN = "ADD_TEMPFACT_ANN";
+   public static final String FIGURE_AUTOIDWIDTH = "FIGURE_AUTOIDWIDTH";
  
-   public static final String ADD_SS_ANN = "ADD_SS_ANN";
+   public static final String FIGURE_FIXEDIDWIDTH = "FIGURE_FIXEDIDWIDTH";
  
-   public static final String USE_RNAVIEW = "USE_RNAVIEW";
+   public static final String FOLLOW_SELECTIONS = "FOLLOW_SELECTIONS";
  
-   public static final String STRUCT_FROM_PDB = "STRUCT_FROM_PDB";
+   public static final String FONT_NAME = "FONT_NAME";
  
-   public static final String STRUCTURE_DISPLAY = "STRUCTURE_DISPLAY";
+   public static final String FONT_SIZE = "FONT_SIZE";
  
-   public static final String CHIMERA_PATH = "CHIMERA_PATH";
+   public static final String FONT_STYLE = "FONT_STYLE";
 +  
 +  public static final String HMMER_PATH = "HMMER_PATH";
 +
 +  public static final String CYGWIN_PATH = "CYGWIN_PATH";
 +
 +  public static final String HMMSEARCH_DBS = "HMMSEARCH_DBS";
  
-   public static final String SORT_ANNOTATIONS = "SORT_ANNOTATIONS";
+   public static final String GAP_COLOUR = "GAP_COLOUR";
+   public static final String GAP_SYMBOL = "GAP_SYMBOL";
+   public static final String HIDDEN_COLOUR = "HIDDEN_COLOUR";
+   public static final String HIDE_INTRONS = "HIDE_INTRONS";
+   public static final String ID_ITALICS = "ID_ITALICS";
+   public static final String ID_ORG_HOSTURL = "ID_ORG_HOSTURL";
+   public static final String MAP_WITH_SIFTS = "MAP_WITH_SIFTS";
+   public static final String NOQUESTIONNAIRES = "NOQUESTIONNAIRES";
+   public static final String NORMALISE_CONSENSUS_LOGO = "NORMALISE_CONSENSUS_LOGO";
+   public static final String NORMALISE_LOGO = "NORMALISE_LOGO";
+   public static final String PAD_GAPS = "PAD_GAPS";
+   public static final String PDB_DOWNLOAD_FORMAT = "PDB_DOWNLOAD_FORMAT";
+   public static final String QUESTIONNAIRE = "QUESTIONNAIRE";
+   public static final String RELAXEDSEQIDMATCHING = "RELAXEDSEQIDMATCHING";
+   public static final String RIGHT_ALIGN_IDS = "RIGHT_ALIGN_IDS";
+   public static final String SCALE_PROTEIN_TO_CDNA = "SCALE_PROTEIN_TO_CDNA";
+   public static final String SHOW_ANNOTATIONS = "SHOW_ANNOTATIONS";
  
    public static final String SHOW_AUTOCALC_ABOVE = "SHOW_AUTOCALC_ABOVE";
  
      /*
       * Save Visual settings
       */
-     Cache.applicationProperties.setProperty("SHOW_JVSUFFIX",
+     Cache.setPropertyNoSave("SHOW_JVSUFFIX",
              Boolean.toString(seqLimit.isSelected()));
-     Cache.applicationProperties.setProperty("RIGHT_ALIGN_IDS",
+     Cache.setPropertyNoSave("RIGHT_ALIGN_IDS",
              Boolean.toString(rightAlign.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_FULLSCREEN",
+     Cache.setPropertyNoSave("SHOW_FULLSCREEN",
              Boolean.toString(fullScreen.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_OVERVIEW",
+     Cache.setPropertyNoSave("SHOW_OVERVIEW",
              Boolean.toString(openoverv.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS",
+     Cache.setPropertyNoSave("SHOW_ANNOTATIONS",
              Boolean.toString(annotations.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_CONSERVATION",
+     Cache.setPropertyNoSave("SHOW_CONSERVATION",
              Boolean.toString(conservation.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_QUALITY",
+     Cache.setPropertyNoSave("SHOW_QUALITY",
              Boolean.toString(quality.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_IDENTITY",
+     Cache.setPropertyNoSave("SHOW_IDENTITY",
              Boolean.toString(identity.isSelected()));
  
-     Cache.applicationProperties.setProperty("GAP_SYMBOL",
+     Cache.setPropertyNoSave("GAP_SYMBOL",
              gapSymbolCB.getSelectedItem().toString());
  
-     Cache.applicationProperties.setProperty("FONT_NAME",
+     Cache.setPropertyNoSave("FONT_NAME",
              fontNameCB.getSelectedItem().toString());
-     Cache.applicationProperties.setProperty("FONT_STYLE",
+     Cache.setPropertyNoSave("FONT_STYLE",
              fontStyleCB.getSelectedItem().toString());
-     Cache.applicationProperties.setProperty("FONT_SIZE",
+     Cache.setPropertyNoSave("FONT_SIZE",
              fontSizeCB.getSelectedItem().toString());
  
-     Cache.applicationProperties.setProperty("ID_ITALICS",
+     Cache.setPropertyNoSave("ID_ITALICS",
              Boolean.toString(idItalics.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_UNCONSERVED",
+     Cache.setPropertyNoSave("SHOW_UNCONSERVED",
              Boolean.toString(showUnconserved.isSelected()));
-     Cache.applicationProperties.setProperty(SHOW_OCCUPANCY,
+     Cache.setPropertyNoSave(SHOW_OCCUPANCY,
              Boolean.toString(showOccupancy.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_GROUP_CONSENSUS",
+     Cache.setPropertyNoSave("SHOW_GROUP_CONSENSUS",
              Boolean.toString(showGroupConsensus.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_GROUP_CONSERVATION",
+     Cache.setPropertyNoSave("SHOW_GROUP_CONSERVATION",
              Boolean.toString(showGroupConservation.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_CONSENSUS_HISTOGRAM",
+     Cache.setPropertyNoSave("SHOW_CONSENSUS_HISTOGRAM",
              Boolean.toString(showConsensHistogram.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_CONSENSUS_LOGO",
+     Cache.setPropertyNoSave("SHOW_CONSENSUS_LOGO",
              Boolean.toString(showConsensLogo.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_INFORMATION_HISTOGRAM",
++    Cache.setPropertyNoSave("SHOW_INFORMATION_HISTOGRAM",
 +            Boolean.toString(showConsensHistogram.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_HMM_LOGO",
++    Cache.setPropertyNoSave("SHOW_HMM_LOGO",
 +            Boolean.toString(showHMMLogo.isSelected()));
-     Cache.applicationProperties.setProperty("ANTI_ALIAS",
+     Cache.setPropertyNoSave("ANTI_ALIAS",
              Boolean.toString(smoothFont.isSelected()));
-     Cache.applicationProperties.setProperty(SCALE_PROTEIN_TO_CDNA,
+     Cache.setPropertyNoSave(SCALE_PROTEIN_TO_CDNA,
              Boolean.toString(scaleProteinToCdna.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_NPFEATS_TOOLTIP",
+     Cache.setPropertyNoSave("SHOW_NPFEATS_TOOLTIP",
              Boolean.toString(showNpTooltip.isSelected()));
-     Cache.applicationProperties.setProperty("SHOW_DBREFS_TOOLTIP",
+     Cache.setPropertyNoSave("SHOW_DBREFS_TOOLTIP",
              Boolean.toString(showDbRefTooltip.isSelected()));
  
-     Cache.applicationProperties.setProperty("WRAP_ALIGNMENT",
+     Cache.setPropertyNoSave("WRAP_ALIGNMENT",
              Boolean.toString(wrap.isSelected()));
  
-     Cache.applicationProperties.setProperty("STARTUP_FILE",
+     Cache.setPropertyNoSave("STARTUP_FILE",
              startupFileTextfield.getText());
-     Cache.applicationProperties.setProperty("SHOW_STARTUP_FILE",
+     Cache.setPropertyNoSave("SHOW_STARTUP_FILE",
              Boolean.toString(startupCheckbox.isSelected()));
  
-     Cache.applicationProperties.setProperty("SORT_ALIGNMENT",
+     Cache.setPropertyNoSave("SORT_ALIGNMENT",
              sortby.getSelectedItem().toString());
  
      // convert description of sort order to enum name for save
              maxColour.getBackground());
  
      /*
 +     * Save HMMER settings
 +     */
-     Cache.applicationProperties.setProperty(HMMALIGN_TRIM_TERMINI,
++    Cache.setPropertyNoSave(HMMALIGN_TRIM_TERMINI,
 +            Boolean.toString(hmmrTrimTermini.isSelected()));
-     Cache.applicationProperties.setProperty(HMMINFO_GLOBAL_BACKGROUND,
++    Cache.setPropertyNoSave(HMMINFO_GLOBAL_BACKGROUND,
 +            Boolean.toString(hmmerBackgroundUniprot.isSelected()));
-     Cache.applicationProperties.setProperty(HMMSEARCH_SEQCOUNT,
++    Cache.setPropertyNoSave(HMMSEARCH_SEQCOUNT,
 +            hmmerSequenceCount.getText());
 +    Cache.setOrRemove(HMMER_PATH, hmmerPath.getText());
 +    if (cygwinPath != null)
 +    {
 +      Cache.setOrRemove(CYGWIN_PATH, cygwinPath.getText());
 +    }
 +    AlignFrame[] frames = Desktop.getAlignFrames();
 +    if (frames != null && frames.length > 0)
 +    {
 +      for (AlignFrame f : frames)
 +      {
 +        f.updateHMMERStatus();
 +      }
 +    }
 +    
 +    hmmrTrimTermini.setSelected(Cache.getDefault(HMMALIGN_TRIM_TERMINI, false));
 +    if (Cache.getDefault(HMMINFO_GLOBAL_BACKGROUND, false))
 +    {
 +      hmmerBackgroundUniprot.setSelected(true);
 +    }
 +    else
 +    {
 +      hmmerBackgroundAlignment.setSelected(true);
 +    }
 +    hmmerSequenceCount
 +            .setText(Cache.getProperty(HMMSEARCH_SEQCOUNT));
 +    hmmerPath.setText(Cache.getProperty(HMMER_PATH));
 +
 +    /*
       * Save Overview settings
       */
-     Cache.setColourProperty(GAP_COLOUR, gapColour.getBackground());
-     Cache.setColourProperty(HIDDEN_COLOUR, hiddenColour.getBackground());
-     Cache.applicationProperties.setProperty(USE_LEGACY_GAP,
+     Cache.setColourPropertyNoSave(GAP_COLOUR, gapColour.getBackground());
+     Cache.setColourPropertyNoSave(HIDDEN_COLOUR, hiddenColour.getBackground());
+     Cache.setPropertyNoSave(USE_LEGACY_GAP,
              Boolean.toString(useLegacyGap.isSelected()));
-     Cache.applicationProperties.setProperty(SHOW_OV_HIDDEN_AT_START,
+     Cache.setPropertyNoSave(SHOW_OV_HIDDEN_AT_START,
              Boolean.toString(showHiddenAtStart.isSelected()));
  
      /*
      }
      return true;
    }
 +  
 +  /**
 +   * Returns true if the given text field contains a path to a folder that
 +   * contains an executable with the given name, else false (after showing a
 +   * warning dialog). The executable name will be tried with .exe appended if not
 +   * found.
 +   * 
 +   * @param textField
 +   * @param executable
 +   */
 +  protected boolean validateExecutablePath(JTextField textField, String executable)
 +  {
 +    String folder = textField.getText().trim();
 +
 +    if (FileUtils.getExecutable(executable, folder) != null)
 +    {
 +      return true;
 +    }
 +    if (folder.length() > 0)
 +    {
-       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
++      JvOptionPane.showInternalMessageDialog(Desktop.getInstance(),
 +              MessageManager.formatMessage("label.executable_not_found",
 +                      executable),
 +              MessageManager.getString("label.invalid_folder"),
 +              JvOptionPane.ERROR_MESSAGE);
 +    }
 +    return false;
 +  }
 +
 +  /**
 +   * Checks if a file can be executed
 +   * 
 +   * @param path
 +   *          the path to the file
 +   * @return
 +   */
 +  public boolean canExecute(String path)
 +  {
 +    File file = new File(path);
 +    if (!file.canExecute())
 +    {
 +      file = new File(path + ".exe");
 +      {
 +        if (!file.canExecute())
 +        {
 +          return false;
 +        }
 +      }
 +    }
 +    return true;
 +  }
  
    /**
     * If Chimera is selected, check it can be found on default or user-specified
Simple merge
Simple merge
Simple merge
@@@ -198,15 -215,12 +198,17 @@@ public class WsJobParameters extends JP
    public boolean showRunDialog()
    {
  
-     frame = new JDialog(Desktop.instance, true);
+     frame = new JDialog(Desktop.getInstance(), true);
 -
 +    if (service != null)
 +    {
 +      frame.setTitle(MessageManager.formatMessage("label.edit_params_for",
 +              new String[]
 +      { service.getActionText() }));
 +    }
-     Rectangle deskr = Desktop.instance.getBounds();
+     frame.setTitle(MessageManager.formatMessage("label.edit_params_for",
+             new String[]
+             { service.getActionText() }));
+     Rectangle deskr = Desktop.getInstance().getBounds();
      Dimension pref = this.getPreferredSize();
      frame.setBounds(
              new Rectangle((int) (deskr.getCenterX() - pref.width / 2),
      dialogpanel.add(canceljob);
      // JAL-1580: setMaximumSize() doesn't work, so just size for the worst case:
      // check for null is for JUnit usage
-     final int windowHeight = Desktop.instance == null ? DEFAULT_HEIGHT
-             : Desktop.instance.getHeight();
-     // setPreferredSize(new Dimension(PREFERRED_WIDTH, windowHeight));
 -    final int windowHeight = Desktop.getInstance() == null ? 540
++    final int windowHeight = Desktop.getInstance() == null ? DEFAULT_HEIGHT
+             : Desktop.getInstance().getHeight();
+     setPreferredSize(new Dimension(540, windowHeight));
      add(dialogpanel, BorderLayout.SOUTH);
      validate();
    }
     */
    protected void updateWebServiceMenus()
    {
-     if (Desktop.instance == null)
++    if (Desktop.getInstance() == null)
 +    {
 +      return;
 +    }
      for (AlignFrame alignFrame : Desktop.getAlignFrames())
      {
 -      alignFrame.BuildWebServiceMenu();
 +      alignFrame.buildWebServicesMenu();
      }
    }
  
   */
  package jalview.gui;
  
- import jalview.bin.Cache;
- import jalview.jbgui.GWsPreferences;
- import jalview.util.MessageManager;
- import jalview.ws.WSDiscovererI;
- import jalview.ws.jws2.Jws2Discoverer;
- import jalview.ws.rest.RestServiceDescription;
 +
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Component;
@@@ -45,6 -37,12 +39,13 @@@ import javax.swing.JTextField
  import javax.swing.table.AbstractTableModel;
  import javax.swing.table.TableCellRenderer;
  
+ import jalview.bin.Cache;
+ import jalview.jbgui.GWsPreferences;
+ import jalview.util.MessageManager;
++import jalview.ws.WSDiscovererI;
+ import jalview.ws.jws2.Jws2Discoverer;
+ import jalview.ws.rest.RestServiceDescription;
  public class WsPreferences extends GWsPreferences
  {
  
  
        if (validate == JvOptionPane.OK_OPTION)
        {
-         if (Jws2Discoverer.getDiscoverer().testServiceUrl(foo))
 -        if (Jws2Discoverer.testServiceUrl(foo))
++        if (Jws2Discoverer.getInstance().testServiceUrl(foo))
          {
            return foo.toString();
          }
index eea3dae,0000000..a691c53
mode 100644,000000..100644
--- /dev/null
@@@ -1,63 -1,0 +1,63 @@@
 +package jalview.io;
 +
 +import jalview.bin.Jalview;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.ResidueCount;
 +import jalview.datamodel.SequenceI;
 +import jalview.gui.AlignmentPanel;
 +import jalview.gui.Desktop;
 +import jalview.gui.JvOptionPane;
 +import jalview.util.MessageManager;
 +
 +import java.io.File;
 +import java.io.IOException;
 +import java.net.MalformedURLException;
 +
 +import javax.swing.JFileChooser;
 +
 +public class CountReader
 +{
 +  public static ResidueCount getBackgroundFrequencies(AlignmentPanel ap, SequenceI seq) throws MalformedURLException, IOException
 +  {
 +    JFileChooser bkgdFreqChooser = new JFileChooser();
 +    
 +    bkgdFreqChooser.showOpenDialog(ap);
 +    
 +    File file = bkgdFreqChooser.getSelectedFile();
 +    if (file == null)
 +    {
 +      return null;
 +    }
 +    
 +    IdentifyFile identifier = new IdentifyFile();
 +    FileFormatI format = null;
 +    try
 +    {
 +      format = identifier.identify(file.getPath(), DataSourceType.FILE);
 +    } catch (Exception e)
 +    {
 +
 +    }
 +    
 +    if (format == null)
 +    {
 +      if (!Jalview.isHeadlessMode())
 +      {
-         JvOptionPane.showInternalMessageDialog(Desktop.desktop,
++        JvOptionPane.showInternalMessageDialog(Desktop.getInstance(),
 +                MessageManager.getString("label.couldnt_read_data") + " in "
 +                        + file + "\n"
 +                        + AppletFormatAdapter.getSupportedFormats(),
 +                MessageManager.getString("label.couldnt_read_data"),
 +                JvOptionPane.WARNING_MESSAGE);
 +      }
 +    }
 +
 +    FileParse parser = new FileParse(file.getPath(), DataSourceType.FILE);
 +    AlignmentI al = new FormatAdapter().readFromFile(parser, format);
 +    parser.close();
 +    
 +    ResidueCount counts = new ResidueCount(al.getSequences());
 +    
 +    return counts;
 +  }
 +}
@@@ -373,24 -373,22 +373,38 @@@ public enum FileFormat implements FileF
      {
        return true;
      }
 +  },
 +  HMMER3("HMMER3", "hmm", true, true)
 +  {
 +    @Override
 +    public AlignmentFileReaderI getReader(FileParse source)
 +            throws IOException
 +    {
 +      return new HMMFile(source);
 +    }
 +
 +    @Override
 +    public AlignmentFileWriterI getWriter(AlignmentI al)
 +    {
 +      return new HMMFile();
 +    }
+   },   BSML("BSML", "bbb", true, false)
+   {
+     @Override
+     public AlignmentFileReaderI getReader(FileParse source)
+             throws IOException
+     {
+       return new BSMLFile(source);
+     }
+     @Override
+     public AlignmentFileWriterI getWriter(AlignmentI al)
+     {
+       return null;
+     }
    };
  
 +
    private boolean writable;
  
    private boolean readable;
@@@ -46,15 -47,11 +47,16 @@@ import jalview.project.Jalview2XML
  import jalview.schemes.ColourSchemeI;
  import jalview.structure.StructureSelectionManager;
  import jalview.util.MessageManager;
+ import jalview.util.Platform;
  import jalview.ws.utils.UrlDownloadClient;
  
 +import java.util.ArrayList;
 +import java.util.List;
 +
  public class FileLoader implements Runnable
  {
 +  private static final String TAB = "\t";
 +
    String file;
  
    DataSourceType protocol;
Simple merge
@@@ -77,15 -79,105 +79,106 @@@ public class StockholmFile extends Alig
  {
    private static final String ANNOTATION = "annotation";
  
 -  // WUSS extended symbols. Avoid ambiguity with protein SS annotations by using
 -  // NOT_RNASS first.
 +  private static final char UNDERSCORE = '_';
 +  
 +  // WUSS extended symbols. Avoid ambiguity with protein SS annotations by using NOT_RNASS first.
    public static final String RNASS_BRACKETS = "<>[](){}AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz";
  
+   public static final int REGEX_STOCKHOLM = 0;
+   public static final int REGEX_BRACKETS = 1;
    // use the following regex to decide an annotations (whole) line is NOT an RNA
    // SS (it contains only E,H,e,h and other non-brace/non-alpha chars)
-   private static final Regex NOT_RNASS = new Regex(
-           "^[^<>[\\](){}A-DF-Za-df-z]*$");
+   public static final int REGEX_NOT_RNASS = 2;
+   private static final int REGEX_ANNOTATION = 3;
+   private static final int REGEX_PFAM = 4;
+   private static final int REGEX_RFAM = 5;
+   private static final int REGEX_ALIGN_END = 6;
+   private static final int REGEX_SPLIT_ID = 7;
+   private static final int REGEX_SUBTYPE = 8;
+   private static final int REGEX_ANNOTATION_LINE = 9;
+   private static final int REGEX_REMOVE_ID = 10;
+   private static final int REGEX_OPEN_PAREN = 11;
+   private static final int REGEX_CLOSE_PAREN = 12;
+   public static final int REGEX_MAX = 13;
+   private static Regex REGEX[] = new Regex[REGEX_MAX];
+   /**
+    * Centralize all actual Regex instantialization in Platform.
 -   * 
++   * // JBPNote: Why is this 'centralisation' better ?
+    * @param id
+    * @return
+    */
+   private static Regex getRegex(int id)
+   {
+     if (REGEX[id] == null)
+     {
+       String pat = null, pat2 = null;
+       switch (id)
+       {
+       case REGEX_STOCKHOLM:
+         pat = "# STOCKHOLM ([\\d\\.]+)";
+         break;
+       case REGEX_BRACKETS:
+         // for reference; not used
+         pat = "(<|>|\\[|\\]|\\(|\\)|\\{|\\})";
+         break;
+       case REGEX_NOT_RNASS:
+         pat = "^[^<>[\\](){}A-DF-Za-df-z]*$";
+         break;
+       case REGEX_ANNOTATION:
+         pat = "(\\w+)\\s*(.*)";
+         break;
+       case REGEX_PFAM:
+         pat = "PF[0-9]{5}(.*)";
+         break;
+       case REGEX_RFAM:
+         pat = "RF[0-9]{5}(.*)";
+         break;
+       case REGEX_ALIGN_END:
+         pat = "^\\s*\\/\\/";
+         break;
+       case REGEX_SPLIT_ID:
+         pat = "(\\S+)\\/(\\d+)\\-(\\d+)";
+         break;
+       case REGEX_SUBTYPE:
+         pat = "(\\S+)\\s+(\\S*)\\s+(.*)";
+         break;
+       case REGEX_ANNOTATION_LINE:
+         pat = "#=(G[FSRC]?)\\s+(.*)";
+         break;
+       case REGEX_REMOVE_ID:
+         pat = "(\\S+)\\s+(\\S+)";
+         break;
+       case REGEX_OPEN_PAREN:
+         pat = "(<|\\[)";
+         pat2 = "(";
+         break;
+       case REGEX_CLOSE_PAREN:
+         pat = "(>|\\])";
+         pat2 = ")";
+         break;
+       default:
+         return null;
+       }
+       REGEX[id] = Platform.newRegex(pat, pat2);
+     }
+     return REGEX[id];
+   }
  
    StringBuffer out; // output buffer
  
@@@ -64,6 -50,18 +51,21 @@@ import javax.swing.event.ChangeEvent
  import javax.swing.event.MenuEvent;
  import javax.swing.event.MenuListener;
  
+ import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
+ import jalview.analysis.GeneticCodeI;
+ import jalview.analysis.GeneticCodes;
+ import jalview.api.SplitContainerI;
+ import jalview.bin.Cache;
+ import jalview.gui.JvSwingUtils;
+ import jalview.gui.Preferences;
++import jalview.hmmer.HmmerCommand;
++import jalview.io.FileFormatException;
+ import jalview.io.FileFormats;
+ import jalview.schemes.ResidueColourScheme;
+ import jalview.util.MessageManager;
+ import jalview.util.Platform;
++
  @SuppressWarnings("serial")
  public class GAlignFrame extends JInternalFrame
  {
          saveAs_actionPerformed();
        }
      };
 -
 +  
      // FIXME getDefaultToolkit throws an exception in Headless mode
-     KeyStroke keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_S,
-             jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx()
-                     | jalview.util.ShortcutKeyMaskExWrapper.SHIFT_DOWN_MASK,
+     KeyStroke keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_S, Platform.SHORTCUT_KEY_MASK | InputEvent.SHIFT_DOWN_MASK,
              false);
      addMenuActionAndAccelerator(keyStroke, saveAs, al);
 -
 +  
      closeMenuItem.setText(MessageManager.getString("action.close"));
-     keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_W,
-             jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx(), false);
+     keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_W, Platform.SHORTCUT_KEY_MASK, false);
      al = new ActionListener()
      {
        @Override
        }
      };
      addMenuActionAndAccelerator(keyStroke, unGroup, al);
 -
 +  
      copy.setText(MessageManager.getString("action.copy"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_C,
-             jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx(), false);
+             Platform.SHORTCUT_KEY_MASK, false);
  
      al = new ActionListener()
      {
        }
      };
      addMenuActionAndAccelerator(keyStroke, copy, al);
 -
 +  
      cut.setText(MessageManager.getString("action.cut"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_X,
-             jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx(), false);
+             Platform.SHORTCUT_KEY_MASK, false);
      al = new ActionListener()
      {
        @Override
        @Override
        public void menuSelected(MenuEvent e)
        {
+         enableSortMenuOptions();
+       }
+       @Override
+       public void menuDeselected(MenuEvent e)
+       {
+       }
+       @Override
+       public void menuCanceled(MenuEvent e)
+       {
+       }
+     });
+     sortByTreeMenu.addMenuListener(new MenuListener()
+     {
+       @Override
+       public void menuSelected(MenuEvent e)
+       {
          buildTreeSortMenu();
        }
 -
 +  
        @Override
        public void menuDeselected(MenuEvent e)
        {
          tabbedPane_focusGained(e);
        }
      });
 -
 +  
      JMenuItem save = new JMenuItem(MessageManager.getString("action.save"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_S,
-             jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx(), false);
+             Platform.SHORTCUT_KEY_MASK, false);
      al = new ActionListener()
      {
        @Override
      // selectMenu.add(listenToViewSelections);
    }
  
 +  /**
 +   * Constructs the entries on the HMMER menu
 +   */
 +  protected void initHMMERMenu()
 +  {
 +    /*
 +     * hmmbuild
 +     */
 +    JMenu hmmBuild = new JMenu(MessageManager.getString("label.hmmbuild"));
 +    JMenuItem hmmBuildSettings = new JMenuItem(
 +            MessageManager.getString("label.edit_settings_and_run"));
 +    hmmBuildSettings.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        hmmBuild_actionPerformed(false);
 +      }
 +    });
 +    JMenuItem hmmBuildRun = new JMenuItem(MessageManager.formatMessage(
 +            "label.action_with_default_settings", "hmmbuild"));
 +    hmmBuildRun.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        hmmBuild_actionPerformed(true);
 +      }
 +    });
 +    hmmBuild.add(hmmBuildRun);
 +    hmmBuild.add(hmmBuildSettings);
 +
 +    /*
 +     * hmmalign
 +     */
 +    JMenu hmmAlign = new JMenu(MessageManager.getString("label.hmmalign"));
 +    JMenuItem hmmAlignRun = new JMenuItem(MessageManager.formatMessage(
 +            "label.action_with_default_settings", "hmmalign"));
 +    hmmAlignRun.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        hmmAlign_actionPerformed(true);
 +      }
 +    });
 +    JMenuItem hmmAlignSettings = new JMenuItem(
 +            MessageManager.getString("label.edit_settings_and_run"));
 +    hmmAlignSettings.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        hmmAlign_actionPerformed(false);
 +      }
 +    });
 +    hmmAlign.add(hmmAlignRun);
 +    hmmAlign.add(hmmAlignSettings);
 +
 +    /*
 +     * hmmsearch
 +     */
 +    JMenu hmmSearch = new JMenu(
 +            MessageManager.getString("label.hmmsearch"));
 +    JMenuItem hmmSearchSettings = new JMenuItem(
 +            MessageManager.getString("label.edit_settings_and_run"));
 +    hmmSearchSettings.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        hmmSearch_actionPerformed(false);
 +      }
 +    });
 +    JMenuItem hmmSearchRun = new JMenuItem(MessageManager.formatMessage(
 +            "label.action_with_default_settings", "hmmsearch"));
 +    hmmSearchRun.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        hmmSearch_actionPerformed(true);
 +      }
 +    });
 +    JMenuItem addDatabase = new JMenuItem(
 +            MessageManager.getString("label.add_database"));
 +    addDatabase.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        try
 +        {
 +          addDatabase_actionPerformed();
 +        } catch (IOException e1)
 +        {
 +          e1.printStackTrace();
 +        }
 +      }
 +    });
 +    hmmSearch.add(hmmSearchRun);
 +    hmmSearch.add(hmmSearchSettings);
 +    // hmmSearch.add(addDatabase);
 +
 +    /*
 +     * jackhmmer
 +     */
 +    JMenu jackhmmer = new JMenu(
 +            MessageManager.getString("label.jackhmmer"));
 +    JMenuItem jackhmmerSettings = new JMenuItem(
 +            MessageManager.getString("label.edit_settings_and_run"));
 +    jackhmmerSettings.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        jackhmmer_actionPerformed(false);
 +      }
 +    });
 +    JMenuItem jackhmmerRun = new JMenuItem(MessageManager.formatMessage(
 +            "label.action_with_default_settings", "jackhmmer"));
 +    jackhmmerRun.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        jackhmmer_actionPerformed(true);
 +      }
 +
 +    });
 +    /*
 +    JMenuItem addDatabase = new JMenuItem(
 +            MessageManager.getString("label.add_database"));
 +    addDatabase.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        try
 +        {
 +          addDatabase_actionPerformed();
 +        } catch (IOException e1)
 +        {
 +          e1.printStackTrace();
 +        }
 +      }
 +    });
 +    */
 +    jackhmmer.add(jackhmmerRun);
 +    jackhmmer.add(jackhmmerSettings);
 +    // hmmSearch.add(addDatabase);
 +
 +    /*
 +     * top level menu
 +     */
 +    hmmerMenu.setText(MessageManager.getString("action.hmmer"));
 +    hmmerMenu.setEnabled(HmmerCommand.isHmmerAvailable());
 +    hmmerMenu.add(hmmBuild);
 +    hmmerMenu.add(hmmAlign);
 +    hmmerMenu.add(hmmSearch);
 +    hmmerMenu.add(jackhmmer);
 +
 +  }
 +
+   protected void enableSortMenuOptions()
+   {
+   }
+   
    protected void loadVcf_actionPerformed()
    {
    }
Simple merge
@@@ -3647,17 -3687,6 +3731,16 @@@ public class Jalview2XM
            }
          }
  
 +        /*
 +         * load any HMMER profile
 +         */
 +        // TODO fix this
 +
 +        String hmmJarFile = jseqs.get(i).getHmmerProfile();
 +        if (hmmJarFile != null && jprovider != null)
 +        {
 +          loadHmmerProfile(jprovider, hmmJarFile, al.getSequenceAt(i));
 +        }
        }
      } // end !multipleview
  
Simple merge
Simple merge
@@@ -3244,46 -3087,22 +3253,61 @@@ public abstract class AlignmentViewpor
        codingComplement.setUpdateStructures(needToUpdateStructureViews);
      }
    }
 -  
 +  /**
 +   * Filters out sequences with an eValue higher than the specified value. The
 +   * filtered sequences are hidden or deleted. Sequences with no eValues are also
 +   * filtered out.
 +   * 
 +   * @param eValue
 +   * @param delete
 +   */
 +  public void filterByEvalue(double eValue)
 +  {
 +    for (SequenceI seq : alignment.getSequencesArray())
 +    {
 +      if ((seq.getAnnotation("Search Scores") == null
 +              || seq.getAnnotation("Search Scores")[0].getEValue() > eValue)
 +              && seq.getHMM() == null)
 +      {
 +        hideSequence(new SequenceI[] { seq });
 +      }
 +    }
 +  }
 +
 +  /**
 +   * Filters out sequences with an score lower than the specified value. The
 +   * filtered sequences are hidden or deleted.
 +   * 
 +   * @param score
 +   * @param delete
 +   */
 +  public void filterByScore(double score)
 +  {
 +    for (SequenceI seq : alignment.getSequencesArray())
 +    {
 +      if ((seq.getAnnotation("Search Scores") == null
 +              || seq.getAnnotation("Search Scores")[0]
 +                      .getBitScore() < score)
 +              && seq.getHMM() == null)
 +      {
 +        hideSequence(new SequenceI[] { seq });
 +      }
 +    }
++  }  
+   /**
+    * Notify TreePanel and AlignmentPanel of some sort of alignment change.
+    */
+   public void notifyAlignment()
+   {
+     changeSupport.firePropertyChange(PROPERTY_ALIGNMENT, null, alignment.getSequences());
+   }
+   
+   /**
+    * Notify AlignmentPanel of a sequence column selection or visibility changes.
+    */
+   public void notifySequence()
+   {
+     changeSupport.firePropertyChange(PROPERTY_SEQUENCE, null, null);
    }
 -
  }
@@@ -34,9 -30,24 +30,25 @@@ import ext.vamsas.IRegistryServiceLocat
  import ext.vamsas.RegistryServiceSoapBindingStub;
  import ext.vamsas.ServiceHandle;
  import ext.vamsas.ServiceHandles;
++import jalview.bin.Cache;
+ import jalview.bin.ApplicationSingletonProvider;
+ import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
+ import jalview.gui.JvOptionPane;
+ import jalview.util.MessageManager;
  
- public class Discoverer implements Runnable
+ public class Discoverer implements Runnable, ApplicationSingletonI
  {
+   public static Discoverer getInstance()
+   {
+     return (Discoverer) ApplicationSingletonProvider.getInstance(Discoverer.class);
+   }
+   private Discoverer()
+   {
+     // use getInstance()
+   }
    ext.vamsas.IRegistry registry; // the root registry service.
  
    private java.beans.PropertyChangeSupport changeSupport = new java.beans.PropertyChangeSupport(
Simple merge
index 98282fa,0000000..bef7e29
mode 100644,000000..100644
--- /dev/null
@@@ -1,264 -1,0 +1,264 @@@
 +package jalview.ws.jws2;
 +
 +import jalview.gui.WsJobParameters;
 +import jalview.util.MessageManager;
 +import jalview.ws.api.ServiceWithParameters;
 +import jalview.ws.jws2.jabaws2.Jws2Instance;
 +
 +import java.awt.BorderLayout;
 +import java.awt.event.WindowEvent;
 +import java.awt.event.WindowListener;
 +import java.util.Iterator;
 +import java.util.List;
 +import java.util.Vector;
 +
 +import javax.swing.JFrame;
 +import javax.swing.JPanel;
 +
 +import compbio.metadata.Option;
 +import compbio.metadata.Parameter;
 +import compbio.metadata.Preset;
 +import compbio.metadata.PresetManager;
 +
 +public class JabaWsParamTest
 +{
 +
 +  /**
 +   * testing method - grab a service and parameter set and show the window
 +   * 
 +   * @param args
 +   */
 +  public static void main(String[] args)
 +  {
 +    jalview.ws.jws2.Jws2Discoverer disc = jalview.ws.jws2.Jws2Discoverer
-             .getDiscoverer();
++            .getInstance();
 +    int p = 0;
 +    if (args.length > 0)
 +    {
 +      Vector<String> services = new Vector<>();
 +      services.addElement(args[p++]);
-       Jws2Discoverer.getDiscoverer().setServiceUrls(services);
++      Jws2Discoverer.getInstance().setServiceUrls(services);
 +    }
 +    try
 +    {
 +      disc.run();
 +    } catch (Exception e)
 +    {
 +      System.err.println("Aborting. Problem discovering services.");
 +      e.printStackTrace();
 +      return;
 +    }
 +    Jws2Instance lastserv = null;
 +    for (ServiceWithParameters service : disc.getServices())
 +    {
 +      // this will fail for non-JABAWS services !
 +      lastserv = (Jws2Instance) service;
 +      if (p >= args.length || service.getName().equalsIgnoreCase(args[p]))
 +      {
 +        if (lastserv != null)
 +        {
 +          List<Preset> prl = null;
 +          Preset pr = null;
 +          if (++p < args.length)
 +          {
 +            PresetManager prman = lastserv.getPresets();
 +            if (prman != null)
 +            {
 +              pr = prman.getPresetByName(args[p]);
 +              if (pr == null)
 +              {
 +                // just grab the last preset.
 +                prl = prman.getPresets();
 +              }
 +            }
 +          }
 +          else
 +          {
 +            PresetManager prman = lastserv.getPresets();
 +            if (prman != null)
 +            {
 +              prl = prman.getPresets();
 +            }
 +          }
 +          Iterator<Preset> en = (prl == null) ? null : prl.iterator();
 +          while (en != null && en.hasNext())
 +          {
 +            if (en != null)
 +            {
 +              if (!en.hasNext())
 +              {
 +                en = prl.iterator();
 +              }
 +              pr = en.next();
 +            }
 +            {
 +              System.out.println("Testing opts dupes for "
 +                      + lastserv.getUri() + " : " + lastserv.getActionText()
 +                      + ":" + pr.getName());
 +              List<Option> rg = lastserv.getRunnerConfig().getOptions();
 +              for (Option o : rg)
 +              {
 +                try
 +                {
 +                  Option cpy = jalview.ws.jws2.ParameterUtils.copyOption(o);
 +                } catch (Exception e)
 +                {
 +                  System.err.println("Failed to copy " + o.getName());
 +                  e.printStackTrace();
 +                } catch (Error e)
 +                {
 +                  System.err.println("Failed to copy " + o.getName());
 +                  e.printStackTrace();
 +                }
 +              }
 +            }
 +            {
 +              System.out.println("Testing param dupes:");
 +              List<Parameter> rg = lastserv.getRunnerConfig()
 +                      .getParameters();
 +              for (Parameter o : rg)
 +              {
 +                try
 +                {
 +                  Parameter cpy = jalview.ws.jws2.ParameterUtils
 +                          .copyParameter(o);
 +                } catch (Exception e)
 +                {
 +                  System.err.println("Failed to copy " + o.getName());
 +                  e.printStackTrace();
 +                } catch (Error e)
 +                {
 +                  System.err.println("Failed to copy " + o.getName());
 +                  e.printStackTrace();
 +                }
 +              }
 +            }
 +            {
 +              System.out.println("Testing param write:");
 +              List<String> writeparam = null, readparam = null;
 +              try
 +              {
 +                writeparam = jalview.ws.jws2.ParameterUtils
 +                        .writeParameterSet(
 +                                pr.getArguments(lastserv.getRunnerConfig()),
 +                                " ");
 +                System.out.println("Testing param read :");
 +                List<Option> pset = jalview.ws.jws2.ParameterUtils
 +                        .processParameters(writeparam,
 +                                lastserv.getRunnerConfig(), " ");
 +                readparam = jalview.ws.jws2.ParameterUtils
 +                        .writeParameterSet(pset, " ");
 +                Iterator<String> o = pr.getOptions().iterator(),
 +                        s = writeparam.iterator(), t = readparam.iterator();
 +                boolean failed = false;
 +                while (s.hasNext() && t.hasNext())
 +                {
 +                  String on = o.next(), sn = s.next(), st = t.next();
 +                  if (!sn.equals(st))
 +                  {
 +                    System.out.println(
 +                            "Original was " + on + " Phase 1 wrote " + sn
 +                                    + "\tPhase 2 wrote " + st);
 +                    failed = true;
 +                  }
 +                }
 +                if (failed)
 +                {
 +                  System.out.println(
 +                          "Original parameters:\n" + pr.getOptions());
 +                  System.out.println(
 +                          "Wrote parameters in first set:\n" + writeparam);
 +                  System.out.println(
 +                          "Wrote parameters in second set:\n" + readparam);
 +
 +                }
 +              } catch (Exception e)
 +              {
 +                e.printStackTrace();
 +              }
 +            }
 +            WsJobParameters pgui = new WsJobParameters(null, lastserv,
 +                    new JabaPreset(lastserv, pr), null);
 +            JFrame jf = new JFrame(MessageManager
 +                    .formatMessage("label.ws_parameters_for", new String[]
 +                    { lastserv.getActionText() }));
 +            JPanel cont = new JPanel(new BorderLayout());
 +            pgui.validate();
 +            cont.setPreferredSize(pgui.getPreferredSize());
 +            cont.add(pgui, BorderLayout.CENTER);
 +            jf.setLayout(new BorderLayout());
 +            jf.add(cont, BorderLayout.CENTER);
 +            jf.validate();
 +            final Thread thr = Thread.currentThread();
 +            jf.addWindowListener(new WindowListener()
 +            {
 +
 +              @Override
 +              public void windowActivated(WindowEvent e)
 +              {
 +                // TODO Auto-generated method stub
 +
 +              }
 +
 +              @Override
 +              public void windowClosed(WindowEvent e)
 +              {
 +              }
 +
 +              @Override
 +              public void windowClosing(WindowEvent e)
 +              {
 +                thr.interrupt();
 +
 +              }
 +
 +              @Override
 +              public void windowDeactivated(WindowEvent e)
 +              {
 +                // TODO Auto-generated method stub
 +
 +              }
 +
 +              @Override
 +              public void windowDeiconified(WindowEvent e)
 +              {
 +                // TODO Auto-generated method stub
 +
 +              }
 +
 +              @Override
 +              public void windowIconified(WindowEvent e)
 +              {
 +                // TODO Auto-generated method stub
 +
 +              }
 +
 +              @Override
 +              public void windowOpened(WindowEvent e)
 +              {
 +                // TODO Auto-generated method stub
 +
 +              }
 +
 +            });
 +            jf.setVisible(true);
 +            boolean inter = false;
 +            while (!inter)
 +            {
 +              try
 +              {
 +                Thread.sleep(10000);
 +              } catch (Exception e)
 +              {
 +                inter = true;
 +              }
 +              ;
 +            }
 +            jf.dispose();
 +          }
 +        }
 +      }
 +    }
 +  }
 +
 +}
  package jalview.ws.jws2;
  
  import jalview.bin.Cache;
+ import jalview.bin.ApplicationSingletonProvider;
+ import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
  import jalview.gui.AlignFrame;
 -import jalview.gui.Desktop;
 -import jalview.gui.JvSwingUtils;
  import jalview.util.MessageManager;
 -import jalview.ws.WSMenuEntryProviderI;
 +import jalview.ws.ServiceChangeListener;
 +import jalview.ws.WSDiscovererI;
 +import jalview.ws.api.ServiceWithParameters;
  import jalview.ws.jws2.jabaws2.Jws2Instance;
  import jalview.ws.params.ParamDatastoreI;
  
@@@ -58,8 -62,29 +60,20 @@@ import compbio.ws.client.Services
   * @author JimP
   * 
   */
- public class Jws2Discoverer implements WSDiscovererI, Runnable
 -public class Jws2Discoverer
 -        implements Runnable, WSMenuEntryProviderI, ApplicationSingletonI
++public class Jws2Discoverer implements WSDiscovererI, Runnable, ApplicationSingletonI
  {
+   /**
+    * Returns the singleton instance of this class.
+    * 
+    * @return
+    */
+   public static Jws2Discoverer getInstance()
+   {
+     return (Jws2Discoverer) ApplicationSingletonProvider
+             .getInstance(Jws2Discoverer.class);
+   }
 -  /**
 -   * Private constructor enforces use of singleton via getDiscoverer()
 -   */
 -  private Jws2Discoverer()
 -  {
 -    // use getInstance();
 -  }
 -
    public static final String COMPBIO_JABAWS = "http://www.compbio.dundee.ac.uk/jabaws";
  
    /*
  
    // preferred url has precedence over others
    private String preferredUrl;
 -
 -  protected PropertyChangeSupport changeSupport = new PropertyChangeSupport(
 -          this);
 -
 +  
 +  private Set<ServiceChangeListener> serviceListeners = new CopyOnWriteArraySet<>();
    private Vector<String> invalidServiceUrls = null;
  
    private Vector<String> urlsWithoutServices = null;
     */
    public static void main(String[] args)
    {
+     Jws2Discoverer instance = getInstance();
      if (args.length > 0)
      {
-       testUrls = new ArrayList<>();
+       instance.testUrls = new ArrayList<>();
        for (String url : args)
        {
-         testUrls.add(url);
+         instance.testUrls.add(url);
        }
      }
-     var discoverer = getDiscoverer();
 -    Thread runner = instance.startDiscoverer(new PropertyChangeListener()
 -    {
 -
 -      @Override
 -      public void propertyChange(PropertyChangeEvent evt)
++    var discoverer = getInstance();
 +    discoverer.addServiceChangeListener((_discoverer, _services) -> {
 +      if (discoverer.services != null)
        {
 -        if (getInstance().services != null)
 +        System.out.println("Changesupport: There are now "
 +                + discoverer.services.size() + " services");
 +        int i = 1;
-         for (ServiceWithParameters instance : discoverer.services)
++        for (ServiceWithParameters s_instance : discoverer.services)
          {
 -          System.out.println("Changesupport: There are now "
 -                  + getInstance().services.size() + " services");
 -          int i = 1;
 -          for (Jws2Instance instance : getInstance().services)
 -          {
 -            System.out.println("Service " + i++ + " " + instance.getClass()
 -                    + "@" + instance.getHost() + ": "
 -                    + instance.getActionText());
 -          }
 -
 +          System.out.println(
-                   "Service " + i++ + " " + instance.getClass()
-                           + "@" + instance.getHostURL() + ": "
-                           + instance.getActionText());
++                  "Service " + i++ + " " + s_instance.getClass()
++                          + "@" + s_instance.getHostURL() + ": "
++                          + s_instance.getActionText());
          }
 +
        }
      });
 -    while (runner.isAlive())
 +    try
 +    {
 +      discoverer.startDiscoverer().get();
 +    } catch (InterruptedException | ExecutionException e)
      {
 -      try
 -      {
 -        Thread.sleep(50);
 -      } catch (InterruptedException e)
 -      {
 -      }
      }
      try
      {
      }
    }
  
-   /**
-    * Returns the singleton instance of this class.
-    * 
-    * @return
-    */
-   public static Jws2Discoverer getDiscoverer()
-   {
-     if (discoverer == null)
-     {
-       discoverer = new Jws2Discoverer();
-     }
-     return discoverer;
-   }
 +
 +  @Override
    public boolean hasServices()
    {
      return !running && services != null && services.size() > 0;
@@@ -115,12 -104,10 +115,12 @@@ public class MsaWSClient extends Jws2Cl
        return;
      }
  
 -    if (!(sh.service instanceof MsaWS))
 +    if (!(sh instanceof JalviewServiceEndpointProviderI
 +            && ((JalviewServiceEndpointProviderI) sh)
 +                    .getEndpoint() instanceof MultipleSequenceAlignmentI))
      {
        // redundant at mo - but may change
-       JvOptionPane.showMessageDialog(Desktop.desktop,
+       JvOptionPane.showMessageDialog(Desktop.getDesktopPane(),
                MessageManager.formatMessage(
                        "label.service_called_is_not_msa_service",
                        new String[]
  
        return;
      }
 -    server = (MsaWS) sh.service;
 +    serviceHandle = sh;
 +    server = (MultipleSequenceAlignmentI) ((JalviewServiceEndpointProviderI) sh)
 +            .getEndpoint();
      if ((wsInfo = setWebService(sh, false)) == null)
      {
-       JvOptionPane.showMessageDialog(Desktop.desktop, MessageManager
+       JvOptionPane.showMessageDialog(Desktop.getDesktopPane(), MessageManager
                .formatMessage("label.msa_service_is_unknown", new String[]
 -              { sh.serviceType }),
 +              { sh.getName() }),
                MessageManager.getString("label.internal_jalview_error"),
                JvOptionPane.WARNING_MESSAGE);
  
index 53b790d,0000000..4c807e1
mode 100644,000000..100644
--- /dev/null
@@@ -1,843 -1,0 +1,843 @@@
 +/*
 + * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
 + * Copyright (C) $$Year-Rel$$ The Jalview Authors
 + * 
 + * This file is part of Jalview.
 + * 
 + * Jalview is free software: you can redistribute it and/or
 + * modify it under the terms of the GNU General Public License 
 + * as published by the Free Software Foundation, either version 3
 + * of the License, or (at your option) any later version.
 + *  
 + * Jalview is distributed in the hope that it will be useful, but 
 + * WITHOUT ANY WARRANTY; without even the implied warranty 
 + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
 + * PURPOSE.  See the GNU General Public License for more details.
 + * 
 + * You should have received a copy of the GNU General Public License
 + * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
 + * The Jalview Authors are detailed in the 'AUTHORS' file.
 + */
 +package jalview.ws.jws2;
 +
 +import jalview.analysis.AlignSeq;
 +import jalview.analysis.AlignmentAnnotationUtils;
 +import jalview.analysis.SeqsetUtils;
 +import jalview.api.AlignViewportI;
 +import jalview.api.AlignmentViewPanel;
 +import jalview.api.FeatureColourI;
 +import jalview.api.PollableAlignCalcWorkerI;
 +import jalview.bin.Cache;
 +import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.AnnotatedCollectionI;
 +import jalview.datamodel.Annotation;
 +import jalview.datamodel.ContiguousI;
 +import jalview.datamodel.Mapping;
 +import jalview.datamodel.SequenceI;
 +import jalview.datamodel.features.FeatureMatcherSetI;
 +import jalview.gui.AlignFrame;
 +import jalview.gui.Desktop;
 +import jalview.gui.IProgressIndicator;
 +import jalview.gui.IProgressIndicatorHandler;
 +import jalview.gui.JvOptionPane;
 +import jalview.gui.WebserviceInfo;
 +import jalview.schemes.FeatureSettingsAdapter;
 +import jalview.schemes.ResidueProperties;
 +import jalview.util.MapList;
 +import jalview.util.MessageManager;
 +import jalview.workers.AlignCalcWorker;
 +import jalview.ws.JobStateSummary;
 +import jalview.ws.api.CancellableI;
 +import jalview.ws.api.JalviewServiceEndpointProviderI;
 +import jalview.ws.api.JobId;
 +import jalview.ws.api.SequenceAnnotationServiceI;
 +import jalview.ws.api.ServiceWithParameters;
 +import jalview.ws.api.WSAnnotationCalcManagerI;
 +import jalview.ws.gui.AnnotationWsJob;
 +import jalview.ws.jws2.dm.AAConSettings;
 +import jalview.ws.params.ArgumentI;
 +import jalview.ws.params.WsParamSetI;
 +
 +import java.util.ArrayList;
 +import java.util.HashMap;
 +import java.util.List;
 +import java.util.Map;
 +
 +public class SeqAnnotationServiceCalcWorker extends AlignCalcWorker
 +        implements WSAnnotationCalcManagerI, PollableAlignCalcWorkerI
 +{
 +
 +  protected ServiceWithParameters service;
 +
 +  protected WsParamSetI preset;
 +
 +  protected List<ArgumentI> arguments;
 +
 +  protected IProgressIndicator guiProgress;
 +
 +  protected boolean submitGaps = true;
 +
 +  /**
 +   * by default, we filter out non-standard residues before submission
 +   */
 +  protected boolean filterNonStandardResidues = true;
 +
 +  /**
 +   * Recover any existing parameters for this service
 +   */
 +  protected void initViewportParams()
 +  {
 +    if (getCalcId() != null)
 +    {
 +      ((jalview.gui.AlignViewport) alignViewport).setCalcIdSettingsFor(
 +              getCalcId(),
 +              new AAConSettings(true, service, this.preset, arguments),
 +              true);
 +    }
 +  }
 +
 +  /**
 +   * 
 +   * @return null or a string used to recover all annotation generated by this
 +   *         worker
 +   */
 +  public String getCalcId()
 +  {
 +    return service.getAlignAnalysisUI() == null ? null
 +            : service.getAlignAnalysisUI().getCalcId();
 +  }
 +
 +  public WsParamSetI getPreset()
 +  {
 +    return preset;
 +  }
 +
 +  public List<ArgumentI> getArguments()
 +  {
 +    return arguments;
 +  }
 +
 +  /**
 +   * reconfigure and restart the AAConClient. This method will spawn a new
 +   * thread that will wait until any current jobs are finished, modify the
 +   * parameters and restart the conservation calculation with the new values.
 +   * 
 +   * @param newpreset
 +   * @param newarguments
 +   */
 +  public void updateParameters(final WsParamSetI newpreset,
 +          final List<ArgumentI> newarguments)
 +  {
 +    preset = newpreset;
 +    arguments = newarguments;
 +    calcMan.startWorker(this);
 +    initViewportParams();
 +  }
 +  protected boolean alignedSeqs = true;
 +
 +  protected boolean nucleotidesAllowed = false;
 +
 +  protected boolean proteinAllowed = false;
 +
 +  /**
 +   * record sequences for mapping result back to afterwards
 +   */
 +  protected boolean bySequence = false;
 +
 +  protected Map<String, SequenceI> seqNames;
 +
 +  // TODO: convert to bitset
 +  protected boolean[] gapMap;
 +
 +  int realw;
 +
 +  protected int start;
 +
 +  int end;
 +
 +  private AlignFrame alignFrame;
 +
 +  public boolean[] getGapMap()
 +  {
 +    return gapMap;
 +  }
 +
 +  public SeqAnnotationServiceCalcWorker(ServiceWithParameters service,
 +          AlignFrame alignFrame,
 +          WsParamSetI preset, List<ArgumentI> paramset)
 +  {
 +    super(alignFrame.getCurrentView(), alignFrame.alignPanel);
 +    // TODO: both these fields needed ?
 +    this.alignFrame = alignFrame;
 +    this.guiProgress = alignFrame;
 +    this.preset = preset;
 +    this.arguments = paramset;
 +    this.service = service;
 +    try
 +    {
 +      annotService = (jalview.ws.api.SequenceAnnotationServiceI) ((JalviewServiceEndpointProviderI) service)
 +              .getEndpoint();
 +    } catch (ClassCastException cce)
 +    {
 +      annotService = null;
-       JvOptionPane.showMessageDialog(Desktop.desktop,
++      JvOptionPane.showMessageDialog(Desktop.getInstance(),
 +              MessageManager.formatMessage(
 +                      "label.service_called_is_not_an_annotation_service",
 +                      new String[]
 +                      { service.getName() }),
 +              MessageManager.getString("label.internal_jalview_error"),
 +              JvOptionPane.WARNING_MESSAGE);
 +
 +    }
 +    cancellable = CancellableI.class.isInstance(annotService);
 +    // configure submission flags
 +    proteinAllowed = service.isProteinService();
 +    nucleotidesAllowed = service.isNucleotideService();
 +    alignedSeqs = service.isNeedsAlignedSequences();
 +    bySequence = !service.isAlignmentAnalysis();
 +    filterNonStandardResidues = service.isFilterSymbols();
 +    min_valid_seqs = service.getMinimumInputSequences();
 +    submitGaps = service.isAlignmentAnalysis();
 +
 +    if (service.isInteractiveUpdate())
 +    {
 +      initViewportParams();
 +    }
 +  }
 +
 +  /**
 +   * 
 +   * @return true if the submission thread should attempt to submit data
 +   */
 +  public boolean hasService()
 +  {
 +    return annotService != null;
 +  }
 +
 +  protected SequenceAnnotationServiceI annotService;
 +  protected final boolean cancellable;
 +
 +  volatile JobId rslt = null;
 +
 +  AnnotationWsJob running = null;
 +
 +  private int min_valid_seqs;
 +
 +
 +  private long progressId = -1;
 +  JobStateSummary job = null;
 +  WebserviceInfo info = null;
 +  List<SequenceI> seqs = null;
 +  
 +  @Override public void startUp() throws Throwable
 +  {
 +    if (alignViewport.isClosed())
 +    {
 +      abortAndDestroy();
 +      return;
 +    }
 +    if (!hasService())
 +    {
 +      return;
 +    }
 +
 +    StringBuffer msg = new StringBuffer();
 +    job = new JobStateSummary();
 +    info = new WebserviceInfo("foo", "bar", false);
 +
 +    seqs = getInputSequences(
 +            alignViewport.getAlignment(),
 +            bySequence ? alignViewport.getSelectionGroup() : null);
 +
 +    if (seqs == null || !checkValidInputSeqs(seqs))
 +    {
 +      jalview.bin.Cache.log.debug(
 +              "Sequences for analysis service were null or not valid");
 +      return;
 +    }
 +
 +    if (guiProgress != null)
 +    {
 +      guiProgress.setProgressBar(service.getActionText(),
 +              progressId = System.currentTimeMillis());
 +    }
 +    jalview.bin.Cache.log.debug("submitted " + seqs.size()
 +            + " sequences to " + service.getActionText());
 +
 +    rslt = annotService.submitToService(seqs, getPreset(),
 +            getArguments());
 +    if (rslt == null)
 +    {
 +      return;
 +    }
 +    // TODO: handle job submission error reporting here.
 +    Cache.log.debug("Service " + service.getUri() + "\nSubmitted job ID: "
 +            + rslt);
 +    ;
 +    // ///
 +    // otherwise, construct WsJob and any UI handlers
 +    running = new AnnotationWsJob();
 +    running.setJobHandle(rslt);
 +    running.setSeqNames(seqNames);
 +    running.setStartPos(start);
 +    running.setSeqs(seqs);
 +    job.updateJobPanelState(info, "", running);
 +    if (guiProgress != null)
 +    {
 +      guiProgress.registerHandler(progressId,
 +              new IProgressIndicatorHandler()
 +              {
 +
 +                @Override
 +                public boolean cancelActivity(long id)
 +                {
 +                  calcMan.cancelWorker(SeqAnnotationServiceCalcWorker.this);
 +                  return true;
 +                }
 +
 +                @Override
 +                public boolean canCancel()
 +                {
 +                  return cancellable;
 +                }
 +              });
 +    }
 +  }
 +  
 +  @Override public boolean poll() throws Throwable
 +  {
 +    boolean finished = false;
 +    
 +    Cache.log.debug("Updating status for annotation service.");
 +    annotService.updateStatus(running);
 +    job.updateJobPanelState(info, "", running);
 +    if (running.isSubjobComplete())
 +    {
 +      Cache.log.debug(
 +              "Finished polling analysis service job: status reported is "
 +                      + running.getState());
 +      finished = true;
 +    }
 +    else
 +    {
 +      Cache.log.debug("Status now " + running.getState());
 +    }
 +
 +    // pull any stats - some services need to flush log output before
 +    // results are available
 +    Cache.log.debug("Updating progress log for annotation service.");
 +
 +    try
 +    {
 +      annotService.updateJobProgress(running);
 +    } catch (Throwable thr)
 +    {
 +      Cache.log.debug("Ignoring exception during progress update.",
 +              thr);
 +    }
 +    Cache.log.trace("Result of poll: " + running.getStatus());
 +    
 +    
 +    if (finished)
 +    {
 +      Cache.log.debug("Job poll loop exited. Job is " + running.getState());
 +      if (running.isFinished())
 +      {
 +        // expect there to be results to collect
 +        // configure job with the associated view's feature renderer, if one
 +        // exists.
 +        // TODO: here one would also grab the 'master feature renderer' in order
 +        // to enable/disable
 +        // features automatically according to user preferences
 +        running.setFeatureRenderer(
 +                ((jalview.gui.AlignmentPanel) ap).cloneFeatureRenderer());
 +        Cache.log.debug("retrieving job results.");
 +        final Map<String, FeatureColourI> featureColours = new HashMap<>();
 +        final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
 +        List<AlignmentAnnotation> returnedAnnot = annotService
 +                .getAnnotationResult(running.getJobHandle(), seqs,
 +                        featureColours, featureFilters);
 +
 +        Cache.log.debug("Obtained " + (returnedAnnot == null ? "no rows"
 +                : ("" + returnedAnnot.size())));
 +        Cache.log.debug("There were " + featureColours.size()
 +                + " feature colours and " + featureFilters.size()
 +                + " filters defined.");
 +
 +        // TODO
 +        // copy over each annotation row reurned and also defined on each
 +        // sequence, excluding regions not annotated due to gapMap/column
 +        // visibility
 +
 +        // update calcId if it is not already set on returned annotation
 +        if (returnedAnnot != null)
 +        {
 +          for (AlignmentAnnotation aa : returnedAnnot)
 +          {
 +            // assume that any CalcIds already set
 +            if (getCalcId() != null && aa.getCalcId() == null
 +                    || "".equals(aa.getCalcId()))
 +            {
 +              aa.setCalcId(getCalcId());
 +            }
 +            // autocalculated annotation are created by interactive alignment
 +            // analysis services
 +            aa.autoCalculated = service.isAlignmentAnalysis()
 +                    && service.isInteractiveUpdate();
 +          }
 +        }
 +
 +        running.setAnnotation(returnedAnnot);
 +
 +        if (running.hasResults())
 +        {
 +          jalview.bin.Cache.log.debug("Updating result annotation from Job "
 +                  + rslt + " at " + service.getUri());
 +          updateResultAnnotation(true);
 +          if (running.isTransferSequenceFeatures())
 +          {
 +            // TODO
 +            // look at each sequence and lift over any features, excluding
 +            // regions
 +            // not annotated due to gapMap/column visibility
 +
 +            jalview.bin.Cache.log.debug(
 +                    "Updating feature display settings and transferring features from Job "
 +                            + rslt + " at " + service.getUri());
 +            // TODO: consider merge rather than apply here
 +            alignViewport.applyFeaturesStyle(new FeatureSettingsAdapter()
 +            {
 +              @Override
 +              public FeatureColourI getFeatureColour(String type)
 +              {
 +                return featureColours.get(type);
 +              }
 +
 +              @Override
 +              public FeatureMatcherSetI getFeatureFilters(String type)
 +              {
 +                return featureFilters.get(type);
 +              }
 +
 +              @Override
 +              public boolean isFeatureDisplayed(String type)
 +              {
 +                return featureColours.containsKey(type);
 +              }
 +
 +            });
 +            // TODO: JAL-1150 - create sequence feature settings API for
 +            // defining
 +            // styles and enabling/disabling feature overlay on alignment panel
 +
 +            if (alignFrame.alignPanel == ap)
 +            {
 +              alignViewport.setShowSequenceFeatures(true);
 +              alignFrame.setMenusForViewport();
 +            }
 +          }
 +          ap.adjustAnnotationHeight();
 +        }
 +      }
 +      Cache.log.debug("Annotation Service Worker thread finished.");
 +
 +    }
 +    
 +    return finished;
 +  }
 +  
 +  @Override public void cancel()
 +  {
 +    cancelCurrentJob();
 +  }
 +  
 +  @Override public void done()
 +  {
 +    if (ap != null)
 +    {
 +      if (guiProgress != null && progressId != -1)
 +      {
 +        guiProgress.removeProgressBar(progressId);
 +      }
 +      // TODO: may not need to paintAlignment again !
 +      ap.paintAlignment(false, false);
 +    }
 +  }
 +
 +  /**
 +   * validate input for dynamic/non-dynamic update context TODO: move to
 +   * analysis interface ?
 +   * @param seqs
 +   * 
 +   * @return true if input is valid
 +   */
 +  boolean checkValidInputSeqs(List<SequenceI> seqs)
 +  {
 +    int nvalid = 0;
 +    for (SequenceI sq : seqs)
 +    {
 +      if (sq.getStart() <= sq.getEnd()
 +              && (sq.isProtein() ? proteinAllowed : nucleotidesAllowed))
 +      {
 +        if (submitGaps
 +                || sq.getLength() == (sq.getEnd() - sq.getStart() + 1))
 +        {
 +          nvalid++;
 +        }
 +      }
 +    }
 +    return nvalid >= min_valid_seqs;
 +  }
 +
 +  public void cancelCurrentJob()
 +  {
 +    try
 +    {
 +      String id = running.getJobId();
 +      if (cancellable && ((CancellableI) annotService).cancel(running))
 +      {
 +        System.err.println("Cancelled job " + id);
 +      }
 +      else
 +      {
 +        System.err.println("Job " + id + " couldn't be cancelled.");
 +      }
 +    } catch (Exception q)
 +    {
 +      q.printStackTrace();
 +    }
 +  }
 +
 +  /**
 +   * Interactive updating. Analysis calculations that work on the currently
 +   * displayed alignment data should cancel existing jobs when the input data
 +   * has changed.
 +   * 
 +   * @return true if a running job should be cancelled because new input data is
 +   *         available for analysis
 +   */
 +  boolean isInteractiveUpdate()
 +  {
 +    return service.isInteractiveUpdate();
 +  }
 +
 +  /**
 +   * decide what sequences will be analysed TODO: refactor to generate
 +   * List<SequenceI> for submission to service interface
 +   * 
 +   * @param alignment
 +   * @param inputSeqs
 +   * @return
 +   */
 +  public List<SequenceI> getInputSequences(AlignmentI alignment,
 +          AnnotatedCollectionI inputSeqs)
 +  {
 +    if (alignment == null || alignment.getWidth() <= 0
 +            || alignment.getSequences() == null || alignment.isNucleotide()
 +                    ? !nucleotidesAllowed
 +                    : !proteinAllowed)
 +    {
 +      return null;
 +    }
 +    if (inputSeqs == null || inputSeqs.getWidth() <= 0
 +            || inputSeqs.getSequences() == null
 +            || inputSeqs.getSequences().size() < 1)
 +    {
 +      inputSeqs = alignment;
 +    }
 +
 +    List<SequenceI> seqs = new ArrayList<>();
 +
 +    int minlen = 10;
 +    int ln = -1;
 +    if (bySequence)
 +    {
 +      seqNames = new HashMap<>();
 +    }
 +    gapMap = new boolean[0];
 +    start = inputSeqs.getStartRes();
 +    end = inputSeqs.getEndRes();
 +    // TODO: URGENT! unify with JPred / MSA code to handle hidden regions
 +    // correctly
 +    // TODO: push attributes into WsJob instance (so they can be safely
 +    // persisted/restored
 +    for (SequenceI sq : (inputSeqs.getSequences()))
 +    {
 +      if (bySequence
 +              ? sq.findPosition(end + 1)
 +                      - sq.findPosition(start + 1) > minlen - 1
 +              : sq.getEnd() - sq.getStart() > minlen - 1)
 +      {
 +        String newname = SeqsetUtils.unique_name(seqs.size() + 1);
 +        // make new input sequence with or without gaps
 +        if (seqNames != null)
 +        {
 +          seqNames.put(newname, sq);
 +        }
 +        SequenceI seq;
 +        if (submitGaps)
 +        {
 +          seqs.add(seq = new jalview.datamodel.Sequence(newname,
 +                  sq.getSequenceAsString()));
 +          if (gapMap == null || gapMap.length < seq.getLength())
 +          {
 +            boolean[] tg = gapMap;
 +            gapMap = new boolean[seq.getLength()];
 +            System.arraycopy(tg, 0, gapMap, 0, tg.length);
 +            for (int p = tg.length; p < gapMap.length; p++)
 +            {
 +              gapMap[p] = false; // init as a gap
 +            }
 +          }
 +          for (int apos : sq.gapMap())
 +          {
 +            char sqc = sq.getCharAt(apos);
 +            if (!filterNonStandardResidues
 +                    || (sq.isProtein() ? ResidueProperties.aaIndex[sqc] < 20
 +                            : ResidueProperties.nucleotideIndex[sqc] < 5))
 +            {
 +              gapMap[apos] = true; // aligned and real amino acid residue
 +            }
 +            ;
 +          }
 +        }
 +        else
 +        {
 +          // TODO: add ability to exclude hidden regions
 +          seqs.add(seq = new jalview.datamodel.Sequence(newname,
 +                  AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
 +                          sq.getSequenceAsString(start, end + 1))));
 +          // for annotation need to also record map to sequence start/end
 +          // position in range
 +          // then transfer back to original sequence on return.
 +        }
 +        if (seq.getLength() > ln)
 +        {
 +          ln = seq.getLength();
 +        }
 +      }
 +    }
 +    if (alignedSeqs && submitGaps)
 +    {
 +      realw = 0;
 +      for (int i = 0; i < gapMap.length; i++)
 +      {
 +        if (gapMap[i])
 +        {
 +          realw++;
 +        }
 +      }
 +      // try real hard to return something submittable
 +      // TODO: some of AAcon measures need a minimum of two or three amino
 +      // acids at each position, and AAcon doesn't gracefully degrade.
 +      for (int p = 0; p < seqs.size(); p++)
 +      {
 +        SequenceI sq = seqs.get(p);
 +        // strip gapped columns
 +        char[] padded = new char[realw],
 +                orig = sq.getSequence();
 +        for (int i = 0, pp = 0; i < realw; pp++)
 +        {
 +          if (gapMap[pp])
 +          {
 +            if (orig.length > pp)
 +            {
 +              padded[i++] = orig[pp];
 +            }
 +            else
 +            {
 +              padded[i++] = '-';
 +            }
 +          }
 +        }
 +        seqs.set(p, new jalview.datamodel.Sequence(sq.getName(),
 +                new String(padded)));
 +      }
 +    }
 +    return seqs;
 +  }
 +
 +  @Override
 +  public void updateAnnotation()
 +  {
 +    updateResultAnnotation(false);
 +  }
 +
 +  public void updateResultAnnotation(boolean immediate)
 +  {
 +    if ((immediate || !calcMan.isWorking(this)) && running != null
 +            && running.hasResults())
 +    {
 +      List<AlignmentAnnotation> ourAnnot = running.getAnnotation(),
 +              newAnnots = new ArrayList<>();
 +      //
 +      // update graphGroup for all annotation
 +      //
 +      /**
 +       * find a graphGroup greater than any existing ones this could be a method
 +       * provided by alignment Alignment.getNewGraphGroup() - returns next
 +       * unused graph group
 +       */
 +      int graphGroup = 1;
 +      if (alignViewport.getAlignment().getAlignmentAnnotation() != null)
 +      {
 +        for (AlignmentAnnotation ala : alignViewport.getAlignment()
 +                .getAlignmentAnnotation())
 +        {
 +          if (ala.graphGroup > graphGroup)
 +          {
 +            graphGroup = ala.graphGroup;
 +          }
 +        }
 +      }
 +      /**
 +       * update graphGroup in the annotation rows returned from service
 +       */
 +      // TODO: look at sequence annotation rows and update graph groups in the
 +      // case of reference annotation.
 +      for (AlignmentAnnotation ala : ourAnnot)
 +      {
 +        if (ala.graphGroup > 0)
 +        {
 +          ala.graphGroup += graphGroup;
 +        }
 +        SequenceI aseq = null;
 +
 +        /**
 +         * transfer sequence refs and adjust gapmap
 +         */
 +        if (ala.sequenceRef != null)
 +        {
 +          SequenceI seq = running.getSeqNames()
 +                  .get(ala.sequenceRef.getName());
 +          aseq = seq;
 +          while (seq.getDatasetSequence() != null)
 +          {
 +            seq = seq.getDatasetSequence();
 +          }
 +        }
 +        Annotation[] resAnnot = ala.annotations,
 +                gappedAnnot = new Annotation[Math.max(
 +                        alignViewport.getAlignment().getWidth(),
 +                        gapMap.length)];
 +        for (int p = 0, ap = start; ap < gappedAnnot.length; ap++)
 +        {
 +          if (gapMap != null && gapMap.length > ap && !gapMap[ap])
 +          {
 +            gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN);
 +          }
 +          else if (p < resAnnot.length)
 +          {
 +            gappedAnnot[ap] = resAnnot[p++];
 +          }
 +        }
 +        ala.sequenceRef = aseq;
 +        ala.annotations = gappedAnnot;
 +        AlignmentAnnotation newAnnot = getAlignViewport().getAlignment()
 +                .updateFromOrCopyAnnotation(ala);
 +        if (aseq != null)
 +        {
 +
 +          aseq.addAlignmentAnnotation(newAnnot);
 +          newAnnot.adjustForAlignment();
 +
 +          AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(
 +                  newAnnot, newAnnot.label, newAnnot.getCalcId());
 +        }
 +        newAnnots.add(newAnnot);
 +
 +      }
 +      for (SequenceI sq : running.getSeqs())
 +      {
 +        if (!sq.getFeatures().hasFeatures()
 +                && (sq.getDBRefs() == null || sq.getDBRefs().size() == 0))
 +        {
 +          continue;
 +        }
 +        running.setTransferSequenceFeatures(true);
 +        SequenceI seq = running.getSeqNames().get(sq.getName());
 +        SequenceI dseq;
 +        ContiguousI seqRange = seq.findPositions(start, end);
 +
 +        while ((dseq = seq).getDatasetSequence() != null)
 +        {
 +          seq = seq.getDatasetSequence();
 +        }
 +        List<ContiguousI> sourceRange = new ArrayList();
 +        if (gapMap != null && gapMap.length >= end)
 +        {
 +          int lastcol = start, col = start;
 +          do
 +          {
 +            if (col == end || !gapMap[col])
 +            {
 +              if (lastcol <= (col - 1))
 +              {
 +                seqRange = seq.findPositions(lastcol, col);
 +                sourceRange.add(seqRange);
 +              }
 +              lastcol = col + 1;
 +            }
 +          } while (++col <= end);
 +        }
 +        else
 +        {
 +          sourceRange.add(seq.findPositions(start, end));
 +        }
 +        int i = 0;
 +        int source_startend[] = new int[sourceRange.size() * 2];
 +
 +        for (ContiguousI range : sourceRange)
 +        {
 +          source_startend[i++] = range.getBegin();
 +          source_startend[i++] = range.getEnd();
 +        }
 +        Mapping mp = new Mapping(
 +                new MapList(source_startend, new int[]
 +                { seq.getStart(), seq.getEnd() }, 1, 1));
 +        dseq.transferAnnotation(sq, mp);
 +
 +      }
 +      updateOurAnnots(newAnnots);
 +    }
 +  }
 +
 +  protected void updateOurAnnots(List<AlignmentAnnotation> ourAnnot)
 +  {
 +    List<AlignmentAnnotation> our = ourAnnots;
 +    ourAnnots = ourAnnot;
 +    AlignmentI alignment = alignViewport.getAlignment();
 +    if (our != null)
 +    {
 +      if (our.size() > 0)
 +      {
 +        for (AlignmentAnnotation an : our)
 +        {
 +          if (!ourAnnots.contains(an))
 +          {
 +            // remove the old annotation
 +            alignment.deleteAnnotation(an);
 +          }
 +        }
 +      }
 +      our.clear();
 +    }
 +
 +    // validate rows and update Alignmment state
 +    for (AlignmentAnnotation an : ourAnnots)
 +    {
 +      alignViewport.getAlignment().validateAnnotation(an);
 +    }
 +    // TODO: may need a menu refresh after this
 +    // af.setMenusForViewport();
 +    ap.adjustAnnotationHeight();
 +
 +  }
 +
 +  public SequenceAnnotationServiceI getService()
 +  {
 +    return annotService;
 +  }
 +
 +}
@@@ -264,7 -242,7 +264,7 @@@ public class SequenceAnnotationWSClien
            @Override
            public void actionPerformed(ActionEvent arg0)
            {
-             Desktop.instance.showUrl(service.getDocumentationUrl());
 -            Desktop.getInstance().showUrl(service.docUrl);
++            Desktop.getInstance().showUrl(service.getDocumentationUrl());
            }
          });
          annotservice.setToolTipText(
   */
  package jalview.ws.jws2.jabaws2;
  
+ import jalview.bin.ApplicationSingletonProvider;
+ import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
 -import jalview.ws.jws2.AAConClient;
 -import jalview.ws.jws2.RNAalifoldClient;
++
  import jalview.ws.uimodel.AlignAnalysisUIText;
  
  import java.util.HashMap;
@@@ -82,10 -98,10 +97,10 @@@ public class Jws2InstanceFactory implem
            String serviceType, String name, String description,
            JABAService service)
    {
-     init();
+     getInstance().init();
      Jws2Instance svc = new Jws2Instance(jwsservers, serviceType,
              category_rewrite(name), description, service);
-     svc.setAlignAnalysisUI(aaConGUI.get(serviceType.toString()));
 -    svc.aaui = getInstance().aaConGUI.get(serviceType.toString());
++    svc.setAlignAnalysisUI(getInstance().aaConGUI.get(serviceType.toString()));
      return svc;
    }
  
@@@ -358,15 -420,16 +379,16 @@@ public static final String RSBS_SERVICE
      return true;
    }
  
-   protected static Vector<String> services = null;
-   public static final String RSBS_SERVICES = "RSBS_SERVICES";
    public static RestClient[] getRestClients()
    {
+     return getInstance().getClients();
+   }
+     
+   private RestClient[] getClients()
+   {
      if (services == null)
      {
 -      services = new Vector<String>();
 +      services = new Vector<>();
        try
        {
          for (RestServiceDescription descr : RestServiceDescription
@@@ -48,6 -41,12 +41,13 @@@ import org.testng.annotations.BeforeCla
  import org.testng.annotations.BeforeMethod;
  import org.testng.annotations.Test;
  
+ import jalview.analysis.AlignmentGenerator;
+ import jalview.commands.EditCommand;
+ import jalview.commands.EditCommand.Action;
+ import jalview.datamodel.PDBEntry.Type;
+ import jalview.gui.JvOptionPane;
+ import jalview.util.MapList;
++
  import junit.extensions.PA;
  
  public class SequenceTest
Simple merge
@@@ -55,8 -54,8 +55,9 @@@ import jalview.schemes.ColourSchemeI
  import jalview.schemes.PIDColourScheme;
  import jalview.structure.StructureSelectionManager;
  import jalview.util.MapList;
 +import jalview.viewmodel.AlignmentViewport;
  import jalview.viewmodel.ViewportRanges;
+ import jalview.workers.AlignCalcManager;
  
  public class AlignViewportTest
  {
Simple merge
index e3b7067,0000000..04cc3be
mode 100644,000000..100644
--- /dev/null
@@@ -1,134 -1,0 +1,134 @@@
 +package jalview.hmmer;
 +
 +import static org.testng.Assert.assertEquals;
 +import static org.testng.Assert.assertFalse;
 +import static org.testng.Assert.assertNotNull;
 +import static org.testng.Assert.assertTrue;
 +import static org.testng.Assert.fail;
 +
 +import jalview.bin.Jalview;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.HiddenMarkovModel;
 +import jalview.datamodel.SequenceI;
 +import jalview.gui.AlignFrame;
 +import jalview.gui.Desktop;
 +import jalview.io.HMMFile;
 +import jalview.util.MessageManager;
 +import jalview.ws.params.ArgumentI;
 +import jalview.ws.params.simple.Option;
 +
 +import java.io.IOException;
 +import java.net.MalformedURLException;
 +import java.util.ArrayList;
 +import java.util.List;
 +
 +import org.testng.annotations.AfterClass;
 +import org.testng.annotations.BeforeClass;
 +import org.testng.annotations.Test;
 +
 +public class HMMERTest {
 +
 +  AlignFrame frame;
 +
 +  @BeforeClass(alwaysRun = true)
 +  public void setUpBeforeClass() throws Exception
 +  {
 +    /*
 +     * NB: check HMMER_PATH in testProps.jvprops is valid for
 +     * the machine on which this runs
 +     */
 +    Jalview.main(
 +            new String[]
 +    { "-noquestionnaire", "-nonews", "-props",
 +                "test/jalview/hmmer/testProps.jvprops", "-open",
 +                "examples/uniref50.fa" });
 +    frame = Desktop.getAlignFrames()[0];
 +  }
 +
 +  @AfterClass(alwaysRun = true)
 +  public static void tearDownAfterClass() throws Exception
 +  {
-     Desktop.instance.closeAll_actionPerformed(null);
++    Desktop.getInstance().closeAll_actionPerformed(null);
 +  }
 +
 +  /**
 +   * Test with a dependency on locally installed hmmbuild binaries
 +   * 
 +   * @throws MalformedURLException
 +   * @throws IOException
 +   */
 +  @Test(groups = "External")
 +  public void testHMMBuildThenHMMAlign()
 +          throws MalformedURLException, IOException
 +  {
 +    /*
 +     * run hmmbuild
 +     */
 +    testHMMBuild();
 +    List<SequenceI> hmms = frame.getViewport().getAlignment()
 +            .getHmmSequences();
 +    assertFalse(hmms.isEmpty());
 +
 +    /*
 +     * now run hmmalign - by default with respect to the added HMM profile
 +     */
 +    testHMMAlign();
 +  }
 +
 +  public void testHMMBuild()
 +  {
 +    /*
 +     * set up argument to run hmmbuild for the alignment
 +     */
 +    ArrayList<ArgumentI> params = new ArrayList<>();
 +    String argName = MessageManager.getString("label.hmmbuild_for");
 +    String argValue = MessageManager.getString("label.alignment");
 +    params.add(
 +            new Option(argName, null, false, null, argValue, null, null));
 +
 +    HMMBuild builder = new HMMBuild(frame, params);
 +    builder.run();
 +
 +    SequenceI seq = frame.getViewport().getAlignment().getSequenceAt(0);
 +    HiddenMarkovModel hmm = seq.getHMM();
 +    assertNotNull(hmm);
 +
 +    assertEquals(hmm.getLength(), 148);
 +    assertEquals(hmm.getAlphabetType(), "amino");
 +    assertEquals(hmm.getName(), "Alignment_HMM");
 +    assertEquals(hmm.getProperty(HMMFile.EFF_NUMBER_OF_SEQUENCES),
 +            "0.648193");
 +  }
 +
 +  public void testHMMAlign()
 +  {
 +    HmmerCommand thread = new HMMAlign(frame,
 +            new ArrayList<ArgumentI>());
 +    thread.run();
 +
 +    AlignFrame[] alignFrames = Desktop.getAlignFrames();
 +    if (alignFrames == null)
 +    {
 +      fail("No align frame loaded");
 +    }
 +
 +    /*
 +     * now have the original align frame, and another for realigned sequences
 +     */
 +    assertEquals(alignFrames.length, 2);
 +    AlignmentI original = alignFrames[0].getViewport().getAlignment();
 +    assertNotNull(original);
 +    AlignmentI realigned = alignFrames[1].getViewport().getAlignment();
 +    assertNotNull(realigned);
 +    assertFalse(original.getHmmSequences().isEmpty());
 +    assertFalse(realigned.getHmmSequences().isEmpty());
 +
 +    SequenceI ferCapan = original.findName("FER_CAPAN");
 +    assertTrue(ferCapan.getSequenceAsString().startsWith("MA------SVSAT"));
 +
 +    SequenceI ferCapanRealigned = realigned.findName("FER_CAPAN");
 +    assertTrue(ferCapanRealigned.getSequenceAsString()
 +            .startsWith("-------m-A----SVSAT"));
 +  }
 +}
 +
@@@ -55,7 -55,7 +55,7 @@@ public class FileFormatsTes
    @Test(groups = "Functional")
    public void testGetReadableFormats()
    {
-     String expected = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, HMMER3]";
 -    String expected = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, BSML]";
++    String expected = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, HMMER3, BSML]";
      FileFormats formats = FileFormats.getInstance();
      assertEquals(formats.getReadableFormats().toString(), expected);
    }
    @Test(groups = "Functional")
    public void testDeregisterFileFormat()
    {
 -    String writable = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP]";
 -    String readable = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, BSML]";
 +    String writable = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP, HMMER3]";
-     String readable = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, HMMER3]";
++    String readable = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, HMMER3, BSML]";
      FileFormats formats = FileFormats.getInstance();
+     System.out.println(formats.getReadableFormats().toString());
      assertEquals(formats.getWritableFormats(true).toString(), writable);
      assertEquals(formats.getReadableFormats().toString(), readable);
  
      formats.deregisterFileFormat(FileFormat.Fasta.getName());
 -    writable = "[PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP]";
 -    readable = "[PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, BSML]";
 +    writable = "[PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP, HMMER3]";
-     readable = "[PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, HMMER3]";
++    readable = "[PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, HMMER3, BSML]";
      assertEquals(formats.getWritableFormats(true).toString(), writable);
      assertEquals(formats.getReadableFormats().toString(), readable);
  
@@@ -89,8 -90,8 +90,8 @@@
       * re-register the format: it gets added to the end of the list
       */
      formats.registerFileFormat(FileFormat.Fasta);
 -    writable = "[PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP, Fasta]";
 -    readable = "[PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, BSML, Fasta]";
 +    writable = "[PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP, HMMER3, Fasta]";
-     readable = "[PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, HMMER3, Fasta]";
++    readable = "[PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, HMMER3, BSML, Fasta]";
      assertEquals(formats.getWritableFormats(true).toString(), writable);
      assertEquals(formats.getReadableFormats().toString(), readable);
    }
@@@ -1069,59 -1073,6 +1075,59 @@@ public class Jalview2xmlTests extends J
    }
  
    /**
 +   * Load an HMM profile to an alignment, and confirm it is correctly restored
 +   * when reloaded from project
 +   * 
 +   * @throws IOException
 +   */
 +  @Test(groups = { "Functional" })
 +  public void testStoreAndRecoverHmmProfile() throws IOException
 +  {
-     Desktop.instance.closeAll_actionPerformed(null);
++    Desktop.getInstance().closeAll_actionPerformed(null);
 +    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
 +            "examples/uniref50.fa", DataSourceType.FILE);
 +  
 +    AlignViewportI av = af.getViewport();
 +    AlignmentI al = av.getAlignment();
 +
 +    /*
 +     * mimic drag and drop of hmm file on to alignment
 +     */
 +    AlignFrame af2 = new FileLoader().LoadFileWaitTillLoaded(
 +            "examples/uniref50.hmm", DataSourceType.FILE);
 +    al.insertSequenceAt(0,
 +            af2.getViewport().getAlignment().getSequenceAt(0));
 +
 +    /*
 +     * check it loaded in
 +     */
 +    SequenceI hmmSeq = al.getSequenceAt(0);
 +    assertTrue(hmmSeq.hasHMMProfile());
 +    HiddenMarkovModel hmm = hmmSeq.getHMM();
 +    assertSame(hmm.getConsensusSequence(), hmmSeq);
 +
 +    /*
 +     * save project, close windows, reload project, verify
 +     */
 +    File tfile = File.createTempFile("testStoreAndRecoverHmmProfile",
 +            ".jvp");
 +    tfile.deleteOnExit();
 +    new Jalview2XML(false).saveState(tfile);
-     Desktop.instance.closeAll_actionPerformed(null);
++    Desktop.getInstance().closeAll_actionPerformed(null);
 +    af = new FileLoader().LoadFileWaitTillLoaded(tfile.getAbsolutePath(),
 +            DataSourceType.FILE);
 +    Assert.assertNotNull(af, "Failed to reload project");
 +
 +    hmmSeq = al.getSequenceAt(0);
 +    assertTrue(hmmSeq.hasHMMProfile());
 +    assertSame(hmm.getConsensusSequence(), hmmSeq);
 +    Mapping mapToHmmConsensus = (Mapping) PA.getValue(hmm,
 +            "mapToHmmConsensus");
 +    assertNotNull(mapToHmmConsensus);
 +    assertSame(mapToHmmConsensus.getTo(), hmmSeq.getDatasetSequence());
 +  }
 +
 +  /**
     * pre 2.11 - jalview 2.10 erroneously created new dataset entries for each
     * view (JAL-3171) this test ensures we can import and merge those views
     */
@@@ -11,6 -17,7 +17,7 @@@ import jalview.api.FeatureColourI
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceI;
  import jalview.gui.AlignFrame;
 -import jalview.gui.AlignViewport;
++import jalview.api.AlignViewportI;
  import jalview.io.DataSourceType;
  import jalview.io.FileLoader;
  import jalview.schemes.FeatureColour;