Merge commit 'alpha/update_2_12_for_2_11_2_series_merge^2' into HEAD
authorJim Procter <j.procter@dundee.ac.uk>
Mon, 21 Nov 2022 17:02:08 +0000 (17:02 +0000)
committerJim Procter <j.procter@dundee.ac.uk>
Mon, 21 Nov 2022 17:02:08 +0000 (17:02 +0000)
133 files changed:
1  2 
THIRDPARTYLIBS
build.gradle
doc/building.html
gradle.properties
help/help/help.jhm
help/help/helpTOC.xml
help/help/html/features/preferences.html
help/help/html/releases.html
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/Finder.java
src/jalview/api/AlignViewportI.java
src/jalview/api/FeatureSettingsModelI.java
src/jalview/appletgui/APopupMenu.java
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/AlignmentPanel.java
src/jalview/appletgui/js/MouseOverStructureListener.java
src/jalview/bin/Cache.java
src/jalview/bin/Jalview.java
src/jalview/bin/JalviewLite.java
src/jalview/datamodel/AlignmentAnnotation.java
src/jalview/datamodel/PDBEntry.java
src/jalview/datamodel/ResidueCount.java
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceI.java
src/jalview/ext/ensembl/EnsemblGene.java
src/jalview/ext/jmol/JalviewJmolBinding.java
src/jalview/fts/service/pdb/PDBFTSRestClient.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/AnnotationColourChooser.java
src/jalview/gui/AnnotationExporter.java
src/jalview/gui/AnnotationLabels.java
src/jalview/gui/AppJmol.java
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/CutAndPasteTransfer.java
src/jalview/gui/Desktop.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/Finder.java
src/jalview/gui/IdCanvas.java
src/jalview/gui/OptsAndParamsPage.java
src/jalview/gui/PCAPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/Preferences.java
src/jalview/gui/SeqCanvas.java
src/jalview/gui/SeqPanel.java
src/jalview/gui/SequenceFetcher.java
src/jalview/gui/SplashScreen.java
src/jalview/gui/SplitFrame.java
src/jalview/gui/StructureChooser.java
src/jalview/gui/StructureViewer.java
src/jalview/gui/StructureViewerBase.java
src/jalview/gui/TreeCanvas.java
src/jalview/gui/TreePanel.java
src/jalview/gui/UserDefinedColours.java
src/jalview/gui/VamsasApplication.java
src/jalview/gui/WebserviceInfo.java
src/jalview/gui/WsJobParameters.java
src/jalview/gui/WsPreferences.java
src/jalview/io/AppletFormatAdapter.java
src/jalview/io/BackupFiles.java
src/jalview/io/FileFormat.java
src/jalview/io/FileFormats.java
src/jalview/io/FileLoader.java
src/jalview/io/IdentifyFile.java
src/jalview/io/NewickFile.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/io/StockholmFile.java
src/jalview/io/VamsasAppDatastore.java
src/jalview/io/packed/ParsePackedSet.java
src/jalview/jbgui/GPreferences.java
src/jalview/jbgui/GStructureViewer.java
src/jalview/project/Jalview2XML.java
src/jalview/renderer/AnnotationRenderer.java
src/jalview/schemes/ColourSchemes.java
src/jalview/schemes/FeatureSettingsAdapter.java
src/jalview/schemes/ResidueProperties.java
src/jalview/structure/StructureImportSettings.java
src/jalview/structure/StructureSelectionManager.java
src/jalview/util/ColorUtils.java
src/jalview/util/Comparison.java
src/jalview/util/DBRefUtils.java
src/jalview/util/HttpUtils.java
src/jalview/util/MapList.java
src/jalview/util/MappingUtils.java
src/jalview/util/MessageManager.java
src/jalview/util/Platform.java
src/jalview/util/StringUtils.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/ws/SequenceFetcher.java
src/jalview/ws/dbsources/EmblCdsSource.java
src/jalview/ws/dbsources/EmblFlatfileSource.java
src/jalview/ws/dbsources/EmblSource.java
src/jalview/ws/dbsources/EmblXmlSource.java
src/jalview/ws/dbsources/Uniprot.java
src/jalview/ws/jws1/JPredClient.java
src/jalview/ws/jws1/MsaWSClient.java
src/jalview/ws/jws2/Jws2Client.java
src/jalview/ws/jws2/MsaWSClient.java
src/jalview/ws/jws2/SequenceAnnotationWSClient.java
src/jalview/ws/jws2/dm/JabaOption.java
src/jalview/ws/rest/InputType.java
src/jalview/ws/rest/RestJobThread.java
src/jalview/ws/sifts/SiftsClient.java
src/jalview/xml/binding/jalview/DoubleVector.java
src/jalview/xml/binding/jalview/JalviewModel.java
test/jalview/analysis/AlignmentGenerator.java
test/jalview/analysis/FinderTest.java
test/jalview/datamodel/ResidueCountTest.java
test/jalview/datamodel/SequenceTest.java
test/jalview/ext/ensembl/EnsemblCdnaTest.java
test/jalview/ext/ensembl/EnsemblGeneTest.java
test/jalview/ext/jmol/JmolCommandsTest.java
test/jalview/ext/jmol/JmolParserTest.java
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
test/jalview/ext/rbvi/chimera/JalviewChimeraView.java
test/jalview/fts/service/pdb/PDBFTSPanelTest.java
test/jalview/gui/FreeUpMemoryTest.java
test/jalview/gui/SeqCanvasTest.java
test/jalview/gui/SeqPanelTest.java
test/jalview/gui/StructureChooserTest.java
test/jalview/io/BackupFilesTest.java
test/jalview/io/FileFormatsTest.java
test/jalview/project/Jalview2xmlTests.java
test/jalview/structure/StructureSelectionManagerTest.java
test/jalview/structures/models/AAStructureBindingModelTest.java
test/jalview/util/MapListTest.java
test/jalview/util/MappingUtilsTest.java
test/jalview/ws/gui/Jws2ParamView.java
test/jalview/ws/jabaws/DisorderAnnotExportImport.java
test/jalview/ws/jabaws/RNAStructExportImport.java
test/jalview/ws/jws2/ParameterUtilsTest.java

diff --cc THIRDPARTYLIBS
Simple merge
diff --cc build.gradle
@@@ -12,6 -12,6 +12,7 @@@ import java.security.MessageDiges
  import groovy.transform.ExternalizeMethods
  import groovy.util.XmlParser
  import groovy.xml.XmlUtil
++
  import com.vladsch.flexmark.util.ast.Node
  import com.vladsch.flexmark.html.HtmlRenderer
  import com.vladsch.flexmark.parser.Parser
@@@ -56,24 -58,13 +58,14 @@@ def string(Object o) 
    return o == null ? "" : o.toString()
  }
  
 +
- ext {
-   jalviewDirAbsolutePath = file(jalviewDir).getAbsolutePath()
-   jalviewDirRelativePath = jalviewDir
-   // local build environment properties
-   // can be "projectDir/local.properties"
-   def localProps = "${projectDir}/local.properties"
-   def propsFile = null;
-   if (file(localProps).exists()) {
-     propsFile = localProps
+ def overrideProperties(String propsFileName, boolean output = false) {
+   if (propsFileName == null) {
+     return
    }
-   // or "../projectDir_local.properties"
-   def dirLocalProps = projectDir.getParent() + "/" + projectDir.getName() + "_local.properties"
-   if (file(dirLocalProps).exists()) {
-     propsFile = dirLocalProps
-   }
-   if (propsFile != null) {
+   def propsFile = file(propsFileName)
+   if (propsFile != null && propsFile.exists()) {
+     println("Using properties from file '${propsFileName}'")
      try {
        def p = new Properties()
        def localPropsFIS = new FileInputStream(propsFile)
@@@ -146,6 -162,9 +163,8 @@@ ext 
    }
    */
  
+   // datestamp
+   buildDate = new Date().format("yyyyMMdd")
 -
    // essentials
    bareSourceDir = string(source_dir)
    sourceDir = string("${jalviewDir}/${bareSourceDir}")
    //cloverTestClassesDir = cloverClassesDir
    cloverDb = string("${cloverBuildDir}/clover.db")
  
-   resourceClassesDir = useClover ? cloverClassesDir : classesDir
 +
    testSourceDir = useClover ? cloverTestInstrDir : testDir
    testClassesDir = useClover ? cloverTestClassesDir : "${jalviewDir}/${test_output_dir}"
  
      install4jHomeDir = System.getProperty("user.home") + install4jHomeDir.substring(1)
    }
  
+   resourceBuildDir = string("${buildDir}/resources")
+   resourcesBuildDir = string("${resourceBuildDir}/resources_build")
+   helpBuildDir = string("${resourceBuildDir}/help_build")
+   docBuildDir = string("${resourceBuildDir}/doc_build")
  
 +
+   if (buildProperties == null) {
+     buildProperties = string("${resourcesBuildDir}/${build_properties_file}")
+   }
    buildingHTML = string("${jalviewDir}/${doc_dir}/building.html")
-   helpFile = string("${resourceClassesDir}/${help_dir}/help.jhm")
    helpParentDir = string("${jalviewDir}/${help_parent_dir}")
    helpSourceDir = string("${helpParentDir}/${help_dir}")
+   helpFile = string("${helpBuildDir}/${help_dir}/help.jhm")
  
  
    relativeBuildDir = file(jalviewDirAbsolutePath).toPath().relativize(buildDir.toPath())
@@@ -480,12 -512,9 +514,10 @@@ sourceSets 
      }
  
      resources {
-       srcDirs resourceDir
-       srcDirs += helpParentDir
+       srcDirs = [ resourcesBuildDir, docBuildDir, helpBuildDir ]
      }
  
-     jar.destinationDir = file("${jalviewDir}/${package_dir}")
 +
      compileClasspath = files(sourceSets.main.java.outputDir)
      compileClasspath += fileTree(dir: "${jalviewDir}/${libDir}", include: ["*.jar"])
  
@@@ -1156,15 -1158,7 +1161,8 @@@ task convertMdFiles 
      def htmlFilePath = mdFile.getPath().replaceAll(/\..*?$/, ".html")
      htmlFiles.add(file(htmlFilePath))
    }
-   outputs.files(htmlFiles)
- }
 +
- task syncDocs(type: Sync) {
-   dependsOn convertMdFiles
-   def syncDir = "${classesDir}/${doc_dir}"
-   from fileTree("${jalviewDir}/${doc_dir}")
-   into syncDir
+   outputs.files(htmlFiles)
  }
  
  
@@@ -1197,31 -1197,113 +1201,111 @@@ task copyHelp(type: Copy) 
  }
  
  
- task syncLib(type: Sync) {
-   def syncDir = "${resourceClassesDir}/${libDistDir}"
-   from fileTree("${jalviewDir}/${libDistDir}")
-   into syncDir
+ task copyResources(type: Copy) {
+   group = "build"
+   description = "Copy (and make text substitutions in) the resources dir to the build area"
+   def inputDir = resourceDir
+   def outputDir = resourcesBuildDir
+   from(inputDir) {
+     include('**/*.txt')
+     include('**/*.md')
+     include('**/*.html')
+     include('**/*.xml')
+     filter(ReplaceTokens,
+       beginToken: '$$',
+       endToken: '$$',
+       tokens: [
+         'Version-Rel': JALVIEW_VERSION,
+         'Year-Rel': getDate("yyyy")
+       ]
+     )
+   }
+   from(inputDir) {
+     exclude('**/*.txt')
+     exclude('**/*.md')
+     exclude('**/*.html')
+     exclude('**/*.xml')
+   }
+   into outputDir
+   inputs.dir(inputDir)
+   outputs.dir(outputDir)
  }
  
+ task copyChannelResources(type: Copy) {
+   dependsOn copyResources
+   group = "build"
+   description = "Copy the channel resources dir to the build resources area"
+   def inputDir = "${channelDir}/${resource_dir}"
+   def outputDir = resourcesBuildDir
+   from inputDir
+   into outputDir
  
- task syncResources(type: Sync) {
-   dependsOn createBuildProperties
-   from resourceDir
-   include "**/*.*"
-   into "${resourceClassesDir}"
-   preserve {
-     include "**"
+   inputs.dir(inputDir)
+   outputs.dir(outputDir)
+ }
+ task createBuildProperties(type: WriteProperties) {
+   dependsOn copyResources
+   group = "build"
+   description = "Create the ${buildProperties} file"
+   
+   inputs.dir(sourceDir)
+   inputs.dir(resourcesBuildDir)
+   outputFile (buildProperties)
+   // taking time specific comment out to allow better incremental builds
+   comment "--Jalview Build Details--\n"+getDate("yyyy-MM-dd HH:mm:ss")
+   //comment "--Jalview Build Details--\n"+getDate("yyyy-MM-dd")
+   property "BUILD_DATE", getDate("HH:mm:ss dd MMMM yyyy")
+   property "VERSION", JALVIEW_VERSION
+   property "INSTALLATION", INSTALLATION+" git-commit:"+gitHash+" ["+gitBranch+"]"
+   if (getdownSetAppBaseProperty) {
+     property "GETDOWNAPPBASE", getdownAppBase
+     property "GETDOWNAPPDISTDIR", getdownAppDistDir
    }
+   outputs.file(outputFile)
+ }
 -
+ task buildIndices(type: JavaExec) {
+   dependsOn copyHelp
+   classpath = sourceSets.main.compileClasspath
+   main = "com.sun.java.help.search.Indexer"
+   workingDir = "${helpBuildDir}/${help_dir}"
+   def argDir = "html"
+   args = [ argDir ]
+   inputs.dir("${workingDir}/${argDir}")
+   outputs.dir("${classesDir}/doc")
+   outputs.dir("${classesDir}/help")
+   outputs.file("${workingDir}/JavaHelpSearch/DOCS")
+   outputs.file("${workingDir}/JavaHelpSearch/DOCS.TAB")
+   outputs.file("${workingDir}/JavaHelpSearch/OFFSETS")
+   outputs.file("${workingDir}/JavaHelpSearch/POSITIONS")
+   outputs.file("${workingDir}/JavaHelpSearch/SCHEMA")
+   outputs.file("${workingDir}/JavaHelpSearch/TMAP")
+ }
+ task buildResources {
+   dependsOn copyResources
+   dependsOn copyChannelResources
+   dependsOn createBuildProperties
  }
  
 +
  task prepare {
-   dependsOn syncResources
-   dependsOn syncDocs
+   dependsOn buildResources
+   dependsOn copyDocs
    dependsOn copyHelp
+   dependsOn convertMdFiles
+   dependsOn buildIndices
  }
  
  
+ compileJava.dependsOn prepare
+ run.dependsOn compileJava
+ //run.dependsOn prepare
 -
 -
  //testReportDirName = "test-reports" // note that test workingDir will be $jalviewDir
  test {
    dependsOn prepare
  }
  
  
- task buildIndices(type: JavaExec) {
-   dependsOn copyHelp
-   classpath = sourceSets.main.compileClasspath
-   main = "com.sun.java.help.search.Indexer"
-   workingDir = "${classesDir}/${help_dir}"
-   def argDir = "html"
-   args = [ argDir ]
-   inputs.dir("${workingDir}/${argDir}")
-   outputs.dir("${classesDir}/doc")
-   outputs.dir("${classesDir}/help")
-   outputs.file("${workingDir}/JavaHelpSearch/DOCS")
-   outputs.file("${workingDir}/JavaHelpSearch/DOCS.TAB")
-   outputs.file("${workingDir}/JavaHelpSearch/OFFSETS")
-   outputs.file("${workingDir}/JavaHelpSearch/POSITIONS")
-   outputs.file("${workingDir}/JavaHelpSearch/SCHEMA")
-   outputs.file("${workingDir}/JavaHelpSearch/TMAP")
- }
 +
 +
  task compileLinkCheck(type: JavaCompile) {
    options.fork = true
    classpath = files("${jalviewDir}/${utils_dir}")
@@@ -1311,9 -1381,10 +1385,9 @@@ task linkCheck(type: JavaExec) 
    outputs.file(helpLinksCheckerOutFile)
  }
  
 -
  // import the pubhtmlhelp target
  ant.properties.basedir = "${jalviewDir}"
- ant.properties.helpBuildDir = "${jalviewDirAbsolutePath}/${classes_dir}/${help_dir}"
+ ant.properties.helpBuildDir = "${helpBuildDir}/${help_dir}"
  ant.importBuild "${utils_dir}/publishHelp.xml"
  
  
@@@ -1332,13 -1402,17 +1405,16 @@@ jar 
    manifest {
      attributes "Main-Class": main_class,
      "Permissions": "all-permissions",
-     "Application-Name": "Jalview Desktop",
-     "Codebase": application_codebase
+     "Application-Name": install4jApplicationName,
+     "Codebase": application_codebase,
+     "Implementation-Version": JALVIEW_VERSION
    }
  
-   destinationDir = file("${jalviewDir}/${package_dir}")
-   archiveName = rootProject.name+".jar"
+   def outputDir = "${jalviewDir}/${package_dir}"
+   destinationDirectory = file(outputDir)
+   archiveFileName = rootProject.name+".jar"
+   duplicatesStrategy "EXCLUDE"
  
 -
    exclude "cache*/**"
    exclude "*.jar"
    exclude "*.jar.*"
@@@ -1395,8 -1474,12 +1475,10 @@@ shadowJar 
      include("*.jar")
    }
    manifest {
-     attributes 'Implementation-Version': JALVIEW_VERSION
+     attributes "Implementation-Version": JALVIEW_VERSION,
+     "Application-Name": install4jApplicationName
    }
 -
+   duplicatesStrategy "INCLUDE"
 -
    mainClassName = shadow_jar_main_class
    mergeServiceFiles()
    classifier = "all-"+JALVIEW_VERSION+"-j"+JAVA_VERSION
@@@ -1741,12 -1855,6 +1854,7 @@@ task copyInstall4jTemplate 
        }
      }
  
-     // remove the "Uninstall Old Jalview (optional)" symlink from DMG for non-release DS_Stores
-     if (! (CHANNEL == "RELEASE" || CHANNEL == "TEST-RELEASE" ) ) {
-       def symlink = install4jConfigXml.'**'.topLevelFiles.symlink.find { sl -> sl.'@name' == "Uninstall Old Jalview (optional).app" }
-       symlink.parent().remove(symlink)
-     }
 +
      // write install4j file
      install4jConfFile.text = XmlUtil.serialize(install4jConfigXml)
    }
@@@ -1824,6 -1937,11 +1937,10 @@@ task installers(type: com.install4j.gra
      'UNIX_APPLICATION_FOLDER': install4jUnixApplicationFolder,
      'EXECUTABLE_NAME': install4jExecutableName,
      'EXTRA_SCHEME': install4jExtraScheme,
+     'MAC_ICONS_FILE': install4jMacIconsFile,
+     'WINDOWS_ICONS_FILE': install4jWindowsIconsFile,
+     'PNG_ICON_FILE': install4jPngIconFile,
+     'BACKGROUND': install4jBackground,
 -
    ]
  
    //println("INSTALL4J VARIABLES:")
@@@ -1864,17 -1992,13 +1991,12 @@@ spotless 
  task sourceDist(type: Tar) {
    group "distribution"
    description "Create a source .tar.gz file for distribution"
-   
-   dependsOn convertMdFiles
  
+   dependsOn createBuildProperties
+   dependsOn convertMdFiles
 -
    def VERSION_UNDERSCORES = JALVIEW_VERSION.replaceAll("\\.", "_")
    def outputFileName = "${project.name}_${VERSION_UNDERSCORES}.tar.gz"
-   // cater for buildship < 3.1 [3.0.1 is max version in eclipse 2018-09]
-   try {
-     archiveFileName = outputFileName
-   } catch (Exception e) {
-     archiveName = outputFileName
-   }
+   archiveFileName = outputFileName
    
    compression Compression.GZIP
    
  //    exclude(EXCLUDE_FILES)
  //    exclude(PROCESS_FILES)
  //  }
 -
+   from(file(buildProperties).getParent()) {
+     include(file(buildProperties).getName())
+     rename(file(buildProperties).getName(), "build_properties")
+     filter({ line ->
+       line.replaceAll("^INSTALLATION=.*\$","INSTALLATION=Source Release"+" git-commit\\\\:"+gitHash+" ["+gitBranch+"]")
+     })
+   }
 -
  }
  
  
@@@ -2001,8 -2134,8 +2130,6 @@@ task jalviewjsEnableAltFileProperty(typ
      property(jalviewjs_j2s_alt_file_property_config, jalviewjs_j2s_alt_file_property)
    }
  }
--
--
  task jalviewjsSetEclipseWorkspace {
    def propKey = "jalviewjs_eclipse_workspace"
    def propVal = null
@@@ -2266,6 -2399,10 +2393,8 @@@ task jalviewjsSyncAllLibs (type: Sync) 
    preserve {
      include "**"
    }
 -
+   // should this be exclude really ?
+   duplicatesStrategy "INCLUDE"
 -
    outputs.files outputFiles
    inputs.files inputFiles
  }
@@@ -1,10 -1,10 +1,10 @@@
--<html>
  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml">
++<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
      <meta http-equiv="Content-Style-Type" content="text/css" />
--    <meta name="generator" content="flexmark" />
++  <title>Building Jalview from Source</title>
    <title>Building Jalview from Source</title>
      <style type="text/css">code{white-space: pre;}</style>
  <style>
@@@ -672,20 -672,20 +672,17 @@@ body :checked+.radio-label 
  </head>
    <body>
  <ul>
--<li><a href="#tldr">tl;dr</a></li>
--<li><a href="#setting-up">Setting up</a>
  <ul>
++<li><a href="#setting-up">Setting up</a>
++<li><a href="#tldr">tl;dr</a></li>
++<li><a href="#java-11-compliant-jdk">Java 11 compliant JDK</a></li>
  <li><a href="#java-11-compliant-jdk">Java 11 compliant JDK</a></li>
  <li><a href="#gradle-and-git">gradle and git</a></li>
--</ul>
  </li>
  <li><a href="#downloading-the-jalview-source-tree">Downloading the Jalview source tree</a>
--<ul>
++<li><a href="#whats-in-the-source-tree">What's in the source tree?</a></li>
  <li><a href="#whats-in-the-source-tree">What's in the source tree?</a></li>
  </ul>
--</li>
--<li><a href="#building-jalview">Building Jalview</a>
--<ul>
  <li><a href="#minimal-jalview-build">Minimal Jalview Build</a></li>
  <li><a href="#jalview-in-a-jar-file">Jalview in a Jar File</a></li>
  <li><a href="#distributed-jar-files">Distributed Jar Files</a></li>
  <li><a href="#building-the-getdown-launcher">Building the <code>getdown</code> launcher</a></li>
  <li><a href="#running-tests">Running tests</a></li>
  <li><a href="#installer-packaging-with-install4j">Installer packaging with <em>install4j</em></a></li>
--</ul>
--</li>
++<li><a href="#building-the-getdown-launcher">Building the <code>getdown</code> launcher</a></li>
  <li><a href="#gradle-properties">Gradle properties</a></li>
  <li><a href="#enabling-code-coverage-with-openclover">Enabling Code Coverage with OpenClover</a></li>
--<li><a href="#setting-up-in-eclipse-ide">Setting up in Eclipse IDE</a>
--<ul>
++</ul>
  <li><a href="#installing-eclipse-ide">Installing Eclipse IDE</a></li>
  <li><a href="#importing-jalview-as-an-eclipse-project">Importing Jalview as an Eclipse project</a></li>
++<li><a href="#enabling-code-coverage-with-openclover">Enabling Code Coverage with OpenClover</a></li>
++<li><a href="#setting-up-in-eclipse-ide">Setting up in Eclipse IDE</a>
  </ul>
  </li>
  </ul>
  <h1 id="building-jalview-from-source"><a href="#building-jalview-from-source" name="building-jalview-from-source" class="anchor"><span class="octicon octicon-link"></span>Building Jalview from Source</a></h1>
--<h2 id="tldr"><a href="#tldr" name="tldr" class="anchor"><span class="octicon octicon-link"></span>tl;dr</a></h2>
++</head>
++<pre><code># download
++git clone http://source.jalview.org/git/jalview.git
++<ul>
++cd jalview
++<li><a href="#tldr">tl;dr</a></li>
++# run
++<li><a href="#java-11-compliant-jdk">Java 11 compliant JDK</a></li>
++<li><a href="#gradle-and-git">gradle and git</a></li>
++# and/or create launcher
++gradle getdown
++<li><a href="#whats-in-the-source-tree">What's in the source tree?</a></li>
++cd getdown/files
++java -jar getdown-launcher.jar . jalview
++<li><a href="#minimal-jalview-build">Minimal Jalview Build</a></li>
++<li><a href="#jalview-in-a-jar-file">Jalview in a Jar File</a></li>
++<li><a href="#distributed-jar-files">Distributed Jar Files</a></li>
++<li><a href="#single-shadow-jar-file">Single <em>shadow</em> Jar File</a></li>
++<li><a href="#building-the-getdown-launcher">Building the <code>getdown</code> launcher</a></li>
++<li><a href="#running-tests">Running tests</a></li>
++<li><a href="#installer-packaging-with-install4j">Installer packaging with <em>install4j</em></a></li>
++<li>Java 11 compliant JDK</li>
++<li><a href="#gradle-properties">Gradle properties</a></li>
++<li><a href="#enabling-code-coverage-with-openclover">Enabling Code Coverage with OpenClover</a></li>
++</ul>
++<li><a href="#installing-eclipse-ide">Installing Eclipse IDE</a></li>
++<li><a href="#importing-jalview-as-an-eclipse-project">Importing Jalview as an Eclipse project</a></li>
++so are known to work).  If you need or wish to use different implementations (particularly
++you might need a bespoke JDK if you are on an exotic architecture) then the general
++</ul>
++bytecode with any JDK Java 11+.  The resulting bytecode (in particular the shadow jar)
++should be runnable in any JRE Java 1.8+.  Remember that because Jalview and the getdown launcher
++are Java bytecode you can build on one system where you might have gradle, and run
  <pre><code># download
  git clone http://source.jalview.org/git/jalview.git
  # compile
  cd jalview
  gradle shadowJar
  # run
- java -jar build/libs/jalview-all-11.jar
 -java -jar build/libs/jalview-all-*-j11.jar
++# compile
  
  # and/or create launcher
  gradle getdown
  # use launcher
  cd getdown/files
--java -jar getdown-launcher.jar . jalview
--</code></pre>
--<h2 id="setting-up"><a href="#setting-up" name="setting-up" class="anchor"><span class="octicon octicon-link"></span>Setting up</a></h2>
++java -jar getdown-launcher.jar . jalview</code></pre>
++<h2 id="setting-up">Setting up</h2>
  <blockquote>
--<p>To get set up using <em>only</em> the Eclipse IDE (<a href="https://www.eclipse.org/">https://www.eclipse.org/</a>) then please see the section <a href="#setting-up-in-eclipse-ide">Setting up in Eclipse IDE</a></p>
++<p>To get set up using <em>only</em> the Eclipse IDE (<a href="https://www.eclipse.org/" class="uri">https://www.eclipse.org/</a>) then please see the section <a href="#setting-up-in-eclipse-ide">Setting up in Eclipse IDE</a></p>
  </blockquote>
--<p>The method here is described in terms of using a command line.  You can easily do this on linux or in a Terminal window in macOS.  You can do it in Windows.</p>
++<p>The method here is described in terms of using a command line. You can easily do this on linux or in a Terminal window in macOS. You can do it in Windows.</p>
  <ul>
  <li>Java 11 compliant JDK</li>
- <li>gradle 5.2 or above</li>
 -<li>gradle 5.2 or above <em>(NB gradle 6.6 and above currently produces NullPointerExceptions during the build. This is non-fatal and does not affect the build. Use gradle 6.5.1 to avoid this)</em></li>
++<p>To get set up using <em>only</em> the Eclipse IDE (<a href="https://www.eclipse.org/">https://www.eclipse.org/</a>) then please see the section <a href="#setting-up-in-eclipse-ide">Setting up in Eclipse IDE</a></p>
  <li>git</li>
  </ul>
  <blockquote>
--<p>The versions and installation methods here are just suggestions (which we have tested
--so are known to work).  If you need or wish to use different implementations (particularly
--you might need a bespoke JDK if you are on an exotic architecture) then the general
--build instructions should work with any gradle 5+.  You should be able to compile the
--bytecode with any JDK Java 11+.  The resulting bytecode (in particular the shadow jar)
--should be runnable in any JRE Java 1.8+.  Remember that because Jalview and the getdown launcher
--are Java bytecode you can build on one system where you might have gradle, and run
--on another where you don't (JRE 1.8+ required).</p>
++<p>The versions and installation methods here are just suggestions (which we have tested so are known to work). If you need or wish to use different implementations (particularly you might need a bespoke JDK if you are on an exotic architecture) then the general build instructions should work with any gradle 5+. You should be able to compile the bytecode with any JDK Java 11+. The resulting bytecode (in particular the shadow jar) should be runnable in any JRE Java 1.8+. Remember that because Jalview and the getdown launcher are Java bytecode you can build on one system where you might have gradle, and run on another where you don't (JRE 1.8+ required).</p>
  </blockquote>
--<h3 id="java-11-compliant-jdk"><a href="#java-11-compliant-jdk" name="java-11-compliant-jdk" class="anchor"><span class="octicon octicon-link"></span>Java 11 compliant JDK</a></h3>
--<h4 id="all-platforms"><a href="#all-platforms" name="all-platforms" class="anchor"><span class="octicon octicon-link"></span>All platforms</a></h4>
--<p>We recommend obtaining an OpenJDK JDK 11 (since 11 is the long term support release) from AdoptOpenJDK: <a href="https://adoptopenjdk.net/?variant=openjdk11&amp;jvmVariant=hotspot">https://adoptopenjdk.net/?variant=openjdk11&amp;jvmVariant=hotspot</a>, either the <em>Installer</em> or <code>.zip</code>/<code>.tar.gz</code> variants whichever you prefer (if you're not sure, choose the <em>Installer</em>).</p>
++<h3 id="java-11-compliant-jdk">Java 11 compliant JDK</h3>
++<h4 id="all-platforms">All platforms</h4>
++<p>We recommend obtaining an OpenJDK JDK 11 (since 11 is the long term support release) from AdoptOpenJDK: <a href="https://adoptopenjdk.net/?variant=openjdk11&amp;jvmVariant=hotspot" class="uri">https://adoptopenjdk.net/?variant=openjdk11&amp;jvmVariant=hotspot</a>, either the <em>Installer</em> or <code>.zip</code>/<code>.tar.gz</code> variants whichever you prefer (if you're not sure, choose the <em>Installer</em>).</p>
  <blockquote>
--<h5 id="alternativecli-install-of-adoptopenjdk-11"><a href="#alternativecli-install-of-adoptopenjdk-11" name="alternativecli-install-of-adoptopenjdk-11" class="anchor"><span class="octicon octicon-link"></span>Alternative/CLI install of AdoptOpenJDK 11</a></h5>
--<p>You can also install adoptopenjdk11 using either <code>brew</code> (macOS), <code>choco</code> (Windows)
--(see the section on <code>gradle</code> and <code>git</code> for more informaiton on <code>brew</code> and <code>choco</code>)
--or <code>yum</code> or <code>apt</code> (Linux):</p>
--<h6 id="alternative-for-macos-and-homebrew"><a href="#alternative-for-macos-and-homebrew" name="alternative-for-macos-and-homebrew" class="anchor"><span class="octicon octicon-link"></span>alternative for MacOS and Homebrew</a></h6>
++<h5 id="alternativecli-install-of-adoptopenjdk-11">Alternative/CLI install of AdoptOpenJDK 11</h5>
++<p>You can also install adoptopenjdk11 using either <code>brew</code> (macOS), <code>choco</code> (Windows) (see the section on <code>gradle</code> and <code>git</code> for more informaiton on <code>brew</code> and <code>choco</code>) or <code>yum</code> or <code>apt</code> (Linux):</p>
++<h6 id="alternative-for-macos-and-homebrew">alternative for MacOS and Homebrew</h6>
  <pre><code>brew tap adoptopenjdk/openjdk
--brew cask install adoptopenjdk11
--</code></pre>
--<h6 id="alternative-for-windows-and-chocolatey"><a href="#alternative-for-windows-and-chocolatey" name="alternative-for-windows-and-chocolatey" class="anchor"><span class="octicon octicon-link"></span>alternative for Windows and Chocolatey</a></h6>
--<pre><code>choco install adoptopenjdk11
--</code></pre>
--<h6 id="alternative-for-linux-with-yumapt"><a href="#alternative-for-linux-with-yumapt" name="alternative-for-linux-with-yumapt" class="anchor"><span class="octicon octicon-link"></span>alternative for Linux with yum/apt</a></h6>
--<p>see <a href="https://adoptopenjdk.net/installation.html#linux-pkg">https://adoptopenjdk.net/installation.html#linux-pkg</a></p>
++brew cask install adoptopenjdk11</code></pre>
++<h6 id="alternative-for-windows-and-chocolatey">alternative for Windows and Chocolatey</h6>
++<pre><code>choco install adoptopenjdk11</code></pre>
++<h6 id="alternative-for-linux-with-yumapt">alternative for Linux with yum/apt</h6>
++<p>see <a href="https://adoptopenjdk.net/installation.html#linux-pkg" class="uri">https://adoptopenjdk.net/installation.html#linux-pkg</a></p>
  </blockquote>
--<h3 id="gradle-and-git"><a href="#gradle-and-git" name="gradle-and-git" class="anchor"><span class="octicon octicon-link"></span>gradle and git</a></h3>
++<h3 id="gradle-and-git">gradle and git</h3>
  <p>You should be able to install the latest (or sufficiently recent) versions of gradle and git using your OS package manager.</p>
--<h4 id="macos"><a href="#macos" name="macos" class="anchor"><span class="octicon octicon-link"></span>MacOS</a></h4>
--<p>we recommend using <code>brew</code>, which can be installed following the instructions at <a href="https://brew.sh/">https://brew.sh/</a>.
--After installing <code>brew</code>, open a Terminal window and type in (using an Administrator privileged user):</p>
--<pre><code class="language-bash">brew install gradle git
--</code></pre>
++<h4 id="macos">MacOS</h4>
++<p>we recommend using <code>brew</code>, which can be installed following the instructions at <a href="https://brew.sh/" class="uri">https://brew.sh/</a>. After installing <code>brew</code>, open a Terminal window and type in (using an Administrator privileged user):</p>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">brew</span> install gradle git</code></pre></div>
  <p>or if you aready have them installed but need to upgrade the version:</p>
--<pre><code class="language-bash">brew upgrade gradle git
--</code></pre>
--<h4 id="windows"><a href="#windows" name="windows" class="anchor"><span class="octicon octicon-link"></span>Windows</a></h4>
--<p>we suggest using the <strong>Chocolatey</strong> package manager.  See install instructions at <a href="https://chocolatey.org/">https://chocolatey.org/</a>, and you will just need</p>
--<pre><code class="language-bash">choco install gradle
--choco install git
--</code></pre>
--<p>Alternatively, you could install a real <code>bash</code> shell and install both <code>gradle</code> and <code>git</code> through <code>apt-get</code>.
--See <a href="https://devblogs.microsoft.com/commandline/bash-on-ubuntu-on-windows-download-now-3/">https://devblogs.microsoft.com/commandline/bash-on-ubuntu-on-windows-download-now-3/</a>
--for how to install the ubuntu bash shell in Windows 10.</p>
--<p>Another alternative would be to install them separately. For <code>gradle</code> follow the instructions at <a href="https://gradle.org/install/">https://gradle.org/install/</a>, and for <code>git</code> here are a couple of suggestions: Git for Windows <a href="https://gitforwindows.org/">https://gitforwindows.org/</a>.
--Getting the individual installs working together on the command line will be trickier
--so we recommend using Chocolatey or bash.</p>
--<h4 id="linux"><a href="#linux" name="linux" class="anchor"><span class="octicon octicon-link"></span>Linux</a></h4>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">brew</span> upgrade gradle git</code></pre></div>
++<h4 id="windows">Windows</h4>
++<p>we suggest using the <strong>Chocolatey</strong> package manager. See install instructions at <a href="https://chocolatey.org/" class="uri">https://chocolatey.org/</a>, and you will just need</p>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">choco</span> install gradle
++<span class="ex">choco</span> install git</code></pre></div>
++<p>Alternatively, you could install a real <code>bash</code> shell and install both <code>gradle</code> and <code>git</code> through <code>apt-get</code>. See <a href="https://devblogs.microsoft.com/commandline/bash-on-ubuntu-on-windows-download-now-3/" class="uri">https://devblogs.microsoft.com/commandline/bash-on-ubuntu-on-windows-download-now-3/</a> for how to install the ubuntu bash shell in Windows 10.</p>
++<p>Another alternative would be to install them separately. For <code>gradle</code> follow the instructions at <a href="https://gradle.org/install/" class="uri">https://gradle.org/install/</a>, and for <code>git</code> here are a couple of suggestions: Git for Windows <a href="https://gitforwindows.org/" class="uri">https://gitforwindows.org/</a>. Getting the individual installs working together on the command line will be trickier so we recommend using Chocolatey or bash.</p>
++<h4 id="linux">Linux</h4>
  <p>this will depend on which distribution you're using.</p>
--<h5 id="for-debian-based-distributions-eg-mint-ubuntu-debian"><a href="#for-debian-based-distributions-eg-mint-ubuntu-debian" name="for-debian-based-distributions-eg-mint-ubuntu-debian" class="anchor"><span class="octicon octicon-link"></span>For <em>Debian</em> based distributions (e.g. Mint, Ubuntu, Debian)</a></h5>
++<h5 id="for-debian-based-distributions-e.g.-mint-ubuntu-debian">For <em>Debian</em> based distributions (e.g. Mint, Ubuntu, Debian)</h5>
  <p>run</p>
--<pre><code class="language-bash"> sudo apt-get install gradle git
--</code></pre>
--<h5 id="for-rpm-based-distributions-eg-fedora-centos-redhat"><a href="#for-rpm-based-distributions-eg-fedora-centos-redhat" name="for-rpm-based-distributions-eg-fedora-centos-redhat" class="anchor"><span class="octicon octicon-link"></span>for RPM-based distributions (e.g. Fedora, CentOS, RedHat)</a></h5>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"> <span class="fu">sudo</span> apt-get install gradle git</code></pre></div>
++<h5 id="for-rpm-based-distributions-e.g.-fedora-centos-redhat">for RPM-based distributions (e.g. Fedora, CentOS, RedHat)</h5>
  <p>run</p>
--<pre><code class="language-bash">sudo yum install gradle git
--</code></pre>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">sudo</span> yum install gradle git</code></pre></div>
  <p>If you have some other version of linux you'll probably be able to work it out!</p>
--<h2 id="downloading-the-jalview-source-tree"><a href="#downloading-the-jalview-source-tree" name="downloading-the-jalview-source-tree" class="anchor"><span class="octicon octicon-link"></span>Downloading the Jalview source tree</a></h2>
--<p>This can be done with <code>git</code>.
--On the command line, change directory to where you want to download Jalview's build-tree
--top level directory.  Then run</p>
--<pre><code class="language-bash">git clone http://source.jalview.org/git/jalview.git
--</code></pre>
--<p>You'll get some progress output and after a minute or two you should have the full
--Jalview build-tree in the folder <code>jalview</code>.</p>
--<h3 id="whats-in-the-source-tree"><a href="#whats-in-the-source-tree" name="whats-in-the-source-tree" class="anchor"><span class="octicon octicon-link"></span>What's in the source tree?</a></h3>
--<p>Jalview is a mature product with its codebase going back many years.  As such it doesn't
--have a folder structure that most new gradle projects would have, so you might not
--find everything in the place you might expect.  Here's a brief description of what
--you might find in the main folders under the <code>jalview</code> tree.</p>
++<h2 id="downloading-the-jalview-source-tree">Downloading the Jalview source tree</h2>
++<p>This can be done with <code>git</code>. On the command line, change directory to where you want to download Jalview's build-tree top level directory. Then run</p>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">git</span> clone http://source.jalview.org/git/jalview.git</code></pre></div>
++<p>You'll get some progress output and after a minute or two you should have the full Jalview build-tree in the folder <code>jalview</code>.</p>
++<h3 id="whats-in-the-source-tree">What's in the source tree?</h3>
++<p>Jalview is a mature product with its codebase going back many years. As such it doesn't have a folder structure that most new gradle projects would have, so you might not find everything in the place you might expect. Here's a brief description of what you might find in the main folders under the <code>jalview</code> tree.</p>
  <p>Within the <code>jalview</code> folder you will find (of possible interest):</p>
  <table>
++<colgroup>
++<col width="16%" />
++<col width="83%" />
++</colgroup>
  <thead>
--<tr><th>dir/ or file</th><th>contains</th></tr>
++<tr class="header">
++<th>dir/ or file</th>
++<th>contains</th>
++</tr>
  </thead>
  <tbody>
--<tr><td><code>bin/</code></td><td>used by eclipse for compiled classes -- no need to touch this</td></tr>
--<tr><td><code>build/</code></td><td>the gradle build dir</td></tr>
--<tr><td><code>classes/</code></td><td>contains the compiled Java classes for the Jalview application</td></tr>
--<tr><td><code>dist/</code></td><td>assembled <code>.jar</code> files needed to run Jalview application</td></tr>
--<tr><td><code>examples/</code></td><td>example input files usable by Jalview</td></tr>
--<tr><td><code>getdown/</code></td><td>the libraries used by the Javliew launcher (getdown)</td></tr>
--<tr><td><code>getdown/src/</code></td><td>our modified source for <code>getdown</code></td></tr>
--<tr><td><code>getdown/website/</code></td><td>the assembled &quot;download&quot; folder used by getdown for downloads/upgrades</td></tr>
--<tr><td><code>getdown/files/</code></td><td>the minimal fileset to launch the Jalview launcher, which can then download the rest of the Jalview application</td></tr>
--<tr><td><code>help/</code></td><td>the help documents</td></tr>
--<tr><td><code>j8lib/</code></td><td>libraries needed to run Jalview under Java 1.8</td></tr>
--<tr><td><code>j11lib/</code></td><td>libraries needed to run Jalivew under Java 11</td></tr>
--<tr><td><code>resource/</code></td><td>non-java resources used in the Jalview application</td></tr>
--<tr><td><code>src/</code></td><td>the Jalview application source <code>.java</code> files</td></tr>
--<tr><td><code>test/</code></td><td>Test class source files</td></tr>
--<tr><td><code>utils/</code></td><td>helper applications used in the build process</td></tr>
--<tr><td><code>utils/install4j/</code></td><td>files used by the packaging tool, install4j</td></tr>
--<tr><td><code>build.gradle</code></td><td>the build file used by gradle</td></tr>
--<tr><td><code>gradle.properties</code></td><td>configurable properties for the build process</td></tr>
--<tr><td><code>RELEASE</code></td><td>propertyfile configuring JALVIEW_VERSION (from jalview.version) and the release branch (from jalview.release). An alternative file can be specified via JALVIEW_RELEASE_FILE property</td></tr>
++<tr class="odd">
++<td><code>bin/</code></td>
++<td>used by eclipse for compiled classes -- no need to touch this</td>
++</tr>
++<tr class="even">
++<td><code>build/</code></td>
++<td>the gradle build dir</td>
++</tr>
++<tr class="odd">
++<td><code>classes/</code></td>
++<td>contains the compiled Java classes for the Jalview application</td>
++</tr>
++<tr class="even">
++<td><code>dist/</code></td>
++<td>assembled <code>.jar</code> files needed to run Jalview application</td>
++</tr>
++<tr class="odd">
++<td><code>examples/</code></td>
++<td>example input files usable by Jalview</td>
++</tr>
++<tr class="even">
++<td><code>getdown/</code></td>
++<td>the libraries used by the Javliew launcher (getdown)</td>
++</tr>
++<tr class="odd">
++<td><code>getdown/src/</code></td>
++<td>our modified source for <code>getdown</code></td>
++</tr>
++<tr class="even">
++<td><code>getdown/website/</code></td>
++<td>the assembled &quot;download&quot; folder used by getdown for downloads/upgrades</td>
++</tr>
++<tr class="odd">
++<td><code>getdown/files/</code></td>
++<td>the minimal fileset to launch the Jalview launcher, which can then download the rest of the Jalview application</td>
++</tr>
++<tr class="even">
++<td><code>help/</code></td>
++<td>the help documents</td>
++</tr>
++<tr class="odd">
++<td><code>j8lib/</code></td>
++<td>libraries needed to run Jalview under Java 1.8</td>
++</tr>
++<tr class="even">
++<td><code>j11lib/</code></td>
++<td>libraries needed to run Jalivew under Java 11</td>
++</tr>
++<tr class="odd">
++<td><code>resource/</code></td>
++<td>non-java resources used in the Jalview application</td>
++</tr>
++<tr class="even">
++<td><code>src/</code></td>
++<td>the Jalview application source <code>.java</code> files</td>
++</tr>
++<tr class="odd">
++<td><code>test/</code></td>
++<td>Test class source files</td>
++</tr>
++<tr class="even">
++<td><code>utils/</code></td>
++<td>helper applications used in the build process</td>
++</tr>
++<tr class="odd">
++<td><code>utils/install4j/</code></td>
++<td>files used by the packaging tool, install4j</td>
++</tr>
++<tr class="even">
++<td><code>build.gradle</code></td>
++<td>the build file used by gradle</td>
++</tr>
++<tr class="odd">
++<td><code>gradle.properties</code></td>
++<td>configurable properties for the build process</td>
++</tr>
  </tbody>
  </table>
  <p>Note that you need a Java 11 JDK to compile Jalview whether your target build is Java 1.8 or Java 11.</p>
--<h2 id="building-jalview"><a href="#building-jalview" name="building-jalview" class="anchor"><span class="octicon octicon-link"></span>Building Jalview</a></h2>
--<p>You will need to have the Java 11 <code>javac</code> in your path, or alternatively you can configure
--gradle to know where this is by putting</p>
--<pre><code>org.gradle.java.home=/path_to_jdk_directory
--</code></pre>
++<h2 id="building-jalview">Building Jalview</h2>
++<p>You will need to have the Java 11 <code>javac</code> in your path, or alternatively you can configure gradle to know where this is by putting</p>
++<pre><code>org.gradle.java.home=/path_to_jdk_directory</code></pre>
  <p>in the <code>gradle.properties</code> file.</p>
  <blockquote>
  <p><em>You may want to see some of the other properties you can change at the end of this document.</em></p>
  </blockquote>
--<h3 id="minimal-jalview-build"><a href="#minimal-jalview-build" name="minimal-jalview-build" class="anchor"><span class="octicon octicon-link"></span>Minimal Jalview Build</a></h3>
++<h3 id="minimal-jalview-build">Minimal Jalview Build</h3>
  <p>To compile the necessary class files, just run</p>
--<pre><code class="language-bash">gradle compileJava
--</code></pre>
--<p>to compile the classes into the <code>classes</code> folder.
--You should now be able to run the Jalview application directly with</p>
--<pre><code class="language-bash">java -cp &quot;classes:resources:help:j11lib/*&quot; jalview.bin.Jalview
--</code></pre>
--<p>You can also run with an automatic large memory setting (which will set the maximum
--memory heap of the Jalview JVM to 90% of your local physical memory) and docked icon setting
--(if possible in your OS) with</p>
--<pre><code class="language-bash">java -cp &quot;classes:resources:help:j11lib/*&quot; jalview.bin.Launcher
--</code></pre>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> compileJava</code></pre></div>
++<p>to compile the classes into the <code>classes</code> folder. You should now be able to run the Jalview application directly with</p>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">java</span> -cp <span class="st">&quot;classes:resources:help:j11lib/*&quot;</span> jalview.bin.Jalview</code></pre></div>
++<p>You can also run with an automatic large memory setting (which will set the maximum memory heap of the Jalview JVM to 90% of your local physical memory) and docked icon setting (if possible in your OS) with</p>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">java</span> -cp <span class="st">&quot;classes:resources:help:j11lib/*&quot;</span> jalview.bin.Launcher</code></pre></div>
  <blockquote>
--<p><em>You must use just &quot;<code>j11lib/*</code>&quot; and not &quot;<code>j11lib/*.jar</code>&quot; as this is a special Java
--classpath argument wildcard interpreted by <code>java</code>, <strong>not</strong> a shell expansion wildcard interpreted
--by the shell.</em></p>
++<p><em>You must use just &quot;<code>j11lib/*</code>&quot; and not &quot;<code>j11lib/*.jar</code>&quot; as this is a special Java classpath argument wildcard interpreted by <code>java</code>, <strong>not</strong> a shell expansion wildcard interpreted by the shell.</em></p>
  </blockquote>
--<p>Note that <code>jalview.bin.Launcher</code> is a simplified launcher class that re-launches <code>jalview.bin.Jalview</code>
--with the same JRE (<em>not</em> the same JVM instance), classpath and arguments, but with an automatically determined <code>-Xmx...</code>
--memory setting if one hasn't been provided.</p>
--<h3 id="jalview-in-a-jar-file"><a href="#jalview-in-a-jar-file" name="jalview-in-a-jar-file" class="anchor"><span class="octicon octicon-link"></span>Jalview in a Jar File</a></h3>
++<p>Note that <code>jalview.bin.Launcher</code> is a simplified launcher class that re-launches <code>jalview.bin.Jalview</code> with the same JRE (<em>not</em> the same JVM instance), classpath and arguments, but with an automatically determined <code>-Xmx...</code> memory setting if one hasn't been provided.</p>
++<h3 id="jalview-in-a-jar-file">Jalview in a Jar File</h3>
  <p>To package the <code>classes</code>, <code>resources</code>, and <code>help</code> into one jar, you can run</p>
--<pre><code class="language-bash">gradle jar
--</code></pre>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> jar</code></pre></div>
  <p>which assembles the Jalview classes and resources into <code>dist/jalview.jar</code></p>
  <p>To run this, use</p>
--<pre><code class="language-bash">java -cp &quot;dist/jalview.jar:j11lib/*&quot; jalview.bin.Jalview
--</code></pre>
--<h3 id="distributed-jar-files"><a href="#distributed-jar-files" name="distributed-jar-files" class="anchor"><span class="octicon octicon-link"></span>Distributed Jar Files</a></h3>
--<p>To simplify this, all required <code>.jar</code> files can be assembled into the <code>dist</code> folder
--using</p>
--<pre><code class="language-bash">gradle makeDist
--</code></pre>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">java</span> -cp <span class="st">&quot;dist/jalview.jar:j11lib/*&quot;</span> jalview.bin.Jalview</code></pre></div>
++<h3 id="distributed-jar-files">Distributed Jar Files</h3>
++<p>To simplify this, all required <code>.jar</code> files can be assembled into the <code>dist</code> folder using</p>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> makeDist</code></pre></div>
  <p>which puts all required jar files into <code>dist</code> so you can run with</p>
--<pre><code class="language-bash">java -cp &quot;dist/*&quot; jalview.bin.Jalview
--</code></pre>
--<h3 id="single-shadow-jar-file"><a href="#single-shadow-jar-file" name="single-shadow-jar-file" class="anchor"><span class="octicon octicon-link"></span>Single <em>shadow</em> Jar File</a></h3>
--<p>The shadow jar file is a single <code>.jar</code> that contains all required classes and resources from <code>jalview.jar</code>
--and all of the supporting libraries in <code>j11lib/*.jar</code> merged into one <code>.jar</code> archive
--file.  A default launching class (<code>MAIN-CLASS: jalview.bin.Launcher</code>) is specified in the <code>.jar</code>
--manifest file (<code>META/MANIFEST.MF</code>) so a start class doesn't need to be specified.</p>
 -<p>Build the shadow jar file in <code>build/libs/jalview-all-VERSION-j11.jar</code> with</p>
 -<pre><code class="language-bash">gradle shadowJar
 -</code></pre>
 -<p><strong>NB</strong> <code>VERSION</code> will be replaced with a version number or &quot;<code>DEVELOPMENT</code>&quot; or &quot;<code>TEST</code>&quot; depending on how the branch is set up.</p>
 -<p>Run it with</p>
 -<pre><code class="language-bash">java -jar build/libs/jalview-all-VERSION-j11.jar
 -</code></pre>
 -<p>Because no arguments are required, most OSes will associate a <code>.jar</code> file with the
 -<code>java</code> application (if this has been installed through the OS and not just a local
 -unzip) as a <code>-jar</code> argument so you may find you can launch <code>jalview-all-VERSION-j11.jar</code>
 -just by double-clicking on it)!</p>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">java</span> -cp <span class="st">&quot;dist/*&quot;</span> jalview.bin.Jalview</code></pre></div>
++<h3 id="single-shadow-jar-file">Single <em>shadow</em> Jar File</h3>
++<p>The shadow jar file is a single <code>.jar</code> that contains all required classes and resources from <code>jalview.jar</code> and all of the supporting libraries in <code>j11lib/*.jar</code> merged into one <code>.jar</code> archive file. A default launching class (<code>MAIN-CLASS: jalview.bin.Launcher</code>) is specified in the <code>.jar</code> manifest file (<code>META/MANIFEST.MF</code>) so a start class doesn't need to be specified.</p>
 +<p>Build the shadow jar file in <code>build/lib/jalview-all-11.jar</code> with</p>
- <pre><code class="language-bash">gradle shadowJar
- </code></pre>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> shadowJar</code></pre></div>
 +<p>and run it with</p>
- <pre><code class="language-bash">java -jar build/lib/jalview-all-11.jar
- </code></pre>
- <p>Because no arguments are required, most OSes will associate a <code>.jar</code> file with the
- <code>java</code> application (if this has been installed through the OS and not just a local
- unzip) as a <code>-jar</code> argument so you may find you can launch <code>jalview-all-11.jar</code>
- just by double-clicking on it)!</p>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">java</span> -jar build/lib/jalview-all-11.jar</code></pre></div>
++<p>Because no arguments are required, most OSes will associate a <code>.jar</code> file with the <code>java</code> application (if this has been installed through the OS and not just a local unzip) as a <code>-jar</code> argument so you may find you can launch <code>jalview-all-11.jar</code> just by double-clicking on it)!</p>
  <blockquote>
--<p>The <code>shadowJar</code> task is not a requirement for any other task, so to build the shadow
--jar file you must specify the <code>shadowJar</code> task.</p>
++<p>The <code>shadowJar</code> task is not a requirement for any other task, so to build the shadow jar file you must specify the <code>shadowJar</code> task.</p>
  </blockquote>
  <blockquote>
--<p>The shadow jar file represents probably the simplest way to distribute the Jalview application to machines that already have a Java 11 installed,
--although without the many and compelling benefits of the <code>getdown</code> launcher.</p>
++<p>The shadow jar file represents probably the simplest way to distribute the Jalview application to machines that already have a Java 11 installed, although without the many and compelling benefits of the <code>getdown</code> launcher.</p>
  </blockquote>
--<h3 id="building-the-getdown-launcher"><a href="#building-the-getdown-launcher" name="building-the-getdown-launcher" class="anchor"><span class="octicon octicon-link"></span>Building the <code>getdown</code> launcher</a></h3>
--<p>We have made significant customisations to the <code>getdown</code> launcher which you can find
--in <code>getdown/src/getdown</code>.</p>
++<h3 id="building-the-getdown-launcher">Building the <code>getdown</code> launcher</h3>
++<p>We have made significant customisations to the <code>getdown</code> launcher which you can find in <code>getdown/src/getdown</code>.</p>
  <blockquote>
--<p>You don't need to build this afresh as the required <code>gradle-core.jar</code>
--and <code>gradle-launcher.jar</code> files are already distributed in <code>j11lib</code> and <code>getdown/lib</code> but if you want to, then
--you'll need a working Maven and also a Java 8 JDK.  Ensure the Java 8 <code>javac</code> is forefront
--in your path and do</p>
--<pre><code class="language-bash">cd getdown/src/getdown
--mvn clean package -Dgetdown.host.whitelist=&quot;jalview.org,*.jalview.org&quot;
--</code></pre>
--<p>and you will find the required <code>.jar</code> files in <code>core/target/gradle-core-XXX.jar</code>
--and <code>launcher/target/gradle-launcher-XXX.jar</code>.  The <code>gradle-core.jar</code> should then be copied
--to all three of the <code>j8lib</code>, <code>j11lib</code> and <code>getdown/lib</code> folders, whilst the <code>gradle-launcher.jar</code> only
--needs to be copied to <code>getdown/lib</code>.</p>
--<p>The <code>mvn</code> command should ideally include the <code>-Dgetdown.host.whitelist=*.jalview.org</code> setting.
--This, and the necessary file copying commands, can be found in <code>getdown/src/getdown/mvn_cmd</code>.</p>
++<p>You don't need to build this afresh as the required <code>gradle-core.jar</code> and <code>gradle-launcher.jar</code> files are already distributed in <code>j11lib</code> and <code>getdown/lib</code> but if you want to, then you'll need a working Maven and also a Java 8 JDK. Ensure the Java 8 <code>javac</code> is forefront in your path and do</p>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="bu">cd</span> getdown/src/getdown
++<span class="ex">mvn</span> clean package -Dgetdown.host.whitelist=<span class="st">&quot;jalview.org,*.jalview.org&quot;</span></code></pre></div>
++<p>and you will find the required <code>.jar</code> files in <code>core/target/gradle-core-XXX.jar</code> and <code>launcher/target/gradle-launcher-XXX.jar</code>. The <code>gradle-core.jar</code> should then be copied to all three of the <code>j8lib</code>, <code>j11lib</code> and <code>getdown/lib</code> folders, whilst the <code>gradle-launcher.jar</code> only needs to be copied to <code>getdown/lib</code>.</p>
++<p>The <code>mvn</code> command should ideally include the <code>-Dgetdown.host.whitelist=*.jalview.org</code> setting. This, and the necessary file copying commands, can be found in <code>getdown/src/getdown/mvn_cmd</code>.</p>
  </blockquote>
  <p>To assemble Jalview with <code>getdown</code> use the following gradle task:</p>
--<pre><code class="language-bash">gradle getdown
--</code></pre>
--<p>This puts all the necessary files to launch Jalview with <code>getdown</code>
--into <code>getdown/website/11/</code>.  This could be treated as the reference folder
--for <code>getdown</code>, which is where a getdown launcher will check to see if the Jalview application
--files it has are up to date, and download if they aren't or it simply doesn't have
--them.</p>
--<p>A minimal getdown-launcher can be found in <code>getdown/files/11/</code> which checks its up-to-date
--status with (the absolute path to) <code>getdown/website/11/</code>.</p>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> getdown</code></pre></div>
++<p>This puts all the necessary files to launch Jalview with <code>getdown</code> into <code>getdown/website/11/</code>. This could be treated as the reference folder for <code>getdown</code>, which is where a getdown launcher will check to see if the Jalview application files it has are up to date, and download if they aren't or it simply doesn't have them.</p>
++<p>A minimal getdown-launcher can be found in <code>getdown/files/11/</code> which checks its up-to-date status with (the absolute path to) <code>getdown/website/11/</code>.</p>
  <p>This can be launched with</p>
--<pre><code class="language-bash">java -jar getdown/files/11/getdown-launcher.jar getdown/files/11/ jalview
--</code></pre>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">java</span> -jar getdown/files/11/getdown-launcher.jar getdown/files/11/ jalview</code></pre></div>
  <blockquote>
--<p>We've already met the <code>-jar file.jar</code> arguments.  The next argument is the working folder for
--getdown, and the final argument, &quot;<code>jalview</code>&quot;, is a getdown application id (only &quot;<code>jalview</code>&quot;
--is defined here).</p>
++<p>We've already met the <code>-jar file.jar</code> arguments. The next argument is the working folder for getdown, and the final argument, &quot;<code>jalview</code>&quot;, is a getdown application id (only &quot;<code>jalview</code>&quot; is defined here).</p>
  </blockquote>
--<h3 id="running-tests"><a href="#running-tests" name="running-tests" class="anchor"><span class="octicon octicon-link"></span>Running tests</a></h3>
++<h3 id="running-tests">Running tests</h3>
  <p>There are substantial tests written for Jalview that use TestNG, which you can run with</p>
--<pre><code class="language-bash">gradle test
--</code></pre>
--<p>These normally take around 5 - 10 minutes to complete and outputs its full results into
--the <code>tests/</code> folder.  A summary of results should appear in your console.</p>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> test</code></pre></div>
++<p>These normally take around 5 - 10 minutes to complete and outputs its full results into the <code>tests/</code> folder. A summary of results should appear in your console.</p>
  <p>You can run different defined groups of tests with</p>
--<pre><code class="language-bash">gradle test -PtestngGroups=Network
--</code></pre>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> test -PtestngGroups=Network</code></pre></div>
  <p>Available groups include Functional (default), Network, External.</p>
--<h4 id="excluding-some-tests"><a href="#excluding-some-tests" name="excluding-some-tests" class="anchor"><span class="octicon octicon-link"></span>Excluding some tests</a></h4>
++<h4 id="excluding-some-tests">Excluding some tests</h4>
  <p>Some of Jalview's Functional tests don't pass reliably in all environments. We tag these tests with a group like 'Not-bamboo' to mark them for exclusion when we run tests as part of continuous integration.</p>
  <p>To exclude one or more groups of tests, add them as a comma separated list in testngExcludedGroups.</p>
--<pre><code class="language-bash">gradle test -PtestngExcludedGroups=Not-bamboo
--</code></pre>
--<h3 id="installer-packaging-with-install4j"><a href="#installer-packaging-with-install4j" name="installer-packaging-with-install4j" class="anchor"><span class="octicon octicon-link"></span>Installer packaging with <em>install4j</em></a></h3>
--<p>Jalview is currently using <em>install4j</em> <a href="https://www.ej-technologies.com/products/install4j/overview.html">https://www.ej-technologies.com/products/install4j/overview.html</a>
--as its installer packaging tool.</p>
--<p>If you have a licensed installation of <em>install4j</em> you can build Jalview installers
--by running</p>
--<pre><code class="language-bash">gradle installers
--</code></pre>
--<p>though you may need to fiddle with the <code>install4j</code> and <code>copyInstall4jTemplate</code> tasks
--in <code>build.gradle</code> file to point to your installation of <em>install4j</em> and also to bundled
--JREs if you want to bundle those into the installers.</p>
--<p>If you want more details, get in touch on our development mailing list <a href="mailto:jalview-dev@jalview.org">jalview-dev@jalview.org</a>.
--Sign up at <a href="http://www.compbio.dundee.ac.uk/mailman/listinfo/jalview-dev">http://www.compbio.dundee.ac.uk/mailman/listinfo/jalview-dev</a>.</p>
--<h2 id="gradle-properties"><a href="#gradle-properties" name="gradle-properties" class="anchor"><span class="octicon octicon-link"></span>Gradle properties</a></h2>
--<p>There are a lot of properties configured in <code>gradle.properties</code> which we strongly recommend
--being left as they are unless you have a specific problem with the build process.</p>
--<p>There are a few gradle properties you might want to set on the command line with the
--<code>-P</code> flag when building a version of Jalview with specific requirements:</p>
--<h4 id="java-version"><a href="#java-version" name="java-version" class="anchor"><span class="octicon octicon-link"></span><code>JAVA_VERSION</code></a></h4>
--<p>This changes the <em>target</em> java bytecode version</p>
--<blockquote>
--<p>NOTE that you will need to use a Java 11 (or greater) JDK Java compiler to build
--Jalview for any byte-code target version.</p>
--</blockquote>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> test -PtestngExcludedGroups=Not-bamboo</code></pre></div>
++<h3 id="installer-packaging-with-install4j">Installer packaging with <em>install4j</em></h3>
++<p>Jalview is currently using <em>install4j</em> <a href="https://www.ej-technologies.com/products/install4j/overview.html" class="uri">https://www.ej-technologies.com/products/install4j/overview.html</a> as its installer packaging tool.</p>
++<p>If you have a licensed installation of <em>install4j</em> you can build Jalview installers by running</p>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> installers</code></pre></div>
++<p>though you may need to fiddle with the <code>install4j</code> and <code>copyInstall4jTemplate</code> tasks in <code>build.gradle</code> file to point to your installation of <em>install4j</em> and also to bundled JREs if you want to bundle those into the installers.</p>
++<p>If you want more details, get in touch on our development mailing list <a href="mailto:jalview-dev@jalview.org">jalview-dev@jalview.org</a>. Sign up at <a href="http://www.compbio.dundee.ac.uk/mailman/listinfo/jalview-dev" class="uri">http://www.compbio.dundee.ac.uk/mailman/listinfo/jalview-dev</a>.</p>
++<h2 id="gradle-properties">Gradle properties</h2>
++<p>There are a lot of properties configured in <code>gradle.properties</code> which we strongly recommend being left as they are unless you have a specific problem with the build process.</p>
++<p>There are a few gradle properties you might want to set on the command line with the <code>-P</code> flag when building a version of Jalview with specific requirements:</p>
++<h4 id="java_version"><code>JAVA_VERSION</code></h4>
++<p>This changes the <em>target</em> java bytecode version &gt; NOTE that you will need to use a Java 11 (or greater) JDK Java compiler to build Jalview for any byte-code target version.</p>
  <p>Valid values are <code>11</code> and <code>1.8</code>.</p>
  <p>e.g.</p>
--<pre><code class="language-bash">gradle shadowJar -PJAVA_VERSION=1.8
--</code></pre>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> shadowJar -PJAVA_VERSION=1.8</code></pre></div>
  <p>When using <code>-PJAVA_VERSION=1.8</code> the libraries from <code>j8lib</code> (instead of <code>j11lib</code>) will be used in the compile<br />
--and runtime classpath and also used in the <code>makeDist</code> build step.  Where a Java version of <code>11</code> is used in folder and file names, it will
--instead use <code>1.8</code>.  Also if you are building installer packages with <em>install4j</em> the
--package builder will look for JRE 1.8 bundles to package in the installers.</p>
++and runtime classpath and also used in the <code>makeDist</code> build step. Where a Java version of <code>11</code> is used in folder and file names, it will instead use <code>1.8</code>. Also if you are building installer packages with <em>install4j</em> the package builder will look for JRE 1.8 bundles to package in the installers.</p>
  <blockquote>
--<p>Note that continued development of Jalview will assume a Java 11+ runtime environment,
--the 2.11.0 release will run under a Java 1.8 JRE with a few minor features disabled.</p>
++<p>Note that continued development of Jalview will assume a Java 11+ runtime environment, the 2.11.0 release will run under a Java 1.8 JRE with a few minor features disabled.</p>
  </blockquote>
--<h4 id="channel"><a href="#channel" name="channel" class="anchor"><span class="octicon octicon-link"></span><code>CHANNEL</code></a></h4>
--<p>This changes the <code>appbase</code> setting in <code>getdown.txt</code> (<code>appbase</code> is where the getdown launcher
--looks to see if there's an updated file) to point to a particular Jalview channel or some other appropriate
--place to look for required files.  If the selected channel type requires the getdown <code>appbase</code> to be a local
--directory on the filesystem (instead of a website URL) then a modified version of the <code>getdown-launcher.jar</code> will
--be used to allow this.  The two versions of the <code>getdown-launcher.jar</code> can be found in <code>getdown/lib</code>.
--Some other variables used in the build process might also be set differently depending on the value of <code>CHANNEL</code>
--to allow smooth operation of getdown in the given context.</p>
--<p>There are several values of <code>CHANNEL</code> that can be chosen, with a default of <code>LOCAL</code>.  Here's what they're for and what they do:</p>
++<h4 id="channel"><code>CHANNEL</code></h4>
++<p>This changes the <code>appbase</code> setting in <code>getdown.txt</code> (<code>appbase</code> is where the getdown launcher looks to see if there's an updated file) to point to a particular Jalview channel or some other appropriate place to look for required files. If the selected channel type requires the getdown <code>appbase</code> to be a local directory on the filesystem (instead of a website URL) then a modified version of the <code>getdown-launcher.jar</code> will be used to allow this. The two versions of the <code>getdown-launcher.jar</code> can be found in <code>getdown/lib</code>. Some other variables used in the build process might also be set differently depending on the value of <code>CHANNEL</code> to allow smooth operation of getdown in the given context.</p>
++<p>There are several values of <code>CHANNEL</code> that can be chosen, with a default of <code>LOCAL</code>. Here's what they're for and what they do:</p>
  <ul>
--<li><code>LOCAL</code>: This is for running the compiled application from the development directory.
--It will set
++<li><code>LOCAL</code>: This is for running the compiled application from the development directory. It will set
  <ul>
--<li><code>appbase</code> as <code>file://PATH/TO/YOUR/DEVELOPMENT/getdown/files/JAVA_VERSION</code>
--(e.g. <code>file://home/user/git/jalview/getdown/files/11</code>)</li>
++<li><code>appbase</code> as <code>file://PATH/TO/YOUR/DEVELOPMENT/getdown/files/JAVA_VERSION</code> (e.g. <code>file://home/user/git/jalview/getdown/files/11</code>)</li>
  <li>application subdir as <code>alt</code></li>
  <li>Getdown launcher can use a <code>file://</code> scheme appbase.</li>
--</ul>
--</li>
--<li><code>BUILD</code>: This is for creating an appbase channel on the build server by an automatic or manually started build.
--It will set
++</ul></li>
++<li><code>BUILD</code>: This is for creating an appbase channel on the build server by an automatic or manually started build. It will set
  <ul>
--<li><code>appbase</code> as <code>https://builds.jalview.org/browse/${bamboo_planKey}/latest/artifact/shared/getdown-channel/JAVA_VERSION</code>
--Note that bamboo_planKey should be set by the build plan with <code>-Pbamboo_planKey=${bamboo.planKey}</code></li>
++<li><code>appbase</code> as <code>https://builds.jalview.org/browse/${bamboo_planKey}/latest/artifact/shared/getdown-channel/JAVA_VERSION</code> Note that bamboo_planKey should be set by the build plan with <code>-Pbamboo_planKey=${bamboo.planKey}</code></li>
  <li>application subdir as <code>alt</code></li>
  <li>Getdown launcher cannot use a <code>file://</code> scheme appbase.</li>
--</ul>
--</li>
--<li><code>DEVELOP</code>: This is for creating a <code>develop</code> appbase channel on the main web server. This won't become live until the actual getdown artefact is synced to the web server.
--It will set
++</ul></li>
++<li><code>DEVELOP</code>: This is for creating a <code>develop</code> appbase channel on the main web server. This won't become live until the actual getdown artefact is synced to the web server. It will set
  <ul>
  <li><code>appbase</code> as <code>http://www.jalview.org/getdown/develop/JAVA_VERSION</code></li>
  <li>application subdir as <code>alt</code></li>
  <li>Getdown launcher cannot use a <code>file://</code> scheme appbase.</li>
--</ul>
--</li>
--<li><code>SCRATCH-NAME</code>: This is for creating a temporary scratch appbase channel on the main web server.  This won't become live until the actual getdown artefact is synced to the web server.  This is meant for testing an over-the-air update without interfering with the live <code>release</code> or <code>develop</code> channels.  The value of <code>NAME</code> can be any &quot;word-character&quot; [A-Za-z0-9_]
--It will set
++</ul></li>
++<li><code>SCRATCH-NAME</code>: This is for creating a temporary scratch appbase channel on the main web server. This won't become live until the actual getdown artefact is synced to the web server. This is meant for testing an over-the-air update without interfering with the live <code>release</code> or <code>develop</code> channels. The value of <code>NAME</code> can be any &quot;word-character&quot; [A-Za-z0-9_] It will set
  <ul>
  <li><code>appbase</code> as <code>http://www.jalview.org/getdown/SCRATCH-NAME/JAVA_VERSION</code></li>
  <li>application subdir as <code>alt</code></li>
  <li>Getdown launcher cannot use a <code>file://</code> scheme appbase.</li>
--</ul>
--</li>
--<li><code>TEST-LOCAL</code>:  Like <code>SCRATCH</code> but with a specific <code>test-local</code> channel name and a local filesystem appbase.  This is meant for testing an over-the-air update on the local filesystem.  An extra property <code>LOCALDIR</code> must be given (e.g. <code>-PLOCALDIR=/home/user/tmp/test</code>)
--It will set
++</ul></li>
++<li><code>TEST-LOCAL</code>: Like <code>SCRATCH</code> but with a specific <code>test-local</code> channel name and a local filesystem appbase. This is meant for testing an over-the-air update on the local filesystem. An extra property <code>LOCALDIR</code> must be given (e.g. <code>-PLOCALDIR=/home/user/tmp/test</code>) It will set
  <ul>
  <li><code>appbase</code> as <code>file://${LOCALDIR}</code></li>
  <li>application subdir as <code>alt</code></li>
  <li>Getdown launcher can use a <code>file://</code> scheme appbase.</li>
--</ul>
--</li>
--<li><code>TEST-RELEASE</code>:  Like <code>SCRATCH</code> but with a specific <code>test-release</code> channel name.  This won't become live until the actual getdown artefact is synced to the web server.  This is meant for testing an over-the-air update without interfering with the live <code>release</code> or <code>develop</code> channels.
--It will set
++</ul></li>
++<li><code>TEST-RELEASE</code>: Like <code>SCRATCH</code> but with a specific <code>test-release</code> channel name. This won't become live until the actual getdown artefact is synced to the web server. This is meant for testing an over-the-air update without interfering with the live <code>release</code> or <code>develop</code> channels. It will set
  <ul>
  <li><code>appbase</code> as <code>http://www.jalview.org/getdown/test-release/JAVA_VERSION</code></li>
  <li>application subdir as <code>alt</code></li>
  <li>Getdown launcher cannot use a <code>file://</code> scheme appbase.</li>
--</ul>
--</li>
--<li><code>RELEASE</code>:  This is for an actual release build, and will use an appbase on the main web server with the main <code>release</code> channel name.  This won't become live until the actual getdown artefact is synced to the web server.
--It will set
++</ul></li>
++<li><code>RELEASE</code>: This is for an actual release build, and will use an appbase on the main web server with the main <code>release</code> channel name. This won't become live until the actual getdown artefact is synced to the web server. It will set
  <ul>
  <li><code>appbase</code> as <code>http://www.jalview.org/getdown/release/JAVA_VERSION</code></li>
  <li>application subdir as <code>release</code></li>
  <li>Getdown launcher cannot use a <code>file://</code> scheme appbase.</li>
--</ul>
--</li>
--<li><code>ARCHIVE</code>:  This is a helper to create a channel for a specific release version, and will use an appbase on the main web server with a specific <code>archive/JALVIEW_VERSION</code> channel name.  This won't become live until the actual getdown artefact is synced to the web server.
--You must also specify an <code>ARCHIVEDIR</code> property that points to an earlier version of Jalview with a <code>dist</code> directory containing the required jar files.  This should create a getdown structure and digest with the older jar files.
--It will set
++</ul></li>
++<li><code>ARCHIVE</code>: This is a helper to create a channel for a specific release version, and will use an appbase on the main web server with a specific <code>archive/JALVIEW_VERSION</code> channel name. This won't become live until the actual getdown artefact is synced to the web server. You must also specify an <code>ARCHIVEDIR</code> property that points to an earlier version of Jalview with a <code>dist</code> directory containing the required jar files. This should create a getdown structure and digest with the older jar files. It will set
  <ul>
  <li><code>appbase</code> as <code>http://www.jalview.org/getdown/archive/JALVIEW_VERSION/JAVA_VERSION</code></li>
  <li>application subdir as <code>alt</code></li>
  <li>Getdown launcher cannot use a <code>file://</code> scheme appbase.</li>
--</ul>
--</li>
--<li><code>ARCHIVELOCAL</code>:  Like <code>ARCHIVE</code> but with a local filesystem appbase for local testing.
--You must also specify an <code>ARCHIVEDIR</code> property that points to an earlier version of Jalview with a <code>dist</code> directory containing the required jar files.  This should create a getdown structure and digest with the older jar files.
--It will set
++</ul></li>
++<li><code>ARCHIVELOCAL</code>: Like <code>ARCHIVE</code> but with a local filesystem appbase for local testing. You must also specify an <code>ARCHIVEDIR</code> property that points to an earlier version of Jalview with a <code>dist</code> directory containing the required jar files. This should create a getdown structure and digest with the older jar files. It will set
  <ul>
  <li><code>appbase</code> as <code>file://PATH/TO/YOUR/DEVELOPMENT/getdown/website/JAVA_VERSION</code> (where the old jars will have been copied and digested)</li>
  <li>application subdir as <code>alt</code></li>
  <li>Getdown launcher can use a <code>file://</code> scheme appbase.</li>
--</ul>
--</li>
++</ul></li>
  </ul>
  <p>e.g.</p>
--<pre><code class="language-bash">gradle getdown -PCHANNEL=SCRATCH-my_test_version
--</code></pre>
--<h4 id="jalview-version-and-the-release-file"><a href="#jalview-version-and-the-release-file" name="jalview-version-and-the-release-file" class="anchor"><span class="octicon octicon-link"></span>JALVIEW_VERSION and the RELEASE file</a></h4>
--<p>Any Jalview build will include the value of JALVIEW_VERSION in various places, including the 'About' and Jalview Desktop window title, and in filenames for the stand-alone executable jar. You can specify a custom version for a build via the JALVIEW_VERSION property, but for most situations, JALVIEW_VERSION will be automatically configured according to the value of the CHANNEL property, using the <code>jalview.version</code> property specified in the RELEASE file:</p>
--<ul>
--<li><code>CHANNEL=RELEASE</code> will set version to jalview.version</li>
--<li><code>CHANNEL=TEST or DEVELOP</code> will append '-test' or '-develop' to jalview.version</li>
--</ul>
--<p>It is also possible to specify a custom location for the RELEASE file via an optional JALVIEW_RELEASE_FILE property.</p>
--<h4 id="install4jmediatypes"><a href="#install4jmediatypes" name="install4jmediatypes" class="anchor"><span class="octicon octicon-link"></span><code>install4jMediaTypes</code></a></h4>
--<p>If you are building <em>install4j</em> installers (requires <em>install4j</em> to be installed) then this property specifies a comma-separated
--list of media types (i.e. platform specific installers) <em>install4j</em> should actually build.</p>
--<p>Currently the valid values are
--<code>linuxDeb</code>,
--<code>linuxRPM</code>,
--<code>macosArchive</code>,
--<code>unixArchive</code>,
--<code>unixInstaller</code>,
--<code>windows</code></p>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> getdown -PCHANNEL=SCRATCH-my_test_version</code></pre></div>
++<h4 id="install4jmediatypes"><code>install4jMediaTypes</code></h4>
++<p>If you are building <em>install4j</em> installers (requires <em>install4j</em> to be installed) then this property specifies a comma-separated list of media types (i.e. platform specific installers) <em>install4j</em> should actually build.</p>
++<p>Currently the valid values are <code>linuxDeb</code>, <code>linuxRPM</code>, <code>macosArchive</code>, <code>unixArchive</code>, <code>unixInstaller</code>, <code>windows</code></p>
  <p>The default value is all of them.</p>
  <p>e.g.</p>
--<pre><code class="language-bash">gradle installers -PJAVA_VERSION=1.8 -Pinstall4jMediaTypes=macosArchive
--</code></pre>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> installers -PJAVA_VERSION=1.8 -Pinstall4jMediaTypes=macosArchive</code></pre></div>
  <p>To get an up-to-date list of possible values, you can run</p>
--<pre><code class="language-bash">perl -n -e 'm/^\s*&lt;(\w+)[^&gt;]*\bmediaFileName=/ &amp;&amp; print &quot;$1\n&quot;;' utils/install4j/install4j_template.install4j  | sort -u
--</code></pre>
++<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">perl</span> -n -e <span class="st">&#39;m/^\s*&lt;(\w+)[^&gt;]*\bmediaFileName=/ &amp;&amp; print &quot;$1\n&quot;;&#39;</span> utils/install4j/install4j_template.install4j  <span class="kw">|</span> <span class="fu">sort</span> -u</code></pre></div>
  <p>in the <code>jalview</code> root folder.</p>
--<h2 id="enabling-code-coverage-with-openclover"><a href="#enabling-code-coverage-with-openclover" name="enabling-code-coverage-with-openclover" class="anchor"><span class="octicon octicon-link"></span>Enabling Code Coverage with OpenClover</a></h2>
++<h2 id="enabling-code-coverage-with-openclover">Enabling Code Coverage with OpenClover</h2>
  <p>Bytecode instrumentation tasks are enabled by specifying 'true' (or just a non-whitespace non-numeric word) in the 'clover' property. This adds the 'openclover' plugin to the build script's classpath, making it possible to track code execution during test which can be viewed as an HTML report published at build/reports/clover/index.html.</p>
  <p><code>gradle -Pclover=true test cloverReport</code></p>
--<h4 id="troubleshooting-report-generation"><a href="#troubleshooting-report-generation" name="troubleshooting-report-generation" class="anchor"><span class="octicon octicon-link"></span>Troubleshooting report generation</a></h4>
++<h4 id="troubleshooting-report-generation">Troubleshooting report generation</h4>
  <p>The build forks a new JVM process to run the clover report generation tools (both XML and HTML reports are generated by default). The following properties can be used to specify additional options or adjust JVM memory settings. Default values for these options are:</p>
--<h5 id="jvm-memory-settings---increase-if-out-of-memory-errors-are-reported"><a href="#jvm-memory-settings---increase-if-out-of-memory-errors-are-reported" name="jvm-memory-settings---increase-if-out-of-memory-errors-are-reported" class="anchor"><span class="octicon octicon-link"></span>JVM Memory settings - increase if out of memory errors are reported</a></h5>
++<h5 id="jvm-memory-settings---increase-if-out-of-memory-errors-are-reported">JVM Memory settings - increase if out of memory errors are reported</h5>
  <p><code>cloverReportJVMHeap = 2g</code></p>
--<h5 id="-dfileencodingutf-8-is-an-essential-parameters-for-report-generation-add-additional-ones-separated-by-a-space"><a href="#-dfileencodingutf-8-is-an-essential-parameters-for-report-generation-add-additional-ones-separated-by-a-space" name="-dfileencodingutf-8-is-an-essential-parameters-for-report-generation-add-additional-ones-separated-by-a-space" class="anchor"><span class="octicon octicon-link"></span>-Dfile.encoding=UTF-8 is an essential parameters for report generation. Add additional ones separated by a space.</a></h5>
++<h5 id="dfile.encodingutf-8-is-an-essential-parameters-for-report-generation.-add-additional-ones-separated-by-a-space.">-Dfile.encoding=UTF-8 is an essential parameters for report generation. Add additional ones separated by a space.</h5>
  <p><code>cloverReportJVMArgs = -Dfile.encoding=UTF-8</code></p>
--<h5 id="add--v-to-debug-velocity-html-generation-errors-or--d-to-track-more-detailed-issues-with-the-coverage-database"><a href="#add--v-to-debug-velocity-html-generation-errors-or--d-to-track-more-detailed-issues-with-the-coverage-database" name="add--v-to-debug-velocity-html-generation-errors-or--d-to-track-more-detailed-issues-with-the-coverage-database" class="anchor"><span class="octicon octicon-link"></span>Add -v to debug velocity html generation errors, or -d to track more detailed issues with the coverage database</a></h5>
++<h5 id="add--v-to-debug-velocity-html-generation-errors-or--d-to-track-more-detailed-issues-with-the-coverage-database">Add -v to debug velocity html generation errors, or -d to track more detailed issues with the coverage database</h5>
  <p><code>cloverReportHTMLOptions =</code></p>
--<h5 id="-v-for-verbose--d-for-debug-level-messages-as-above"><a href="#-v-for-verbose--d-for-debug-level-messages-as-above" name="-v-for-verbose--d-for-debug-level-messages-as-above" class="anchor"><span class="octicon octicon-link"></span>-v for verbose, -d for debug level messages (as above)</a></h5>
++<h5 id="v-for-verbose--d-for-debug-level-messages-as-above">-v for verbose, -d for debug level messages (as above)</h5>
  <p><code>cloverReportXMLOptions =</code></p>
  <p><em>Note</em> do not forget to include the -Dfile.encoding=UTF-8 option: this is essential for some platforms in order for Clover to correctly parse some Jalview source files that contain characters that are UTF-8 encoded.</p>
--<h2 id="setting-up-in-eclipse-ide"><a href="#setting-up-in-eclipse-ide" name="setting-up-in-eclipse-ide" class="anchor"><span class="octicon octicon-link"></span>Setting up in Eclipse IDE</a></h2>
--<h3 id="installing-eclipse-ide"><a href="#installing-eclipse-ide" name="installing-eclipse-ide" class="anchor"><span class="octicon octicon-link"></span>Installing Eclipse IDE</a></h3>
--<p>We develop in Eclipse, and support settings to develop and save Jalview source code
--in our preferred style.  We also support running the Jalview application, debugging
--and running tests with TestNG from within Eclipse.</p>
- <p>To get Jalview set up as a project in Eclipse, we recommend using at least the 2019-12
- version of Eclipse IDE for Java Developers which you can download from the Eclipse
- website: <a href="https://www.eclipse.org/downloads/">https://www.eclipse.org/downloads/</a>.  Since Eclipse 2020-03 you are encouraged to use the Eclipse Installer (see the Eclipse Downloads page).
- In the installer, when given a choice of packages for Eclipse you should choose the &quot;Eclipse IDE for Enterprise Java Developers&quot; package.</p>
- <p><img src="./images/eclipse_installer.png" alt="" title="Eclipse Installer screenshot" /></p>
 -<p>To get Jalview set up as a project in Eclipse, we recommend using at least the 2020-03
 -version of Eclipse IDE for Java Developers which you can download from the Eclipse
 -website: <a href="https://www.eclipse.org/downloads/">https://www.eclipse.org/downloads/</a>.  Since Eclipse 2020-03 you are encouraged to use the Eclipse Installer (see the Eclipse Downloads page).
 -In the installer, when given a choice of packages for Eclipse you should choose the &quot;Eclipse IDE for Enterprise Java Developers&quot; package.</p>
 -<p><img src="./images/eclipse_installer.png" alt="" title="Eclipse Installer screenshot" /></p>
++<h2 id="setting-up-in-eclipse-ide">Setting up in Eclipse IDE</h2>
++<h3 id="installing-eclipse-ide">Installing Eclipse IDE</h3>
++<p>We develop in Eclipse, and support settings to develop and save Jalview source code in our preferred style. We also support running the Jalview application, debugging and running tests with TestNG from within Eclipse.</p>
++<p>To get Jalview set up as a project in Eclipse, we recommend using at least the 2019-12 version of Eclipse IDE for Java Developers which you can download from the Eclipse website: <a href="https://www.eclipse.org/downloads/" class="uri">https://www.eclipse.org/downloads/</a>. Since Eclipse 2020-03 you are encouraged to use the Eclipse Installer (see the Eclipse Downloads page). In the installer, when given a choice of packages for Eclipse you should choose the &quot;Eclipse IDE for Enterprise Java Developers&quot; package.</p>
++<div class="figure">
++<img src="./images/eclipse_installer.png" title="Eclipse Installer screenshot" />
++
++</div>
  <p>Once Eclipse is installed, we also recommend installing several plugins from the Eclipse Marketplace.</p>
  <p>Some of these should already be installed with the Enterprise Java Developer package:</p>
--<ol>
++<ol style="list-style-type: decimal">
  <li>Buildship Gradle Integration 3.0 (or greater)</li>
  <li>EclEmma Java Code Coverage</li>
  <li>Egit - Git Integration for Eclipse</li>
  </ol>
  <p>To install the others, launch Eclipse, and go to Help-&gt;Eclipse Marketplace...</p>
  <p>Search for and install:</p>
--<ol>
++<ol style="list-style-type: decimal">
  <li>Groovy Development Tools 3.4.0 (or greater)</li>
  <li>Checkstyle Plug-in (optional)</li>
  <li>TestNG for Eclipse (optional -- only needed if you want to run tests from Eclipse)</li>
  </ol>
  <blockquote>
--<p>At time of writing, TestNG for Eclipse does not show up in the Eclipse Marketplace
- as the latest released version does not install in Eclipse 2019-03.
- However, you can install a working release of TestNG for Eclipse by going to</p>
 -as the latest released version does not install in Eclipse 2020-03.
 -However, you can install a working release of TestNG for Eclipse by going to</p>
++<p>At time of writing, TestNG for Eclipse does not show up in the Eclipse Marketplace as the latest released version does not install in Eclipse 2019-03. However, you can install a working release of TestNG for Eclipse by going to</p>
  <p>Help-&gt;Install New Software...</p>
  <p>and entering</p>
  <p><code>TestNG Release - https://dl.bintray.com/testng-team/testng-eclipse-release</code></p>
  <p>into the <em>Work with</em> box and click on the <em>Add...</em> button.</p>
--<p>Eclipse might pause for a bit with the word <em>Pending</em> in the table below at this point, but it will eventually list TestNG with
--a selection box under the <em>Name</em> column.</p>
--<p>Select <em>TestNG</em> and carry on through the
--install process to install the TestNG plugin.</p>
++<p>Eclipse might pause for a bit with the word <em>Pending</em> in the table below at this point, but it will eventually list TestNG with a selection box under the <em>Name</em> column.</p>
++<p>Select <em>TestNG</em> and carry on through the install process to install the TestNG plugin.</p>
  </blockquote>
  <p>After installing the plugins, check that Java 11 is set up in Eclipse as the default JRE (see section <a href="#java-11-compliant-jdk">Java 11 compliant JDK</a>).</p>
--<p>To do this go to Preferences (Eclipse-&gt;Preferences in macOS, File-&gt;Preferences
--on Windows or Window-&gt;Preferences on Linux) and find</p>
++<p>To do this go to Preferences (Eclipse-&gt;Preferences in macOS, File-&gt;Preferences on Windows or Window-&gt;Preferences on Linux) and find</p>
  <p>Java -&gt; Installed JREs</p>
  <p>If your Java 11 installation is not listed, click on</p>
  <p><em>Add</em> -&gt; Standard VM -&gt; <em>Next</em></p>
--<p>and enter the JRE home.  You can browse to where it is installed. Give it a name (like &quot;AdoptOpenJDK 11&quot;).  Select this JDK
--as the default JRE and click on <em>Apply and Close</em>.</p>
++<p>and enter the JRE home. You can browse to where it is installed. Give it a name (like &quot;AdoptOpenJDK 11&quot;). Select this JDK as the default JRE and click on <em>Apply and Close</em>.</p>
  <p>You can now import Jalview.</p>
--<h3 id="importing-jalview-as-an-eclipse-project"><a href="#importing-jalview-as-an-eclipse-project" name="importing-jalview-as-an-eclipse-project" class="anchor"><span class="octicon octicon-link"></span>Importing Jalview as an Eclipse project</a></h3>
--<h4 id="importing-an-already-downloaded-git-repo"><a href="#importing-an-already-downloaded-git-repo" name="importing-an-already-downloaded-git-repo" class="anchor"><span class="octicon octicon-link"></span>Importing an already downloaded git repo</a></h4>
++<h3 id="importing-jalview-as-an-eclipse-project">Importing Jalview as an Eclipse project</h3>
++<h4 id="importing-an-already-downloaded-git-repo">Importing an already downloaded git repo</h4>
  <p>If you have already downloaded Jalview using <code>git clone</code> then you can import this folder into Eclipse directly.</p>
- <p>It is important to import
- Jalview as a Gradle project (not as a Java project), so go to</p>
 -<p><strong>Before importing the cloned git repo you must create the Eclipse project files.</strong> You can do this by either running</p>
 -<p><code>gradle eclipse</code></p>
 -<p>or</p>
 -<p>Unzipping the file <code>utils/eclipse/eclipse_startup_files.zip</code> in the base repo directory (<code>jalview</code>)</p>
 -<p>It is important to import
 -Jalview as a Gradle project (not as a Java project), so go to</p>
++<p>It is important to import Jalview as a Gradle project (not as a Java project), so go to</p>
  <p>File-&gt;Import...</p>
  <p>find and select</p>
  <p>Gradle-&gt;Existing Gradle Project</p>
  <p>and then click on the <em>Next</em> button.</p>
--<p>In the following options, it is the <strong>Project Root Directory</strong> you should set to be the
--<code>jalview</code> folder that git downloaded.  Then you can click on the <em>Finish</em> button.</p>
--<h4 id="using-eclipse-ide-to-download-the-git-repo"><a href="#using-eclipse-ide-to-download-the-git-repo" name="using-eclipse-ide-to-download-the-git-repo" class="anchor"><span class="octicon octicon-link"></span>Using Eclipse IDE to download the git repo</a></h4>
--<p>If you don't have git as a command line tool or would prefer to work entirely within Eclipse IDE then
--Eclipse's eGit plugin can set up a git repo of the jalview source.  Go to</p>
++<p>In the following options, it is the <strong>Project Root Directory</strong> you should set to be the <code>jalview</code> folder that git downloaded. Then you can click on the <em>Finish</em> button.</p>
++<h4 id="using-eclipse-ide-to-download-the-git-repo">Using Eclipse IDE to download the git repo</h4>
++<p>If you don't have git as a command line tool or would prefer to work entirely within Eclipse IDE then Eclipse's eGit plugin can set up a git repo of the jalview source. Go to</p>
  <p>File-&gt;Import...</p>
  <p>Find and select</p>
  <p>Git-&gt;Projects from Git</p>
  <p>and then click on the <em>Next</em> button.</p>
  <p>Then select Clone URI and click on <em>Next</em>.</p>
--<p>In the next window (Source Git Repository) you should put the <code>git clone</code> URL in the text box labelled <code>URI</code>.  If you have a Jalview developer account (with a username and password for the Jalview git repository) then you should enter
--<code>https://source.jalview.org/git/jalview.git</code>.
--If you do not have a Jalview developer account then you should enter
--<code>http://source.jalview.org/git/jalview.git</code>.
--You will not be able to push any of your changes back to the Jalview git repository. However you can still pull all branches of the Jalview source code to your computer and develop the code there.</p>
--<blockquote>
--<p>You can sign up for a Jalview developer account at <a href="https://source.jalview.org/crucible/">https://source.jalview.org/crucible/</a></p>
--</blockquote>
--<p>If you have a Jalview developer account, enter the username and password and decide if you want to use Eclipse's secure storage.  If you don't have an account you can leave the Authentication section blank.</p>
--<p><img src="./images/eclipse_egit_connection.png" alt="Eclipse eGit connection configuration" /></p>
++<p>In the next window (Source Git Repository) you should put the <code>git clone</code> URL in the text box labelled <code>URI</code>. If you have a Jalview developer account (with a username and password for the Jalview git repository) then you should enter <code>https://source.jalview.org/git/jalview.git</code>. If you do not have a Jalview developer account then you should enter <code>http://source.jalview.org/git/jalview.git</code>. You will not be able to push any of your changes back to the Jalview git repository. However you can still pull all branches of the Jalview source code to your computer and develop the code there. &gt; You can sign up for a Jalview developer account at <a href="https://source.jalview.org/crucible/" class="uri">https://source.jalview.org/crucible/</a></p>
++<p>If you have a Jalview developer account, enter the username and password and decide if you want to use Eclipse's secure storage. If you don't have an account you can leave the Authentication section blank.</p>
++<div class="figure">
++<img src="./images/eclipse_egit_connection.png" alt="Eclipse eGit connection configuration" />
++<p class="caption">Eclipse eGit connection configuration</p>
++</div>
  <p>Click on the <em>Next</em> button.</p>
--<p>The next window (Branch Selection) gives a list of the many Jalview branches, which by default will be all checked.  You probably only want to download one branch (you can always download others at a later time).  This is likely to be the <code>develop</code> branch so you can click on the <em>Deselect All</em> button, find the <code>develop</code> branch (the filter text helps), select that, and then click on the <em>Next</em> button.</p>
--<p>Choose a directory to your copy of the git repo in, and leave the other options as they are and click on the <em>Next</em> button.  The next stage may take a minute or two as it checks out the selected branch(es) from the Jalview git repository.</p>
--<p>When it has finished it is important to select <strong>Import as general project</strong> and then click on <em>Next</em>.</p>
--<blockquote>
--<p>Ideally there would be an <em>Import as gradle project</em> here but there isn't -- we'll sort that out later.</p>
--</blockquote>
--<p><img src="./images/eclipse_egit_import.png" alt="Eclipse eGit import choice" /></p>
++<p>The next window (Branch Selection) gives a list of the many Jalview branches, which by default will be all checked. You probably only want to download one branch (you can always download others at a later time). This is likely to be the <code>develop</code> branch so you can click on the <em>Deselect All</em> button, find the <code>develop</code> branch (the filter text helps), select that, and then click on the <em>Next</em> button.</p>
++<p>Choose a directory to your copy of the git repo in, and leave the other options as they are and click on the <em>Next</em> button. The next stage may take a minute or two as it checks out the selected branch(es) from the Jalview git repository.</p>
++<p>When it has finished it is important to select <strong>Import as general project</strong> and then click on <em>Next</em>. &gt; Ideally there would be an <em>Import as gradle project</em> here but there isn't -- we'll sort that out later.</p>
++<div class="figure">
++<img src="./images/eclipse_egit_import.png" alt="Eclipse eGit import choice" />
++<p class="caption">Eclipse eGit import choice</p>
++</div>
  <p>Click on the <em>Next</em> button.</p>
--<p>You can change the project name here.  By default it will show as <strong>jalview</strong> which is fine unless you have another instance of the a Jalview project also called jalview, in which case you could change this project's name now to avoid a conflict within Eclipse.</p>
++<p>You can change the project name here. By default it will show as <strong>jalview</strong> which is fine unless you have another instance of the a Jalview project also called jalview, in which case you could change this project's name now to avoid a conflict within Eclipse.</p>
  <p>Click on <em>Finish</em>!</p>
  <p>However, we haven't finished...</p>
--<p>You should now see, and be able to expand, the jalview project in the Project Explorer.  We need to tell eclipse that this is a Gradle project, which will then allow the Eclipse Buildship plugin to automatically configure almost everything else!</p>
++<p>You should now see, and be able to expand, the jalview project in the Project Explorer. We need to tell eclipse that this is a Gradle project, which will then allow the Eclipse Buildship plugin to automatically configure almost everything else!</p>
  <p>Right click on the project name (jalview) in the Project Explorer and find Configure towards the bottom of this long context menu, then choose Add Gradle Nature.</p>
--<p><img src="./images/eclipse_add_gradle_nature.png" alt="Eclipse Add Gradle Nature" /></p>
++<div class="figure">
++<img src="./images/eclipse_add_gradle_nature.png" alt="Eclipse Add Gradle Nature" />
++<p class="caption">Eclipse Add Gradle Nature</p>
++</div>
  <p>The project should now reconfigure itself using the <code>build.gradle</code> file to dynamically set various aspects of the project including classpath.</p>
--<h4 id="additional-views"><a href="#additional-views" name="additional-views" class="anchor"><span class="octicon octicon-link"></span>Additional views</a></h4>
--<p>Some views that are automatically added when Importing a Gradle Project are not added when simply Adding a Gradle Nature, but we can add these manually by clicking on
--Window-&gt;Show View-&gt;Console
--and
--Window-&gt;Show View-&gt;Other...
--Filter with the word &quot;gradle&quot; and choose both <strong>Gradle Executions</strong> and <strong>Gradle Tasks</strong> and then click on the <em>Open</em> button.</p>
--<p>Okay, ready to code!  Use of Eclipse is beyond the scope of this document, but you can find more information about developing jalview and our developer workflow in the google doc <a href="https://docs.google.com/document/d/1lZo_pZRkazDBJGNachXr6qCVlw8ByuMYG6e9SZlPUlQ/edit?usp=sharing">https://docs.google.com/document/d/1lZo_pZRkazDBJGNachXr6qCVlw8ByuMYG6e9SZlPUlQ/edit?usp=sharing</a></p>
++<h4 id="additional-views">Additional views</h4>
++<p>Some views that are automatically added when Importing a Gradle Project are not added when simply Adding a Gradle Nature, but we can add these manually by clicking on Window-&gt;Show View-&gt;Console and Window-&gt;Show View-&gt;Other... Filter with the word &quot;gradle&quot; and choose both <strong>Gradle Executions</strong> and <strong>Gradle Tasks</strong> and then click on the <em>Open</em> button.</p>
++<p>Okay, ready to code! Use of Eclipse is beyond the scope of this document, but you can find more information about developing jalview and our developer workflow in the google doc <a href="https://docs.google.com/document/d/1lZo_pZRkazDBJGNachXr6qCVlw8ByuMYG6e9SZlPUlQ/edit?usp=sharing" class="uri">https://docs.google.com/document/d/1lZo_pZRkazDBJGNachXr6qCVlw8ByuMYG6e9SZlPUlQ/edit?usp=sharing</a></p>
  <hr />
  <p><a href="mailto:help@jalview.org">Jalview Development Team</a></p>
--
--  </body>
++</body>
  </html>
@@@ -113,6 -111,8 +111,7 @@@ j11modules = com.sun.istack.runtime,com
  
  flexmark_css = utils/doc/github.css
  
+ channel_properties_dir = utils/channels
 -
  install4j_home_dir = ~/buildtools/install4j8
  install4j_copyright_message = ...
  install4j_bundle_id = org.jalview.jalview-desktop
@@@ -130,7 -142,9 +141,10 @@@ getdown_batch_wrapper_script = jalview.
  OSX_KEYSTORE =
  OSX_KEYPASS =
  JSIGN_SH = echo
 +
+ OSX_APPLEID =
+ OSX_ALTOOLPASS =
  CHANNEL=LOCAL
  getdown_channel_base = https://www.jalview.org/getdown
  getdown_app_dir_release = release
@@@ -163,7 -177,7 +177,6 @@@ jalviewjs_j2s_alt_settings = build/jalv
  # these are used for actual .j2s file to pass on to alt j2s file
  jalviewjs_j2s_settings = .j2s
  jalviewjs_j2s_alt_file_property_config = j2s.config.altfileproperty
--
  # for developing in Eclipse as IDE, set this to automatically copy current swingjs/net.sf.j2s.core.jar to your dropins dir
  jalviewjs_eclipseIDE_auto_copy_j2s_plugin = false
  # Override this in a local.properties file
Simple merge
Simple merge
@@@ -57,21 -58,352 +57,6 @@@ li:before 
      </tr>
      <tr>
        <td width="60" align="center" nowrap><strong><a
-           id="Jalview.2.11.1">2.11.1</a><a id="Jalview.2.11.1.1">.1</a><br />
-           <em>13/07/2020</em></strong></td>
-       <td align="left" valign="top">
-         <ul>
-         <!-- -->
-       </ul>
-       </td>
-       <td align="left" valign="top">
-         <ul>
-          <li><!-- JAL-3493 -->Escape does not clear highlights on the alignment (Since Jalview 2.10.3)</li>
-       </ul>
-       </td>
-     </tr>
-     <tr>
-       <td width="60" align="center" nowrap><strong><a
 -          id="Jalview.2.11.2">2.11.2</a><a id="Jalview.2.11.2.0">.0</a><br />
 -          <em>29/09/2021</em></strong></td>
 -      <td align="left" valign="top">
 -      <ul>
 -        <li><!-- --></li>
 -      </ul>
 -      <em>Development</em>
 -        <ul>
 -          <li>Updated building instructions</li>
 -        </ul></td>
 -      <td>
 -        <ul>
 -          <li>
 -            <!-- JAL-3840 -->Occupancy calculation is incorrect for
 -            alignment columns with over -1+2^32 gaps (breaking filtering
 -            and display)
 -          </li>
 -          <li>
 -            <!-- JAL-3833 -->Caps on Hi-DPI scaling to prevent crazy
 -            scale factors being set with buggy window-managers (linux
 -            only)
 -          </li>
 -          <li>
 -              <!-- JAL-3915 -->Removed RNAview checkbox and logic from Structure Preferences
 -          </li>
 -        </ul> <em>Development</em>
 -        <ul>
 -          <li>Fixed non-fatal gradle errors during build</li>
 -        </ul>
 -      </td>
 -    </tr>
 -    <tr>
 -      <td width="60" align="center" nowrap><strong><a
 -          id="Jalview.2.11.1">2.11.1</a><a id="Jalview.2.11.1.4">.4</a><br />
 -          <em>09/03/2021</em></strong></td>
 -      <td align="left" valign="top"><em>Improved control of
 -          Jalview's use of network services via jalview_properties</em>
 -        <ul>
 -          <li>
 -            <!-- JAL-3814 -->New .jalview_properties token controlling
 -            launch of the news browser (like -nonews argument)
 -          </li>
 -          <li>
 -            <!-- JAL-3813 -->New .jalview_properties token controlling
 -            download of linkout URLs from
 -            www.jalview.org/services/identifiers
 -          </li>
 -          <li>
 -            <!-- JAL-3812 -->New .jalview_properties token controlling
 -            download of BIOJSHTML templates
 -          </li>
 -          <li>
 -            <!-- JAL-3811 -->New 'Discover Web Services' option to
 -            trigger a one off JABAWS discovery if autodiscovery was
 -            disabled
 -          </li>
 -        </ul></td>
 -      <td align="left" valign="top">
 -        <ul>
 -          <li>
 -            <!-- JAL-3818 -->Intermittent deadlock opening structure in
 -            Jmol
 -          </li>
 -        </ul> <em>New Known defects</em>
 -        <ul>
 -          <li>
 -            <!-- JAL-3705 -->Protein Cross-Refs for Gene Sequence not
 -            always restored from project (since 2.10.3)
 -          </li>
 -          <li>
 -            <!-- JAL-3806 -->Selections from tree built from CDS aren't
 -            propagated to Protein alignment (since 2.11.1.3)
 -          </li>
 -        </ul>
 -      </td>
 -    </tr>
 -    <tr>
 -      <td width="60" align="center" nowrap><strong><a
 -          id="Jalview.2.11.1">2.11.1</a><a id="Jalview.2.11.1.3">.3</a><br />
 -          <em>29/10/2020</em></strong></td>
 -      <td align="left" valign="top">
 -        <ul>
 -
 -        </ul>
 -      </td>
 -      <td align="left" valign="top">
 -        <ul>
 -          <li>
 -            <!-- JAL-3765 -->Find doesn't always highlight all matching
 -            positions in a sequence (bug introduced in 2.11.1.2)
 -          </li>
 -          <li>
 -            <!-- JAL-3760 -->Alignments containing one or more protein
 -            sequences can be classed as nucleotide
 -          </li>
 -          <li>
 -            <!-- JAL-3748 -->CDS alignment doesn't match original CDS
 -            sequences after alignment of protein products (known defect
 -            first reported for 2.11.1.0)
 -          </li>
 -          <li>
 -            <!-- JAL-3725 -->No tooltip or popup menu for genomic
 -            features outwith CDS shown overlaid on protein
 -          </li>
 -          <li>
 -            <!-- JAL-3751 -->Overlapping CDS in ENA accessions are not
 -            correctly mapped by Jalview (e.g. affects viral CDS with
 -            ribosomal slippage, since 2.9.0)
 -          </li>
 -          <li>
 -            <!-- JAL-3763 -->Spliced transcript CDS sequences don't show
 -            CDS features
 -          </li>
 -          <li>
 -            <!-- JAL-3700 -->Selections in CDS sequence panel don't
 -            always select corresponding protein sequences
 -          </li>
 -          <li>
 -            <!-- JAL-3759 --> <em>Make groups from selection</em> for a
 -            column selection doesn't always ignore hidden columns
 -          </li>
 -        </ul> <em>Installer</em>
 -        <ul>
 -          <li>
 -            <!-- JAL-3611 -->Space character in Jalview install path on
 -            Windows prevents install4j launching getdown
 -          </li>
 -        </ul> <em>Development</em>
 -        <ul>
 -          <li>
 -            <!-- JAL-3248 -->Fixed typos and specified compatible gradle
 -            version numbers in doc/building.md
 -          </li>
 -        </ul>
 -      </td>
 -    </tr>
 -    <tr>
 -      <td width="60" align="center" nowrap><strong><a
 -          id="Jalview.2.11.1">2.11.1</a><a id="Jalview.2.11.1.2">.2</a><br />
 -          <em>25/09/2020</em></strong></td>
 -      <td align="left" valign="top">
 -        <ul>
 -        </ul>
 -      </td>
 -      <td align="left" valign="top">
 -        <ul>
 -          <li>
 -            <!-- JAL-3757 -->Fresh install of Jalview 2.11.1.1 reports
 -            "Encountered problems opening
 -            https://www.jalview.org/examples/exampleFile_2_7.jvp"
 -          </li>
 -        </ul>
 -      </td>
 -    </tr>
 -    <tr>
 -      <td width="60" align="center" nowrap><strong><a
 -          id="Jalview.2.11.1">2.11.1</a><a id="Jalview.2.11.1.1">.1</a><br />
 -          <em>17/09/2020</em></strong></td>
 -      <td align="left" valign="top">
 -        <ul>
 -          <li>
 -            <!-- JAL-3638 -->Shift+arrow keys navigate to next gap or
 -            residue in cursor mode
 -          </li>
 -          <li>
 -            <!-- JAL-3695 -->Support import of VCF 4.3 by updating
 -            HTSJDK from 2.12 to 2.23
 -          </li>
 -          <li>
 -            <!-- JAL-3621 -->IntervalStore library updated to v.1.1:
 -            optimisations and improvements suggested by Bob Hanson and
 -            improved compatibility with JalviewJS
 -          </li>
 -          <li>
 -            <!-- JAL-3615 -->Retrieve GZipped stockholm formatted
 -            alignments from Pfam and Rfam
 -          </li>
 -          <li>
 -            <!-- JAL-2656 -->Recognise GZipped content for URLs and File
 -            import (no longer based on .gz extension)
 -          </li>
 -          <li>
 -            <!-- JAL-3570 -->Updated Spanish Translation for 2.11.1
 -          </li>
 -          <li>
 -            <!-- JAL-3692 -->Migrate EMBL record retrieval to use latest
 -            ENA Browser (https://www.ebi.ac.uk/ena/browser/home) and
 -            EMBL flat file
 -          </li>
 -          <li>
 -            <!-- JAL-3667 -->Improved warning messages, debug logging
 -            and fixed Retry action when Jalview encounters errors when
 -            saving or making backup files.
 -          </li>
 -          <li>
 -            <!-- JAL-3676 -->Enhanced Jalview Java Console:
 -            <ul>
 -              <li>Jalview's logging level can be configured</li>
 -              <li>Copy to Clipboard Buttion</li>
 -            </ul>
 -          </li>
 -          <li>
 -            <!-- JAL-3541 -->Improved support for Hi-DPI (4K) screens
 -            when running on Linux (Requires Java 11+)
 -          </li>
 -        </ul> <em>Launching Jalview</em>
 -        <ul>
 -          <li>
 -            <!-- JAL-3608 -->Configure Jalview Desktop's look and feel
 -            through a system property
 -          </li>
 -          <li>
 -            <!-- JAL-3477 -->Improved built-in documentation and command
 -            line help for configuring Jalview's memory
 -          </li>                   
 -        </ul>
 -      </td>
 -      <td align="left" valign="top">
 -        <ul>
 -          <li>
 -            <!-- JAL-3691 -->Conservation and Quality tracks are shown
 -            but not calculated and no protein or DNA score models are
 -            available for tree/PCA calculation when launched with
 -            Turkish language locale
 -          </li>
 -          <li>
 -            <!-- JAL-3493 -->Escape does not clear highlights on the
 -            alignment (Since Jalview 2.10.3)
 -          </li>
 -          <li>
 -            <!--  JAL-3680 -->Alt+Left or Right arrow in cursor mode
 -            doesn't slide selected sequences, just sequence under cursor
 -          </li>
 -          <li>
 -            <!-- JAL-3732 -->Alt+Up/Down in cursor mode doesn't move
 -            sequence under the cursor
 -          </li>
 -          <li>
 -            <!-- JAL-3613 -->Peptide-to-CDS tracking broken when
 -            multiple EMBL gene products shown for a single contig
 -          </li>
 -          <li>
 -            <!-- JAL-3696 -->Errors encountered when processing variants
 -            from VCF files yield "Error processing VCF: Format specifier
 -            '%s'" on the console
 -          </li>
 -          <li>
 -            <!-- JAL-3697 -->Count of features not shown can be wrong
 -            when there are both local and complementary features mapped
 -            to the position under the cursor
 -          </li>
 -          <li>
 -            <!-- JAL-3673 -->Sequence ID for reference sequence is
 -            clipped when Right align Sequence IDs enabled
 -          </li>
 -          <li>
 -            <!-- JAL-2983 -->Slider with negative range values not
 -            rendered correctly in VAqua4 (Since 2.10.4)
 -          </li>
 -          <li>
 -            <!-- JAL-3685 -->Single quotes not displayed correctly in
 -            internationalised text for some messages and log output
 -          </li>
 -          <li>
 -            <!-- JAL-3490 -->Find doesn't report matches that span
 -            hidden gapped columns
 -          </li>
 -          <li>
 -            <!-- JAL-3597 -->Resolved memory leaks in Tree and PCA
 -            panels, Alignment viewport and annotation renderer.
 -          </li>
 -          <li>
 -            <!-- JAL-3561 -->Jalview ignores file format parameter
 -            specifying output format when exporting an alignment via the
 -            command line
 -          </li>
 -          <li>
 -            <!-- JAL-3667 -->Windows 10: For a minority of users, if
 -            backups are not enabled, Jalview sometimes fails to
 -            overwrite an existing file and raises a warning dialog. (in
 -            2.11.0, and 2.11.1.0, the workaround is to try to save the
 -            file again, and if that fails, delete the original file and
 -            save in place.)
 -          </li>
 -          <li>
 -            <!-- JAL-3750 -->Cannot process alignments from HTTPS urls
 -            via command line
 -          </li>
 -          <li>
 -            <!-- JAL-3741 -->References to http://www.jalview.org in
 -            program and documentation
 -          </li>
 -        </ul> <em>Launching Jalview</em>
 -        <ul>
 -          <li>
 -            <!-- JAL-3718 -->Jalview application fails when launched the
 -            first time for a version that has different jars to the
 -            previous launched version.
 -          </li>
 -        </ul> <em>Developing Jalview</em>
 -        <ul>
 -          <li>
 -            <!-- JAL-3541 -->Fixed issue with cleaning up old coverage
 -            data, causing cloverReport gradle task to fail with an
 -            OutOfMemory error.
 -          </li>
 -          <li>
 -            <!-- JAL-3280 -->Migrated the Jalview Version Checker to
 -            monitor the release channel
 -          </li>
 -        </ul> <em>New Known defects</em>
 -        <ul>
 -          <li>
 -            <!-- JAL-3748 -->CDS shown in result of submitting proteins
 -            in a CDS/Protein alignment to a web service is wrong when
 -            proteins share a common transcript sequence (e.g.
 -            genome of RNA viruses)
 -          </li>
 -          <li>
 -            <!-- JAL-3576 -->Co-located features exported and re-imported
 -            are ordered differently when shown on alignment and in
 -            tooltips. (Also affects v2.11.1.0)
 -          </li>
 -          <li>
 -            <!-- JAL-3702 -->Drag and drop of alignment file onto
 -            alignment window when in a HiDPI scaled mode in Linux only
 -            works for the top left quadrant of the alignment window
 -          </li>
 -          <li>
 -            <!-- JAL-3701 -->Stale build data in jalview standalone jar
 -            builds (only affects 2.11.1.1 branch)
 -          </li>
 -          <li>
 -            <!-- JAL-3127 -->Sequence ID colourscheme not re-applied
 -            when alignment view restored from project (since Jalview 2.11.0)
 -          </li>
 -          <li>
 -            <!-- JAL-3749 -->Duplicate CDS sequences are generated when
 -            protein products for certain ENA records are repeatedly
 -            shown via Calculate-&gt;Show Cross Refs
 -          </li>
 -        </ul>
 -      </td>
 -    </tr>
 -    <tr>
 -      <td width="60" align="center" nowrap><strong><a
            id="Jalview.2.11.1">2.11.1</a><a id="Jalview.2.11.1.0">.0</a><br />
            <em>22/04/2020</em></strong></td>
        <td align="left" valign="top">
@@@ -886,9 -876,6 +885,7 @@@ label.save_text_to_file = Save Text to 
  label.save_state = Save State
  label.restore_state = Restore State
  label.saving_jalview_project = Saving jalview project {0}
 +label.loading_jalview_project = Loading jalview project {0}
- label.save_vamsas_document_archive = Save Vamsas Document Archive
- label.saving_vamsas_doc = Saving VAMSAS Document to {0}
  label.load_feature_colours = Load Feature Colours
  label.save_feature_colours = Save Feature Colour Scheme
  label.select_startup_file = Select startup file
@@@ -1119,10 -1097,7 +1110,9 @@@ label.png_image = PNG imag
  status.export_complete = {0} Export completed
  status.fetching_pdb = Fetching PDB {0}
  status.refreshing_news = Refreshing news
- status.importing_vamsas_session_from = Importing VAMSAS session from {0}
  status.opening_params = Opening {0}
 +status.waiting_sequence_database_fetchers_init = Waiting for Sequence Database Fetchers to initialise
 +status.init_sequence_database_fetchers = Initialising Sequence Database Fetchers
  status.fetching_sequence_queries_from = Fetching {0} sequence queries from {1}
  status.finshed_querying = Finished querying
  status.parsing_results = Parsing results.
@@@ -1132,15 -1107,12 +1122,17 @@@ status.collecting_job_results = Collect
  status.fetching_db_refs = Fetching db refs
  status.loading_cached_pdb_entries = Loading Cached PDB Entries
  status.searching_for_pdb_structures = Searching for PDB Structures
+ status.searching_3d_beacons = Searching 3D Beacons
+ status.no_structures_discovered_from_3d_beacons = No models discovered from 3D Beacons
  status.opening_file_for = opening file for
- status.colouring_chimera = Colouring Chimera
 +status.running_hmmbuild = Building Hidden Markov Model
 +status.running_hmmalign = Creating alignment with Hidden Markov Model
 +status.running_search = Searching for matching sequences
+ status.colouring_structures = Colouring structures
  label.font_doesnt_have_letters_defined = Font doesn't have letters defined\nso cannot be used\nwith alignment data
  label.font_too_small = Font size is too small
 +label.error_loading_file_params = Error loading file {0}
 +label.error_loading_jalview_file = Error loading Jalview file
  warn.out_of_memory_when_action = Out of memory when {0}\!\!\nSee help files for increasing Java Virtual Machine memory.
  warn.out_of_memory_loading_file = Out of memory loading file {0}\!\!\nSee help files for increasing Java Virtual Machine memory.
  label.out_of_memory = Out of memory
@@@ -796,9 -794,6 +797,7 @@@ label.save_text_to_file = Guardar Text
  label.save_state = Guardar estado
  label.restore_state = Restaurar estado
  label.saving_jalview_project = Guardando el proyecto de Jalview {0}
 +label.loading_jalview_project = Cargando el proyecto de Jalview {0}
- label.save_vamsas_document_archive = Guardar el archivo de documento Vamsas
- label.saving_vamsas_doc = Guardando el documento VAMSAS en {0}
  label.load_feature_colours = Cargar colores de características
  label.save_feature_colours = Guardar esquema cromático de características
  label.select_startup_file = Seleccionar fichero de arranque
@@@ -875,10 -865,8 +869,9 @@@ error.implementation_error_cannot_find_
  error.implementation_error_old_jalview_object_not_bound =Error de implementación: Â¡el objeto Jalview antiguo no está enlazado! ({0})
  error.implementation_error_vamsas_doc_class_should_bind_to_type = Error de implementación: la clase de documento VAMSAS {0} debe enlazar a {1} (pero se ha encontrado que lo está a {2})
  error.invalid_vamsas_rangetype_cannot_resolve_lists = RangeType VAMSAS no válido - Â¡no es posible resolver ambas listas de Pos y Seg con los valores elegidos!
 +error.implementation_error_maplist_is_null = Error de implementación. MapList es nulo en initMapType.
  error.implementation_error_cannot_have_null_alignment = Error de implementación: no es posible tener una clave nula en el alineamiento
  error.implementation_error_null_fileparse = Error de implementación. FileParse nulo en el construictor de copia
- error.implementation_error_cannot_map_alignment_sequences = Error de implementación: no es posible maper un alineamiento de secuencias desde distintos conjuntos de datos en un Ãºnico alineamiento en el documento VAMSAS.
  error.implementation_error_structure_selection_manager_null = Error de implementación. El contexto structure selection manager's es nulo
  exception.ssm_context_is_null = El contexto SSM es nulo
  error.idstring_seqstrings_only_one_per_sequence = idstrings y seqstrings contienen una cadena por cada secuencia
@@@ -29,12 -30,13 +30,14 @@@ import jalview.datamodel.SearchResults
  import jalview.datamodel.SearchResultsI;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
- import jalview.datamodel.VisibleContigsIterator;
  import jalview.util.Comparison;
 +import jalview.util.Platform;
+ import jalview.util.MapList;
  
+ import java.util.ArrayList;
+ import java.util.Arrays;
+ import java.util.Iterator;
  import java.util.List;
- import java.util.Vector;
  
  import com.stevesoft.pat.Regex;
  
@@@ -124,18 -141,19 +142,19 @@@ public class Finder implements Finder
     * @param matchCase
     * @param searchDescription
     * @param findAll
+    * @param ignoreHidden
     */
    protected void doFind(String theSearchString, boolean matchCase,
-           boolean searchDescription, boolean findAll)
+           boolean searchDescription, boolean findAll, boolean ignoreHidden)
    {
+     searchResults = new SearchResults();
+     idMatches = new ArrayList<>();
      String searchString = matchCase ? theSearchString
-             : theSearchString.toUpperCase();
-     Regex searchPattern = Platform.newRegex(searchString);
+             : theSearchString.toUpperCase(Locale.ROOT);
 -    Regex searchPattern = new Regex(searchString);
++  Regex searchPattern = Platform.newRegex(searchString);
      searchPattern.setIgnoreCase(!matchCase);
  
-     searchResults = new SearchResults();
-     idMatches = new Vector<>();
      SequenceGroup selection = viewport.getSelectionGroup();
      if (selection != null && selection.getSize() < 1)
      {
Simple merge
Simple merge
Simple merge
@@@ -42,8 -52,8 +52,9 @@@ import org.apache.log4j.Level
  import org.apache.log4j.Logger;
  import org.apache.log4j.SimpleLayout;
  
 +import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
  import jalview.datamodel.PDBEntry;
+ import jalview.gui.Preferences;
  import jalview.gui.UserDefinedColours;
  import jalview.schemes.ColourSchemeLoader;
  import jalview.schemes.ColourSchemes;
@@@ -280,8 -277,30 +293,30 @@@ public class Cache implements Applicati
     */
    public static Logger log;
  
+   // save the proxy properties set at startup
+   public final static String[] startupProxyProperties = {
+       System.getProperty("http.proxyHost"),
+       System.getProperty("http.proxyPort"),
+       System.getProperty("https.proxyHost"),
+       System.getProperty("https.proxyPort"),
+       System.getProperty("http.proxyUser"),
+       System.getProperty("http.proxyPassword"),
+       System.getProperty("https.proxyUser"),
+       System.getProperty("https.proxyPassword"),
+       System.getProperty("http.nonProxyHosts") };
+   public final static String PROXYTYPE_NONE = "none";
+   // "false" and "true" for backward compatibility
+   public final static String PROXYTYPE_SYSTEM = "false";
+   public final static String PROXYTYPE_CUSTOM = "true";
+   // in-memory only storage of proxy password, safer to use char array
+   public static char[] proxyAuthPassword = null;
    /** Jalview Properties */
 -  public static Properties applicationProperties = new Properties()
 +  private Properties applicationProperties = new Properties()
    {
      // override results in properties output in alphabetical order
      @Override
     */
    public static void loadProperties(String propsFile)
    {
 +
 +    getInstance().loadPropertiesImpl(propsFile);
 +
 +  }
 +
 +  private void loadPropertiesImpl(String propsFile)
 +  {
 +
      propertiesFile = propsFile;
+     String releasePropertiesFile = null;
+     boolean defaultProperties = false;
      if (propsFile == null && !propsAreReadOnly)
      {
-       propertiesFile = Platform.getUserPath(".jalview_properties");
++      // TODO: @bsoares - for 2.12 testing: check test,develop,release props are located correctly
+       String channelPrefsFilename = ChannelProperties
+               .getProperty("preferences.filename");
+       String releasePrefsFilename = ".jalview_properties";
 -      propertiesFile = System.getProperty("user.home") + File.separatorChar
 -              + channelPrefsFilename;
 -      releasePropertiesFile = System.getProperty("user.home")
 -              + File.separatorChar + releasePrefsFilename;
++      propertiesFile = Platform.getUserPath(channelPrefsFilename);
++      releasePropertiesFile = Platform.getUserPath(releasePrefsFilename);
+       defaultProperties = true;
      }
      else
      {
    public static String getVersionDetailsForConsole()
    {
      StringBuilder sb = new StringBuilder();
-     sb.append("Jalview Version: "
-             + jalview.bin.Cache.getDefault("VERSION", "TEST"));
+     sb.append(ChannelProperties.getProperty("app_name"))
+             .append(" Version: ");
+     sb.append(jalview.bin.Cache.getDefault("VERSION", "TEST"));
      sb.append("\n");
-     sb.append("Jalview Installation: "
-             + jalview.bin.Cache.getDefault("INSTALLATION", "unknown"));
+     sb.append(ChannelProperties.getProperty("app_name"))
+             .append(" Installation: ");
+     sb.append(jalview.bin.Cache.getDefault("INSTALLATION", "unknown"));
      sb.append("\n");
 -    sb.append("Build Date: ");
 -    sb.append(jalview.bin.Cache.getDefault("BUILD_DATE", "unknown"));
 +    sb.append("Build Date: "
 +            + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown"));
      sb.append("\n");
-     sb.append("Java version: " + System.getProperty("java.version"));
+     sb.append("Java version: ");
+     sb.append(System.getProperty("java.version"));
      sb.append("\n");
-     sb.append(System.getProperty("os.arch") + " "
-             + System.getProperty("os.name") + " "
-             + System.getProperty("os.version"));
+     sb.append(System.getProperty("os.arch"));
+     sb.append(" ");
+     sb.append(System.getProperty("os.name"));
+     sb.append(" ");
+     sb.append(System.getProperty("os.version"));
      sb.append("\n");
      appendIfNotNull(sb, "Install4j version: ",
              System.getProperty("sys.install4jVersion"), "\n", null);
      return jalview.bin.Cache.getDefault("INSTALLATION", "unknown");
    }
  
 +  /**
 +   * 
 +   * For AppletParams and Preferences ok_actionPerformed and
 +   * startupFileTextfield_mouseClicked
 +   * 
 +   * Sets a property value for the running application, without saving it to the
 +   * properties file
 +   * 
 +   * @param key
 +   * @param obj
 +   */
 +  public static void setPropertyNoSave(String key, String obj)
 +  {
 +    getInstance().setPropertyImpl(key, obj, false);
 +  }
 +
 +  /**
 +   * Sets a property value, and optionally also saves the current properties to
 +   * file
 +   * 
 +   * @param key
 +   * @param obj
 +   * @param andSave
 +   * @return
 +   */
 +  private Object setPropertyImpl(
 +          String key, String obj, boolean andSave)
 +  {
 +    Object oldValue = null;
 +    try
 +    {
 +      oldValue = applicationProperties.setProperty(key, obj);
 +      if (andSave && !propsAreReadOnly && propertiesFile != null)
 +      {
 +        FileOutputStream out = new FileOutputStream(propertiesFile);
 +        applicationProperties.store(out, "---JalviewX Properties File---");
 +        out.close();
 +      }
 +    } catch (Exception ex)
 +    {
 +      System.out.println(
 +              "Error setting property: " + key + " " + obj + "\n" + ex);
 +    }
 +    return oldValue;
 +  }
 +
+   public static String getStackTraceString(Throwable t)
+   {
+     StringWriter sw = new StringWriter();
+     PrintWriter pw = new PrintWriter(sw);
+     t.printStackTrace(pw);
+     return sw.toString();
+   }
+   // proxy properties methods
+   public static void clearProxyProperties()
+   {
+     setProxyProperties(null, null, null, null, null, null, null, null,
+             null);
+   }
+   public static void resetProxyProperties()
+   {
+     setProxyProperties(startupProxyProperties[0], startupProxyProperties[1],
+             startupProxyProperties[2], startupProxyProperties[3],
+             startupProxyProperties[4],
+             startupProxyProperties[5] == null ? null
+                     : startupProxyProperties[5].toCharArray(),
+             startupProxyProperties[6],
+             startupProxyProperties[7] == null ? null
+                     : startupProxyProperties[7].toCharArray(),
+             startupProxyProperties[8]);
+     StringBuilder sb = new StringBuilder();
+     sb.append("Setting proxy properties to: http.proxyHost=")
+             .append(startupProxyProperties[0]).append(", http.proxyPort=")
+             .append(startupProxyProperties[1])
+             .append(startupProxyProperties[4] != null
+                     && !startupProxyProperties[4].isEmpty()
+                             ? " [" + startupProxyProperties[4] + "]"
+                             : "")
+             .append(", https.proxyHost=").append(startupProxyProperties[2])
+             .append(", https.proxyPort=").append(startupProxyProperties[3])
+             .append(startupProxyProperties[6] != null
+                     && !startupProxyProperties[6].isEmpty()
+                             ? " [" + startupProxyProperties[6] + "]"
+                             : "");
+     Cache.debug(sb.toString());
+   }
+   public static void setProxyPropertiesFromPreferences()
+   {
+     setProxyPropertiesFromPreferences(Cache.PROXYTYPE_SYSTEM);
+   }
+   public static void setProxyPropertiesFromPreferences(
+           String previousProxyType)
+   {
+     String proxyType = Cache.getDefault("USE_PROXY",
+             Cache.PROXYTYPE_SYSTEM);
+     if (previousProxyType != null
+             && !proxyType.equals(Cache.PROXYTYPE_CUSTOM) // always apply
+                                                          // customProxy
+             && proxyType.equals(previousProxyType))
+     {
+       // no change
+       return;
+     }
+     switch (proxyType)
+     {
+     case Cache.PROXYTYPE_NONE:
+       if (!previousProxyType.equals(proxyType))
+       {
+         Cache.log.info("Setting no proxy settings");
+         Cache.setProxyProperties(null, null, null, null, null, null, null,
+                 null, null);
+       }
+       break;
+     case Cache.PROXYTYPE_CUSTOM:
+       // always re-set a custom proxy -- it might have changed, particularly
+       // password
+       Cache.log.info("Setting custom proxy settings");
+       boolean proxyAuthSet = Cache.getDefault("PROXY_AUTH", false);
+       Cache.setProxyProperties(Cache.getDefault("PROXY_SERVER", null),
+               Cache.getDefault("PROXY_PORT", null),
+               Cache.getDefault("PROXY_SERVER_HTTPS", null),
+               Cache.getDefault("PROXY_PORT_HTTPS", null),
+               proxyAuthSet ? Cache.getDefault("PROXY_AUTH_USERNAME", "")
+                       : null,
+               proxyAuthSet ? Cache.proxyAuthPassword : null,
+               proxyAuthSet ? Cache.getDefault("PROXY_AUTH_USERNAME", "")
+                       : null,
+               proxyAuthSet ? Cache.proxyAuthPassword : null, "localhost");
+       break;
+     default: // system proxy settings by default
+       Cache.log.info("Setting system proxy settings");
+       Cache.resetProxyProperties();
+     }
+   }
+   public static void setProxyProperties(String httpHost, String httpPort,
+           String httpsHost, String httpsPort, String httpUser,
+           char[] httpPassword, String httpsUser, char[] httpsPassword,
+           String nonProxyHosts)
+   {
+     setOrClearSystemProperty("http.proxyHost", httpHost);
+     setOrClearSystemProperty("http.proxyPort", httpPort);
+     setOrClearSystemProperty("https.proxyHost", httpsHost);
+     setOrClearSystemProperty("https.proxyPort", httpsPort);
+     setOrClearSystemProperty("http.proxyUser", httpUser);
+     setOrClearSystemProperty("https.proxyUser", httpsUser);
+     // note: passwords for http.proxyPassword and https.proxyPassword are sent
+     // via the Authenticator, properties do not need to be set
+     // are we using a custom proxy (password prompt might be required)?
+     boolean customProxySet = getDefault("USE_PROXY", PROXYTYPE_SYSTEM)
+             .equals(PROXYTYPE_CUSTOM);
+     /*
+      * A bug in Java means the AuthCache does not get reset, so once it has working credentials,
+      * it never asks for more, so changing the Authenticator has no effect (as getPasswordAuthentication()
+      * is not re-called).
+      * This could lead to password leak to a hostile proxy server, so I'm putting in a hack to clear
+      * the AuthCache.
+      * see https://www.generacodice.com/en/articolo/154918/Reset-the-Authenticator-credentials
+      * ...
+      * Turns out this is only accessible in Java 8, and not in Java 9 onwards, so commenting out
+      */
+     /*
+     try
+     {
+       sun.net.www.protocol.http.AuthCacheValue
+               .setAuthCache(new sun.net.www.protocol.http.AuthCacheImpl());
+     } catch (Throwable t)
+     {
+       Cache.error(t.getMessage());
+       Cache.debug(getStackTraceString(t));
+     }
+     */
+     if (httpUser != null || httpsUser != null)
+     {
+       try
+       {
+         char[] displayHttpPw = new char[httpPassword == null ? 0
+                 : httpPassword.length];
+         Arrays.fill(displayHttpPw, '*');
+         Cache.debug("CACHE Proxy: setting new Authenticator with httpUser='"
+                 + httpUser + "' httpPassword='" + displayHttpPw + "'");
+         if (!Platform.isJS())
+         /* *
+          * java.net.Authenticator not implemented in SwingJS yet
+          * 
+          * @j2sIgnore
+          * 
+          */
+         {
+           Authenticator.setDefault(new Authenticator()
+           {
+             @Override
+             protected PasswordAuthentication getPasswordAuthentication()
+             {
+               if (getRequestorType() == RequestorType.PROXY)
+               {
+                 String protocol = getRequestingProtocol();
+                 boolean needProxyPasswordSet = false;
+                 if (customProxySet &&
+                 // we have a username but no password for the scheme being
+                 // requested
+                 (protocol.equalsIgnoreCase("http")
+                         && (httpUser != null && httpUser.length() > 0
+                                 && (httpPassword == null
+                                         || httpPassword.length == 0)))
+                         || (protocol.equalsIgnoreCase("https")
+                                 && (httpsUser != null
+                                         && httpsUser.length() > 0
+                                         && (httpsPassword == null
+                                                 || httpsPassword.length == 0))))
+                 {
+                   // open Preferences -> Connections
+                   String message = MessageManager
+                           .getString("label.proxy_password_required");
+                   Preferences.openPreferences(
+                           Preferences.TabRef.CONNECTIONS_TAB, message);
+                   Preferences.getInstance()
+                           .proxyAuthPasswordCheckHighlight(true, true);
+                 }
+                 else
+                 {
+                   try
+                   {
+                     if (protocol.equalsIgnoreCase("http")
+                             && getRequestingHost()
+                                     .equalsIgnoreCase(httpHost)
+                             && getRequestingPort() == Integer
+                                     .valueOf(httpPort))
+                     {
+                       Cache.debug(
+                               "AUTHENTICATOR returning PasswordAuthentication(\""
+                                       + httpUser + "\", '"
+                                       + new String(displayHttpPw) + "')");
+                       return new PasswordAuthentication(httpUser,
+                               httpPassword);
+                     }
+                     if (protocol.equalsIgnoreCase("https")
+                             && getRequestingHost()
+                                     .equalsIgnoreCase(httpsHost)
+                             && getRequestingPort() == Integer
+                                     .valueOf(httpsPort))
+                     {
+                       char[] displayHttpsPw = new char[httpPassword.length];
+                       Arrays.fill(displayHttpsPw, '*');
+                       Cache.debug(
+                               "AUTHENTICATOR returning PasswordAuthentication(\""
+                                       + httpsUser + "\", '" + displayHttpsPw
+                                       + "'");
+                       return new PasswordAuthentication(httpsUser,
+                               httpsPassword);
+                     }
+                   } catch (NumberFormatException e)
+                   {
+                     Cache.error("Problem with proxy port values [http:"
+                             + httpPort + ", https:" + httpsPort + "]");
+                   }
+                   Cache.debug(
+                           "AUTHENTICATOR after trying to get PasswordAuthentication");
+                 }
+               }
+               // non proxy request
+               Cache.debug("AUTHENTICATOR returning null");
+               return null;
+             }
+           });
+         } // end of j2sIgnore for java.net.Authenticator
+         // required to re-enable basic authentication (should be okay for a
+         // local proxy)
+         Cache.debug(
+                 "AUTHENTICATOR setting property 'jdk.http.auth.tunneling.disabledSchemes' to \"\"");
+         System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
+       } catch (SecurityException e)
+       {
+         Cache.error("Could not set default Authenticator");
+         Cache.debug(getStackTraceString(e));
+       }
+     }
+     else
+     {
+       // reset the Authenticator to protect http.proxyUser and
+       // http.proxyPassword Just In Case
+       /* as noted above, due to bug in java this doesn't work if the sun.net.www.protocol.http.AuthCache
+        * has working credentials. No workaround for Java 11.
+        */
+       if (!Platform.isJS())
+       /* *
+        * java.net.Authenticator not implemented in SwingJS yet
+        * 
+        * @j2sIgnore
+        * 
+        */
+       {
+         Cache.debug("AUTHENTICATOR setting default Authenticator to null");
+         Authenticator.setDefault(null);
+       }
+     }
+     // nonProxyHosts not currently configurable in Preferences
+     Cache.debug("AUTHENTICATOR setting property 'http.nonProxyHosts' to \""
+             + nonProxyHosts + "\"");
+     setOrClearSystemProperty("http.nonProxyHosts", nonProxyHosts);
+   }
+   public static void setOrClearSystemProperty(String key, char[] value)
+   {
+     setOrClearSystemProperty(key,
+             (value == null) ? null : new String(value));
+   }
+   public static void setOrClearSystemProperty(String key, String value)
+   {
+     if (key == null)
+     {
+       return;
+     }
+     if (value == null)
+     {
+       System.clearProperty(key);
+     }
+     else
+     {
+       System.setProperty(key, value);
+     }
+   }
+   public final static int TRACE = 10;
+   public final static int DEBUG = 20;
+   public final static int INFO = 30;
+   public final static int WARN = 40;
+   public final static int ERROR = 50;
+   public static boolean println(int level, String message)
+   {
+     if (Cache.log == null)
+     {
+       if (level >= WARN)
+         System.err.println(message);
+       else if (level >= INFO)
+         System.out.println(message);
+       // not printing debug or trace messages
+       return false;
+     }
+     if (level >= ERROR)
+     {
+       Cache.log.error(message);
+     }
+     else if (level >= WARN)
+     {
+       Cache.log.warn(message);
+     }
+     else if (level >= INFO)
+     {
+       Cache.log.info(message);
+     }
+     else if (level >= DEBUG)
+     {
+       Cache.log.debug(message);
+     }
+     else
+     {
+       Cache.log.trace(message);
+     }
+     return true;
+   }
+   public static void trace(String message)
+   {
+     println(TRACE, message);
+   }
+   public static void debug(String message)
+   {
+     println(DEBUG, message);
+   }
+   public static void info(String message)
+   {
+     println(INFO, message);
+   }
+   public static void warn(String message)
+   {
+     println(WARN, message);
+   }
+   public static void error(String message)
+   {
+     println(ERROR, message);
+   }
+   /**
+    * Getdown appbase methods
+    */
+   private static final String releaseAppbase;
+   private static String getdownAppbase;
+   private static String getdownDistDir;
+   static
+   {
+     if (!Platform.isJS())
+     {
+       Float specversion = Float
+               .parseFloat(System.getProperty("java.specification.version"));
+       releaseAppbase = (specversion < 9)
+               ? "https://www.jalview.org/getdown/release/1.8"
+               : "https://www.jalview.org/getdown/release/11";
+     }
+     else
+     {
+       // this value currenly made up, can be changed to URL that will be
+       // "https://www.jalview.org/jalview-js/swingjs/j2s/build_properties"
+       releaseAppbase = "https://www.jalview.org/jalview-js";
+       getdownAppbase = releaseAppbase;
+       getdownDistDir = "/swingjs/j2s";
+     }
+   }
+   // look for properties (passed in by getdown) otherwise default to release
+   private static void setGetdownAppbase()
+   {
+     if (getdownAppbase != null)
+     {
+       return;
+     }
+     String appbase = System.getProperty("getdownappbase");
+     String distDir = System.getProperty("getdowndistdir");
+     if (appbase == null)
+     {
+       appbase = buildProperties.getProperty("GETDOWNAPPBASE");
+       distDir = buildProperties.getProperty("GETDOWNAPPDISTDIR");
+     }
+     if (appbase == null)
+     {
+       appbase = releaseAppbase;
+       distDir = "release";
+     }
+     if (appbase.endsWith("/"))
+     {
+       appbase = appbase.substring(0, appbase.length() - 1);
+     }
+     if (distDir == null)
+     {
+       distDir = appbase.equals(releaseAppbase) ? "release" : "alt";
+     }
+     getdownAppbase = appbase;
+     getdownDistDir = distDir;
+   }
+   public static String getGetdownAppbase()
+   {
+     setGetdownAppbase();
+     return getdownAppbase;
+   }
+   public static String getAppbaseBuildProperties()
+   {
+     String appbase = getGetdownAppbase();
+     return appbase + "/" + getdownDistDir + "/build_properties";
+   }
  }
@@@ -20,7 -20,8 +20,8 @@@
   */
  package jalview.bin;
  
 -import java.util.Locale;
 +import java.awt.GraphicsEnvironment;
  import java.io.BufferedReader;
  import java.io.File;
  import java.io.FileOutputStream;
@@@ -38,11 -39,14 +39,15 @@@ import java.security.PermissionCollecti
  import java.security.Permissions;
  import java.security.Policy;
  import java.util.HashMap;
++import java.util.Locale;
  import java.util.Map;
  import java.util.Vector;
+ import java.util.logging.ConsoleHandler;
+ import java.util.logging.Level;
+ import java.util.logging.Logger;
  
- import javax.swing.LookAndFeel;
  import javax.swing.UIManager;
+ import javax.swing.UIManager.LookAndFeelInfo;
  
  import com.threerings.getdown.util.LaunchUtil;
  
@@@ -89,36 -91,17 +96,35 @@@ import jalview.ws.jws2.Jws2Discoverer
   * @author $author$
   * @version $Revision$
   */
 -public class Jalview
 +public class Jalview implements ApplicationSingletonI
  {
 -  static
 +  // for testing those nasty messages you cannot ever find.
 +  // static
 +  // {
 +  // System.setOut(new PrintStream(new ByteArrayOutputStream())
 +  // {
 +  // @Override
 +  // public void println(Object o)
 +  // {
 +  // if (o != null)
 +  // {
 +  // System.err.println(o);
 +  // }
 +  // }
 +  //
 +  // });
 +  // }
 +  public static Jalview getInstance()
 +  {
 +    return (Jalview) ApplicationSingletonProvider
 +            .getInstance(Jalview.class);
 +  }
 +
 +  private Jalview()
    {
+     Platform.getURLCommandArguments();
    }
  
-   private boolean headless;
 -  /*
 -   * singleton instance of this class
 -   */
 -  private static Jalview instance;
  
    private Desktop desktop;
  
     */
    public static void main(String[] args)
    {
 -    // setLogging(); // BH - for event debugging in JavaScript
 -    instance = new Jalview();
 -    instance.doMain(args);
 -  }
 -
 -  private static void logClass(String name)
 -  {
 -    // BH - for event debugging in JavaScript
 -    ConsoleHandler consoleHandler = new ConsoleHandler();
 -    consoleHandler.setLevel(Level.ALL);
 -    Logger logger = Logger.getLogger(name);
 -    logger.setLevel(Level.ALL);
 -    logger.addHandler(consoleHandler);
 -  }
 -
 -  @SuppressWarnings("unused")
 -  private static void setLogging()
 -  {
 -
 -    /**
 -     * @j2sIgnore
 -     * 
 -     */
 +    if (doPlatformLogging)
      {
 -      System.out.println("not in js");
 +      Platform.startJavaLogging();
      }
 -    // BH - for event debugging in JavaScript (Java mode only)
 -    if (!Platform.isJS())
 -    /**
 -     * Java only
 -     * 
 -     * @j2sIgnore
 -     */
 -    {
 -      Logger.getLogger("").setLevel(Level.ALL);
 -      logClass("java.awt.EventDispatchThread");
 -      logClass("java.awt.EventQueue");
 -      logClass("java.awt.Component");
 -      logClass("java.awt.focus.Component");
 -      logClass("java.awt.focus.DefaultKeyboardFocusManager");
 -    }
 +    getInstance().doMain(args);
    }
++  
++
++  
  
    /**
     * @param args
      }
  
      // report Jalview version
 -    Cache.loadBuildProperties(true);
 +    Cache.getInstance().loadBuildProperties(true);
  
      ArgsParser aparser = new ArgsParser(args);
-     headless = false;
+     boolean headless = false;
  
      String usrPropsFile = aparser.getValue("props");
      Cache.loadProperties(usrPropsFile); // must do this before
 -    if (usrPropsFile != null)
 +    boolean allowServices = true;
 +    
 +    if (isJS)
      {
 -      System.out.println(
 -              "CMD [-props " + usrPropsFile + "] executed successfully!");
 +      j2sAppletID = Platform.getAppID(null);
 +      Preferences.setAppletDefaults();
 +      Cache.loadProperties(usrPropsFile); // again, because we
 +      // might be changing defaults here?
 +      appletResourcePath = (String) aparser.getAppletValue("resourcepath",
 +              null, true);
      }
 -    if (!Platform.isJS())
 +    else
      /**
       * Java only
       * 
       * @j2sIgnore
       */
      {
 +      if (usrPropsFile != null)
 +      {
 +        System.out.println(
 +                "CMD [-props " + usrPropsFile + "] executed successfully!");
 +      }
        if (aparser.contains("help") || aparser.contains("h"))
        {
          showUsage();
          System.exit(0);
        }
 +      // BH note: Only -nodisplay is official; others are deprecated?
        if (aparser.contains("nodisplay") || aparser.contains("nogui")
 -              || aparser.contains("headless"))
 +              || aparser.contains("headless")
 +              || GraphicsEnvironment.isHeadless())
        {
 -        System.setProperty("java.awt.headless", "true");
 +        if (!isJS) {
 +          // BH Definitely not a good idea in JavaScript; 
 +          // probably should not be here for Java, either.  
 +          System.setProperty("java.awt.headless", "true");
 +        }
          headless = true;
        }
+       // anything else!
  
 -      final String jabawsUrl = aparser.getValue("jabaws");
 -      if (jabawsUrl != null)
 +      final String jabawsUrl = aparser.getValue(ArgsParser.JABAWS);
 +      allowServices = !("none".equals(jabawsUrl));
 +      if (allowServices && jabawsUrl != null)
        {
          try
          {
       * configure 'full' SO model if preferences say to, else use the default (full SO)
       * - as JS currently doesn't have OBO parsing, it must use 'Lite' version
       */
 -    boolean soDefault = !Platform.isJS();
 +    boolean soDefault = !isJS;
      if (Cache.getDefault("USE_FULL_SO", soDefault))
      {
-       SequenceOntologyFactory.setSequenceOntology(new SequenceOntology());
+       SequenceOntologyFactory.setInstance(new SequenceOntology());
      }
  
-     desktop = null;
      if (!headless)
      {
+       Desktop.nosplash = aparser.contains("nosplash");
 -      desktop = new Desktop();
 +      desktop = Desktop.getInstance();
        desktop.setInBatchMode(true); // indicate we are starting up
        try
        {
          JalviewTaskbar.setTaskbar(this);
+       } catch (Exception e)
+       {
+         Cache.log.info("Cannot set Taskbar");
+         Cache.log.error(e.getMessage());
+         // e.printStackTrace();
        } catch (Throwable t)
        {
-         System.out.println("Error setting Taskbar: " + t.getMessage());
+         Cache.log.info("Cannot set Taskbar");
+         Cache.log.error(t.getMessage());
+         // t.printStackTrace();
        }
 -
+       // set Proxy settings before all the internet calls
+       Cache.setProxyPropertiesFromPreferences();
        desktop.setVisible(true);
-       if (Platform.isJS())
 -      if (!Platform.isJS())
++      if (isJS)
++      {
 +        Cache.setProperty("SHOW_JWS2_SERVICES", "false");
-       if (allowServices)
++      }
++      if (allowServices && !aparser.contains("nowebservicediscovery"))
 +      {
 +        desktop.startServiceDiscovery();
 +      }
++
 +      if (!isJS)
        /**
         * Java only
         * 
          BioJsHTMLOutput.updateBioJS();
        }
      }
 +    parseArguments(aparser, true);
 +  }
 +
 +  /**
 +   * Parse all command-line String[] arguments as well as all JavaScript-derived
 +   * parameters from Info.
 +   * 
 +   * We allow for this method to be run from JavaScript. Basically allowing
 +   * simple scripting.
 +   * 
 +   * @param aparser
 +   * @param isStartup
 +   */
 +  public void parseArguments(ArgsParser aparser, boolean isStartup)
 +  {
  
 -    // Move any new getdown-launcher-new.jar into place over old
 -    // getdown-launcher.jar
 -    String appdirString = System.getProperty("getdownappdir");
 -    if (appdirString != null && appdirString.length() > 0)
 +    String groovyscript = null; // script to execute after all loading is
 +    boolean isJS = Platform.isJS();
 +    if (!isJS)
 +    /** @j2sIgnore */
      {
 -      final File appdir = new File(appdirString);
 -      new Thread()
 +      // Move any new getdown-launcher-new.jar into place over old
 +      // getdown-launcher.jar
 +      String appdirString = System.getProperty("getdownappdir");
 +      if (appdirString != null && appdirString.length() > 0)
        {
 -        @Override
 -        public void run()
 +        final File appdir = new File(appdirString);
 +        new Thread()
          {
 -          LaunchUtil.upgradeGetdown(
 -                  new File(appdir, "getdown-launcher-old.jar"),
 -                  new File(appdir, "getdown-launcher.jar"),
 -                  new File(appdir, "getdown-launcher-new.jar"));
 -        }
 -      }.start();
 -    }
 +          @Override
 +          public void run()
 +          {
 +            LaunchUtil.upgradeGetdown(
 +                    new File(appdir, "getdown-launcher-old.jar"),
 +                    new File(appdir, "getdown-launcher.jar"),
 +                    new File(appdir, "getdown-launcher-new.jar"));
 +          }
 +        }.start();
 +      }
  
 -    String file = null, data = null;
 -    FileFormatI format = null;
 -    DataSourceType protocol = null;
 -    FileLoader fileLoader = new FileLoader(!headless);
 +      // completed one way or another
 +      // extract groovy argument and execute if necessary
 +      groovyscript = aparser.getValue("groovy", true);
 +    }
  
 -    String groovyscript = null; // script to execute after all loading is
 -    // completed one way or another
 -    // extract groovy argument and execute if necessary
 -    groovyscript = aparser.getValue("groovy", true);
 -    file = aparser.getValue("open", true);
 +    String file = aparser.getValue("open", true);
  
 -    if (file == null && desktop == null)
 +    if (!isJS && file == null && desktop == null)
      {
        System.out.println("No files to open!");
        System.exit(1);
      }
 +    setDisplayParameters(aparser);
 +    
 +    // time to open a file.
      long progress = -1;
 +    DataSourceType protocol = null;
 +    FileLoader fileLoader = new FileLoader(!headless);
 +    FileFormatI format = null;
      // Finally, deal with the remaining input data.
 -    if (file != null)
 +    AlignFrame af = null;
 +
 +    JalviewJSApp jsApp = (isJS ? new JalviewJSApp(this, aparser) : null);
 +
 +    if (file == null)
 +    {
 +      if (isJS)
 +      {
 +        // JalviewJS allows sequence1 sequence2 ....
 +        
 +      }
 +      else if (!headless && Cache.getDefault("SHOW_STARTUP_FILE", true))
 +      /**
 +       * Java only
 +       * 
 +       * @j2sIgnore
 +       */
 +      {
 +
 +        // We'll only open the default file if the desktop is visible.
 +        // And the user
 +        // ////////////////////
 +
 +        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"))
 +        {
 +          // hardwire upgrade of the startup file
 +          file.replace("_2_3.jar", "_2_7.jar");
 +          // and remove the stale setting
 +          Cache.removeProperty("STARTUP_FILE");
 +        }
 +
 +        protocol = DataSourceType.FILE;
 +
 +        if (file.indexOf("http:") > -1)
 +        {
 +          protocol = DataSourceType.URL;
 +        }
 +
 +        if (file.endsWith(".jar"))
 +        {
 +          format = FileFormat.Jalview;
 +        }
 +        else
 +        {
 +          try
 +          {
 +            format = new IdentifyFile().identify(file, protocol);
 +          } catch (FileFormatException e)
 +          {
 +            // TODO what?
 +          }
 +        }
 +        af = fileLoader.LoadFileWaitTillLoaded(file, protocol, format);
 +      }
 +    }
 +    else
      {
        if (!headless)
        {
            }
          }
        }
-       
++      // JS Only argument to provide a format parameter to specify what format to use
 +      String fileFormat = (isJS
 +              ? (String) aparser.getAppletValue("format", null, true)
 +              : null);
        protocol = AppletFormatAdapter.checkProtocol(file);
        try
        {
 -        format = new IdentifyFile().identify(file, protocol);
 +        format = (fileFormat != null
 +                ? FileFormats.getInstance().forName(fileFormat)
 +                : null);
 +        if (format == null)
 +        {
 +          format = new IdentifyFile().identify(file, protocol);
 +        }
        } catch (FileFormatException e1)
        {
          // TODO ?
        }
        else
        {
 -        setCurrentAlignFrame(af);
 -        data = aparser.getValue("colour", true);
 -        if (data != null)
++        
 +        // JalviewLite interface for JavaScript allows second file open
 +        String file2 = aparser.getValue(ArgsParser.OPEN2, true);
 +        if (file2 != null)
          {
 -          data.replaceAll("%20", " ");
 -
 -          ColourSchemeI cs = ColourSchemeProperty.getColourScheme(
 -                  af.getViewport(), af.getViewport().getAlignment(), data);
 -
 -          if (cs != null)
 +          protocol = AppletFormatAdapter.checkProtocol(file2);
 +          try
            {
 -            System.out.println(
 -                    "CMD [-color " + data + "] executed successfully!");
 +            format = new IdentifyFile().identify(file2, protocol);
 +          } catch (FileFormatException e1)
 +          {
 +            // TODO ?
            }
 -          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"))
 -        {
 -          af.getViewport().setSortByTree(true);
 -          if (af.getViewport().getSortByTree())
 +          AlignFrame af2 = new FileLoader(!headless)
 +                  .LoadFileWaitTillLoaded(file2, protocol, format);
 +          if (af2 == null)
            {
 -            System.out.println("CMD [-sortbytree] executed successfully!");
 +            System.out.println("error");
            }
 -        }
 -        if (aparser.contains("no-annotation"))
 -        {
 -          af.getViewport().setShowAnnotation(false);
 -          if (!af.getViewport().isShowAnnotation())
 +          else
            {
 -            System.out.println("CMD no-annotation executed successfully!");
 +            AlignViewport.openLinkedAlignmentAs(af,
 +                    af.getViewport().getAlignment(),
 +                    af2.getViewport().getAlignment(), "",
 +                    AlignViewport.SPLIT_FRAME);
 +            System.out.println(
 +                    "CMD [-open2 " + file2 + "] executed successfully!");
            }
          }
 -        if (aparser.contains("nosortbytree"))
++        // af is loaded - so set it as current frame
 +        setCurrentAlignFrame(af);
 +
 +        setFrameDependentProperties(aparser, af);
 +        
 +        if (isJS)
          {
 -          af.getViewport().setSortByTree(false);
 -          if (!af.getViewport().getSortByTree())
 -          {
 -            System.out
 -                    .println("CMD [-nosortbytree] executed successfully!");
 -          }
 +          jsApp.initFromParams(af);
          }
 -        data = aparser.getValue("tree", true);
 -        if (data != null)
 +        else
 +        /**
 +         * Java only
 +         * 
 +         * @j2sIgnore
 +         */
          {
 -          try
 +          if (groovyscript != null)
            {
 -            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);
 +            // 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;
            }
          }
 -        // TODO - load PDB structure(s) to alignment JAL-629
 -        // (associate with identical sequence in alignment, or a specified
 -        // sequence)
 -        if (groovyscript != null)
 -        {
 -          // 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;
 +        if (!isJS || !isStartup) {
 +          createOutputFiles(aparser, format);
          }
 -        String imageName = "unnamed.png";
 -        while (aparser.getSize() > 1)
 -        {
 -          String outputFormat = aparser.nextValue();
 -          file = aparser.nextValue();
 +      }
 +      if (headless)
 +      {
 +        af.getViewport().getCalcManager().shutdown();
 +      }
 +    }
 +    // 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())
 +      {
++        // TODO: DECIDE IF THIS SECOND PASS AT GROOVY EXECUTION IS STILL REQUIRED !!
 +        System.out.println("Executing script " + groovyscript);
 +        executeGroovyScript(groovyscript, af);
++        System.out.println("CMD groovy[" + groovyscript
++                    + "] executed successfully!");
 -          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);
 +      }
 +      else
 +      {
 +        System.err.println(
 +                "Sorry. Groovy Support is not available, so ignoring the provided groovy script "
 +                        + groovyscript);
 +      }
 +    }
  
 -            System.out.println("Creating HTML image: " + file);
 -            continue;
 -          }
 -          else if (outputFormat.equalsIgnoreCase("biojsmsa"))
 -          {
 -            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;
 -          }
 -          FileFormatI outFormat = null;
 -          try
 -          {
 -            outFormat = FileFormats.getInstance().forName(outputFormat);
 -          } catch (Exception formatP)
 -          {
 -            System.out.println("Couldn't parse " + outFormat
 -                    + " as a valid Jalview format string.");
 -          }
 -          if (outFormat != null)
 -          {
 -            if (!outFormat.isWritable())
 -            {
 -              System.out.println(
 -                      "This version of Jalview does not support alignment export as "
 -                              + outputFormat);
 -            }
 -            else
 -            {
 -              af.saveAlignment(file, outFormat);
 -              if (af.isSaveAlignmentSuccessful())
 -              {
 -                System.out.println("Written alignment in "
 -                        + outFormat.getName() + " format to " + file);
 -              }
 -              else
 -              {
 -                System.out.println("Error writing file " + file + " in "
 -                        + outFormat.getName() + " format!!");
 -              }
 -            }
 -          }
 +    // and finally, turn off batch mode indicator - if the desktop still exists
 +    if (desktop != null)
 +    {
 +      if (progress != -1)
 +      {
 +        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!");
 +    }
  
 -        while (aparser.getSize() > 0)
 -        {
 -          System.out.println("Unknown arg: " + aparser.nextValue());
 -        }
 -      }
 +    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!");
      }
 -    AlignFrame startUpAlframe = null;
 -    // We'll only open the default file if the desktop is visible.
 -    // And the user
 -    // ////////////////////
 +  }
  
 -    if (!Platform.isJS() && !headless && file == null
 -            && Cache.getDefault("SHOW_STARTUP_FILE", true))
 -    /**
 -     * Java only
 -     * 
 -     * @j2sIgnore
 -     */
 +
 +  private void setFrameDependentProperties(ArgsParser aparser,
 +          AlignFrame af)
 +  {
 +    String data = aparser.getValue(ArgsParser.COLOUR, true);
 +    if (data != null)
      {
 -      file = jalview.bin.Cache.getDefault("STARTUP_FILE",
 -              jalview.bin.Cache.getDefault("www.jalview.org",
 -                      "https://www.jalview.org")
 -                      + "/examples/exampleFile_2_7.jvp");
 -      if (file.equals(
 -              "http://www.jalview.org/examples/exampleFile_2_3.jar") || file.equals(
 -                      "http://www.jalview.org/examples/exampleFile_2_7.jar"))
 +      data.replaceAll("%20", " ");
 +
 +      ColourSchemeI cs = ColourSchemeProperty.getColourScheme(
 +              af.getViewport(), af.getViewport().getAlignment(), data);
 +
 +      if (cs != null)
        {
 -        file.replace("http:", "https:");
 -        // hardwire upgrade of the startup file
 -        file.replace("_2_3", "_2_7");
 -        file.replace("2_7.jar", "2_7.jvp");
 -        // and remove the stale setting
 -        Cache.removeProperty("STARTUP_FILE");
 +        System.out.println(
 +                "CMD [-color " + data + "] executed successfully!");
        }
 +      af.changeColour(cs);
 +    }
  
 -      protocol = AppletFormatAdapter.checkProtocol(file);
 +    // 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!");
 +    }
  
 -      if (file.endsWith(".jar"))
 +    // JavaScript feature
 +
 +    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)
 +  {
++    // logic essentially the same as 2.11.2/2.11.3 but uses a switch instead
 +    AlignFrame af = currentAlignFrame;
 +    while (aparser.getSize() >= 2)
      {
 -      if (progress != -1)
 +      String outputFormat = aparser.nextValue();
 +      File imageFile;
 +      String fname;
-       switch (outputFormat.toLowerCase())
++      switch (outputFormat.toLowerCase(Locale.ROOT))
        {
 -        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());
      }
    }
  
+   private static void setLookAndFeel()
+   {
+     // property laf = "crossplatform", "system", "gtk", "metal", "nimbus" or
+     // "mac"
+     // If not set (or chosen laf fails), use the normal SystemLaF and if on Mac,
+     // try Quaqua/Vaqua.
+     String lafProp = System.getProperty("laf");
+     String lafSetting = Cache.getDefault("PREFERRED_LAF", null);
+     String laf = "none";
+     if (lafProp != null)
+     {
+       laf = lafProp;
+     }
+     else if (lafSetting != null)
+     {
+       laf = lafSetting;
+     }
+     boolean lafSet = false;
+     switch (laf)
+     {
+     case "crossplatform":
+       lafSet = setCrossPlatformLookAndFeel();
+       if (!lafSet)
+       {
+         Cache.log.error("Could not set requested laf=" + laf);
+       }
+       break;
+     case "system":
+       lafSet = setSystemLookAndFeel();
+       if (!lafSet)
+       {
+         Cache.log.error("Could not set requested laf=" + laf);
+       }
+       break;
+     case "gtk":
+       lafSet = setGtkLookAndFeel();
+       if (!lafSet)
+       {
+         Cache.log.error("Could not set requested laf=" + laf);
+       }
+       break;
+     case "metal":
+       lafSet = setMetalLookAndFeel();
+       if (!lafSet)
+       {
+         Cache.log.error("Could not set requested laf=" + laf);
+       }
+       break;
+     case "nimbus":
+       lafSet = setNimbusLookAndFeel();
+       if (!lafSet)
+       {
+         Cache.log.error("Could not set requested laf=" + laf);
+       }
+       break;
+     case "quaqua":
+       lafSet = setQuaquaLookAndFeel();
+       if (!lafSet)
+       {
+         Cache.log.error("Could not set requested laf=" + laf);
+       }
+       break;
+     case "vaqua":
+       lafSet = setVaquaLookAndFeel();
+       if (!lafSet)
+       {
+         Cache.log.error("Could not set requested laf=" + laf);
+       }
+       break;
+     case "mac":
+       lafSet = setMacLookAndFeel();
+       if (!lafSet)
+       {
+         Cache.log.error("Could not set requested laf=" + laf);
+       }
+       break;
+     case "none":
+       break;
+     default:
+       Cache.log.error("Requested laf=" + laf + " not implemented");
+     }
+     if (!lafSet)
+     {
+       setSystemLookAndFeel();
+       if (Platform.isLinux())
+       {
+         setMetalLookAndFeel();
+       }
+       if (Platform.isMac())
+       {
+         setMacLookAndFeel();
+       }
+     }
+   }
+   private static boolean setCrossPlatformLookAndFeel()
+   {
+     boolean set = false;
+     try
+     {
+       UIManager.setLookAndFeel(
+               UIManager.getCrossPlatformLookAndFeelClassName());
+       set = true;
+     } catch (Exception ex)
+     {
+       Cache.log.error("Unexpected Look and Feel Exception");
+       Cache.log.error(ex.getMessage());
+       Cache.log.debug(Cache.getStackTraceString(ex));
+     }
+     return set;
+   }
+   private static boolean setSystemLookAndFeel()
+   {
+     boolean set = false;
+     try
+     {
+       UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+       set = true;
+     } catch (Exception ex)
+     {
+       Cache.log.error("Unexpected Look and Feel Exception");
+       Cache.log.error(ex.getMessage());
+       Cache.log.debug(Cache.getStackTraceString(ex));
+     }
+     return set;
+   }
+   private static boolean setSpecificLookAndFeel(String name,
+           String className, boolean nameStartsWith)
+   {
+     boolean set = false;
+     try
+     {
+       for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels())
+       {
+         if (info.getName() != null && nameStartsWith
+                 ? info.getName().toLowerCase(Locale.ROOT)
+                         .startsWith(name.toLowerCase(Locale.ROOT))
+                 : info.getName().toLowerCase(Locale.ROOT).equals(name.toLowerCase(Locale.ROOT)))
+         {
+           className = info.getClassName();
+           break;
+         }
+       }
+       UIManager.setLookAndFeel(className);
+       set = true;
+     } catch (Exception ex)
+     {
+       Cache.log.error("Unexpected Look and Feel Exception");
+       Cache.log.error(ex.getMessage());
+       Cache.log.debug(Cache.getStackTraceString(ex));
+     }
+     return set;
+   }
+   private static boolean setGtkLookAndFeel()
+   {
+     return setSpecificLookAndFeel("gtk",
+             "com.sun.java.swing.plaf.gtk.GTKLookAndFeel", true);
+   }
+   private static boolean setMetalLookAndFeel()
+   {
+     return setSpecificLookAndFeel("metal",
+             "javax.swing.plaf.metal.MetalLookAndFeel", false);
+   }
+   private static boolean setNimbusLookAndFeel()
+   {
+     return setSpecificLookAndFeel("nimbus",
+             "javax.swing.plaf.nimbus.NimbusLookAndFeel", false);
+   }
+   private static boolean setQuaquaLookAndFeel()
+   {
+     return setSpecificLookAndFeel("quaqua",
+             ch.randelshofer.quaqua.QuaquaManager.getLookAndFeel().getClass()
+                     .getName(),
+             false);
+   }
+   private static boolean setVaquaLookAndFeel()
+   {
+     return setSpecificLookAndFeel("vaqua",
+             "org.violetlib.aqua.AquaLookAndFeel", false);
+   }
+   private static boolean setMacLookAndFeel()
+   {
+     boolean set = false;
+     System.setProperty("com.apple.mrj.application.apple.menu.about.name",
+             ChannelProperties.getProperty("app_name"));
+     System.setProperty("apple.laf.useScreenMenuBar", "true");
+     set = setQuaquaLookAndFeel();
+     if ((!set) || !UIManager.getLookAndFeel().getClass().toString()
+             .toLowerCase(Locale.ROOT).contains("quaqua"))
+     {
+       set = setVaquaLookAndFeel();
+     }
+     return set;
+   }
 -
    private static void showUsage()
    {
      System.out.println(
    }
  
    /**
--   * Quit method delegates to Desktop.quit - unless running in headless mode
--   * when it just ends the JVM
++   * Quit method delegates to Desktop.quit - unless running in headless mode when
++   * it just ends the JVM
     */
    public void quit()
    {
  
    public static void setCurrentAlignFrame(AlignFrame currentAlignFrame)
    {
 -    Jalview.currentAlignFrame = currentAlignFrame;
 +    Jalview.getInstance().currentAlignFrame = currentAlignFrame;
 +  }
 +  
 +  public void notifyWorker(AlignCalcWorkerI worker, String status)
 +  {
 +    // System.out.println("Jalview worker " + worker.getClass().getSimpleName()
 +    // + " " + status);
 +  }
 +
 +
 +  private static boolean isInteractive = true;
 +
 +  public static boolean isInteractive()
 +  {
 +    return isInteractive;
 +  }
 +
 +  public static void setInteractive(boolean tf)
 +  {
 +    isInteractive = tf;
    }
  }
Simple merge
Simple merge
@@@ -83,10 -82,6 +83,9 @@@ public class Sequence extends ASequenc
  
    private String vamsasId;
  
 +  HiddenMarkovModel hmm;
 +
 +  boolean isHMMConsensusSequence = false;
    private DBModList<DBRefEntry> dbrefs; // controlled access
  
    /**
          this.addPDBId(new PDBEntry(pdb));
        }
      }
 +    if (seq.getHMM() != null)
 +    {
 +      this.hmm = new HiddenMarkovModel(seq.getHMM(), this);
 +    }
    }
  
    @Override
      {
        for (AlignmentAnnotation ann : annotation)
        {
 -        if ((ann.calcId != null && ann.calcId.equals(calcId))
 +        String id = ann.getCalcId();
-         if (id != null && id.equals(calcId)
-                 && ann.label != null && ann.label.equals(label))
++        if ((id != null && id.equals(calcId))
+                 && (ann.label != null && ann.label.equals(label))
+                 && ((ignoreDescription && description == null)
+                         || (ann.description != null
+                                 && ann.description.equals(description))))
 -
          {
            result.add(ann);
          }
      }
    }
  
 +  @Override
 +  public HiddenMarkovModel getHMM()
 +  {
 +    return hmm;
 +  }
 +
 +  @Override
 +  public void setHMM(HiddenMarkovModel hmm)
 +  {
 +    this.hmm = hmm;
 +  }
 +
 +  @Override
 +  public boolean hasHMMAnnotation()
 +  {
 +    if (this.annotation == null) {
 +      return false;
 +    }
 +    for (AlignmentAnnotation ann : annotation)
 +    {
 +      if (InformationThread.HMM_CALC_ID.equals(ann.getCalcId()))
 +      {
 +        return true;
 +      }
 +    }
 +    return false;
 +  }
    /**
     * {@inheritDoc}
     */
Simple merge
@@@ -625,13 -625,13 +625,13 @@@ public class EnsemblGene extends Ensemb
    {
      return new FeatureSettingsAdapter()
      {
 -      SequenceOntologyI so = SequenceOntologyFactory.getInstance();
 +      SequenceOntologyI so = SequenceOntologyFactory.getSequenceOntology();
  
        @Override
-       public boolean isFeatureDisplayed(String type)
+       public boolean isFeatureHidden(String type)
        {
-         return (so.isA(type, SequenceOntologyI.EXON)
-                 || so.isA(type, SequenceOntologyI.SEQUENCE_VARIANT));
+         return (!so.isA(type, SequenceOntologyI.EXON)
+                 && !so.isA(type, SequenceOntologyI.SEQUENCE_VARIANT));
        }
  
        @Override
@@@ -52,23 -31,42 +31,41 @@@ import java.util.Map
  import java.util.StringTokenizer;
  import java.util.Vector;
  
+ import javax.swing.SwingUtilities;
 -
  import org.jmol.adapter.smarter.SmarterJmolAdapter;
  import org.jmol.api.JmolAppConsoleInterface;
  import org.jmol.api.JmolSelectionListener;
  import org.jmol.api.JmolStatusListener;
  import org.jmol.api.JmolViewer;
  import org.jmol.c.CBK;
- import org.jmol.script.T;
  import org.jmol.viewer.Viewer;
  
+ import jalview.api.AlignmentViewPanel;
+ import jalview.api.FeatureRenderer;
+ import jalview.api.FeatureSettingsModelI;
+ import jalview.api.SequenceRenderer;
+ import jalview.bin.Cache;
+ import jalview.datamodel.PDBEntry;
+ import jalview.datamodel.SequenceI;
+ import jalview.gui.AppJmol;
+ import jalview.gui.IProgressIndicator;
+ import jalview.gui.StructureViewer.ViewerType;
+ import jalview.io.DataSourceType;
+ import jalview.io.StructureFile;
+ import jalview.structure.AtomSpec;
+ import jalview.structure.StructureCommand;
+ import jalview.structure.StructureCommandI;
+ import jalview.structure.StructureSelectionManager;
+ import jalview.structures.models.AAStructureBindingModel;
+ import jalview.ws.dbsources.Pdb;
+ import javajs.util.BS;
 -
  public abstract class JalviewJmolBinding extends AAStructureBindingModel
          implements JmolStatusListener, JmolSelectionListener,
          ComponentListener
  {
    private String lastMessage;
  
-   boolean allChainsSelected = false;
 +
    /*
     * when true, try to search the associated datamodel for sequences that are
     * associated with any unknown structures in the Jmol view.
      {
        return null;
      }
 +
-     StringBuilder selectioncom = new StringBuilder(256);
-     // In principle - nSeconds specifies the speed of animation for each
-     // superposition - but is seems to behave weirdly, so we don't specify it.
-     String nSeconds = " ";
-     if (files.length > 10)
-     {
-       nSeconds = " 0.005 ";
-     }
-     else
-     {
-       nSeconds = " " + (2.0 / files.length) + " ";
-       // if (nSeconds).substring(0,5)+" ";
-     }
-     // see JAL-1345 - should really automatically turn off the animation for
-     // large numbers of structures, but Jmol doesn't seem to allow that.
-     // nSeconds = " ";
-     // union of all aligned positions are collected together.
-     for (int a = 0; a < _alignment.length; a++)
-     {
-       int refStructure = _refStructure[a];
-       AlignmentI alignment = _alignment[a];
-       HiddenColumns hiddenCols = _hiddenCols[a];
-       if (a > 0 && selectioncom.length() > 0 && !selectioncom
-               .substring(selectioncom.length() - 1).equals("|"))
-       {
-         selectioncom.append("|");
-       }
-       // process this alignment
-       if (refStructure >= files.length)
-       {
-         System.err.println(
-                 "Invalid reference structure value " + refStructure);
-         refStructure = -1;
-       }
-       /*
-        * 'matched' bit j will be set for visible alignment columns j where
-        * all sequences have a residue with a mapping to the PDB structure
-        */
-       BitSet matched = new BitSet();
-       for (int m = 0; m < alignment.getWidth(); m++)
-       {
-         if (hiddenCols == null || hiddenCols.isVisible(m))
-         {
-           matched.set(m);
-         }
-       }
-       SuperposeData[] structures = new SuperposeData[files.length];
-       for (int f = 0; f < files.length; f++)
-       {
-         structures[f] = new SuperposeData(alignment.getWidth());
-       }
-       /*
-        * Calculate the superposable alignment columns ('matched'), and the
-        * corresponding structure residue positions (structures.pdbResNo)
-        */
-       int candidateRefStructure = findSuperposableResidues(alignment,
-               matched, structures);
-       if (refStructure < 0)
-       {
-         /*
-          * If no reference structure was specified, pick the first one that has
-          * a mapping in the alignment
-          */
-         refStructure = candidateRefStructure;
-       }
-       String[] selcom = new String[files.length];
-       int nmatched = matched.cardinality();
-       if (nmatched < 4)
-       {
-         return (MessageManager.formatMessage("label.insufficient_residues",
-                 nmatched));
-       }
-       /*
-        * generate select statements to select regions to superimpose structures
-        */
-       {
-         // TODO extract method to construct selection statements
-         for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
-         {
-           String chainCd = ":" + structures[pdbfnum].chain;
-           int lpos = -1;
-           boolean run = false;
-           StringBuilder molsel = new StringBuilder();
-           molsel.append("{");
-           int nextColumnMatch = matched.nextSetBit(0);
-           while (nextColumnMatch != -1)
-           {
-             int pdbResNo = structures[pdbfnum].pdbResNo[nextColumnMatch];
-             if (lpos != pdbResNo - 1)
-             {
-               // discontinuity
-               if (lpos != -1)
-               {
-                 molsel.append(lpos);
-                 molsel.append(chainCd);
-                 molsel.append("|");
-               }
-               run = false;
-             }
-             else
-             {
-               // continuous run - and lpos >-1
-               if (!run)
-               {
-                 // at the beginning, so add dash
-                 molsel.append(lpos);
-                 molsel.append("-");
-               }
-               run = true;
-             }
-             lpos = pdbResNo;
-             nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
-           }
-           /*
-            * add final selection phrase
-            */
-           if (lpos != -1)
-           {
-             molsel.append(lpos);
-             molsel.append(chainCd);
-             molsel.append("}");
-           }
-           if (molsel.length() > 1)
-           {
-             selcom[pdbfnum] = molsel.toString();
-             selectioncom.append("((");
-             selectioncom.append(selcom[pdbfnum].substring(1,
-                     selcom[pdbfnum].length() - 1));
-             selectioncom.append(" )& ");
-             selectioncom.append(pdbfnum + 1);
-             selectioncom.append(".1)");
-             if (pdbfnum < files.length - 1)
-             {
-               selectioncom.append("|");
-             }
-           }
-           else
-           {
-             selcom[pdbfnum] = null;
-           }
-         }
-       }
-       StringBuilder command = new StringBuilder(256);
-       // command.append("set spinFps 10;\n");
-       for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
-       {
-         if (pdbfnum == refStructure || selcom[pdbfnum] == null
-                 || selcom[refStructure] == null)
-         {
-           continue;
-         }
-         command.append("echo ");
-         command.append("\"Superposing (");
-         command.append(structures[pdbfnum].pdbId);
-         command.append(") against reference (");
-         command.append(structures[refStructure].pdbId);
-         command.append(")\";\ncompare " + nSeconds);
-         command.append("{");
-         command.append(Integer.toString(1 + pdbfnum));
-         command.append(".1} {");
-         command.append(Integer.toString(1 + refStructure));
-         // conformation=1 excludes alternate locations for CA (JAL-1757)
-         command.append(
-                 ".1} SUBSET {(*.CA | *.P) and conformation=1} ATOMS ");
-         // for (int s = 0; s < 2; s++)
-         // {
-         // command.append(selcom[(s == 0 ? pdbfnum : refStructure)]);
-         // }
-         command.append(selcom[pdbfnum]);
-         command.append(selcom[refStructure]);
-         command.append(" ROTATE TRANSLATE;\n");
-       }
-       if (selectioncom.length() > 0)
-       {
-         // TODO is performing selectioncom redundant here? is done later on
-         // System.out.println("Select regions:\n" + selectioncom.toString());
-         evalStateCommand("select *; cartoons off; backbone; select ("
-                 + selectioncom.toString() + "); cartoons; ");
-         // selcom.append("; ribbons; ");
-         String cmdString = command.toString();
-         // System.out.println("Superimpose command(s):\n" + cmdString);
-         evalStateCommand(cmdString);
-       }
-     }
-     if (selectioncom.length() > 0)
-     {// finally, mark all regions that were superposed.
-       if (selectioncom.substring(selectioncom.length() - 1).equals("|"))
-       {
-         selectioncom.setLength(selectioncom.length() - 1);
-       }
-       // System.out.println("Select regions:\n" + selectioncom.toString());
-       evalStateCommand("select *; cartoons off; backbone; select ("
-               + selectioncom.toString() + "); cartoons; ");
-       // evalStateCommand("select *; backbone; select "+selcom.toString()+";
-       // cartoons; center "+selcom.toString());
-     }
-     return null;
-   }
-   public void evalStateCommand(String command)
-   {
+     String cmd = command.getCommand();
      jmolHistory(false);
-     if (lastCommand == null || !lastCommand.equals(command))
+     if (lastCommand == null || !lastCommand.equals(cmd))
      {
-       jmolScript(command + "\n");
+       jmolScript(cmd + "\n");
      }
      jmolHistory(true);
-     lastCommand = command;
-   }
-   Thread colourby = null;
-   /**
-    * Sends a set of colour commands to the structure viewer
-    * 
-    * @param colourBySequenceCommands
-    */
-   @Override
-   protected void colourBySequence(
-           final StructureMappingcommandSet[] colourBySequenceCommands)
-   {
-     if (colourby != null)
-     {
-       colourby.interrupt();
-       colourby = null;
-     }
-     Thread colourby = new Thread(new Runnable()
-     {
-       @Override
-       public void run()
-       {
-         for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands)
-         {
-           for (String cbyseq : cpdbbyseq.commands)
-           {
-             executeWhenReady(cbyseq);
-           }
-         }
-       }
-     });
-     colourby.start();
-     this.colourby = colourby;
-   }
-   /**
-    * @param files
-    * @param sr
-    * @param viewPanel
-    * @return
-    */
-   @Override
-   protected StructureMappingcommandSet[] getColourBySequenceCommands(
-           String[] files, SequenceRenderer sr, AlignmentViewPanel viewPanel)
-   {
-     return JmolCommands.getColourBySequenceCommand(getSsm(), files,
-             getSequence(), sr, viewPanel);
-   }
-   /**
-    * @param command
-    */
-   protected void executeWhenReady(String command)
-   {
-     evalStateCommand(command);
+     lastCommand = cmd;
+     return null;
    }
  
    public void createImage(String file, String type, int quality)
      return null;
    }
  
-   public Color getColour(int atomIndex, int pdbResNum, String chain,
-           String pdbfile)
-   {
-     if (getModelNum(pdbfile) < 0)
-     {
-       return null;
-     }
-     // TODO: verify atomIndex is selecting correct model.
-     // return new Color(viewer.getAtomArgb(atomIndex)); Jmol 12.2.4
-     int colour = viewer.ms.at[atomIndex].atomPropertyInt(T.color);
-     return new Color(colour);
-   }
-   /**
-    * instruct the Jalview binding to update the pdbentries vector if necessary
-    * prior to matching the jmol view's contents to the list of structure files
-    * Jalview knows about.
-    */
-   public abstract void refreshPdbEntries();
-   private int getModelNum(String modelFileName)
-   {
-     String[] mfn = getStructureFiles();
-     if (mfn == null)
-     {
-       return -1;
-     }
-     for (int i = 0; i < mfn.length; i++)
-     {
-       if (mfn[i].equalsIgnoreCase(modelFileName))
-       {
-         return i;
-       }
-     }
-     return -1;
-   }
 +
    /**
     * map between index of model filename returned from getPdbFile and the first
     * index of models from this file in the viewer. Note - this is not trimmed -
  
      jmolScript(cmd.toString());
      jmolHistory(true);
 +
    }
  
-   boolean debug = true;
+   private boolean debug = true;
  
    private void jmolHistory(boolean enable)
    {
      setLoadingFromArchive(false);
    }
  
-   @Override
-   public List<String> getChainNames()
-   {
-     return chainNames;
-   }
 +
    protected IProgressIndicator getIProgressIndicator()
    {
      return null;
  
    }
  
-   @Override
-   public void setJalviewColourScheme(ColourSchemeI cs)
-   {
-     colourBySequence = false;
-     if (cs == null)
-     {
-       return;
-     }
-     jmolHistory(false);
-     StringBuilder command = new StringBuilder(128);
-     command.append("select *;color white;");
-     List<String> residueSet = ResidueProperties.getResidues(isNucleotide(),
-             false);
-     for (String resName : residueSet)
-     {
-       char res = resName.length() == 3
-               ? ResidueProperties.getSingleCharacterCode(resName)
-               : resName.charAt(0);
-       Color col = cs.findColour(res, 0, null, null, 0f);
-       command.append("select " + resName + ";color[" + col.getRed() + ","
-               + col.getGreen() + "," + col.getBlue() + "];");
-     }
-     evalStateCommand(command.toString());
-     jmolHistory(true);
-   }
 +
    public void showHelp()
    {
      showUrl("http://wiki.jmol.org"
@@@ -55,16 -61,14 +63,19 @@@ import jalview.util.Platform
   * 
   * @author tcnofoegbu
   */
- public class PDBFTSRestClient extends FTSRestClient implements ApplicationSingletonI
+ public class PDBFTSRestClient extends FTSRestClient
 -        implements StructureFTSRestClientI
++        implements StructureFTSRestClientI,ApplicationSingletonI
  {
+   private static FTSRestClientI instance = null;
    public static final String PDB_SEARCH_ENDPOINT = "https://www.ebi.ac.uk/pdbe/search/pdb/select?";
  
 +  public static FTSRestClientI getInstance()
 +  {
 +    return (FTSRestClientI) ApplicationSingletonProvider
 +            .getInstance(PDBFTSRestClient.class);
 +  }
    protected PDBFTSRestClient()
    {
    }
  
        URI uri = webResource.getURI();
  
-       // System.out.println(uri);
-       // Execute the REST request
-       ClientResponse clientResponse = webResource
-               .accept(MediaType.APPLICATION_JSON).get(clientResponseClass );
+       System.out.println(uri);
+       ClientResponse clientResponse = null;
+       int responseStatus = -1;
 +
        // Get the JSON string from the response object or directly from the
        // client (JavaScript)
        Map<String, Object> jsonObj = null;
        switch (responseStatus)
        {
        case 200:
-         if (Platform.isJS())
 -
+         if (isMocked())
          {
-           jsonObj = clientResponse.getEntity(Map.class);
+           responseString = mockQueries.get(uri.toString());
          }
          else
          {
      {
        if (jsonObj == null)
        {
-         jsonObj = (Map<String, Object>) JSONUtils.parse(pdbJsonResponseString);
+         jsonObj = (Map<String, Object>) JSONUtils
+                 .parse(pdbJsonResponseString);
        }
-       Map<String, Object> pdbResponse = (Map<String, Object>) jsonObj.get("response");
-       String queryTime = ((Map<String, Object>) jsonObj.get("responseHeader"))
-               .get("QTime").toString();
+       Map<String, Object> pdbResponse = (Map<String, Object>) jsonObj
+               .get("response");
+       String queryTime = ((Map<String, Object>) jsonObj
+               .get("responseHeader")).get("QTime").toString();
        int numFound = Integer
                .valueOf(pdbResponse.get("numFound").toString());
+       List<Object> docs = (List<Object>) pdbResponse.get("docs");
+       result = new ArrayList<FTSData>();
        if (numFound > 0)
        {
-         result = new ArrayList<>();
-         List<Object> docs = (List<Object>) pdbResponse.get("docs");
-         for (Iterator<Object> docIter = docs.iterator(); docIter
-                 .hasNext();)
 -
+         for (Iterator<Object> docIter = docs.iterator(); docIter.hasNext();)
          {
            Map<String, Object> doc = (Map<String, Object>) docIter.next();
            result.add(getFTSData(doc, pdbRestRequest));
          }
-         searchResult.setNumberOfItemsFound(numFound);
-         searchResult.setResponseTime(queryTime);
-         searchResult.setSearchSummary(result);
        }
+       // this is the total number found by the query,
+       // rather than the set returned in SearchSummary
+       searchResult.setNumberOfItemsFound(numFound);
+       searchResult.setResponseTime(queryTime);
+       searchResult.setSearchSummary(result);
 -
      } catch (ParseException e)
      {
        e.printStackTrace();
      return "/fts/pdb_data_columns.txt";
    }
  
 -  public static FTSRestClientI getInstance()
 -  {
 -    if (instance == null)
 -    {
 -      instance = new PDBFTSRestClient();
 -    }
 -    return instance;
 -  }
    private Collection<FTSDataColumnI> allDefaultDisplayedStructureDataColumns;
  
+   @Override
    public Collection<FTSDataColumnI> getAllDefaultDisplayedStructureDataColumns()
    {
      if (allDefaultDisplayedStructureDataColumns == null
      }
      return allDefaultDisplayedStructureDataColumns;
    }
-   
-   
 -
+   @Override
+   public String[] getPreferencesColumnsFor(PreferenceSource source)
+   {
+     String[] columnNames = null;
+     switch (source)
+     {
+     case SEARCH_SUMMARY:
+       columnNames = new String[] { "", "Display", "Group" };
+       break;
+     case STRUCTURE_CHOOSER:
+       columnNames = new String[] { "", "Display", "Group" };
+       break;
+     case PREFERENCES:
+       columnNames = new String[] { "PDB Field", "Show in search summary",
+           "Show in structure summary" };
+       break;
+     default:
+       break;
+     }
+     return columnNames;
+   }
  }
   */
  package jalview.gui;
  
+ import java.util.Locale;
++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.IOException;
+ 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;
  import jalview.analysis.AlignmentSorter;
  import jalview.analysis.AlignmentUtils;
  import jalview.analysis.CrossRef;
@@@ -188,13 -166,10 +255,12 @@@ import ext.vamsas.ServiceHandle
   * @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.
     * 
    public AlignFrame(AlignmentI al, HiddenColumns hiddenColumns, int width,
            int height, String sequenceSetId, String viewId)
    {
 +    id = (++frameCount);
      setSize(width, height);
  
      if (al.getDataset() == null)
     * initalise the alignframe from the underlying viewport data and the
     * configurations
     */
    void init()
    {
 -    // setBackground(Color.white); // BH 2019
 +    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())
      {
        progressBar = new ProgressBar(this.statusPanel, this.statusBar);
     * @param format
     *          format of file
     */
 +  @Deprecated
    public void setFileName(String file, FileFormatI format)
    {
      fileName = file;
      return viewport;
    }
  
 +  @Override
 +  public void servicesChanged(WSDiscovererI discoverer,
 +          Collection<? extends ServiceWithParameters> services)
 +  {
 +    buildWebServicesMenu();
 +  }
    /* Set up intrinsic listeners for dynamically generated GUI bits. */
    private void addServiceListeners()
    {
        }
        else
        {
 -        Rectangle bounds = this.getBounds();
 +        JOptionPane.showMessageDialog(this,
 +                MessageManager.getString("warn.invalid_format"));
 +      }
 +    }
 +  }
  
 -        FileLoader loader = new FileLoader();
 +  /**
 +   * 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"));
  
 -        AlignFrame newframe = null;
 +    int value = chooser.showOpenDialog(this);
  
 -        if (fileObject == null)
 -        {
 +    if (value == JFileChooser.APPROVE_OPTION)
 +    {
 +      choice = chooser.getSelectedFile().getPath();
 +    }
 +    return choice;
 +  }
  
 -          DataSourceType protocol = HttpUtils.startsWithHttpOrHttps(
 -                  fileName) ? DataSourceType.URL : DataSourceType.FILE;
 -          newframe = loader.LoadFileWaitTillLoaded(fileName, protocol,
 -                  currentFileFormat);
 -        }
 -        else
 +  @Override
 +  public void reload_actionPerformed(ActionEvent e)
 +  {
 +    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))
 +    {
 +      JInternalFrame[] frames = Desktop.getDesktopPane().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))
          {
 -          newframe = loader.LoadFileWaitTillLoaded(fileObject,
 -                  DataSourceType.FILE, currentFileFormat);
 +          try
 +          {
 +            frames[i].setSelected(true);
 +            Desktop.getInstance().closeAssociatedWindows();
 +          } catch (java.beans.PropertyVetoException ex)
 +          {
 +          }
          }
  
 -        newframe.setBounds(bounds);
 -        if (featureSettings != null && featureSettings.isShowing())
 +      }
 +      Desktop.getInstance().closeAssociatedWindows();
 +
 +      FileLoader loader = new FileLoader();
- //      DataSourceType protocol = fileName.startsWith("http:")
- //              ? DataSourceType.URL
- //              : DataSourceType.FILE;
-         loader.LoadFile(viewport, (fileObject == null ? fileName : fileObject), protocol, currentFileFormat);
++      loader.LoadFile(viewport, (fileObject == null ? fileName : fileObject), protocol, currentFileFormat);
 +    }
 +    else
 +    {
 +      Rectangle bounds = this.getBounds();
 +
 +      FileLoader loader = new FileLoader();
 +
 +      AlignFrame newframe = null;
 +
 +      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())
 +      {
 +        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
        }
        lastSaveSuccessful = new Jalview2XML().saveAlignment(this, file,
                shortName);
--
        statusBar.setText(MessageManager.formatMessage(
                "label.successfully_saved_to_file_in_format", new Object[]
 -              { file, format }));
 -
 +              { fileName, format }));
        return;
      }
  
      bjs.exportHTML(null);
    }
  
 +  // ??
    public void createImageMap(File file, String image)
    {
      alignPanel.makePNGImageMap(file, image);
        // && viewport.getColumnSelection().getHiddenColumns() != null &&
        // viewport.getColumnSelection()
        // .getHiddenColumns().size() > 0);
 -      originalSource.firePropertyChange("alignment", null,
 -              originalSource.getAlignment().getSequences());
 +      originalSource.notifyAlignment();
      }
    }
  
        // && viewport.getColumnSelection().getHiddenColumns() != null &&
        // viewport.getColumnSelection()
        // .getHiddenColumns().size() > 0);
 -      originalSource.firePropertyChange("alignment", null,
 -              originalSource.getAlignment().getSequences());
 +      originalSource.notifyAlignment();
      }
    }
  
  
      if (sg == null)
      {
+       if (viewport.cursorMode)
+       {
+         sg = new SequenceGroup();
+         sg.addSequence(viewport.getAlignment().getSequenceAt(
+                 alignPanel.getSeqPanel().seqCanvas.cursorY), false);
+       }
+       else
+       {
+         return;
+       }
+     }
+     if (sg.getSize() < 1)
+     {
        return;
      }
 -
+     // TODO: JAL-3733 - add an event to the undo buffer for this !
 -
      viewport.getAlignment().moveSelectedSequencesByOne(sg,
              viewport.getHiddenRepSequences(), up);
      alignPanel.paintAlignment(true, false);
                .setContents(new StringSelection(""), null);
  
        Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss,
-               Desktop.getInstance());
 -              Desktop.instance);
++              d);
      } catch (OutOfMemoryError er)
      {
        new OOMWarning("copying region", er);
     * 
     * @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);
    }
        AlignFrame af = new AlignFrame(alignment, DEFAULT_WIDTH,
                DEFAULT_HEIGHT);
        String newtitle = new String("Flanking alignment");
 -      if (Desktop.jalviewClipboard != null
 -              && Desktop.jalviewClipboard[2] != null)
 +      Desktop d = Desktop.getInstance();
 +      if (d.jalviewClipboard != null && d.jalviewClipboard[2] != null)
        {
 -        HiddenColumns hc = (HiddenColumns) Desktop.jalviewClipboard[2];
 +        HiddenColumns hc = (HiddenColumns) d.jalviewClipboard[2];
          af.viewport.setHiddenColumns(hc);
        }
  
          }
        }
  
 -      viewport.firePropertyChange("alignment", null,
 -              viewport.getAlignment().getSequences());
 +      viewport.notifyAlignment();
      }
    }
  
      // if (viewport.hasHiddenColumns)
      // viewport.getColumnSelection().compensateForEdits(shifts);
      ranges.setStartRes(seq.findIndex(startRes) - 1);
 -    viewport.firePropertyChange("alignment", null,
 -            viewport.getAlignment().getSequences());
 +    viewport.notifyAlignment();
  
    }
  
    /**
              viewport.getAlignment()));
  
      viewport.getRanges().setStartRes(seq.findIndex(startRes) - 1);
 -    viewport.firePropertyChange("alignment", null,
 -            viewport.getAlignment().getSequences());
 +    viewport.notifyAlignment();
  
    }
  
    public void padGapsMenuitem_actionPerformed(ActionEvent e)
    {
      viewport.setPadGaps(padGapsMenuitem.isSelected());
 -    viewport.firePropertyChange("alignment", null,
 -            viewport.getAlignment().getSequences());
 +    viewport.notifyAlignment();
    }
  
    /**
        JLabel textLabel = new JLabel();
        textLabel.setText(content);
        textLabel.setBackground(Color.WHITE);
--
        pane = new JPanel(new BorderLayout());
        ((JPanel) pane).setOpaque(true);
        pane.setBackground(Color.WHITE);
      }
  
      JInternalFrame frame = new JInternalFrame();
 -    final OverviewPanel overview = new OverviewPanel(alignPanel);
 +    // BH 2019.07.26 we allow for an embedded
 +    // undecorated overview with defined size
 +    frame.setName(Platform.getAppID("overview"));
 +    //
 +    Dimension dim = Platform.getDimIfEmbedded(frame, -1, -1);
 +    if (dim != null && dim.width == 0)
 +    {
 +      dim = null; // hidden, not embedded
 +    }
 +    OverviewPanel overview = new OverviewPanel(alignPanel, dim);
      frame.setContentPane(overview);
 +    if (dim == null)
 +    {
 +      dim = new Dimension();
 +      // was frame.getSize(), but that is 0,0 at this point;
 +    }
 +    else
 +    {
 +      // we are imbedding, and so we have an undecorated frame
 +      // and we can set the the frame dimensions accordingly.
 +    }
 +    // allowing for unresizable option using, style="resize:none"
 +    boolean resizable = (Platform.getEmbeddedAttribute(frame,
 +            "resize") != "none");
      Desktop.addInternalFrame(frame, MessageManager
              .formatMessage("label.overview_params", new Object[]
 -            { this.getTitle() }), true, frame.getWidth(), frame.getHeight(),
 -            true, true);
 +            { this.getTitle() }), Desktop.FRAME_MAKE_VISIBLE, dim.width,
 +            dim.height, resizable, Desktop.FRAME_ALLOW_ANY_SIZE);
      frame.pack();
      frame.setLayer(JLayeredPane.PALETTE_LAYER);
      frame.addInternalFrameListener(
      alignPanel.paintAlignment(true, false);
    }
  
 +  @Override
 +  public void sortEValueMenuItem_actionPerformed(ActionEvent e)
 +  {
 +    SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
 +    AlignmentSorter.sortByEValue(viewport.getAlignment());
 +    addHistoryItem(new OrderCommand("Group Sort", oldOrder,
 +            viewport.getAlignment()));
 +    alignPanel.paintAlignment(true, false);
 +
 +  }
 +
 +  @Override
 +  public void sortBitScoreMenuItem_actionPerformed(ActionEvent e)
 +  {
 +    SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
 +    AlignmentSorter.sortByBitScore(viewport.getAlignment());
 +    addHistoryItem(new OrderCommand("Group Sort", oldOrder,
 +            viewport.getAlignment()));
 +    alignPanel.paintAlignment(true, false);
 +
 +  }
    /**
     * DOCUMENT ME!
     * 
    @Override
    public void autoCalculate_actionPerformed(ActionEvent e)
    {
 -    viewport.autoCalculateConsensus = autoCalculate.isSelected();
 -    if (viewport.autoCalculateConsensus)
 +    viewport.setAutoCalculateConsensusAndConservation(
 +            autoCalculate.isSelected());
 +    if (viewport.getAutoCalculateConsensusAndConservation())
-     // ??
-     // viewport.autoCalculateConsensus = autoCalculate.isSelected();
-     // if (viewport.autoCalculateConsensus)
      {
 -      viewport.firePropertyChange("alignment", null,
 -              viewport.getAlignment().getSequences());
 +      viewport.notifyAlignment();
      }
    }
  
        sortByTreeMenu.add(item);
      }
    }
 -
 +
 +  private List<TreePanel> getTreePanels()
 +  {
 +    List<Component> comps = PaintRefresher.components
 +            .get(viewport.getSequenceSetId());
 +    List<TreePanel> treePanels = new ArrayList<>();
 +    for (Component comp : comps)
 +    {
 +      if (comp instanceof TreePanel)
 +      {
 +        treePanels.add((TreePanel) comp);
 +      }
 +    }
 +    return treePanels;
 +  }
    public boolean sortBy(AlignmentOrder alorder, String undoname)
    {
      SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
        if (nf.getTree() != null)
        {
          tp = new TreePanel(alignPanel, nf, treeTitle, input);
 -        tp.setSize(w, h);
 +        Dimension dim = Platform.getDimIfEmbedded(tp, -1, -1);
 +        if (dim == null)
 +        {
 +          dim = new Dimension(w, h);
 +        }
 +        else
 +        {
 +          // no offset, either
 +          x = 0;
 +        }
 +        tp.setSize(dim.width, dim.height);
  
          if (x > 0 && y > 0)
          {
      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.getInstance();
 +        JMenu submenu = new JMenu("JABAWS");
 +        buildLegacyWebServicesMenu(submenu);
 +        buildWebServicesMenu(jws2servs, submenu);
 +        webService.add(submenu);
        }
 -    }
 -    final AlignFrame me = this;
 -    buildingMenu = true;
 -    new Thread(new Runnable()
 +    });
 +  }
 +
 +  private void buildLegacyWebServicesMenu(JMenu menu)
 +  {
 +    JMenu secstrmenu = new JMenu("Secondary Structure Prediction");
 +    if (Discoverer.getServices() != null && Discoverer.getServices().size() > 0) 
      {
 -      @Override
 -      public void run()
 +      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
 -          if (// Cache.getDefault("SHOW_JWS1_SERVICES", true)
 -              // &&
 -          Discoverer.services != null && (Discoverer.services.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 = Discoverer.services
 -                    .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.getDiscoverer();
 -                  if (jws2servs != null)
 -                  {
 -                    if (jws2servs.hasServices())
 -                    {
 -                      jws2servs.attachWSMenuEntry(webService, me);
 -                      for (Jws2Instance sv : jws2servs.getServices())
 -                      {
 -                        if (sv.description.toLowerCase(Locale.ROOT).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);
 +    }
    }
  
    /**
      {
        e.printStackTrace();
      }
 -    if (files != null)
 +  }
 +
 +  protected void loadDroppedFiles(List<Object> files,
 +          List<DataSourceType> protocols, DropTargetDropEvent evt,
 +          Transferable t)
 +  {
 +    try
      {
 -      new Thread(new Runnable()
 +      // check to see if any of these files have names matching sequences
 +      // in
 +      // the alignment
 +      SequenceIdMatcher idm = new SequenceIdMatcher(
 +              viewport.getAlignment().getSequencesArray());
 +      /**
 +       * Object[] { String,SequenceI}
 +       */
 +      ArrayList<Object[]> filesmatched = new ArrayList<>();
 +      ArrayList<Object> filesnotmatched = new ArrayList<>();
 +      for (int i = 0; i < files.size(); i++)
        {
 -        @Override
 -        public void run()
 +        // BH 2018
 +        Object fileObj = files.get(i);
 +        String fileName = fileObj.toString();
 +        String pdbfn = "";
 +        DataSourceType protocol = (fileObj instanceof File
 +                ? DataSourceType.FILE
 +                : FormatAdapter.checkProtocol(fileName));
 +        if (protocol == DataSourceType.FILE)
          {
 -          try
 +          File file;
 +          if (fileObj instanceof File)
 +          {
 +            file = (File) fileObj;
 +            Platform.cacheFileData(file);
 +          }
 +          else
 +          {
 +            file = new File(fileName);
 +          }
 +          pdbfn = file.getName();
 +        }
 +        else if (protocol == DataSourceType.URL)
 +        {
 +          URL url = new URL(fileName);
 +          pdbfn = url.getFile();
 +        }
 +        if (pdbfn.length() > 0)
 +        {
 +          // attempt to find a match in the alignment
 +          SequenceI[] mtch = idm.findAllIdMatches(pdbfn);
 +          int l = 0, c = pdbfn.indexOf(".");
 +          while (mtch == null && c != -1)
            {
 -            // check to see if any of these files have names matching sequences
 -            // in
 -            // the alignment
 -            SequenceIdMatcher idm = new SequenceIdMatcher(
 -                    viewport.getAlignment().getSequencesArray());
 -            /**
 -             * Object[] { String,SequenceI}
 -             */
 -            ArrayList<Object[]> filesmatched = new ArrayList<>();
 -            ArrayList<Object> filesnotmatched = new ArrayList<>();
 -            for (int i = 0; i < files.size(); i++)
 +            do
              {
 -              // BH 2018
 -              Object file = files.get(i);
 -              String fileName = file.toString();
 -              String pdbfn = "";
 -              DataSourceType protocol = (file instanceof File
 -                      ? DataSourceType.FILE
 -                      : FormatAdapter.checkProtocol(fileName));
 -              if (protocol == DataSourceType.FILE)
 -              {
 -                File fl;
 -                if (file instanceof File)
 -                {
 -                  fl = (File) file;
 -                  Platform.cacheFileData(fl);
 -                }
 -                else
 -                {
 -                  fl = new File(fileName);
 -                }
 -                pdbfn = fl.getName();
 -              }
 -              else if (protocol == DataSourceType.URL)
 -              {
 -                URL url = new URL(fileName);
 -                pdbfn = url.getFile();
 -              }
 -              if (pdbfn.length() > 0)
 -              {
 -                // attempt to find a match in the alignment
 -                SequenceI[] mtch = idm.findAllIdMatches(pdbfn);
 -                int l = 0, c = pdbfn.indexOf(".");
 -                while (mtch == null && c != -1)
 -                {
 -                  do
 -                  {
 -                    l = c;
 -                  } while ((c = pdbfn.indexOf(".", l)) > l);
 -                  if (l > -1)
 -                  {
 -                    pdbfn = pdbfn.substring(0, l);
 -                  }
 -                  mtch = idm.findAllIdMatches(pdbfn);
 -                }
 -                if (mtch != null)
 -                {
 -                  FileFormatI type;
 -                  try
 -                  {
 -                    type = new IdentifyFile().identify(file, protocol);
 -                  } catch (Exception ex)
 -                  {
 -                    type = null;
 -                  }
 -                  if (type != null && type.isStructureFile())
 -                  {
 -                    filesmatched.add(new Object[] { file, protocol, mtch });
 -                    continue;
 -                  }
 -                }
 -                // File wasn't named like one of the sequences or wasn't a PDB
 -                // file.
 -                filesnotmatched.add(file);
 -              }
 +              l = c;
 +            } while ((c = pdbfn.indexOf(".", l)) > l);
 +            if (l > -1)
 +            {
 +              pdbfn = pdbfn.substring(0, l);
              }
 -            int assocfiles = 0;
 -            if (filesmatched.size() > 0)
 +            mtch = idm.findAllIdMatches(pdbfn);
 +          }
 +          if (mtch != null)
 +          {
 +            FileFormatI type;
 +            try
              {
 -              boolean autoAssociate = Cache
 -                      .getDefault("AUTOASSOCIATE_PDBANDSEQS", false);
 -              if (!autoAssociate)
 -              {
 -                String msg = MessageManager.formatMessage(
 -                        "label.automatically_associate_structure_files_with_sequences_same_name",
 -                        new Object[]
 -                        { Integer.valueOf(filesmatched.size())
 -                                .toString() });
 -                String ttl = MessageManager.getString(
 -                        "label.automatically_associate_structure_files_by_name");
 -                int choice = JvOptionPane.showConfirmDialog(thisaf, msg,
 -                        ttl, JvOptionPane.YES_NO_OPTION);
 -                autoAssociate = choice == JvOptionPane.YES_OPTION;
 -              }
 -              if (autoAssociate)
 -              {
 -                for (Object[] fm : filesmatched)
 -                {
 -                  // try and associate
 -                  // TODO: may want to set a standard ID naming formalism for
 -                  // associating PDB files which have no IDs.
 -                  for (SequenceI toassoc : (SequenceI[]) fm[2])
 -                  {
 -                    PDBEntry pe = new AssociatePdbFileWithSeq()
 -                            .associatePdbWithSeq(fm[0].toString(),
 -                                    (DataSourceType) fm[1], toassoc, false,
 -                                    Desktop.instance);
 -                    if (pe != null)
 -                    {
 -                      System.err.println("Associated file : "
 -                              + (fm[0].toString()) + " with "
 -                              + toassoc.getDisplayId(true));
 -                      assocfiles++;
 -                    }
 -                  }
 -                  // TODO: do we need to update overview ? only if features are
 -                  // shown I guess
 -                  alignPanel.paintAlignment(true, false);
 -                }
 -              }
 -              else
 -              {
 -                /*
 -                 * add declined structures as sequences
 -                 */
 -                for (Object[] o : filesmatched)
 -                {
 -                  filesnotmatched.add(o[0]);
 -                }
 -              }
 +              type = new IdentifyFile().identify(fileObj, protocol);
 +            } catch (Exception ex)
 +            {
 +              type = null;
              }
 -            if (filesnotmatched.size() > 0)
 +            if (type != null && type.isStructureFile())
              {
 -              if (assocfiles > 0 && (Cache.getDefault(
 -                      "AUTOASSOCIATE_PDBANDSEQS_IGNOREOTHERS", false)
 -                      || JvOptionPane.showConfirmDialog(thisaf,
 -                              "<html>" + MessageManager.formatMessage(
 -                                      "label.ignore_unmatched_dropped_files_info",
 -                                      new Object[]
 -                                      { Integer.valueOf(
 -                                              filesnotmatched.size())
 -                                              .toString() })
 -                                      + "</html>",
 -                              MessageManager.getString(
 -                                      "label.ignore_unmatched_dropped_files"),
 -                              JvOptionPane.YES_NO_OPTION) == JvOptionPane.YES_OPTION))
 -              {
 -                return;
 -              }
 -              for (Object fn : filesnotmatched)
 +              filesmatched.add(new Object[] { fileObj, protocol, mtch });
 +              continue;
 +            }
 +          }
 +          // File wasn't named like one of the sequences or wasn't a PDB
 +          // file.
 +          filesnotmatched.add(fileObj);
 +        }
 +      }
 +      int assocfiles = 0;
 +      if (filesmatched.size() > 0)
 +      {
 +        boolean autoAssociate = Cache
 +                .getDefault(Preferences.AUTOASSOCIATE_PDBANDSEQS, false);
 +        if (!autoAssociate)
 +        {
 +          String msg = MessageManager.formatMessage(
 +                  "label.automatically_associate_structure_files_with_sequences_same_name",
 +                  new Object[]
 +                  { Integer.valueOf(filesmatched.size()).toString() });
 +          String ttl = MessageManager.getString(
 +                  "label.automatically_associate_structure_files_by_name");
 +          int choice = JvOptionPane.showConfirmDialog(this, msg, ttl,
 +                  JvOptionPane.YES_NO_OPTION);
 +          autoAssociate = choice == JvOptionPane.YES_OPTION;
 +        }
 +        if (autoAssociate)
 +        {
 +          for (Object[] fm : filesmatched)
 +          {
 +            // try and associate
 +            // TODO: may want to set a standard ID naming formalism for
 +            // associating PDB files which have no IDs.
 +            for (SequenceI toassoc : (SequenceI[]) fm[2])
 +            {
 +              PDBEntry pe = AssociatePdbFileWithSeq.associatePdbWithSeq(
 +                      fm[0].toString(), (DataSourceType) fm[1], toassoc,
 +                      false);
 +              if (pe != null)
                {
 -                loadJalviewDataFile(fn, null, null, null);
 +                System.err.println("Associated file : " + (fm[0].toString())
 +                        + " with " + toassoc.getDisplayId(true));
 +                assocfiles++;
                }
              }
 -          } catch (Exception ex)
 +            // TODO: do we need to update overview ? only if features are
 +            // shown I guess
 +            alignPanel.paintAlignment(true, false);
 +          }
 +        }
 +        else
 +        {
 +          /*
 +           * add declined structures as sequences
 +           */
 +          for (Object[] o : filesmatched)
            {
 -            ex.printStackTrace();
 +            filesnotmatched.add(o[0]);
            }
          }
 -      }).start();
 +      }
 +      if (filesnotmatched.size() > 0)
 +      {
 +        if (assocfiles > 0 && (Cache
 +                .getDefault("AUTOASSOCIATE_PDBANDSEQS_IGNOREOTHERS", false)
 +                || JvOptionPane.showConfirmDialog(this,
 +                        "<html>" + MessageManager.formatMessage(
 +                                "label.ignore_unmatched_dropped_files_info",
 +                                new Object[]
 +                                { Integer.valueOf(filesnotmatched.size())
 +                                        .toString() })
 +                                + "</html>",
 +                        MessageManager.getString(
 +                                "label.ignore_unmatched_dropped_files"),
 +                        JvOptionPane.YES_NO_OPTION) == JvOptionPane.YES_OPTION))
 +        {
 +          return;
 +        }
 +        for (Object fn : filesnotmatched)
 +        {
 +          loadJalviewDataFile(fn, null, null, null);
 +        }
 +
 +      }
 +    } catch (Exception ex)
 +    {
 +      ex.printStackTrace();
      }
    }
  
     * 
     * @param file
     *          either a filename or a URL string.
 +   * @throws InterruptedException
 +   * @throws IOException
     */
    public void loadJalviewDataFile(Object file, DataSourceType sourceType,
            FileFormatI format, SequenceI assocSeq)
    {
        }
        if (isAnnotation)
        {
 -        alignPanel.adjustAnnotationHeight();
 -        viewport.updateSequenceIdColours();
 -        buildSortByAnnotationScoresMenu();
 -        alignPanel.paintAlignment(true, true);
 +        updateForAnnotations();
        }
      } catch (Exception ex)
      {
        @Override
        public void run()
        {
 -        final jalview.ws.SequenceFetcher sf = jalview.gui.SequenceFetcher
 -                .getSequenceFetcherSingleton();
 +        // ??
 +        // final jalview.ws.SequenceFetcher sf = jalview.gui.SequenceFetcher
 +        // .getSequenceFetcherSingleton();
          javax.swing.SwingUtilities.invokeLater(new Runnable()
          {
            @Override
            public void run()
            {
      }
    }
  
 +  /**
 +   * Sets the status of the HMMER menu
 +   */
 +  public void updateHMMERStatus()
 +  {
 +    hmmerMenu.setEnabled(HmmerCommand.isHmmerAvailable());
 +  }
    @Override
    protected void loadVcf_actionPerformed()
    {
    }
  
    private Rectangle lastFeatureSettingsBounds = null;
--
    @Override
    public void setFeatureSettingsGeometry(Rectangle bounds)
    {
@@@ -74,14 -72,6 +74,11 @@@ import javax.swing.JInternalFrame
  public class AlignViewport extends AlignmentViewport
          implements SelectionSource
  {
 +  public final static int NO_SPLIT = 0;
 +
 +  public final static int SPLIT_FRAME = 1;
 +
 +  public final static int NEW_WINDOW = 2;
    Font font;
  
    boolean cursorMode = false;
      }
      setColourAppliesToAllGroups(true);
    }
 +  
    boolean validCharWidth;
  
    /**
    public StructureSelectionManager getStructureSelectionManager()
    {
      return StructureSelectionManager
 -            .getStructureSelectionManager(Desktop.instance);
 +            .getStructureSelectionManager(Desktop.getInstance());
    }
 +  
    @Override
    public boolean isNormaliseSequenceLogo()
    {
@@@ -763,8 -720,7 +735,7 @@@ public void setNormaliseSequenceLogo(bo
      }
  
      ranges.setEndSeq(getAlignment().getHeight() - 1); // BH 2019.04.18
 -    firePropertyChange("alignment", null, getAlignment().getSequences());
 +    notifyAlignment();
    }
  
    /**
                @Override
                public void run()
                {
 -                us.openLinkedAlignmentAs(al, title, true);
++                // Make a copy of this one to open it in a splitframe
 +                openLinkedAlignmentAs(getAlignPanel().alignFrame,
 +                        new Alignment(getAlignment()), al, title,
 +                        SPLIT_FRAME);
- //                us.openLinkedAlignmentAs(al, title, true);
                }
 -            }).setResponseHandler(2, new Runnable()
 +            }).setResponseHandler(NEW_WINDOW, new Runnable()
              {
                @Override
                public void run()
              JvOptionPane.DEFAULT_OPTION, JvOptionPane.PLAIN_MESSAGE, null,
              options, options[0]);
    }
 -  protected void openLinkedAlignmentAs(AlignmentI al, String title,
 -          boolean newWindowOrSplitPane)
 -    {
 +  /**
 +   * Open a split frame or a new window
 +   * 
 +   * @param al
 +   * @param title
 +   * @param mode
 +   *          SPLIT_FRAME or NEW_WINDOW
 +   */
 +  public static void openLinkedAlignmentAs(AlignFrame thisFrame,
 +          AlignmentI thisAlignment, AlignmentI al, String title, int mode)
 +  {
      /*
--     * Identify protein and dna alignments. Make a copy of this one if opening
--     * in a new split pane.
++     * Identify protein and dna alignments. 
       */
 -    AlignmentI thisAlignment = newWindowOrSplitPane
 -            ? new Alignment(getAlignment())
 -            : getAlignment();
      AlignmentI protein = al.isNucleotide() ? thisAlignment : al;
 -    final AlignmentI cdna = al.isNucleotide() ? al : thisAlignment;
 +    AlignmentI cdna = al.isNucleotide() ? al : thisAlignment;
      /*
       * Map sequences. At least one should get mapped as we have already passed
       * the test for 'mappability'. Any mappings made will be added to the
       */
      AlignFrame copyMe = new AlignFrame(complement, AlignFrame.DEFAULT_WIDTH,
              AlignFrame.DEFAULT_HEIGHT);
 -    copyMe.setTitle(getAlignPanel().alignFrame.getTitle());
 +    copyMe.setTitle(thisFrame.getTitle());
  
      AlignmentI al = newAlignFrame.viewport.getAlignment();
      final AlignFrame proteinFrame = al.isNucleotide() ? copyMe
              : newAlignFrame;
      {
        FeatureColourI preferredColour = featureSettings
                .getFeatureColour(type);
 +      FeatureMatcherSetI preferredFilters = featureSettings
 +              .getFeatureFilters(type);
        FeatureColourI origColour = fr.getFeatureStyle(type);
        if (!mergeOnly || (!origRenderOrder.contains(type)
                || origColour == null
        fr.orderFeatures(featureSettings);
      }
      fr.setTransparency(featureSettings.getTransparency());
 -
+     fr.notifyFeaturesChanged();
    }
  
    public String getViewName()
   */
  package jalview.gui;
  
+ import java.util.Locale;
+ 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;
  import java.awt.Color;
  import java.awt.Cursor;
  import java.awt.Dimension;
@@@ -353,10 -354,6 +355,8 @@@ public class AnnotationLabels extends J
        pop.show(this, evt.getX(), evt.getY());
        return;
      }
 +    final AlignmentAnnotation ann = aa[selectedRow];
 +    final boolean isSequenceAnnotation = ann.sequenceRef != null;
      item = new JMenuItem(EDITNAME);
      item.addActionListener(this);
      pop.add(item);
@@@ -52,6 -37,22 +37,21 @@@ import javax.swing.SwingUtilities
  import javax.swing.event.InternalFrameAdapter;
  import javax.swing.event.InternalFrameEvent;
  
+ import jalview.api.AlignmentViewPanel;
+ import jalview.bin.Cache;
+ import jalview.datamodel.PDBEntry;
+ import jalview.datamodel.SequenceI;
+ import jalview.datamodel.StructureViewerModel;
+ import jalview.datamodel.StructureViewerModel.StructureData;
+ import jalview.fts.service.alphafold.AlphafoldRestClient;
+ import jalview.gui.ImageExporter.ImageWriterI;
+ import jalview.gui.StructureViewer.ViewerType;
+ import jalview.structure.StructureCommand;
+ import jalview.structures.models.AAStructureBindingModel;
+ import jalview.util.BrowserLauncher;
+ import jalview.util.ImageMaker;
+ import jalview.util.MessageManager;
+ import jalview.util.Platform;
 -
  public class AppJmol extends StructureViewerBase
  {
    // ms to wait for Jmol to load files
     * @param bounds
     * @param viewid
     */
-   public AppJmol(String[] files, String[] ids, SequenceI[][] seqs,
-           AlignmentPanel ap, boolean usetoColour, boolean useToAlign,
-           boolean leaveColouringToJmol, String loadStatus, Rectangle bounds,
-           String viewid)
+   public AppJmol(StructureViewerModel viewerModel, AlignmentPanel ap,
+           String sessionFile, String viewid)
    {
-     PDBEntry[] pdbentrys = new PDBEntry[files.length];
-     for (int i = 0; i < pdbentrys.length; i++)
+     Map<File, StructureData> pdbData = viewerModel.getFileData();
+     PDBEntry[] pdbentrys = new PDBEntry[pdbData.size()];
+     SequenceI[][] seqs = new SequenceI[pdbData.size()][];
+     int i = 0;
+     for (StructureData data : pdbData.values())
      {
-       // PDBEntry pdbentry = new PDBEntry(files[i], ids[i]);
-       PDBEntry pdbentry = new PDBEntry(ids[i], null, PDBEntry.Type.PDB,
-               files[i]);
+       PDBEntry pdbentry = new PDBEntry(data.getPdbId(), null,
+               PDBEntry.Type.PDB, data.getFilePath());
        pdbentrys[i] = pdbentry;
+       List<SequenceI> sequencesForPdb = data.getSeqList();
+       seqs[i] = sequencesForPdb
+               .toArray(new SequenceI[sequencesForPdb.size()]);
+       i++;
      }
-     // / TODO: check if protocol is needed to be set, and if chains are
 -
+     // TODO: check if protocol is needed to be set, and if chains are
      // autodiscovered.
      jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
              pdbentrys, seqs, null);
        seqColour.setSelected(true);
        viewerColour.setSelected(false);
      }
-     this.setBounds(bounds);
 -
+     this.setBounds(viewerModel.getX(), viewerModel.getY(),
+             viewerModel.getWidth(), viewerModel.getHeight());
      setViewId(viewid);
-     // jalview.gui.Desktop.addInternalFrame(this, "Loading File",
-     // bounds.width,bounds.height);
  
      this.addInternalFrameListener(new InternalFrameAdapter()
      {
    {
      super.initMenus();
  
-     viewerActionMenu.setText(MessageManager.getString("label.jmol"));
 +
      viewerColour
              .setText(MessageManager.getString("label.colour_with_jmol"));
      viewerColour.setToolTipText(MessageManager
      }
    }
  
+   @Override
    public void showConsole(boolean showConsole)
    {
 +
      if (showConsole)
      {
        if (splitPane == null)
   */
  package jalview.gui;
  
- import jalview.api.FeatureRenderer;
- import jalview.bin.Cache;
- import jalview.datamodel.AlignmentI;
- import jalview.datamodel.PDBEntry;
- import jalview.datamodel.SequenceI;
- import jalview.ext.rbvi.chimera.ChimeraCommands;
- import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
- import jalview.gui.StructureViewer.ViewerType;
- import jalview.io.DataSourceType;
- import jalview.io.StructureFile;
- import jalview.structures.models.AAStructureBindingModel;
- import jalview.util.BrowserLauncher;
- import jalview.util.ImageMaker.TYPE;
- import jalview.util.MessageManager;
- import jalview.util.Platform;
- import jalview.ws.dbsources.Pdb;
 +
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.MouseAdapter;
@@@ -57,6 -36,22 +37,21 @@@ import javax.swing.JMenuItem
  import javax.swing.event.InternalFrameAdapter;
  import javax.swing.event.InternalFrameEvent;
  
+ import jalview.api.AlignmentViewPanel;
+ import jalview.api.FeatureRenderer;
+ import jalview.bin.Cache;
+ import jalview.datamodel.PDBEntry;
+ import jalview.datamodel.SequenceI;
+ import jalview.datamodel.StructureViewerModel;
+ import jalview.datamodel.StructureViewerModel.StructureData;
+ import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
+ import jalview.gui.StructureViewer.ViewerType;
+ import jalview.io.DataSourceType;
+ import jalview.io.StructureFile;
+ import jalview.structures.models.AAStructureBindingModel;
+ import jalview.util.ImageMaker.TYPE;
+ import jalview.util.MessageManager;
+ import jalview.util.Platform;
 -
  /**
   * GUI elements for handling an external chimera display
   * 
@@@ -67,8 -62,6 +62,7 @@@ public class ChimeraViewFrame extends S
  {
    private JalviewChimeraBinding jmb;
  
-   private IProgressIndicator progressBar = null;
 +
    /*
     * Path to Chimera session file. This is set when an open Jalview/Chimera
     * session is saved, or on restore from a Jalview project (if it holds the
@@@ -76,8 -69,6 +70,7 @@@
     */
    private String chimeraSessionFile = null;
  
-   private Random random = new Random();
 +
    private int myWidth = 500;
  
    private int myHeight = 150;
      });
      viewerActionMenu.add(fetchAttributes);
    }
 +
+   @Override
+   protected void buildActionMenu()
+   {
+     super.buildActionMenu();
+     // add these back in after menu is refreshed
+     viewerActionMenu.add(writeFeatures);
+     viewerActionMenu.add(fetchAttributes);
+     
+   };
    /**
-    * Query Chimera for its residue attribute names and add them as items off the
-    * attributes menu
+    * Query the structure viewer for its residue attribute names and add them as
+    * items off the attributes menu
     * 
     * @param attributesMenu
     */
  
    }
  
+   protected JalviewChimeraBindingModel newBindingModel(AlignmentPanel ap,
+           PDBEntry[] pdbentrys, SequenceI[][] seqs)
+   {
+     return new JalviewChimeraBindingModel(this,
+             ap.getStructureSelectionManager(), pdbentrys, seqs, null);
+   }
 -
    /**
     * Create a new viewer from saved session state data including Chimera session
     * file
  
      if (!jmb.launchChimera())
      {
 -      JvOptionPane.showMessageDialog(Desktop.desktop,
 +      JvOptionPane.showMessageDialog(Desktop.getDesktopPane(),
-               MessageManager.getString("label.chimera_failed"),
+               MessageManager.formatMessage("label.open_viewer_failed",
+                       getViewerName()),
                MessageManager.getString("label.error_loading_file"),
                JvOptionPane.ERROR_MESSAGE);
        this.dispose();
      worker = null;
    }
  
-   /**
-    * Fetch PDB data and save to a local file. Returns the full path to the file,
-    * or null if fetch fails. TODO: refactor to common with Jmol ? duplication
-    * 
-    * @param processingEntry
-    * @return
-    * @throws Exception
-    */
-   private void stashFoundChains(StructureFile pdb, String file)
-   {
-     for (int i = 0; i < pdb.getChains().size(); i++)
-     {
-       String chid = new String(
-               pdb.getId() + ":" + pdb.getChains().elementAt(i).id);
-       jmb.getChainNames().add(chid);
-       jmb.getChainFile().put(chid, file);
-     }
-   }
-   private String fetchPdbFile(PDBEntry processingEntry) throws Exception
-   {
-     String filePath = null;
-     Pdb pdbclient = new Pdb();
-     AlignmentI pdbseq = null;
-     String pdbid = processingEntry.getId();
-     long handle = System.currentTimeMillis()
-             + Thread.currentThread().hashCode();
-     /*
-      * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
-      */
-     String msg = MessageManager.formatMessage("status.fetching_pdb",
-             new Object[]
-             { pdbid });
-     getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
-     // long hdl = startProgressBar(MessageManager.formatMessage(
-     // "status.fetching_pdb", new Object[]
-     // { pdbid }));
-     try
-     {
-       pdbseq = pdbclient.getSequenceRecords(pdbid);
-     } catch (OutOfMemoryError oomerror)
-     {
-       new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
-     } finally
-     {
-       msg = pdbid + " " + MessageManager.getString("label.state_completed");
-       getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
-       // stopProgressBar(msg, hdl);
-     }
-     /*
-      * If PDB data were saved and are not invalid (empty alignment), return the
-      * file path.
-      */
-     if (pdbseq != null && pdbseq.getHeight() > 0)
-     {
-       // just use the file name from the first sequence's first PDBEntry
-       filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
-               .elementAt(0).getFile()).getAbsolutePath();
-       processingEntry.setFile(filePath);
-     }
-     return filePath;
-   }
-   /**
-    * Convenience method to update the progress bar if there is one. Be sure to
-    * call stopProgressBar with the returned handle to remove the message.
-    * 
-    * @param msg
-    * @param handle
-    */
-   public long startProgressBar(String msg)
-   {
-     // TODO would rather have startProgress/stopProgress as the
-     // IProgressIndicator interface
-     long tm = random.nextLong();
-     if (progressBar != null)
-     {
-       progressBar.setProgressBar(msg, tm);
-     }
-     return tm;
-   }
-   /**
-    * End the progress bar with the specified handle, leaving a message (if not
-    * null) on the status bar
-    * 
-    * @param msg
-    * @param handle
-    */
-   public void stopProgressBar(String msg, long handle)
-   {
-     if (progressBar != null)
-     {
-       progressBar.setProgressBar(msg, handle);
-     }
-   }
 +
    @Override
    public void makePDBImage(TYPE imageType)
    {
      return jmb;
    }
  
-   /**
-    * Ask Chimera to save its session to the designated file path, or to a
-    * temporary file if the path is null. Returns the file path if successful,
-    * else null.
-    * 
-    * @param filepath
-    * @see getStateInfo
-    */
-   protected String saveSession(String filepath)
-   {
-     String pathUsed = filepath;
-     try
-     {
-       if (pathUsed == null)
-       {
-         File tempFile = File.createTempFile("chimera", ".py");
-         tempFile.deleteOnExit();
-         pathUsed = tempFile.getPath();
-       }
-       boolean result = jmb.saveSession(pathUsed);
-       if (result)
-       {
-         this.chimeraSessionFile = pathUsed;
-         return pathUsed;
-       }
-     } catch (IOException e)
-     {
-     }
-     return null;
-   }
-   /**
-    * Returns a string representing the state of the Chimera session. This is
-    * done by requesting Chimera to save its session to a temporary file, then
-    * reading the file contents. Returns an empty string on any error.
-    */
-   @Override
-   public String getStateInfo()
-   {
-     String sessionFile = saveSession(null);
-     if (sessionFile == null)
-     {
-       return "";
-     }
-     InputStream is = null;
-     try
-     {
-       File f = new File(sessionFile);
-       byte[] bytes = new byte[(int) f.length()];
-       is = new FileInputStream(sessionFile);
-       is.read(bytes);
-       return new String(bytes);
-     } catch (IOException e)
-     {
-       return "";
-     } finally
-     {
-       if (is != null)
-       {
-         try
-         {
-           is.close();
-         } catch (IOException e)
-         {
-           // ignore
-         }
-       }
-     }
-   }
-   @Override
-   protected void fitToWindow_actionPerformed()
-   {
-     jmb.focusView();
-   }
 +
    @Override
    public ViewerType getViewerType()
    {
@@@ -20,6 -20,8 +20,7 @@@
   */
  package jalview.gui;
  
+ import java.util.Locale;
 -
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Dimension;
@@@ -124,9 -124,9 +130,10 @@@ import jalview.util.ChannelProperties
  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;
  
   * @author $author$
   * @version $Revision: 1.155 $
   */
 -public class Desktop extends jalview.jbgui.GDesktop
 -    implements DropTargetListener, ClipboardOwner, IProgressIndicator, jalview.api.StructureSelectionManagerProvider {
 +@SuppressWarnings("serial")
 +public class Desktop extends GDesktop
 +        implements DropTargetListener, ClipboardOwner, IProgressIndicator,
 +        StructureSelectionManagerProvider, ApplicationSingletonI
 +
 +{
-   private static final String CITATION = "<br><br>Development managed by The Barton Group, University of Dundee, Scotland, UK.<br>"
-           + "<br><br>For help, see the FAQ at <a href=\"http://www.jalview.org/faq\">www.jalview.org/faq</a> and/or join the jalview-discuss@jalview.org mailing list"
-           + "<br><br>If  you use Jalview, please cite:"
-           + "<br>Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)"
-           + "<br>Jalview Version 2 - a multiple sequence alignment editor and analysis workbench"
-           + "<br>Bioinformatics doi: 10.1093/bioinformatics/btp033";
+   private static final String CITATION;
+   static {
+     URL bg_logo_url = ChannelProperties.getImageURL("bg_logo." + String.valueOf(SplashScreen.logoSize));
+     URL uod_logo_url = ChannelProperties.getImageURL("uod_banner." + String.valueOf(SplashScreen.logoSize));
+     boolean logo = (bg_logo_url != null || uod_logo_url != null);
+     StringBuilder sb = new StringBuilder();
+     sb.append("<br><br>Development managed by The Barton Group, University of Dundee, Scotland, UK.");
+     if (logo) {
+       sb.append("<br>");
+     }
+     sb.append(bg_logo_url == null ? "" : "<img alt=\"Barton Group logo\" src=\"" + bg_logo_url.toString() + "\">");
+     sb.append(uod_logo_url == null ? ""
+         : "&nbsp;<img alt=\"University of Dundee shield\" src=\"" + uod_logo_url.toString() + "\">");
+     sb.append(
+         "<br><br>For help, see the FAQ at <a href=\"https://www.jalview.org/faq\">www.jalview.org/faq</a> and/or join the jalview-discuss@jalview.org mailing list");
+     sb.append("<br><br>If  you use Jalview, please cite:"
+         + "<br>Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)"
+         + "<br>Jalview Version 2 - a multiple sequence alignment editor and analysis workbench"
+         + "<br>Bioinformatics doi: 10.1093/bioinformatics/btp033");
+     CITATION = sb.toString();
+   }
  
    private static final String DEFAULT_AUTHORS = "The Jalview Authors (See AUTHORS file for current list)";
  
  
    public static HashMap<String, FileWriter> savingFiles = new HashMap<String, FileWriter>();
  
 +  @SuppressWarnings("deprecation")
    private JalviewChangeSupport changeSupport = new JalviewChangeSupport();
  
+   public static boolean nosplash = false;
 -
    /**
     * news reader - null if it was never started.
     */
    }
  
    /**
 -   * Creates a new Desktop object.
 +   * Private constructor enforces singleton pattern. It is called by reflection
 +   * from ApplicationSingletonProvider.getInstance().
     */
 -  public Desktop() {
 +  private Desktop()
 +  {
+     super();
 -    /**
 -     * A note to implementors. It is ESSENTIAL that any activities that might block
 -     * are spawned off as threads rather than waited for during this constructor.
 -     */
 -    instance = this;
 +    Cache.initLogger();
 +    try
 +    {
 +      /**
 +       * A note to implementors. It is ESSENTIAL that any activities that might
 +       * block are spawned off as threads rather than waited for during this
 +       * constructor.
 +       */
  
 -    doConfigureStructurePrefs();
 +      doConfigureStructurePrefs();
-       setTitle("Jalview " + Cache.getProperty("VERSION"));
-       /*
-       if (!Platform.isAMac())
-       {
-       // this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
-       }
-       else
-       {
-        this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
-       }
-       */
+     setTitle(ChannelProperties.getProperty("app_name") + " " + Cache.getProperty("VERSION"));
  
-       try
-       {
-         APQHandlers.setAPQHandlers(this);
-       } catch (Throwable t)
-       {
-         System.out.println("Error setting APQHandlers: " + t.toString());
-         // t.printStackTrace();
+     /**
+      * Set taskbar "grouped windows" name for linux desktops (works in GNOME and
+      * KDE). This uses sun.awt.X11.XToolkit.awtAppClassName which is not officially
+      * documented or guaranteed to exist, so we access it via reflection. There
+      * appear to be unfathomable criteria about what this string can contain, and it
+      * if doesn't meet those criteria then "java" (KDE) or "jalview-bin-Jalview"
+      * (GNOME) is used. "Jalview", "Jalview Develop" and "Jalview Test" seem okay,
+      * but "Jalview non-release" does not. The reflection access may generate a
+      * warning: WARNING: An illegal reflective access operation has occurred
+      * WARNING: Illegal reflective access by jalview.gui.Desktop () to field
+      * sun.awt.X11.XToolkit.awtAppClassName which I don't think can be avoided.
+      */
+     if (Platform.isLinux()) {
+       try {
+         Toolkit xToolkit = Toolkit.getDefaultToolkit();
+         Field[] declaredFields = xToolkit.getClass().getDeclaredFields();
+         Field awtAppClassNameField = null;
+         if (Arrays.stream(declaredFields).anyMatch(f -> f.getName().equals("awtAppClassName"))) {
+           awtAppClassNameField = xToolkit.getClass().getDeclaredField("awtAppClassName");
+         }
+         String title = ChannelProperties.getProperty("app_name");
+         if (awtAppClassNameField != null) {
+           awtAppClassNameField.setAccessible(true);
+           awtAppClassNameField.set(xToolkit, title);
+         } else {
+           Cache.log.debug("XToolkit: awtAppClassName not found");
+         }
+       } catch (Exception e) {
+         Cache.debug("Error setting awtAppClassName");
+         Cache.trace(Cache.getStackTraceString(e));
        }
+     }
  
-       addWindowListener(new WindowAdapter()
-       {
+     /**
+      * APQHandlers sets handlers for About, Preferences and Quit actions peculiar to
+      * macOS's application menu. APQHandlers will check to see if a handler is
+      * supported before setting it.
+      */
+     try {
+       APQHandlers.setAPQHandlers(this);
+     } catch (Exception e) {
+       System.out.println("Cannot set APQHandlers");
+       // e.printStackTrace();
+     } catch (Throwable t) {
+       Cache.warn("Error setting APQHandlers: " + t.toString());
+       Cache.trace(Cache.getStackTraceString(t));
+     }
+     setIconImages(ChannelProperties.getIconList());
  
-         @Override
-         public void windowClosing(WindowEvent ev)
-         {
-           quit();
-         }
-       });
+     addWindowListener(new WindowAdapter() {
+       @Override
+       public void windowClosing(WindowEvent ev) {
+         quit();
+       }
+     });
  
 -    boolean selmemusage = Cache.getDefault("SHOW_MEMUSAGE", false);
 +      boolean selmemusage = Cache.getDefault("SHOW_MEMUSAGE", false);
  
 -    boolean showjconsole = Cache.getDefault("SHOW_JAVA_CONSOLE", false);
 -    desktop = new MyDesktopPane(selmemusage);
 +      boolean showjconsole = Cache.getDefault("SHOW_JAVA_CONSOLE", false);
 +      desktopPane = new MyDesktopPane(selmemusage);
  
 -    showMemusage.setSelected(selmemusage);
 -    desktop.setBackground(Color.white);
 +      showMemusage.setSelected(selmemusage);
 +      desktopPane.setBackground(Color.white);
  
 -    getContentPane().setLayout(new BorderLayout());
 -    // alternate config - have scrollbars - see notes in JAL-153
 -    // JScrollPane sp = new JScrollPane();
 -    // sp.getViewport().setView(desktop);
 -    // getContentPane().add(sp, BorderLayout.CENTER);
 +      getContentPane().setLayout(new BorderLayout());
 +      // alternate config - have scrollbars - see notes in JAL-153
 +      // JScrollPane sp = new JScrollPane();
 +      // sp.getViewport().setView(desktop);
 +      // getContentPane().add(sp, BorderLayout.CENTER);
  
 -    // BH 2018 - just an experiment to try unclipped JInternalFrames.
 -    if (Platform.isJS()) {
 -      getRootPane().putClientProperty("swingjs.overflow.hidden", "false");
 -    }
 +      // BH 2018 - just an experiment to try unclipped JInternalFrames.
 +      if (Platform.isJS())
 +      {
 +        getRootPane().putClientProperty("swingjs.overflow.hidden", "false");
 +      }
  
 -    getContentPane().add(desktop, BorderLayout.CENTER);
 -    desktop.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
 +      getContentPane().add(desktopPane, BorderLayout.CENTER);
 +      desktopPane.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
  
 -    // This line prevents Windows Look&Feel resizing all new windows to maximum
 -    // if previous window was maximised
 -    desktop.setDesktopManager(new MyDesktopManager((Platform.isWindowsAndNotJS() ? new DefaultDesktopManager()
 -        : Platform.isAMacAndNotJS() ? new AquaInternalFrameManager(desktop.getDesktopManager())
 -            : desktop.getDesktopManager())));
 -    Rectangle dims = getLastKnownDimensions("");
 -    if (dims != null) {
 -      setBounds(dims);
 -    } else {
 -      Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
 -      int xPos = Math.max(5, (screenSize.width - 900) / 2);
 -      int yPos = Math.max(5, (screenSize.height - 650) / 2);
 -      setBounds(xPos, yPos, 900, 650);
 -    }
 +      // This line prevents Windows Look&Feel resizing all new windows to
 +      // maximum
 +      // if previous window was maximised
 +      desktopPane.setDesktopManager(new MyDesktopManager(
 +              (Platform.isWindowsAndNotJS() ? new DefaultDesktopManager()
 +                      : Platform.isAMacAndNotJS()
 +                              ? new AquaInternalFrameManager(
 +                                      desktopPane.getDesktopManager())
 +                              : desktopPane.getDesktopManager())));
  
 -    if (!Platform.isJS())
 -    /**
 -     * Java only
 -     * 
 -     * @j2sIgnore
 -     */
 -    {
 -      jconsole = new Console(this, showjconsole);
 -      jconsole.setHeader(Cache.getVersionDetailsForConsole());
 -      showConsole(showjconsole);
 +      Rectangle dims = getLastKnownDimensions("");
 +      if (dims != null)
 +      {
 +        setBounds(dims);
 +      }
 +      else
 +      {
 +        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
 +        int xPos = Math.max(5, (screenSize.width - 900) / 2);
 +        int yPos = Math.max(5, (screenSize.height - 650) / 2);
 +        setBounds(xPos, yPos, 900, 650);
 +      }
  
-       getIdentifiersOrgData();
 -      showNews.setVisible(false);
 +      if (!Platform.isJS())
 +      /**
 +       * Java only
 +       * 
 +       * @j2sIgnore
 +       */
 +      {
 +        jconsole = new Console(this, showjconsole);
 +        jconsole.setHeader(Cache.getVersionDetailsForConsole());
 +        showConsole(showjconsole);
  
-         showNews.setVisible(false);
 -      experimentalFeatures.setSelected(showExperimental());
++        showNews.setVisible(false); // not sure if we should only do this for interactive session?
  
 -      getIdentifiersOrgData();
 +        experimentalFeatures.setSelected(showExperimental());
  
 -      checkURLLinks();
++        getIdentifiersOrgData();
 -      // Spawn a thread that shows the splashscreen
 -      if (!nosplash) {
 -        SwingUtilities.invokeLater(new Runnable() {
 -          @Override
 -          public void run() {
 -            new SplashScreen(true);
 +        if (Jalview.isInteractive())
 +        {
 +          // disabled for SeqCanvasTest
 +          checkURLLinks();
 +
 +          // Spawn a thread that shows the splashscreen
++          if (!nosplash) {
 +          SwingUtilities.invokeLater(new Runnable()
-           {
-             @Override
-             public void run()
-             {
-               new SplashScreen(true);
-             }
-           });
++           {
++             @Override
++             public void run()
++             {
++               new SplashScreen(true);
++             }
++           });
+           }
 -        });
 -      }
  
 -      // Thread off a new instance of the file chooser - this reduces the time
 -      // it
 -      // takes to open it later on.
 -      new Thread(new Runnable() {
 -        @Override
 -        public void run() {
 -          Cache.log.debug("Filechooser init thread started.");
 -          String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
 -          JalviewFileChooser.forRead(Cache.getProperty("LAST_DIRECTORY"), fileFormat);
 -          Cache.log.debug("Filechooser init thread finished.");
 +          // Thread off a new instance of the file chooser - this reduces the
 +          // time
 +          // it
 +          // takes to open it later on.
 +          new Thread(new Runnable()
 +          {
 +            @Override
 +            public void run()
 +            {
 +              Cache.log.debug("Filechooser init thread started.");
 +              String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
 +              JalviewFileChooser.forRead(
 +                      Cache.getProperty("LAST_DIRECTORY"), fileFormat);
 +              Cache.log.debug("Filechooser init thread finished.");
 +            }
 +          }).start();
 +          // Add the service change listener
 +          changeSupport.addJalviewPropertyChangeListener("services",
 +                  new PropertyChangeListener()
 +                  {
 +
 +                    @Override
 +                    public void propertyChange(PropertyChangeEvent evt)
 +                    {
 +                      Cache.log.debug("Firing service changed event for "
 +                              + evt.getNewValue());
 +                      JalviewServicesChanged(evt);
 +                    }
 +                  });
          }
 -      }).start();
 -      // Add the service change listener
 -      changeSupport.addJalviewPropertyChangeListener("services", new PropertyChangeListener() {
 +      }
 +      this.setDropTarget(new java.awt.dnd.DropTarget(desktopPane, this));
 +
 +      this.addWindowListener(new WindowAdapter()
 +      {
          @Override
 -        public void propertyChange(PropertyChangeEvent evt) {
 -          Cache.log.debug("Firing service changed event for " + evt.getNewValue());
 -          JalviewServicesChanged(evt);
 +        public void windowClosing(WindowEvent evt)
 +        {
 +          quit();
          }
        });
 -    }
 -
 -    this.setDropTarget(new java.awt.dnd.DropTarget(desktop, this));
  
 -    this.addWindowListener(new WindowAdapter() {
 -      @Override
 -      public void windowClosing(WindowEvent evt) {
 -        quit();
 -      }
 -    });
 -
 -    MouseAdapter ma;
 -    this.addMouseListener(ma = new MouseAdapter() {
 -      @Override
 -      public void mousePressed(MouseEvent evt) {
 -        if (evt.isPopupTrigger()) // Mac
 +      MouseAdapter ma;
 +      this.addMouseListener(ma = new MouseAdapter()
 +      {
 +        @Override
 +        public void mousePressed(MouseEvent evt)
          {
 -          showPasteMenu(evt.getX(), evt.getY());
 +          if (evt.isPopupTrigger()) // Mac
 +          {
 +            showPasteMenu(evt.getX(), evt.getY());
 +          }
          }
 -      }
 -
 -      @Override
 -      public void mouseReleased(MouseEvent evt) {
 -        if (evt.isPopupTrigger()) // Windows
 +        @Override
 +        public void mouseReleased(MouseEvent evt)
          {
 -          showPasteMenu(evt.getX(), evt.getY());
 +          if (evt.isPopupTrigger()) // Windows
 +          {
 +            showPasteMenu(evt.getX(), evt.getY());
 +          }
          }
 -      }
 -    });
 -    desktop.addMouseListener(ma);
 +      });
 +      desktopPane.addMouseListener(ma);
 +    } catch (Throwable t)
 +    {
 +      t.printStackTrace();
 +    }
 +
    }
  
    /**
      return Boolean.valueOf(experimental).booleanValue();
    }
  
 -  public void doConfigureStructurePrefs() {
 +  public void doConfigureStructurePrefs()
 +  {
      // configure services
-     StructureSelectionManager ssm = StructureSelectionManager
-             .getStructureSelectionManager(this);
-     if (Cache.getDefault(Preferences.ADD_SS_ANN, true))
-     {
-       ssm.setAddTempFacAnnot(
-               Cache.getDefault(Preferences.ADD_TEMPFACT_ANN, true));
-       ssm.setProcessSecondaryStructure(
-               Cache.getDefault(Preferences.STRUCT_FROM_PDB, true));
-       ssm.setSecStructServices(
-               Cache.getDefault(Preferences.USE_RNAVIEW, true));
-     }
-     else
-     {
+     StructureSelectionManager ssm = StructureSelectionManager.getStructureSelectionManager(this);
+     if (Cache.getDefault(Preferences.ADD_SS_ANN, true)) {
+       ssm.setAddTempFacAnnot(Cache.getDefault(Preferences.ADD_TEMPFACT_ANN, true));
+       ssm.setProcessSecondaryStructure(Cache.getDefault(Preferences.STRUCT_FROM_PDB, true));
+       // JAL-3915 - RNAView is no longer an option so this has no effect
+       ssm.setSecStructServices(Cache.getDefault(Preferences.USE_RNAVIEW, false));
+     } else {
        ssm.setAddTempFacAnnot(false);
        ssm.setProcessSecondaryStructure(false);
        ssm.setSecStructServices(false);
      }
    }
  
- //  /**
- //   * Add an internal frame to the Jalview desktop that is allowed to be resized,
- //   * has a minimum size of 300px and might or might not be visible
- //   * 
- //   * @param frame
- //   *          Frame to show
- //   * @param title
- //   *          Visible Title
- //   * @param makeVisible
- //   *          When true, display frame immediately, otherwise, caller must call
- //   *          setVisible themselves.
- //   * @param w
- //   *          width
- //   * @param h
- //   *          height
- //   */
- //  @Deprecated
- //  public static synchronized void addInternalFrame(
- //          final JInternalFrame frame, String title, boolean makeVisible,
- //          int w, int h)
- //  {
- //    // textbox, web services, sequenceFetcher, featureSettings
- //    getInstance().addFrame(frame, title, makeVisible, w, h,
- //            FRAME_ALLOW_RESIZE, FRAME_SET_MIN_SIZE_300);
- //  }
- //
- //  /**
- //   * Add an internal frame to the Jalview desktop that is visible, has a minimum
- //   * size of 300px, and may or may not be resizable
- //   * 
- //   * @param frame
- //   *          Frame to show
- //   * @param title
- //   *          Visible Title
- //   * @param w
- //   *          width
- //   * @param h
- //   *          height
- //   * @param resizable
- //   *          Allow resize
- //   */
- //  @Deprecated
- //  public static synchronized void addInternalFrame(
- //          final JInternalFrame frame, String title, int w, int h,
- //          boolean resizable)
- //  {
- //    // annotation, font, calculation, user-defined colors
- //    getInstance().addFrame(frame, title, FRAME_MAKE_VISIBLE, w, h,
- //            resizable, FRAME_SET_MIN_SIZE_300);
- //  }
 -  /**
 -   * Adds and opens the given frame to the desktop
 -   * 
 -   * @param frame Frame to show
 -   * @param title Visible Title
 -   * @param w     width
 -   * @param h     height
 -   */
 -  public static synchronized void addInternalFrame(final JInternalFrame frame, String title, int w, int h) {
 -    addInternalFrame(frame, title, true, w, h, true, false);
 -  }
 -  /**
 -   * Add an internal frame to the Jalview desktop
 -   * 
 -   * @param frame       Frame to show
 -   * @param title       Visible Title
 -   * @param makeVisible When true, display frame immediately, otherwise, caller
 -   *                    must call setVisible themselves.
 -   * @param w           width
 -   * @param h           height
 -   */
 -  public static synchronized void addInternalFrame(final JInternalFrame frame, String title, boolean makeVisible, int w,
 -      int h) {
 -    addInternalFrame(frame, title, makeVisible, w, h, true, false);
 -  }
  
    /**
 -   * Add an internal frame to the Jalview desktop and make it visible
 +   * Adds and opens the given frame to the desktop that is visible, allowed to
 +   * resize, and has a 300px minimum width.
     * 
 -   * @param frame     Frame to show
 -   * @param title     Visible Title
 -   * @param w         width
 -   * @param h         height
 -   * @param resizable Allow resize
 -   */
 -  public static synchronized void addInternalFrame(final JInternalFrame frame, String title, int w, int h,
 -      boolean resizable) {
 -    addInternalFrame(frame, title, true, w, h, resizable, false);
 +   * @param frame
 +   *          Frame to show
 +   * @param title
 +   *          Visible Title
 +   * @param w
 +   *          width
 +   * @param h
 +   *          height
 +   */
 +  public static synchronized void addInternalFrame(
 +          final JInternalFrame frame, String title, int w, int h)
 +  {
 +    // 58 classes
 +    
 +    addInternalFrame(frame, title, Desktop.FRAME_MAKE_VISIBLE, w, h, 
 +            FRAME_ALLOW_RESIZE, FRAME_SET_MIN_SIZE_300);
    }
  
    /**
 -   * Add an internal frame to the Jalview desktop
 +   * Add an internal frame to the Jalview desktop that may optionally be
 +   * visible, resizable, and allowed to be any size
     * 
 -   * @param frame         Frame to show
 -   * @param title         Visible Title
 -   * @param makeVisible   When true, display frame immediately, otherwise, caller
 -   *                      must call setVisible themselves.
 -   * @param w             width
 -   * @param h             height
 -   * @param resizable     Allow resize
 -   * @param ignoreMinSize Do not set the default minimum size for frame
 -   */
 -  public static synchronized void addInternalFrame(final JInternalFrame frame, String title, boolean makeVisible, int w,
 -      int h, boolean resizable, boolean ignoreMinSize) {
 +   * @param frame
 +   *          Frame to show
 +   * @param title
 +   *          Visible Title
 +   * @param makeVisible
 +   *          When true, display frame immediately, otherwise, caller must call
 +   *          setVisible themselves.
 +   * @param w
 +   *          width
 +   * @param h
 +   *          height
 +   * @param resizable
 +   *          Allow resize
 +   * @param ignoreMinSize
 +   *          Do not set the default minimum size for frame
 +   */
 +  public static synchronized void addInternalFrame(
 +          final JInternalFrame frame, String title, boolean makeVisible,
 +          int w, int h, boolean resizable, boolean ignoreMinSize)
 +  {
 +    // 15 classes call this method directly.
 +    
      // TODO: allow callers to determine X and Y position of frame (eg. via
      // bounds object).
      // TODO: consider fixing method to update entries in the window submenu with
      // the current window title
  
      frame.setTitle(title);
 -    if (frame.getWidth() < 1 || frame.getHeight() < 1) {
 +    if (frame.getWidth() < 1 || frame.getHeight() < 1)
 +    {
        frame.setSize(w, h);
      }
 -    // THIS IS A PUBLIC STATIC METHOD, SO IT MAY BE CALLED EVEN IN
 -    // A HEADLESS STATE WHEN NO DESKTOP EXISTS. MUST RETURN
 -    // IF JALVIEW IS RUNNING HEADLESS
 -    // ///////////////////////////////////////////////
 -    if (instance == null || (System.getProperty("java.awt.headless") != null
 -        && System.getProperty("java.awt.headless").equals("true"))) {
 -      return;
 -    }
 +    if (getInstance() != null)
 +      getInstance().addFrame(frame, makeVisible, resizable,
 +            ignoreMinSize);
 +  }
 +
 +  // These can now by put into a single int flag, if desired:
 +  
 +  public final static boolean FRAME_MAKE_VISIBLE = true;
 +
 +  public final static boolean FRAME_NOT_VISIBLE = false;
 +
 +  public final static boolean FRAME_ALLOW_RESIZE = true;
 +
 +  public final static boolean FRAME_NOT_RESIZABLE = false;
 +
 +  public final static boolean FRAME_ALLOW_ANY_SIZE = true;
 +
 +  public final static boolean FRAME_SET_MIN_SIZE_300 = false;
 +  
 +  private void addFrame(JInternalFrame frame,
 +          boolean makeVisible, boolean resizable,
 +          boolean ignoreMinSize)
 +  {
  
      openFrameCount++;
 -    if (!ignoreMinSize) {
 -      frame.setMinimumSize(new Dimension(DEFAULT_MIN_WIDTH, DEFAULT_MIN_HEIGHT));
 +    
 +    boolean isEmbedded = (Platform.getEmbeddedAttribute(frame, "id") != null);
 +    boolean hasEmbeddedSize = (Platform.getDimIfEmbedded(frame, -1, -1) != null);
 +    // Web page embedding allows us to ignore minimum size
 +    ignoreMinSize |= hasEmbeddedSize;
 +    
 +    if (!ignoreMinSize)
 +    {
        // Set default dimension for Alignment Frame window.
        // The Alignment Frame window could be added from a number of places,
        // hence,
      frame.setMaximizable(resizable);
      frame.setIconifiable(resizable);
      frame.setOpaque(Platform.isJS());
 -    if (frame.getX() < 1 && frame.getY() < 1) {
 -      frame.setLocation(xOffset * openFrameCount, yOffset * ((openFrameCount - 1) % 10) + yOffset);
 +    if (!isEmbedded && frame.getX() < 1 && frame.getY() < 1)
 +    {
 +      frame.setLocation(xOffset * openFrameCount,
 +              yOffset * ((openFrameCount - 1) % 10) + yOffset);
      }
  
      /*
      /*
       * set up key bindings for Ctrl-W and Cmd-W, with the same (Close) action
       */
-     KeyStroke ctrlWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
-             InputEvent.CTRL_DOWN_MASK);
-     KeyStroke cmdWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
-             Platform.SHORTCUT_KEY_MASK);
+     KeyStroke ctrlWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK);
+     KeyStroke cmdWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W, ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx());
  
 -    InputMap inputMap = frame.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
 +    InputMap inputMap = frame
 +            .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
      String ctrlW = ctrlWKey.toString();
      inputMap.put(ctrlWKey, ctrlW);
      inputMap.put(cmdWKey, ctrlW);
  
      Object[] options = new Object[] { MessageManager.getString("action.ok"),
          MessageManager.getString("action.cancel") };
 -    Runnable action = new Runnable() {
 +    Runnable action = new Runnable()
 +    {
        @Override
 -      public void run() {
 +      public void run()
 +      {
          @SuppressWarnings("unchecked")
-         String url = (history instanceof JTextField
-                 ? ((JTextField) history).getText()
-                 : ((JComboBox<String>) history).getSelectedItem()
-                         .toString());
-         if (url.toLowerCase().endsWith(".jar"))
-         {
-           if (viewport != null)
-           {
-             new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
-                     FileFormat.Jalview);
-           }
-           else
-           {
-             new FileLoader().LoadFile(url, DataSourceType.URL,
-                     FileFormat.Jalview);
+         String url = (history instanceof JTextField ? ((JTextField) history).getText()
+             : ((JComboBox<String>) history).getEditor().getItem().toString().trim());
+         if (url.toLowerCase(Locale.ROOT).endsWith(".jar")) {
+           if (viewport != null) {
+             new FileLoader().LoadFile(viewport, url, DataSourceType.URL, FileFormat.Jalview);
+           } else {
+             new FileLoader().LoadFile(url, DataSourceType.URL, FileFormat.Jalview);
            }
-         }
-         else
-         {
+         } else {
            FileFormatI format = null;
 -          try {
 +          try
 +          {
              format = new IdentifyFile().identify(url, DataSourceType.URL);
 -          } catch (FileFormatException e) {
 +          } catch (FileFormatException e)
 +          {
              // TODO revise error handling, distinguish between
              // URL not found and response not valid
            }
     * 
     * @return
     */
 -  public String getAboutMessage() {
 +  public String getAboutMessage()
 +  {
      StringBuilder message = new StringBuilder(1024);
-     message.append("<h1><strong>Version: ")
-             .append(Cache.getProperty("VERSION")).append("</strong></h1>")
-             .append("<strong>Built: <em>")
-             .append(Cache.getDefault("BUILD_DATE", "unknown"))
-             .append("</em> from ").append(Cache.getBuildDetailsForSplash())
-             .append("</strong>");
+     message.append("<div style=\"font-family: sans-serif;\">").append("<h1><strong>Version: ")
+         .append(Cache.getProperty("VERSION")).append("</strong></h1>").append("<strong>Built: <em>")
+         .append(Cache.getDefault("BUILD_DATE", "unknown")).append("</em> from ")
+         .append(Cache.getBuildDetailsForSplash()).append("</strong>");
  
      String latestVersion = Cache.getDefault("LATEST_VERSION", "Checking");
-     if (latestVersion.equals("Checking"))
-     {
+     if (latestVersion.equals("Checking")) {
        // JBP removed this message for 2.11: May be reinstated in future version
        // message.append("<br>...Checking latest version...</br>");
-     }
-     else if (!latestVersion.equals(Cache.getProperty("VERSION")))
-     {
+     } else if (!latestVersion.equals(Cache.getProperty("VERSION"))) {
        boolean red = false;
-       if (Cache.getProperty("VERSION").toLowerCase()
-               .indexOf("automated build") == -1)
-       {
+       if (Cache.getProperty("VERSION").toLowerCase(Locale.ROOT).indexOf("automated build") == -1) {
          red = true;
          // Displayed when code version and jnlp version do not match and code
          // version is not a development build
      message.append(Cache.getDefault("AUTHORFNAMES", DEFAULT_AUTHORS));
      message.append(CITATION);
  
+     message.append("</div>");
 -
      return message.toString();
    }
  
    /**
     * DOCUMENT ME!
     * 
 -   * @param e DOCUMENT ME!
 +   * @param e
 +   *          DOCUMENT ME!
     */
    @Override
-   protected void preferences_actionPerformed(ActionEvent e)
-   {
-     new Preferences();
+   protected void preferences_actionPerformed(ActionEvent e) {
+     Preferences.openPreferences();
    }
  
    /**
          approveSave = true;
        }
      }
--
-     if (approveSave || autoSave)
-     {
+     if (approveSave || autoSave) {
        final Desktop me = this;
        final java.io.File chosenFile = projectFile;
 -      new Thread(new Runnable() {
 +      new Thread(new Runnable()
 +      {
          @Override
 -        public void run() {
 +        public void run()
 +        {
            // TODO: refactor to Jalview desktop session controller action.
 -          setProgressBar(
 -              MessageManager.formatMessage("label.saving_jalview_project", new Object[] { chosenFile.getName() }),
 -              chosenFile.hashCode());
 +          setProgressBar(MessageManager.formatMessage(
 +                  "label.saving_jalview_project", new Object[]
 +                  { chosenFile.getName() }), chosenFile.hashCode());
            Cache.setProperty("LAST_DIRECTORY", chosenFile.getParent());
            // TODO catch and handle errors for savestate
            // TODO prevent user from messing with the Desktop whilst we're saving
          setProjectFile(selectedFile);
          String choice = selectedFile.getAbsolutePath();
          Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
 -        new Thread(new Runnable() {
 +        new Thread(new Runnable()
 +        {
            @Override
 -          public void run() {
 -            try {
 +          public void run()
 +          {
 +            try
 +            {
                new Jalview2XML().loadJalviewAlign(selectedFile);
 -            } catch (OutOfMemoryError oom) {
 +            } catch (OutOfMemoryError oom)
 +            {
                new OOMWarning("Whilst loading project from " + choice, oom);
 -            } catch (Exception ex) {
 -              Cache.log.error("Problems whilst loading project from " + choice, ex);
 -              JvOptionPane.showMessageDialog(Desktop.desktop,
 -                  MessageManager.formatMessage("label.error_whilst_loading_project_from", new Object[] { choice }),
 -                  MessageManager.getString("label.couldnt_load_project"), JvOptionPane.WARNING_MESSAGE);
 +            } catch (Exception ex)
 +            {
 +              Cache.log.error(
 +                      "Problems whilst loading project from " + choice, ex);
 +              JvOptionPane.showMessageDialog(getDesktopPane(),
 +                      MessageManager.formatMessage(
 +                              "label.error_whilst_loading_project_from",
 +                              new Object[]
 +                              { choice }),
 +                      MessageManager
 +                              .getString("label.couldnt_load_project"),
 +                      JvOptionPane.WARNING_MESSAGE);
              }
            }
-         }).start();
+         }, "Project Loader").start();
        }
      });
--
      chooser.showOpenDialog(this);
    }
  
            g.setColor(Color.red);
          }
          FontMetrics fm = g.getFontMetrics();
 -        if (fm != null) {
 -          g.drawString(
 -              MessageManager.formatMessage("label.memory_stats",
 -                  new Object[] { df.format(totalFreeMemory), df.format(maxMemory), df.format(percentUsage) }),
 -              10, getHeight() - fm.getHeight());
 +        if (fm != null)
 +        {
 +          g.drawString(MessageManager.formatMessage("label.memory_stats",
 +                  new Object[]
 +                  { df.format(totalFreeMemory), df.format(maxMemory),
 +                      df.format(percentUsage) }),
 +                  10, getHeight() - fm.getHeight());
          }
        }
 -
+       // output debug scale message. Important for jalview.bin.HiDPISettingTest2
+       Desktop.debugScaleMessage(Desktop.getDesktop().getGraphics());
      }
    }
  
Simple merge
@@@ -110,19 -111,36 +111,36 @@@ public class Finder extends GFinde
      frame = new JInternalFrame();
      frame.setContentPane(this);
      frame.setLayer(JLayeredPane.PALETTE_LAYER);
-     frame.addInternalFrameListener(
-             new InternalFrameAdapter()
-             {
-               @Override
-               public void internalFrameClosing(InternalFrameEvent e)
-               {
-                 closeAction();
-               }
-             });
+     frame.addInternalFrameListener(new InternalFrameAdapter()
+     {
+       @Override
+       public void internalFrameClosing(InternalFrameEvent e)
+       {
+         closeAction();
+       }
+     });
+     frame.addFocusListener(new FocusAdapter()
+     {
+       @Override
+       public void focusGained(FocusEvent e)
+       {
+         /*
+          * ensure 'ignore hidden columns' is only enabled
+          * if the alignment with focus has hidden columns
+          */
+         getFocusedViewport();
+       }
+     });
      addEscapeHandler();
-     Desktop.addInternalFrame(frame, MessageManager.getString("label.find"), 
-             Desktop.FRAME_MAKE_VISIBLE, MY_WIDTH, MY_HEIGHT, Desktop.FRAME_ALLOW_RESIZE, Desktop.FRAME_ALLOW_ANY_SIZE);
+     String title = MessageManager.getString("label.find");
+     if (scope != null)
+     {
+       title += " " + scope;
+     }
 -    Desktop.addInternalFrame(frame, title, MY_WIDTH, MY_HEIGHT);
++    Desktop.addInternalFrame(frame, title, Desktop.FRAME_MAKE_VISIBLE, MY_WIDTH, MY_HEIGHT, Desktop.FRAME_ALLOW_RESIZE, Desktop.FRAME_ALLOW_ANY_SIZE);
+     frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
      searchBox.getComponent().requestFocus();
    }
  
Simple merge
@@@ -126,13 -91,10 +126,14 @@@ public class OptsAndParamsPag
  
      OptionI option;
  
 -    JLabel optlabel = new JLabel();
 +    JComboBox<Object> val;
  
 -    JComboBox<String> val = new JComboBox<>();
 +    /**
 +     * Constructs and adds labels and controls to the panel for one Option
 +     * 
 +     * @param opt
 +     */
      public OptionBox(OptionI opt)
      {
        option = opt;
        if (finfo != null)
        {
          hasLink = true;
 -        enabled.setToolTipText(JvSwingUtils.wrapTooltip(true,
 -                ((desc == null || desc.trim().length() == 0)
 -                        ? MessageManager.getString(
 -                                "label.opt_and_params_further_details")
 -                        : desc) + "<br><img src=\"" + linkImageURL
 -                        + "\"/>"));
 -        enabled.addMouseListener(this);
 +        String description = desc;
 +        if (desc == null || desc.trim().isEmpty())
 +        {
 +          description = MessageManager
 +                  .getString("label.opt_and_params_further_details");
 +        }
 +        description = description + "<br><img src=\"" + linkImageURL
 +                + "\"/>";
 +        String text = JvSwingUtils.wrapTooltip(true, description);
 +        enabled.setToolTipText(text);
 +        enabled.addMouseListener(this); // for popup menu to show link
        }
        else
        {
      @Override
      public void mouseEntered(MouseEvent e)
      {
 -      // TODO Auto-generated method stub
      }
  
      @Override
      public void mouseExited(MouseEvent e)
      {
 -      // TODO Auto-generated method stub
      }
  
      @Override
        }
      }
  
 +    /**
 +     * toString representation for identification in the debugger only
 +     */
 +    @Override
 +    public String toString()
 +    {
 +      return option == null ? super.toString() : option.toString();
 +    }
    }
  
 +  /**
 +   * A class that models a panel rendering a single parameter
 +   */
    public class ParamBox extends JPanel
            implements ChangeListener, ActionListener, MouseListener
    {
        finfo = parm.getFurtherDetails();
        validator = parm.getValidValue();
        parameter = parm;
 +      isLogarithmicParameter = parm instanceof LogarithmicParameter;
        if (validator != null)
        {
 -        integ = validator.getType() == ValueType.Integer;
 -      }
 -      else
 -      {
 -        if (parameter.getPossibleValues() != null)
 +        ValueType type = validator.getType();
 +        isIntegerParameter = type == ValueType.Integer;
 +        isStringParameter = type == ValueType.String
 +                || type == ValueType.File;
 +
 +        /*
 +         * ensure slider has an integer range corresponding to
 +         * the min-max range of the parameter
 +         */
 +        if (validator.getMin() != null && validator.getMax() != null
 +        // && !isIntegerParameter
 +                && !isStringParameter)
          {
 -          choice = true;
 +          double min = validator.getMin().doubleValue();
 +          double max = validator.getMax().doubleValue();
 +          if (isLogarithmicParameter)
 +          {
 +            min = Math.log(min);
 +            max = Math.log(max);
 +          }
 +          sliderScaleFactor = (int) (1000000 / (max - min));
 +          // todo scaleMin, scaleMax could also be final fields
          }
        }
  
        }
        else
        {
 -        makeCompactParam(parm);
 +        addExpandableParam(parm);
        }
      }
  
 -    private void makeCompactParam(ParameterI parm)
 +    /**
 +     * Adds a 'compact' format parameter, with any help text shown as a tooltip
 +     * 
 +     * @param parm
 +     */
 +    private void addCompactParameter(ParameterI parm)
      {
        setLayout(new MigLayout("", "[][grow]"));
        String ttipText = null;
  
 -      controlPanel.setLayout(new BorderLayout());
 +      controlsPanel.setLayout(new BorderLayout());
  
        if (parm.getDescription() != null
                && parm.getDescription().trim().length() > 0)
        firstrow.setBounds(new Rectangle(10, 20, PARAM_WIDTH - 30,
                PARAM_CLOSEDHEIGHT - 30));
  
 -      final ParamBox me = this;
        if (parm.getDescription() != null
                && parm.getDescription().trim().length() > 0)
        {
      }
  
      /**
 -     * Action on input in text field
 +     * Adds a button which can be clicked to show or hide help text
 +     * 
 +     * @param container
 +     * @param param
       */
 +    protected void addExpandableHelp(JPanel container, ParameterI param)
 +    {
 +      JButton showDescBtn = new JButton("+");
 +      showDescBtn.setFont(new Font("Verdana", Font.PLAIN, 8));
 +      if (finfo != null)
 +      {
 +        descTooltip = JvSwingUtils.wrapTooltip(true,
 +                MessageManager.formatMessage(
 +                        "label.opt_and_params_show_brief_desc_image_link",
 +                        new String[]
 +                        { linkImageURL.toExternalForm() }));
 +        showDescBtn.addMouseListener(this);
 +      }
 +      else
 +      {
 +        descTooltip = JvSwingUtils.wrapTooltip(true, MessageManager
 +                .getString("label.opt_and_params_show_brief_desc"));
 +      }
 +      showDescBtn.setToolTipText(descTooltip);
 +      showDescBtn.addActionListener(new ActionListener()
 +      {
 +        @Override
 +        public void actionPerformed(ActionEvent e)
 +        {
 +          descriptionIsVisible = !descriptionIsVisible;
 +          showDescBtn.setText(descriptionIsVisible ? "-" : "+");
 +          showDescBtn.setToolTipText(
 +                  descriptionIsVisible ? null : descTooltip);
 +          descPanel.setVisible(descriptionIsVisible);
 +          descPanel.getVerticalScrollBar().setValue(0);
 +          ParamBox.this.setPreferredSize(new Dimension(PARAM_WIDTH,
 +                  (descriptionIsVisible) ? PARAM_HEIGHT
 +                          : PARAM_CLOSEDHEIGHT));
 +          ParamBox.this.validate();
 +          pmdialogbox.refreshParamLayout();
 +        }
 +      });
 +      descriptionText.setWrapStyleWord(true);
 +      descriptionText.setLineWrap(true);
 +      descriptionText.setColumns(32);
 +      descriptionText.setText(param.getDescription());
 +      showDescBtn.setBounds(new Rectangle(10, 10, 16, 16));
 +      container.add(showDescBtn);
 +    }
      @Override
      public void actionPerformed(ActionEvent e)
      {
        }
        else
        {
 -        prm.setValue(valueField.getText());
 +        value = valueField.getText();
        }
 +      prm.setValue(value);
        return prm;
      }
  
      @Override
      public void mouseEntered(MouseEvent e)
      {
 -      // TODO Auto-generated method stub
      }
  
      @Override
      public void mouseExited(MouseEvent e)
      {
 -      // TODO Auto-generated method stub
      }
  
      @Override
      @Override
      public void mouseReleased(MouseEvent e)
      {
 -      // TODO Auto-generated method stub
      }
  
 -    /**
 -     * Action on change of slider value
 -     */
      @Override
      public void stateChanged(ChangeEvent e)
      {
          }
          else
          {
 -          valueField = new JTextField();
 +          slider = new JSlider();
 +          slider.addChangeListener(this);
 +          int cols = parm instanceof StringParameter ? 20 : 0;
 +          valueField = new JTextField(cols);
            valueField.addActionListener(this);
 -          valueField.addKeyListener(new KeyListener()
 +          valueField.addKeyListener(new KeyAdapter()
            {
 -            @Override
 -            public void keyTyped(KeyEvent e)
 -            {
 -            }
              @Override
              public void keyReleased(KeyEvent e)
              {
                  }
                }
              }
 -            @Override
 -            public void keyPressed(KeyEvent e)
 -            {
 -            }
+           });
+           valueField.addFocusListener(new FocusAdapter() {
+             @Override
+             public void focusLost(FocusEvent e)
+             {
+               actionPerformed(null);
+             }
+             
            });
 -          valueField.setPreferredSize(new Dimension(60, 25));
 -          valueField.setText(parm.getValue());
 -          slider = makeSlider(parm.getValidValue());
 -          updateSliderFromValueField();
 -          slider.addChangeListener(this);
 -
 -          controlPanel.add(slider, BorderLayout.WEST);
 -          controlPanel.add(valueField, BorderLayout.EAST);
 +          valueField.setPreferredSize(new Dimension(65, 25));
 +          if (parm instanceof FileParameter)
 +          {
 +            valueField.setToolTipText(MessageManager
 +                    .getString("label.double_click_to_browse"));
 +            valueField.addMouseListener(new MouseAdapter()
 +            {
 +              @Override
 +              public void mouseClicked(MouseEvent e)
 +              {
 +                if (e.getClickCount() == 2)
 +                {
 +                  String dir = Cache.getProperty("LAST_DIRECTORY");
 +                  JalviewFileChooser chooser = new JalviewFileChooser(dir);
 +                  chooser.setFileView(new JalviewFileView());
 +                  chooser.setDialogTitle(
 +                          MessageManager.getString("action.select_ddbb"));
 +
 +                  int val = chooser.showOpenDialog(ParamBox.this);
 +                  if (val == JalviewFileChooser.APPROVE_OPTION)
 +                  {
 +                    File choice = chooser.getSelectedFile();
 +                    String path = choice.getPath();
 +                    valueField.setText(path);
 +                    Cache.setProperty("LAST_DIRECTORY", choice.getParent());
 +                    FileLoader.updateRecentlyOpened(path,
 +                            DataSourceType.FILE);
 +                  }
 +                }
 +              }
 +            });
 +          }
 +          
 +          controlsPanel.add(slider, BorderLayout.WEST);
 +          controlsPanel.add(valueField, BorderLayout.EAST);
          }
        }
  
            }
            else
            {
 -            slider.setVisible(false);
 +            return getSelectedValue(this.parameter,
 +                    choicebox.getSelectedIndex());
            }
          }
 +        slider.setVisible(false);
 +        return valueField.getText().trim();
 +      }
 +
 +      if (validator.getMin() == null || validator.getMax() == null)
 +      {
 +        slider.setVisible(false);
 +      }
 +
 +      valueField.setText(valueField.getText().trim());
 +
 +      /*
 +       * ensure not outside min-max range
 +       * TODO: provide some visual indicator if limit reached
 +       */
 +      try
 +      {
 +        valueField.setBackground(Color.WHITE);
 +        double d = Double.parseDouble(valueField.getText());
 +        if (validator.getMin() != null
 +                && validator.getMin().doubleValue() > d)
 +        {
 +          valueField.setText(formatNumber(validator.getMin()));
 +        }
 +        if (validator.getMax() != null
 +                && validator.getMax().doubleValue() < d)
 +        {
 +          valueField.setText(formatNumber(validator.getMax()));
 +        }
 +      } catch (NumberFormatException e)
 +      {
 +        valueField.setBackground(Color.yellow);
 +        return Float.NaN;
 +      }
 +      if (isIntegerParameter)
 +      {
 +        int iVal = 0;
 +        try
 +        {
 +          iVal = Integer.valueOf(valueField.getText());
 +        } catch (Exception e)
 +        {
 +          valueField.setBackground(Color.yellow);
 +          return Integer.valueOf(0);
 +        }
 +
 +        if (validator.getMin() != null && validator.getMax() != null)
 +        {
 +          slider.getModel().setRangeProperties(iVal, 1,
 +                  validator.getMin().intValue(),
 +                  validator.getMax().intValue() + 1, true);
 +        }
          else
          {
 -          float fVal = 0f;
 -          try
 -          {
 -            valueField.setText(valueField.getText().trim());
 -            fVal = Float.valueOf(valueField.getText());
 -            if (minValue != null
 -                    && minValue.floatValue() > fVal)
 -            {
 -              fVal = minValue.floatValue();
 -              // TODO: provide visual indication that hard limit was reached for
 -              // this parameter
 -              // update value field to reflect any bound checking we performed.
 -              valueField.setText("" + fVal);
 -            }
 -            if (maxValue != null
 -                    && maxValue.floatValue() < fVal)
 -            {
 -              fVal = maxValue.floatValue();
 -              // TODO: provide visual indication that hard limit was reached for
 -              // this parameter
 -              // update value field to reflect any bound checking we performed.
 -              valueField.setText("" + fVal);
 -            }
 -          } catch (NumberFormatException e)
 -          {
 -            System.err.println(e.toString());
 -          }
 -          if (minValue != null && maxValue != null)
 -          {
 -            slider.setSliderModel(minValue.floatValue(),
 -                    maxValue.floatValue(), fVal);
 -          }
 -          else
 -          {
 -            slider.setVisible(false);
 -          }
 +          slider.setVisible(false);
          }
 +        return Integer.valueOf(iVal);
        }
 -      else
 +
 +      if (isLogarithmicParameter)
        {
 -        if (!choice)
 +        double dVal = 0d;
 +        try
 +        {
 +          double eValue = Double.valueOf(valueField.getText());
 +          dVal = Math.log(eValue);
 +        } catch (Exception e)
 +        {
 +          // shouldn't be possible here
 +          valueField.setBackground(Color.yellow);
 +          return Double.NaN;
 +        }
 +        if (validator.getMin() != null && validator.getMax() != null)
 +        {
 +          double scaleMin = Math.log(validator.getMin().doubleValue())
 +                  * sliderScaleFactor;
 +          double scaleMax = Math.log(validator.getMax().doubleValue())
 +                  * sliderScaleFactor;
 +          slider.getModel().setRangeProperties(
 +                  (int) (sliderScaleFactor * dVal), 1,
 +                  (int) scaleMin, 1 + (int) scaleMax, true);
 +        }
 +        else
          {
            slider.setVisible(false);
          }
      }
    }
  
 -  public static final int PARAM_WIDTH = 340;
 -
 -  public static final int PARAM_HEIGHT = 150;
 -
 -  public static final int PARAM_CLOSEDHEIGHT = 80;
 -
 -  public OptsAndParamsPage(OptsParametersContainerI paramContainer)
 -  {
 -    this(paramContainer, false);
 -  }
 +  /**
 +   * Constructor with the option to show 'compact' format (parameter description
 +   * as tooltip) or 'expanded' format (parameter description in a textbox which
 +   * may be opened or closed). Use compact for simple description text, expanded
 +   * for more wordy or formatted text.
 +   * 
 +   * @param paramContainer
 +   */
    public OptsAndParamsPage(OptsParametersContainerI paramContainer,
            boolean compact)
    {
      mnu.show(invoker, x, y);
    }
  
 -  URL linkImageURL = getClass().getResource("/images/link.gif");
 -
 -  Map<String, OptionBox> optSet = new java.util.LinkedHashMap<>();
 -
 -  Map<String, ParamBox> paramSet = new java.util.LinkedHashMap<>();
    public Map<String, OptionBox> getOptSet()
    {
      return optSet;
      this.paramSet = paramSet;
    }
  
 -  OptsParametersContainerI poparent;
    OptionBox addOption(OptionI opt)
    {
      OptionBox cb = optSet.get(opt.getName());
Simple merge
Simple merge
@@@ -20,6 -20,6 +20,7 @@@
   */
  package jalview.gui;
  
++
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Component;
@@@ -53,9 -51,6 +55,8 @@@ 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;
@@@ -86,18 -82,21 +88,19 @@@ import jalview.ws.sifts.SiftsSettings
   * @author $author$
   * @version $Revision$
   */
- public class Preferences extends GPreferences
 -/*
 - * for merge with Jalview-JS
 - public class Preferences extends GPreferences implements ApplicationSingletonI
 - */
 -public class Preferences extends GPreferences
 -{
 -  public static final String ENABLE_SPLIT_FRAME = "ENABLE_SPLIT_FRAME";
 -  public static final String SCALE_PROTEIN_TO_CDNA = "SCALE_PROTEIN_TO_CDNA";
++public class Preferences extends GPreferences implements ApplicationSingletonI
 +{
 +  // suggested list delimiter character
 +  public static final String COMMA = ",";
  
 -  public static final String DEFAULT_COLOUR = "DEFAULT_COLOUR";
 +  public static final String HMMSEARCH_SEQCOUNT = "HMMSEARCH_SEQCOUNT";
  
 -  public static final String DEFAULT_COLOUR_PROT = "DEFAULT_COLOUR_PROT";
 +  public static final String HMMINFO_GLOBAL_BACKGROUND = "HMMINFO_GLOBAL_BACKGROUND";
  
 -  public static final String DEFAULT_COLOUR_NUC = "DEFAULT_COLOUR_NUC";
 +  public static final String HMMALIGN_TRIM_TERMINI = "HMMALIGN_TRIM_TERMINI";
 +  
 +  public static final String ADD_SS_ANN = "ADD_SS_ANN";
  
    public static final String ADD_TEMPFACT_ANN = "ADD_TEMPFACT_ANN";
  
  
    public static final String CHIMERA_PATH = "CHIMERA_PATH";
  
+   public static final String CHIMERAX_PATH = "CHIMERAX_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 ENABLE_SPLIT_FRAME = "ENABLE_SPLIT_FRAME";
 +
 +  public static final String FIGURE_AUTOIDWIDTH = "FIGURE_AUTOIDWIDTH";
 +
 +  public static final String FIGURE_FIXEDIDWIDTH = "FIGURE_FIXEDIDWIDTH";
 +
 +  public static final String FOLLOW_SELECTIONS = "FOLLOW_SELECTIONS";
 +
 +  public static final String FONT_NAME = "FONT_NAME";
 +
 +  public static final String FONT_SIZE = "FONT_SIZE";
 +
 +  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 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 PYMOL_PATH = "PYMOL_PATH";
 -  public static final String SORT_ANNOTATIONS = "SORT_ANNOTATIONS";
 +  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";
  
 +  public static final String SHOW_CONSENSUS = "SHOW_CONSENSUS";
 +
 +  public static final String SHOW_CONSENSUS_HISTOGRAM = "SHOW_CONSENSUS_HISTOGRAM";
 +
 +  public static final String SHOW_CONSENSUS_LOGO = "SHOW_CONSENSUS_LOGO";
 +
 +  public static final String SHOW_CONSERVATION = "SHOW_CONSERVATION";
 +
 +  public static final String SHOW_DBREFS_TOOLTIP = "SHOW_DBREFS_TOOLTIP";
 +
 +  public static final String SHOW_GROUP_CONSENSUS = "SHOW_GROUP_CONSENSUS";
 +
 +  public static final String SHOW_GROUP_CONSERVATION = "SHOW_GROUP_CONSERVATION";
 +
 +  public static final String SHOW_JVSUFFIX = "SHOW_JVSUFFIX";
 +
 +  public static final String SHOW_NPFEATS_TOOLTIP = "SHOW_NPFEATS_TOOLTIP";
    public static final String SHOW_OCCUPANCY = "SHOW_OCCUPANCY";
  
    public static final String SHOW_OV_HIDDEN_AT_START = "SHOW_OV_HIDDEN_AT_START";
  
 +  public static final String SHOW_OVERVIEW = "SHOW_OVERVIEW";
 +
 +  public static final String SHOW_QUALITY = "SHOW_QUALITY";
 +
 +  public static final String SHOW_UNCONSERVED = "SHOW_UNCONSERVED";
 +
 +  public static final String SORT_ALIGNMENT = "SORT_ALIGNMENT";
 +
 +  public static final String SORT_ANNOTATIONS = "SORT_ANNOTATIONS";
 +
 +  public static final String SORT_BY_TREE = "SORT_BY_TREE";
 +
 +  public static final String STRUCT_FROM_PDB = "STRUCT_FROM_PDB";
 +
 +  public static final String STRUCTURE_DISPLAY = "STRUCTURE_DISPLAY";
 +
 +  public static final String STRUCTURE_DIMENSIONS = "STRUCTURE_DIMENSIONS";
 +
 +  public static final String UNIPROT_DOMAIN = "UNIPROT_DOMAIN";
 +
 +  public static final String USE_FULL_SO = "USE_FULL_SO";
    public static final String USE_LEGACY_GAP = "USE_LEGACY_GAP";
  
 -  public static final String GAP_COLOUR = "GAP_COLOUR";
 +  public static final String USE_RNAVIEW = "USE_RNAVIEW";
  
 -  public static final String HIDDEN_COLOUR = "HIDDEN_COLOUR";
 +  public static final String USER_DEFINED_COLOURS = "USER_DEFINED_COLOURS";
 +
 +  public static final String WRAP_ALIGNMENT = "WRAP_ALIGNMENT";
  
    private static final int MIN_FONT_SIZE = 1;
  
    private static final int MAX_FONT_SIZE = 30;
  
+   private String previousProxyType;
+   private static Preferences INSTANCE = null; // add "final"
 -
    /**
     * Holds name and link separated with | character. Sequence ID must be
     * $SEQUENCE_ID$ or $SEQUENCE_ID=/.possible | chars ./=$
  
    private WsPreferences wsPrefs;
  
 +  private SlivkaPreferences slivkaPrefs;
    private OptionsParam promptEachTimeOpt = new OptionsParam(
            MessageManager.getString("label.prompt_each_time"),
            "Prompt each time");
    private OptionsParam textOpt = new OptionsParam(
            MessageManager.getString("action.text"), "Text");
  
+   // get singleton Preferences instance
+   public static Preferences getInstance()
+   {
+     if (INSTANCE == null || INSTANCE.frame == null
+             || INSTANCE.frame.isClosed())
+     {
+       INSTANCE = new Preferences();
+     }
+     return INSTANCE;
+     /*
+      * Replace code with the following for Jalvew-JS
+     Preferences INSTANCE = ApplicationSingletonProvider.getInstance(Preferences.class);
+     if (INSTANCE == null || INSTANCE.frame == null
+             || INSTANCE.frame.isClosed())
+     {
+       ApplicationSingletonProvider.remove(Preferences.class);
+       INSTANCE = ApplicationSingletonProvider.getInstance(Preferences.class);
+     }
+     return INSTANCE;
+     */
+   }
+   public static void openPreferences()
+   {
+     openPreferences(null, null);
+   }
+   public static void openPreferences(TabRef selectTab, String message)
+   {
+     Preferences p = getInstance();
+     if (selectTab != null)
+       p.selectTab(selectTab, message);
+     p.frame.show();
+     p.frame.moveToFront();
+     p.frame.grabFocus();
+   }
+   public void selectTab(TabRef selectTab, String message)
+   {
+     this.selectTab(selectTab);
+     if (message != null)
+       this.setMessage(message);
+     this.frame.show();
+   }
 -
    /**
     * Creates a new Preferences object.
     */
              .setSelected(Cache.getDefault(SHOW_OV_HIDDEN_AT_START, false));
  
      /*
-      * Set Structure tab defaults.
+      * Set Structure tab defaults
       */
-     final boolean structSelected = Cache.getDefault(STRUCT_FROM_PDB, false);
+     final boolean structSelected = Cache.getDefault(STRUCT_FROM_PDB, true);
      structFromPdb.setSelected(structSelected);
-     useRnaView.setSelected(Cache.getDefault(USE_RNAVIEW, false));
-     useRnaView.setEnabled(structSelected);
-     addSecondaryStructure.setSelected(Cache.getDefault(ADD_SS_ANN, false));
+     addSecondaryStructure.setSelected(Cache.getDefault(ADD_SS_ANN, true));
      addSecondaryStructure.setEnabled(structSelected);
-     addTempFactor.setSelected(Cache.getDefault(ADD_TEMPFACT_ANN, false));
+     addTempFactor.setSelected(Cache.getDefault(ADD_TEMPFACT_ANN, true));
      addTempFactor.setEnabled(structSelected);
 -
+     /*
+      * set choice of structure viewer, and path if saved as a preference;
+      * default to Jmol (first choice) if an unexpected value is found
+      */
 -    String viewerType = Cache.getDefault(STRUCTURE_DISPLAY,
++    String viewerType = ViewerType.JMOL.name();
 +    if (!Platform.isJS())
 +    {
-       structViewer.setSelectedItem(
-             Cache.getDefault(STRUCTURE_DISPLAY, ViewerType.JMOL.name()));
++      Cache.getDefault(STRUCTURE_DISPLAY,
+             ViewerType.JMOL.name());
 +    }
-     chimeraPath.setText(Cache.getDefault(CHIMERA_PATH, ""));
-     chimeraPath.addActionListener(new ActionListener()
++    // TODO - disable external viewers for JS
+     structViewer.setSelectedItem(viewerType);
+     String viewerPath = "";
+     ViewerType type = null;
+     try
+     {
+       type = ViewerType.valueOf(viewerType);
+       switch (type)
+       {
+       case JMOL:
+         break;
+       case CHIMERA:
+         viewerPath = Cache.getDefault(CHIMERA_PATH, "");
+         break;
+       case CHIMERAX:
+         viewerPath = Cache.getDefault(CHIMERAX_PATH, "");
+         break;
+       case PYMOL:
+         viewerPath = Cache.getDefault(PYMOL_PATH, "");
+         break;
+       }
+     } catch (IllegalArgumentException e)
+     {
+       Cache.log.error("Unknown structure viewer type: " + viewerType
+               + ", defaulting to Jmol");
+       type = ViewerType.JMOL;
+     }
+     structureViewerPath.setText(viewerPath);
+     structureViewerPath.addActionListener(new ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent e)
        return;
      }
  
+     /* 
+      * Set proxy settings first (to be before web services refresh)
+      */
+     saveProxySettings();
 -
      /*
       * Save Visual settings
       */
      /*
       * Save Structure settings
       */
 -    Cache.applicationProperties.setProperty(ADD_TEMPFACT_ANN,
 +    Cache.setPropertyNoSave(ADD_TEMPFACT_ANN,
              Boolean.toString(addTempFactor.isSelected()));
 -    Cache.applicationProperties.setProperty(ADD_SS_ANN,
 +    Cache.setPropertyNoSave(ADD_SS_ANN,
              Boolean.toString(addSecondaryStructure.isSelected()));
-     Cache.setPropertyNoSave(USE_RNAVIEW,
-             Boolean.toString(useRnaView.isSelected()));
 -    Cache.applicationProperties.setProperty(STRUCT_FROM_PDB,
 +    Cache.setPropertyNoSave(STRUCT_FROM_PDB,
              Boolean.toString(structFromPdb.isSelected()));
-     if (!Platform.isJS())
++    if (!Platform.isJS()) {
+     String viewer = structViewer.getSelectedItem().toString();
+     String viewerPath = structureViewerPath.getText();
 -    Cache.applicationProperties.setProperty(STRUCTURE_DISPLAY, viewer);
++    Cache.setPropertyNoSave(STRUCTURE_DISPLAY, viewer);
+     if (viewer.equals(ViewerType.CHIMERA.name()))
+     {
+       Cache.setOrRemove(CHIMERA_PATH, viewerPath);
+     }
+     else if (viewer.equals(ViewerType.CHIMERAX.name()))
      {
-       Cache.setPropertyNoSave(STRUCTURE_DISPLAY,
-               structViewer.getSelectedItem().toString());
+       Cache.setOrRemove(CHIMERAX_PATH, viewerPath);
      }
-     Cache.setOrRemove(CHIMERA_PATH, chimeraPath.getText());
+     else if (viewer.equals(ViewerType.PYMOL.name()))
+     {
+       Cache.setOrRemove(PYMOL_PATH, viewerPath);
+     }
 -    Cache.applicationProperties.setProperty("MAP_WITH_SIFTS",
++    } // nojs
 +    Cache.setPropertyNoSave("MAP_WITH_SIFTS",
              Boolean.toString(siftsMapping.isSelected()));
      SiftsSettings.setMapWithSifts(siftsMapping.isSelected());
  
      /*
       * Save Connections settings
       */
 -    // Proxy settings set first (to catch web services)
 -
++    // Proxy settings were already set first (to catch web services)
      Cache.setOrRemove("DEFAULT_BROWSER", defaultBrowser.getText());
  
      jalview.util.BrowserLauncher.resetBrowser();
                nonMenuLinks.toString());
      }
  
 -    Cache.applicationProperties.setProperty("DEFAULT_URL",
 +    Cache.setPropertyNoSave("DEFAULT_URL",
              sequenceUrlLinks.getPrimaryUrlId());
  
-     Cache.setPropertyNoSave("USE_PROXY",
-             Boolean.toString(useProxy.isSelected()));
-     Cache.setOrRemove("PROXY_SERVER", proxyServerTB.getText());
-     Cache.setOrRemove("PROXY_PORT", proxyPortTB.getText());
-     if (useProxy.isSelected())
-     {
-       System.setProperty("http.proxyHost", proxyServerTB.getText());
-       System.setProperty("http.proxyPort", proxyPortTB.getText());
-     }
-     else
-     {
-       System.setProperty("http.proxyHost", "");
-       System.setProperty("http.proxyPort", "");
-     }
      Cache.setProperty("VERSION_CHECK",
              Boolean.toString(versioncheck.isSelected()));
      if (Cache.getProperty("USAGESTATS") != null || usagestats.isSelected())
      /*
       * Save Backups settings
       */
 -    Cache.applicationProperties.setProperty(BackupFiles.ENABLED,
 +    Cache.setPropertyNoSave(BackupFiles.ENABLED,
              Boolean.toString(enableBackupFiles.isSelected()));
      int preset = getComboIntStringKey(backupfilesPresetsCombo);
-     Cache.setPropertyNoSave(BackupFiles.NS + "_PRESET", Integer.toString(preset));
 -    Cache.applicationProperties.setProperty(BackupFiles.NS + "_PRESET",
 -            Integer.toString(preset));
++    Cache.applicationProperties.setProperty(BackupFiles.NS + "_PRESET", Integer.toString(preset));
  
      if (preset == BackupFilesPresetEntry.BACKUPFILESSCHEMECUSTOM)
      {
        BackupFilesPresetEntry customBFPE = getBackupfilesCurrentEntry();
        BackupFilesPresetEntry.backupfilesPresetEntriesValues.put(
                BackupFilesPresetEntry.BACKUPFILESSCHEMECUSTOM, customBFPE);
-       Cache.setPropertyNoSave(BackupFilesPresetEntry.CUSTOMCONFIG,
 -      Cache.applicationProperties.setProperty(
 -              BackupFilesPresetEntry.CUSTOMCONFIG, customBFPE.toString());
++      Cache.applicationProperties
++              .setProperty(BackupFilesPresetEntry.CUSTOMCONFIG,
 +                      customBFPE.toString());
      }
  
      BackupFilesPresetEntry savedBFPE = BackupFilesPresetEntry.backupfilesPresetEntriesValues
      {
      }
    }
 -
+   public void saveProxySettings()
+   {
+     String newProxyType = customProxy.isSelected() ? Cache.PROXYTYPE_CUSTOM
+             : noProxy.isSelected() ? Cache.PROXYTYPE_NONE
+                     : Cache.PROXYTYPE_SYSTEM;
 -    Cache.applicationProperties.setProperty("USE_PROXY", newProxyType);
++    Cache.setPropertyNoSave("USE_PROXY", newProxyType);
+     Cache.setOrRemove("PROXY_SERVER", proxyServerHttpTB.getText());
+     Cache.setOrRemove("PROXY_PORT", proxyPortHttpTB.getText());
+     Cache.setOrRemove("PROXY_SERVER_HTTPS", proxyServerHttpsTB.getText());
+     Cache.setOrRemove("PROXY_PORT_HTTPS", proxyPortHttpsTB.getText());
+     Cache.setOrRemove("PROXY_AUTH",
+             Boolean.toString(proxyAuth.isSelected()));
+     Cache.setOrRemove("PROXY_AUTH_USERNAME", proxyAuthUsernameTB.getText());
+     Cache.proxyAuthPassword = proxyAuthPasswordPB.getPassword();
+     Cache.setProxyPropertiesFromPreferences(previousProxyType);
+     if (newProxyType.equals(Cache.PROXYTYPE_CUSTOM)
+             || !newProxyType.equals(previousProxyType))
+     {
+       // force a re-lookup of ws if proxytype is custom or has changed
+       wsPrefs.update++;
+     }
+     previousProxyType = newProxyType;
+   }
  
 -  /**
 -   * Do any necessary validation before saving settings. Return focus to the first
 -   * tab which fails validation.
 +  public static void setAppletDefaults()
 +  {
 +
 +    // http://www.jalview.org/old/v2_8/examples/appletParameters.html
 +
 +    // showConservation true or false Default is true.
 +    // showQuality true or false Default is true.
 +    // showConsensus true or false Default is true.
 +    // showFeatureSettings true or false Shows the feature settings window when
 +    // startin
 +    // showTreeBootstraps true or false (default is true) show or hide branch
 +    // bootstraps
 +    // showTreeDistances true or false (default is true) show or hide branch
 +    // lengths
 +    // showUnlinkedTreeNodes true or false (default is false) indicate if
 +    // unassociated nodes should be highlighted in the tree view
 +    // showUnconserved true of false (default is false) When true, only gaps and
 +    // symbols different to the consensus sequence ions of the alignment
 +    // showGroupConsensus true of false (default is false) When true, shows
 +    // consensus annotation row for any groups on the alignment. (since 2.7)
 +    // showGroupConservation true of false (default is false) When true, shows
 +    // amino-acid property conservation annotation row for any groups on the
 +    // showConsensusHistogram true of false (default is true) When true, shows
 +    // the percentage occurence of the consensus symbol for each column as a
 +    // showSequenceLogo true of false (default is false) When true, shows a
 +    // sequence logo above the consensus sequence (overlaid above the Consensus
 +
 +    Cache.setPropertyNoSave(SHOW_CONSERVATION, "true");
 +    Cache.setPropertyNoSave(SHOW_QUALITY, "false");
 +    Cache.setPropertyNoSave(SHOW_CONSENSUS, "true");
 +    Cache.setPropertyNoSave(SHOW_UNCONSERVED, "false");
 +    Cache.setPropertyNoSave(SHOW_GROUP_CONSERVATION, "false");
 +    Cache.setPropertyNoSave(SHOW_GROUP_CONSENSUS, "false");
 +
 +    // TODO -- just a start here
 +  }
 + /**
 +   * Do any necessary validation before saving settings. Return focus to the
 +   * first tab which fails validation.
     * 
     * @return
     */
    }
  
    /**
-    * Returns true if chimera path is to a valid executable, else show an error
-    * dialog.
+    * Returns true if structure viewer path is to a valid executable, else shows an
+    * error dialog. Does nothing if the path is empty, as is the case for Jmol
+    * (built in to Jalview) or when Jalview is left to try default paths.
     */
-   private boolean validateChimeraPath()
+   private boolean validateViewerPath()
    {
-     if (chimeraPath.getText().trim().length() > 0)
+     if (structureViewerPath.getText().trim().length() > 0)
      {
-       File f = new File(chimeraPath.getText());
+       File f = new File(structureViewerPath.getText());
        if (!f.canExecute())
        {
 -        JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 +        JvOptionPane.showInternalMessageDialog(Desktop.getDesktopPane(),
-                 MessageManager.getString("label.invalid_chimera_path"),
-                 MessageManager.getString("label.invalid_name"),
+                 MessageManager.getString("label.invalid_viewer_path"),
+                 MessageManager.getString("label.invalid_viewer_path"),
                  JvOptionPane.ERROR_MESSAGE);
          return false;
        }
      }
      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.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
-    * path, if not show a warning/help dialog.
+    * If Chimera or ChimeraX or Pymol is selected, check it can be found on default
+    * or user-specified path, if not show a warning/help dialog
     */
    @Override
    protected void structureViewer_actionPerformed(String selectedItem)
      if (!found)
      {
        String[] options = { "OK", "Help" };
 -      int showHelp = JvOptionPane.showInternalOptionDialog(Desktop.desktop,
 +      int showHelp = JvOptionPane.showInternalOptionDialog(Desktop.getDesktopPane(),
                JvSwingUtils.wrapTooltip(true,
-                       MessageManager.getString("label.chimera_missing")),
+                       MessageManager.getString("label.viewer_missing")),
                "", JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE,
                null, options, options[0]);
 -
        if (showHelp == JvOptionPane.NO_OPTION)
        {
+         this.selectTab(Preferences.TabRef.STRUCTURE_TAB, null);
          try
          {
            Help.showHelpWindow(HelpId.StructureViewer);
            e.printStackTrace();
          }
        }
-     }
+       else if (showHelp == JvOptionPane.OK_OPTION)
+       {
+         this.selectTab(Preferences.TabRef.STRUCTURE_TAB, null);
+         CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
+           try
+           {
+             for (int i = 0; i < 3; i++)
+             {
+               structureViewerPath.setBackground(Color.PINK);
+               Thread.sleep(500);
+               structureViewerPath.setBackground(Color.WHITE);
+               Thread.sleep(500);
+             }
+           } catch (InterruptedException e)
+           {
+           }
+         });
+       }
 -    }
++     }
 +  }
 +
 +  @Override
 +  protected void validateHmmerPath()
 +  {
 +    validateExecutablePath(hmmerPath, HmmerCommand.HMMBUILD);
 +  }
 +
 +  @Override
 +  protected void validateCygwinPath()
 +  {
 +    validateExecutablePath(cygwinPath, "run");
    }
  
    public class OptionsParam
@@@ -95,14 -94,8 +95,13 @@@ public class SeqCanvas extends JPanel i
  
    private int wrappedVisibleWidths; // number of wrapped widths displayed
  
 +  private int availWidth;
 +
 +  private int availHeight;
 +
 +  private boolean allowFastPaint;
    // Don't do this! Graphics handles are supposed to be transient
 -  //private Graphics2D gg;
 +  // private Graphics2D gg;
  
    /**
     * Creates a new SeqCanvas object.
      int yPos = ypos + charHeight;
      int startX = startx;
      int endX = endx;
 -    
      if (av.hasHiddenColumns())
      {
        HiddenColumns hiddenColumns = av.getAlignment().getHiddenColumns();
        gg.copyArea(horizontal * charWidth, vertical * charHeight,
                img.getWidth(), img.getHeight(), -horizontal * charWidth,
                -vertical * charHeight);
 -      /** @j2sNative xxi = this.img */
        gg.translate(transX, transY);
        drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
        gg.translate(-transX, -transY);
  
        if (av.getWrapAlignment())
        {
-         drawWrappedPanel(gg, availWidth, availHeight, ranges.getStartRes());
 -        drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
++        drawWrappedPanel(gg, width, height, ranges.getStartRes());
        }
        else
        {
        drawCursor(g, startRes, endRes, startSeq, endSeq);
      }
    }
 -  
    /**
     * Draw an alignment panel for printing
     * 
     * @param startColumn
     *          the first column (0...) of the alignment to draw
     */
 -  public void drawWrappedPanel(Graphics g, int canvasWidth,
 -          int canvasHeight, final int startColumn)
 +  public void drawWrappedPanel(Graphics g, int availWidth, int availHeight,
 +          final int startColumn)
    {
 -    int wrappedWidthInResidues = calculateWrappedGeometry(canvasWidth,
 -            canvasHeight);
 +    int wrappedWidthInResidues = calculateWrappedGeometry();
      av.setWrappedWidth(wrappedWidthInResidues);
      ViewportRanges ranges = av.getRanges();
      ranges.setViewportStartAndWidth(startColumn, wrappedWidthInResidues);
  
      drawWrappedDecorators(g, startColumn);
    }
  
 +  private void getAvailSizes()
 +  {
 +    int charHeight = av.getCharHeight();
 +    int charWidth = av.getCharWidth();
 +    availWidth = getWidth();
 +    availHeight = getHeight();
 +    availWidth -= (availWidth % charWidth);
 +    availHeight -= (availHeight % charHeight);
 +  }
    /**
     * Calculates and saves values needed when rendering a wrapped alignment.
     * These depend on many factors, including
     * <li>whether scales are shown left, right or above the alignment</li>
     * </ul>
     * 
 +   * @param availWidth
 +   * @param availHeight
 +   * @return the number of residue columns in each width
 +   */
 +  protected int calculateWrappedGeometry()
 +  {
 +    getAvailSizes();
 +    return calculateWrappedGeometry(availWidth, availHeight);
 +
 +  }
 +
 +  /**
 +   * for test only
     * @param canvasWidth
     * @param canvasHeight
 -   * @return the number of residue columns in each width
 +   * @return
     */
 -  protected int calculateWrappedGeometry(int canvasWidth, int canvasHeight)
 +  public int calculateWrappedGeometry(int canvasWidth, int canvasHeight)
    {
      int charHeight = av.getCharHeight();
  
      /*
        if (av.getScaleRightWrapped())
        {
          int x = labelWidthWest + viewportWidth * charWidth;
 -        
          g.translate(x, 0);
          drawVerticalScale(g, startCol, endColumn, ypos, false);
          g.translate(-x, 0);
      }
    }
  
 +  private final static BasicStroke dottedStroke = new BasicStroke(1,
 +          BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 3f, new float[]
 +          { 5f, 3f }, 0f);
 +
 +  private final static BasicStroke basicStroke = new BasicStroke();
    /*
     * Draw a selection group over a wrapped alignment
     */
              / charWidth;
      int startx = startRes;
      int maxwidth = av.getAlignment().getVisibleWidth();
 +    // JAL-3253-applet had this:
 +    // // height gap above each panel
 +    // int charHeight = av.getCharHeight();
 +    // int hgap = charHeight;
 +    // if (av.getScaleAboveWrapped())
 +    // {
 +    // hgap += charHeight;
 +    // }
 +    // int dy = getAnnotationHeight() + hgap
 +    // + av.getAlignment().getHeight() * charHeight;
 +    // int ypos = hgap; // vertical offset
 +
 +    // this is from 0b573ed (gmungoc)
 +    int dy = wrappedRepeatHeightPx;
      int ypos = wrappedSpaceAboveAlignment;
  
      while ((ypos <= canvasHeight) && (startx < maxwidth))
        }
  
        g.translate(labelWidthWest, 0);
        drawUnwrappedSelection(g, group, startx, endx, 0,
 -              av.getAlignment().getHeight() - 1,
 -              ypos);
 +              av.getAlignment().getHeight() - 1, ypos);
        g.translate(-labelWidthWest, 0);
  
 -      ypos += wrappedRepeatHeightPx;
 +      // update vertical offset
 +      ypos += dy;
  
 +      // update horizontal offset
        startx += cWidth;
      }
 -    g.setStroke(new BasicStroke());
 +    g.setStroke(basicStroke);
    }
  
    /**
            int startRes, int endRes, int startSeq, int endSeq, int offset)
    {
      int charWidth = av.getCharWidth();
 -          
      if (!av.hasHiddenColumns())
      {
        drawPartialGroupOutline(g, group, startRes, endRes, startSeq, endSeq,
        g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
      }
    }
 -  
    /**
     * Highlights search results in the visible region by rendering as white text
     * on a black background. Any previous highlighting is removed. Answers true
      return highlightSearchResults(results, false);
  
    }
 -  
    /**
     * Highlights search results in the visible region by rendering as white text
     * on a black background. Any previous highlighting is removed. Answers true
    public void propertyChange(PropertyChangeEvent evt)
    {
      String eventName = evt.getPropertyName();
 -    // System.err.println(">>SeqCanvas propertyChange " + eventName);
 -    if (eventName.equals(SequenceGroup.SEQ_GROUP_CHANGED))
 +    // BH 2019.07.27 removes dead code introduced in aad3650 and simplifies
 +    // logic, emphasizing no check for ENDRES or ENDSEQ
 +
 +    // Both scrolling and resizing change viewport ranges: scrolling changes
 +    // both start and end points, but resize only changes end values.
 +    // Here we only want to fastpaint on a scroll, with resize using a normal
 +    // paint, so scroll events are identified as changes to the horizontal or
 +    // vertical start value.
 +
 +    // Make sure we're not trying to draw a panel
 +    // larger than the visible window
 +    int scrollX = 0;
 +    int scrollY = 0;
 +    switch (eventName)
      {
 +    case SequenceGroup.SEQ_GROUP_CHANGED:
        fastPaint = true;
        repaint();
        return;
  
      try
      {
 -      
        Graphics gg = img.getGraphics();
 -      
 -      calculateWrappedGeometry(getWidth(), getHeight());
 +      calculateWrappedGeometry();
  
        /*
         * relocate the regions of the alignment that are still visible
        drawWrappedDecorators(gg, ranges.getStartRes());
  
        gg.dispose();
 -      
        repaint();
      } finally
      {
      }
  
      Graphics gg = img.getGraphics();
 -    
      ViewportRanges ranges = av.getRanges();
      int viewportWidth = ranges.getViewportWidth();
      int charWidth = av.getCharWidth();
        /*
         * white fill first to erase annotations
         */
 -      
 -      
        gg.translate(xOffset, 0);
        gg.setColor(Color.white);
 -      gg.fillRect(labelWidthWest, ypos,
 -              (endRes - startRes + 1) * charWidth, wrappedRepeatHeightPx);
 +      gg.fillRect(labelWidthWest, ypos, (endRes - startRes + 1) * charWidth,
 +              wrappedRepeatHeightPx);
        gg.translate(-xOffset, 0);
  
        drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
 -      
      }
  
      /*
          }
        }
      }
 -  
      gg.dispose();
  
      return matchFound;
Simple merge
@@@ -66,6 -67,45 +66,44 @@@ import javax.swing.SwingConstants
   */
  public class SequenceFetcher extends JPanel implements Runnable
  {
+   private class StringPair
+   {
+     private String key;
+     private String display;
+     public StringPair(String s1, String s2)
+     {
+       key = s1;
+       display = s2;
+     }
+     public StringPair(String s)
+     {
+       this(s, s);
+     }
+     public String getKey()
+     {
+       return key;
+     }
+     public String getDisplay()
+     {
+       return display;
+     }
+     @Override
+     public String toString()
+     {
+       return display;
+     }
+     public boolean equals(StringPair other)
+     {
+       return other.key == this.key;
+     }
+   }
 -
    private static jalview.ws.SequenceFetcher sfetch = null;
  
    JLabel exampleAccession;
      this.add(databasePanel, BorderLayout.NORTH);
    }
  
+   private void setDatabaseSelectedItem(String db)
+   {
+     for (int i = 0; i < database.getItemCount(); i++)
+     {
+       StringPair sp = database.getItemAt(i);
+       if (sp != null && db != null && db.equals(sp.getKey()))
+       {
+         database.setSelectedIndex(i);
+         return;
+       }
+     }
+   }
 -
    /**
     * Answers a semi-colon-delimited string with the example query or queries for
     * the selected database
   */
  package jalview.gui;
  
++
  import java.awt.BorderLayout;
  import java.awt.Color;
+ import java.awt.Component;
  import java.awt.Dimension;
  import java.awt.Font;
  import java.awt.Graphics;
@@@ -38,32 -39,38 +40,43 @@@ import javax.swing.JTextPane
  import javax.swing.event.HyperlinkEvent;
  import javax.swing.event.HyperlinkListener;
  
+ import jalview.util.ChannelProperties;
  import jalview.util.Platform;
- import javajs.async.SwingJSUtils.StateHelper;
- import javajs.async.SwingJSUtils.StateMachine;
--
  /**
   * DOCUMENT ME!
   * 
   * @author $author$
   * @version $Revision$
   */
 +@SuppressWarnings("serial")
  public class SplashScreen extends JPanel
 -        implements Runnable, HyperlinkListener
 +        implements HyperlinkListener, StateMachine
  {
 +  
 +  private static final int STATE_INIT = 0;
 +
 +  private static final int STATE_LOOP = 1;
 +
 +  private static final int STATE_DONE = 2;
 +
    private static final int SHOW_FOR_SECS = 5;
  
-   private int FONT_SIZE = (Platform.isJS() ? 14 : 11);
 -  private static final int FONT_SIZE = 11;
++  private static final int FONT_SIZE = (Platform.isJS() ? 14 : 11);
+   private boolean visible = true;
+   private JPanel iconimg = new JPanel(new BorderLayout());
+   // could change fg, bg, font later to use ChannelProperties (these are not
+   // actually being used!)
+   private static Color bg = Color.WHITE;
+   private static Color fg = Color.BLACK;
  
-   private JPanel imgPanel = new JPanel(new BorderLayout());
 -  private static Font font = new Font("SansSerif", Font.PLAIN, FONT_SIZE);
 -
+   /*
+    * as JTextPane in Java, JLabel in javascript
+    */
+   private Component splashText;
  
    private JInternalFrame iframe;
  
@@@ -73,7 -80,8 +86,6 @@@
  
    private long oldTextLength = -1;
  
-   private StateHelper helper;
 -  public static int logoSize = 32;
 -
    /*
     * allow click in the initial splash screen to dismiss it
     * immediately (not if opened from About menu)
    /**
     * Constructor that displays the splash screen
     * 
 -   * @param isTransient
 +   * @param isStartup
     *          if true the panel removes itself on click or after a few seconds;
 -   *          if false it stays up until closed by the user
 +   *          if false it stays up until closed by the user (from Help..About menu)
     */
 -  public SplashScreen(boolean isTransient)
 +  public SplashScreen(boolean isStartup)
    {
-     this.isStartup = isStartup;
 -    this.transientDialog = isTransient;
++    this.transientDialog = isStartup;
 +    // we must get the image in JavaScript BEFORE starting the helper,
 +    // as it will take a 1 ms clock tick to obtain width and height information.
-     image = Toolkit.getDefaultToolkit().createImage(
-             getClass().getResource("/images/Jalview_Logo.png"));
++    image = ChannelProperties.getImage("banner");
++    logo = ChannelProperties.getImage("logo.48");
++    font = new Font("SansSerif", Font.PLAIN, FONT_SIZE);
 +    helper = new StateHelper(this);
 +    helper.next(STATE_INIT);
 +  }
  
 -    if (Platform.isJS()) // BH 2019
 -    {
 -      splashText = new JLabel("");
 -      run();
 -    }
 -    else
 -    {
 -      /**
 -       * Java only
 -       *
 -       * @j2sIgnore
 -       */
 -      {
 -        splashText = new JTextPane();
 -        splashText.setBackground(bg);
 -        splashText.setForeground(fg);
 -        splashText.setFont(font);
 -        Thread t = new Thread(this);
 -        t.start();
 -      }
 -    }
 +  protected void initSplashScreenWindow()
 +  {
 +    addMouseListener(closer);
 +    waitForImages();
 +    setLayout(new BorderLayout());
 +    iframe = new JInternalFrame();
 +    iframe.setFrameIcon(null);
 +    iframe.setClosable(true);
 +    iframe.setContentPane(this);
 +    iframe.setLayer(JLayeredPane.PALETTE_LAYER);  
 +    SplashImage splashimg = new SplashImage(image);
 +    imgPanel.add(splashimg, BorderLayout.CENTER);
 +    add(imgPanel, BorderLayout.NORTH);
 +    Desktop.getDesktopPane().add(iframe);
 +    refreshText();
    }
  
    /**
 -   * ping the jalview version page then create and display the jalview
 -   * splashscreen window.
 +   * Both Java and JavaScript have to wait for images, but this method will
 +   * accomplish nothing for JavaScript. We have already taken care of image
 +   * loading with our state loop in JavaScript.
 +   * 
     */
 -  void initSplashScreenWindow()
 +  private void waitForImages()
    {
 -    addMouseListener(closer);
 -
 -    try
 +    if (Platform.isJS())
 +      return;
 +    MediaTracker mt = new MediaTracker(this);
-     try
-     {
-       mt.addImage(image, 0);
-       logo = Toolkit.getDefaultToolkit().createImage(
-               getClass().getResource("/images/Jalview_Logo_small.png"));
-     } catch (Exception ex)
-     {
-     }
-     if (logo != null)
-     {
-       mt.addImage(logo, 1);
-     }
++    mt.addImage(image, 0);
++    mt.addImage(logo, 1);
 +    do
      {
 -      if (!Platform.isJS())
 +      try
 +      {
 +        mt.waitForAll();
 +      } catch (InterruptedException x)
        {
 -        image = ChannelProperties.getImage("banner");
 -        Image logo = ChannelProperties.getImage("logo.48");
 -        MediaTracker mt = new MediaTracker(this);
 -        if (image != null)
 -        {
 -          mt.addImage(image, 0);
 -        }
 -        if (logo != null)
 -        {
 -          mt.addImage(logo, 1);
 -        }
 -        do
 -        {
 -          try
 -          {
 -            mt.waitForAll();
 -          } catch (InterruptedException x)
 -          {
 -          }
 -          if (mt.isErrorAny())
 -          {
 -            System.err.println("Error when loading images!");
 -          }
 -        } while (!mt.checkAll());
 -        Desktop.instance.setIconImages(ChannelProperties.getIconList());
        }
 -    } catch (Exception ex)
 +      if (mt.isErrorAny())
 +      {
 +        System.err.println("Error when loading images!");
 +        break;
 +      }
 +    } while (!mt.checkAll());
 +    if (logo != null)
      {
 +      Desktop.getInstance().setIconImage(logo);
      }
 -
+     this.setBackground(bg);
+     this.setForeground(fg);
+     this.setFont(font);
 +  }
  
 -    iframe = new JInternalFrame();
 -    iframe.setFrameIcon(null);
 -    iframe.setClosable(true);
 -    this.setLayout(new BorderLayout());
 -    iframe.setContentPane(this);
 -    iframe.setLayer(JLayeredPane.PALETTE_LAYER);
 -    iframe.setBackground(bg);
 -    iframe.setForeground(fg);
 -    iframe.setFont(font);
 -
 -    if (Platform.isJS())
 +  /**
 +   * update text in author text panel reflecting current version information
 +   */
 +  protected boolean refreshText()
 +  {
 +    String newtext = Desktop.getInstance().getAboutMessage();
++    // System.err.println("Text found: \n"+newtext+"\nEnd of newtext.");
 +    if (oldTextLength == newtext.length())
      {
 -      // ignore in JavaScript
 +      return false;
 +    }
-     oldTextLength = newtext.length();
++  
 +    iframe.setVisible(false);
-     JTextPane jtp = new JTextPane();
-     jtp.setEditable(false);
-     jtp.setContentType("text/html");
-     jtp.setText("<html>" + newtext + "</html>");
-     jtp.addHyperlinkListener(this);
-     jtp.setFont(new Font("Verdana", Font.PLAIN, FONT_SIZE));
-     jtp.addMouseListener(closer);
-     jtp.setVisible(true);
-     jtp.setSize(new Dimension(750, 425));
-     add(jtp, BorderLayout.CENTER);
++    oldTextLength = newtext.length();
++    if (Platform.isJS()) // BH 2019
++    {
++      /*
++       * SwingJS doesn't have HTMLEditorKit, required for a JTextPane
++       * to display formatted html, so we use a simple alternative
++       */
++      String text = "<html><br><img src=\""
++              + ChannelProperties.getImageURL("banner") + "\"/>" + newtext
++              + "<br></html>";
++      JLabel ta = new JLabel(text);
++      ta.setOpaque(true);
++      ta.setBackground(Color.white);
++      splashText = ta;
+     }
+     else
+     /**
+      * Java only
 -     * 
++     *
+      * @j2sIgnore
+      */
+     {
 -      ((JTextPane) splashText).setEditable(false);
 -      splashText.setBackground(bg);
 -      splashText.setForeground(fg);
 -      splashText.setFont(font);
 -
 -      SplashImage splashimg = new SplashImage(image);
 -      iconimg.add(splashimg, BorderLayout.LINE_START);
 -      iconimg.setBackground(bg);
 -      add(iconimg, BorderLayout.NORTH);
++      JTextPane jtp = new JTextPane();
++      jtp.setEditable(false);
++      jtp.setBackground(bg);
++      jtp.setForeground(fg);
++      jtp.setFont(font);
++      jtp.setContentType("text/html");
++      jtp.setText("<html>" + newtext + "</html>");
++      jtp.addHyperlinkListener(this);
++      splashText = jtp;
+     }
 -    add(splashText, BorderLayout.CENTER);
+     splashText.addMouseListener(closer);
 -    Desktop.desktop.add(iframe);
 -    refreshText();
++
++    splashText.setVisible(true);
++    splashText.setSize(new Dimension(750,
++            375 + logoSize + (Platform.isJS() ? 40 : 0)));
++    splashText.setBackground(bg);
++    splashText.setForeground(fg);
++    splashText.setFont(font);
++    add(splashText, BorderLayout.CENTER);
 +    revalidate();
-     int h = jtp.getHeight() + imgPanel.getHeight();
-     iframe.setBounds(Math.max(0, (iframe.getParent().getWidth() - 750) / 2),
-            Math.max(0,  (iframe.getParent().getHeight() - h)/2), 750, h);
++    int width = Math.max(splashText.getWidth(), iconimg.getWidth());
++    int height = splashText.getHeight() + iconimg.getHeight();
++    iframe.setBounds((iframe.getParent().getWidth() - width) / 2,
++            (iframe.getParent().getHeight() - height) / 2, 750,
++            width,height);
 +    iframe.validate();
 +    iframe.setVisible(true);
 +    return true;
    }
  
 -  /**
 -   * update text in author text panel reflecting current version information
 -   */
 -  protected boolean refreshText()
 +  protected void closeSplash()
    {
 -    String newtext = Desktop.instance.getAboutMessage();
 -    // System.err.println("Text found: \n"+newtext+"\nEnd of newtext.");
 -    if (oldTextLength != newtext.length())
 +    try
 +    {
 +
 +      iframe.setClosed(true);
 +    } catch (Exception ex)
      {
 -      iframe.setVisible(false);
 -      oldTextLength = newtext.length();
 -      if (Platform.isJS()) // BH 2019
 -      {
 -        /*
 -         * SwingJS doesn't have HTMLEditorKit, required for a JTextPane
 -         * to display formatted html, so we use a simple alternative
 -         */
 -        String text = "<html><br><img src=\""
 -                + ChannelProperties.getImageURL("banner") + "\"/>" + newtext
 -                + "<br></html>";
 -        JLabel ta = new JLabel(text);
 -        ta.setOpaque(true);
 -        ta.setBackground(Color.white);
 -        splashText = ta;
 -      }
 -      else
 -      /**
 -       * Java only
 -       *
 -       * @j2sIgnore
 -       */
 -      {
 -        JTextPane jtp = new JTextPane();
 -        jtp.setEditable(false);
 -        jtp.setBackground(bg);
 -        jtp.setForeground(fg);
 -        jtp.setFont(font);
 -        jtp.setContentType("text/html");
 -        jtp.setText("<html>" + newtext + "</html>");
 -        jtp.addHyperlinkListener(this);
 -        splashText = jtp;
 -      }
 -      splashText.addMouseListener(closer);
 -
 -      splashText.setVisible(true);
 -      splashText.setSize(new Dimension(750,
 -              375 + logoSize + (Platform.isJS() ? 40 : 0)));
 -      splashText.setBackground(bg);
 -      splashText.setForeground(fg);
 -      splashText.setFont(font);
 -      add(splashText, BorderLayout.CENTER);
 -      revalidate();
 -      int width = Math.max(splashText.getWidth(), iconimg.getWidth());
 -      int height = splashText.getHeight() + iconimg.getHeight();
 -      iframe.setBounds(
 -              Math.max(0, (Desktop.instance.getWidth() - width) / 2),
 -              Math.max(0, (Desktop.instance.getHeight() - height) / 2),
 -              width, height);
 -      iframe.validate();
 -      iframe.setVisible(true);
 -      return true;
      }
 -    return false;
    }
  
    /**
Simple merge
@@@ -37,7 -41,7 +41,6 @@@ import javax.swing.JPopupMenu
  import javax.swing.JTable;
  import javax.swing.SwingUtilities;
  import javax.swing.table.AbstractTableModel;
--
  import jalview.api.structures.JalviewStructureDisplayI;
  import jalview.bin.Cache;
  import jalview.bin.Jalview;
@@@ -92,6 -116,10 +115,9 @@@ public class StructureChooser extends G
    public StructureChooser(SequenceI[] selectedSeqs, SequenceI selectedSeq,
            AlignmentPanel ap)
    {
+     // which FTS engine to use
+     data = StructureChooserQuerySource.getQuerySourceFor(selectedSeqs);
+     initDialog();
 -
      this.ap = ap;
      this.selectedSequence = selectedSeq;
      this.selectedSequences = selectedSeqs;
  
      // ensure a filter option is in force for search
      populateFilterComboBox(true, cachedPDBExists);
-     Thread discoverPDBStructuresThread = new Thread(new Runnable()
 -
+     // looks for any existing structures already loaded
+     // for the sequences (the cached ones)
+     // then queries the StructureChooserQuerySource to
+     // discover more structures.
+     //
+     // Possible optimisation is to only begin querying
+     // the structure chooser if there are no cached structures.
+     long startTime = System.currentTimeMillis();
+     updateProgressIndicator(
+             MessageManager.getString("status.loading_cached_pdb_entries"),
+             startTime);
+     loadLocalCachedPDBEntries();
+     updateProgressIndicator(null, startTime);
+     updateProgressIndicator(
+             MessageManager.getString("status.searching_for_pdb_structures"),
+             startTime);
+     fetchStructuresMetaData();
+     // revise filter options if no results were found
+     populateFilterComboBox(isStructuresDiscovered(), cachedPDBExists);
+     discoverStructureViews();
+     updateProgressIndicator(null, startTime);
+     mainFrame.setVisible(true);
+     updateCurrentView();
+   }
+   /**
+    * raises dialog for Uniprot fetch followed by 3D beacons search
+    * @param ignoreGui - when true, don't ask, just fetch 
+    */
+   public void promptForTDBFetch(boolean ignoreGui)
+   {
+     final long progressId = System.currentTimeMillis();
+     // final action after prompting and discovering db refs
+     final Runnable strucDiscovery = new Runnable()
      {
        @Override
        public void run()
  
      discoveredStructuresSet = new LinkedHashSet<>();
      HashSet<String> errors = new HashSet<>();
 -
+     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
+             .getSelectedItem());
 -
      for (SequenceI seq : selectedSequences)
      {
-       FTSRestRequest pdbRequest = new FTSRestRequest();
-       pdbRequest.setAllowEmptySeq(false);
-       pdbRequest.setResponseSize(500);
-       pdbRequest.setFieldToSearchBy("(");
-       FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
-               .getSelectedItem());
-       pdbRequest.setFieldToSortBy(selectedFilterOpt.getValue(),
-               !chk_invertFilter.isSelected());
-       pdbRequest.setWantedFields(wantedFields);
-       pdbRequest.setSearchTerm(buildQuery(seq) + ")");
-       pdbRequest.setAssociatedSequence(seq);
 -
        FTSRestResponse resultList;
        try
        {
      if (discoveredStructuresSet != null
              && !discoveredStructuresSet.isEmpty())
      {
-       getResultTable().setModel(FTSRestResponse
-               .getTableModel(lastPdbRequest, discoveredStructuresSet));
+       getResultTable()
+               .setModel(data.getTableModel(discoveredStructuresSet));
 -
        noOfStructuresFound = discoveredStructuresSet.size();
+       lastDiscoveredStructuresSet = discoveredStructuresSet;
        mainFrame.setTitle(MessageManager.formatMessage(
                "label.structure_chooser_no_of_structures",
                noOfStructuresFound, totalTime));
  
          for (SequenceI seq : selectedSequences)
          {
            FTSRestResponse resultList;
            try
            {
-             resultList = pdbRestClient.executeRequest(pdbRequest);
+             resultList = data.selectFirstRankedQuery(seq,
+                     discoveredStructuresSet, wantedFields, fieldToFilterBy,
+                     !chk_invertFilter.isSelected());
 -
            } catch (Exception e)
            {
              e.printStackTrace();
    protected void populateFilterComboBox(boolean haveData,
            boolean cachedPDBExist)
    {
+     populateFilterComboBox(haveData, cachedPDBExist, null);
+   }
+   /**
+    * Populates the filter combo-box options dynamically depending on discovered
+    * structures
+    */
+   protected void populateFilterComboBox(boolean haveData,
+           boolean cachedPDBExist, FilterOption lastSel)
+   {
 -
      /*
       * temporarily suspend the change listener behaviour
       */
      cmb_filterOption.removeItemListener(this);
 +
+     int selSet = -1;
      cmb_filterOption.removeAllItems();
      if (haveData)
      {
-       cmb_filterOption.addItem(new FilterOption(
-               MessageManager.getString("label.best_quality"),
-               "overall_quality", VIEWS_FILTER, false));
-       cmb_filterOption.addItem(new FilterOption(
-               MessageManager.getString("label.best_resolution"),
-               "resolution", VIEWS_FILTER, false));
-       cmb_filterOption.addItem(new FilterOption(
-               MessageManager.getString("label.most_protein_chain"),
-               "number_of_protein_chains", VIEWS_FILTER, false));
-       cmb_filterOption.addItem(new FilterOption(
-               MessageManager.getString("label.most_bound_molecules"),
-               "number_of_bound_molecules", VIEWS_FILTER, false));
-       cmb_filterOption.addItem(new FilterOption(
-               MessageManager.getString("label.most_polymer_residues"),
-               "number_of_polymer_residues", VIEWS_FILTER, true));
+       List<FilterOption> filters = data
+               .getAvailableFilterOptions(VIEWS_FILTER);
+       data.updateAvailableFilterOptions(VIEWS_FILTER, filters,
+               lastDiscoveredStructuresSet);
+       int p = 0;
+       for (FilterOption filter : filters)
+       {
+         if (lastSel != null && filter.equals(lastSel))
+         {
+           selSet = p;
+         }
+         p++;
+         cmb_filterOption.addItem(filter);
+       }
      }
 -
      cmb_filterOption.addItem(
              new FilterOption(MessageManager.getString("label.enter_pdb_id"),
-                     "-", VIEWS_ENTER_ID, false));
+                     "-", VIEWS_ENTER_ID, false, null));
      cmb_filterOption.addItem(
              new FilterOption(MessageManager.getString("label.from_file"),
-                     "-", VIEWS_FROM_FILE, false));
+                     "-", VIEWS_FROM_FILE, false, null));
+     if (canQueryTDB && notQueriedTDBYet)
+     {
+       btn_queryTDB.setVisible(true);
+     }
  
      if (cachedPDBExist)
      {
        FilterOption cachedOption = new FilterOption(
                MessageManager.getString("label.cached_structures"), "-",
-               VIEWS_LOCAL_PDB, false);
+               VIEWS_LOCAL_PDB, false, null);
        cmb_filterOption.addItem(cachedOption);
-       cmb_filterOption.setSelectedItem(cachedOption);
+       if (selSet == -1)
+       {
+         cmb_filterOption.setSelectedItem(cachedOption);
+       }
      }
 +
+     if (selSet > -1)
+     {
+       cmb_filterOption.setSelectedIndex(selSet);
+     }
      cmb_filterOption.addItemListener(this);
    }
  
    {
      FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
              .getSelectedItem());
+     
+     if (lastSelected == selectedFilterOpt)
+     {
+       // don't need to do anything, probably
+       return;
+     }
+     // otherwise, record selection
+     // and update the layout and dialog accordingly
+     lastSelected = selectedFilterOpt;
 -
      layout_switchableViews.show(pnl_switchableViews,
              selectedFilterOpt.getView());
      String filterTitle = mainFrame.getTitle();
    {
      validateSelections();
    }
 +
+   private FilterOption lastSelected=null;
    /**
     * Handles the state change event for the 'filter' combo-box and 'invert'
     * check-box
      }
      return found;
    }
--
    /**
     * Handles the 'New View' action
     */
            List<SequenceI> selectedSeqsToView = new ArrayList<>();
            for (int row : selectedRows)
            {
-             PDBEntry pdbEntry = (PDBEntry) tbl_local_pdb.getValueAt(row,
-                     pdbIdColIndex);
+             PDBEntry pdbEntry = ((PDBEntryTableModel) tbl_local_pdb
+                     .getModel()).getPDBEntryAt(row).getPdbEntry();
 -
              pdbEntriesToView[count++] = pdbEntry;
              SequenceI selectedSeq = (SequenceI) tbl_local_pdb
                      .getValueAt(row, refSeqColIndex);
            {
              selectedSequence = userSelectedSeq;
            }
-           PDBEntry fileEntry = AssociatePdbFileWithSeq.associatePdbWithSeq(selectedPdbFileName,
-                           DataSourceType.FILE, selectedSequence, true);
-           sViewer = StructureViewer.launchStructureViewer(ap, new PDBEntry[] { fileEntry },
-                   new SequenceI[]
-                   { selectedSequence }, superimpose, theViewer,
-                   progressBar);
+           PDBEntry fileEntry = new AssociatePdbFileWithSeq()
+                   .associatePdbWithSeq(selectedPdbFileName,
+                           DataSourceType.FILE, selectedSequence, true,
 -                          Desktop.instance);
++                          Desktop.getInstance());
+           sViewer = launchStructureViewer(ssm, new PDBEntry[] { fileEntry },
+                   ap, new SequenceI[]
+                   { selectedSequence });
          }
          SwingUtilities.invokeLater(new Runnable()
          {
          public void run()
          {
            fetchStructuresMetaData();
+           // populateFilterComboBox(true, cachedPDBExists);
 -
            filterResultSet(
                    ((FilterOption) cmb_filterOption.getSelectedItem())
                            .getValue());
   */
  package jalview.gui;
  
+ import java.util.ArrayList;
+ import java.util.HashMap;
+ import java.util.LinkedHashMap;
+ import java.util.List;
+ import java.util.Map;
+ import java.util.Map.Entry;
 -
  import jalview.api.structures.JalviewStructureDisplayI;
  import jalview.bin.Cache;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.SequenceI;
  import jalview.datamodel.StructureViewerModel;
- import jalview.structure.StructureMapping;
  import jalview.structure.StructureSelectionManager;
- import jalview.util.MessageManager;
- import jalview.util.Platform;
- import jalview.ws.DBRefFetcher;
- import jalview.ws.sifts.SiftsSettings;
  
- import java.awt.Rectangle;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.LinkedHashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.Map.Entry;
 +
  /**
   * A proxy for handling structure viewers, that orchestrates adding selected
   * structures, associated with sequences in Jalview, to an existing viewer, or
@@@ -328,28 -334,20 +344,24 @@@ public class StructureViewe
    }
  
    /**
-    * Create a new panel controlling a structure viewer.
+    * Creates a new panel controlling a structure viewer
     * 
     * @param type
 +   * @param pdbf
 +   * @param id
 +   * @param sq
     * @param alignPanel
     * @param viewerData
-    * @param fileloc
-    * @param rect
+    * @param sessionFile
     * @param vid
     * @return
     */
-   public JalviewStructureDisplayI createView(ViewerType type, String[] pdbf,
-           String[] id, SequenceI[][] sq, AlignmentPanel alignPanel,
-           StructureViewerModel viewerData, String fileloc, Rectangle rect,
-           String vid)
+   public static JalviewStructureDisplayI createView(ViewerType type,
+           AlignmentPanel alignPanel, StructureViewerModel viewerData,
+           String sessionFile, String vid)
    {
-     final boolean useinViewerSuperpos = viewerData.isAlignWithPanel();
-     final boolean usetoColourbyseq = viewerData.isColourWithAlignPanel();
-     final boolean viewerColouring = viewerData.isColourByViewer();
+     JalviewStructureDisplayI viewer = null;
 +
      switch (type)
      {
      case JMOL:
      default:
        Cache.log.error(UNKNOWN_VIEWER_TYPE + type.toString());
      }
-     return sview;
+     return viewer;
    }
  
 -
    public boolean isBusy()
    {
      if (sview != null)
Simple merge
Simple merge
@@@ -36,6 -33,6 +36,7 @@@ import java.awt.MediaTracker
  import java.awt.RenderingHints;
  import java.awt.event.ActionEvent;
  import java.awt.image.BufferedImage;
++import java.util.Locale;
  import java.util.Vector;
  
  import javax.swing.JComponent;
@@@ -351,27 -350,23 +350,28 @@@ public class WebserviceInfo extends GWe
      titlePanel.add(ap, BorderLayout.WEST);
      titlePanel.add(titleText, BorderLayout.CENTER);
      setStatus(currentStatus);
 -    Thread thread = new Thread(ap);
 -    thread.start();
 -    final WebserviceInfo thisinfo = this;
 -    frame.addInternalFrameListener(new InternalFrameAdapter()
 +    if (!Platform.isJS())
      {
 -      @Override
 -      public void internalFrameClosed(InternalFrameEvent evt)
 -      {
 -        // System.out.println("Shutting down webservice client");
 -        WSClientI service = thisinfo.getthisService();
 -        if (service != null && service.isCancellable())
 -        {
 -          service.cancelJob();
 -        }
 -      }
 -    });
 +      // No animation for the moment//
 +      Thread thread = new Thread(ap);
 +      thread.start();
 +    }
 +    final WebserviceInfo thisinfo = this;
 +    frame.addInternalFrameListener(
 +            new InternalFrameAdapter()
 +            {
 +              @Override
 +              public void internalFrameClosed(InternalFrameEvent evt)
 +              {
 +                // System.out.println("Shutting down webservice client");
 +                WSClientI service = thisinfo.getthisService();
 +                if (service != null && service.isCancellable())
 +                {
 +                  service.cancelJob();
 +                }
 +              }
 +            });
      frame.validate();
  
    }
@@@ -186,17 -184,14 +186,15 @@@ public class WsJobParameters extends JP
     * @param preset
     * @param jobArgset
     */
 -  public WsJobParameters(JFrame parent, ParamDatastoreI paramStorei,
 -          Jws2Instance service, WsParamSetI preset,
 -          List<Argument> jobArgset)
 +  public WsJobParameters(ParamDatastoreI paramStorei, UIinfo service,
 +          WsParamSetI preset, List<ArgumentI> jobArgset)
    {
      super();
 +    // parameters dialog in 'expanded' format (help text boxes)
 +    opanp = new OptsAndParamsPage(this, false);
      jbInit();
      this.paramStore = paramStorei;
 -    if (paramStore == null)
 +    if (paramStore == null && service != null)
      {
        paramStore = service.getParamStore();
      }
      validate();
    }
  
 -  int response = -1;
 -
 -  JDialog frame = null;
    /**
 -   * shows a modal dialog containing the parameters.
 +   * Shows a modal dialog containing the parameters and Start or Cancel options.
 +   * Answers true if the job is started, false if cancelled.
     * 
     * @return
     */
 -  public boolean showRunDialog()
 +  public CompletionStage<Boolean> showRunDialog()
    {
 -
 -    frame = new JDialog(Desktop.instance, true);
 -
 -    frame.setTitle(MessageManager.formatMessage("label.edit_params_for",
 -            new String[]
 -            { service.getActionText() }));
 -    Rectangle deskr = Desktop.instance.getBounds();
++    // Should JFrame hahve a parent of getDesktop ?
 +    frame = new JFrame();
 +    frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
 +    if (service != null)
 +    {
 +      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),
  
        }
      });
 +    
      frame.setVisible(true);
 -    if (response > 0)
 -    {
 -      return true;
 -    }
 -    return false;
 +    return completionStage;
    }
  
    private void jbInit()
      paramPane.getViewport().setView(jp);
      paramPane.setBorder(null);
      setLayout(new BorderLayout());
 +    JPanel jobPanel = new JPanel();
      jobPanel.setPreferredSize(null);
      jobPanel.setLayout(new BorderLayout());
      jobPanel.add(setDetails, BorderLayout.NORTH);
        exnames.put(SVC_DEF, SVC_DEF);
        servicePresets.put(SVC_DEF, SVC_DEF);
      }
 -    String curname = (p == null ? "" : p.getName());
 +    // String curname = (p == null ? "" : p.getName());
      for (WsParamSetI pr : paramStore.getPresets())
      {
        if (!pr.isModifiable())
        }
      }
      settingDialog = false;
    }
  
 -  @SuppressWarnings("unchecked")
    private void updateTable(WsParamSetI p, List<ArgumentI> jobArgset)
    {
      boolean setDefaultParams = false;
      return modifiedElements.size() > 0;
    }
  
 -  private Hashtable modifiedElements = new Hashtable();
    /**
     * reset gui and modification state settings
     */
      paramPane.revalidate();
      revalidate();
    }
 -  /**
 -   * testing method - grab a service and parameter set and show the window
 -   * 
 -   * @param args
 -   * @j2sIgnore
 -   */
 -  public static void main(String[] args)
 -  {
 -    jalview.ws.jws2.Jws2Discoverer disc = jalview.ws.jws2.Jws2Discoverer
 -            .getDiscoverer();
 -    int p = 0;
 -    if (args.length > 0)
 -    {
 -      Vector<String> services = new Vector<>();
 -      services.addElement(args[p++]);
 -      Jws2Discoverer.getDiscoverer().setServiceUrls(services);
 -    }
 -    try
 -    {
 -      disc.run();
 -    } catch (Exception e)
 -    {
 -      System.err.println("Aborting. Problem discovering services.");
 -      e.printStackTrace();
 -      return;
 -    }
 -    Jws2Instance lastserv = null;
 -    for (Jws2Instance service : disc.getServices())
 -    {
 -      lastserv = service;
 -      if (p >= args.length || service.serviceType.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(lastserv,
 -                    new JabaPreset(lastserv, pr));
 -            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();
 -          }
 -        }
 -      }
 -    }
 -  }
    public boolean isServiceDefaults()
    {
      return (!isModified()
      return opanp.getCurrentSettings();
    }
  
 -  String lastParmSet = null;
    /*
     * Hashtable<String, Object[]> editedParams = new Hashtable<String,
     * Object[]>();
    @Override
    public void itemStateChanged(ItemEvent e)
    {
 -    if (e.getSource() == setName && e.getStateChange() == e.SELECTED)
 +    if (e.getSource() == setName
 +            && e.getStateChange() == ItemEvent.SELECTED)
      {
        final String setname = (String) setName.getSelectedItem();
-       // System.out.println("Item state changed for " + setname
-       // + " (handling ? " + !settingDialog + ")");
 -      if (Cache.log.isDebugEnabled())
 -      {
 -        Cache.log.debug("Item state changed for " + setname
 -                + " (handling ? " + !settingDialog + ")");
 -      }
++      System.out.println("Item state changed for " + setname
++              + " (handling ? " + !settingDialog + ")");
        if (settingDialog)
        {
          // ignore event
@@@ -38,7 -36,7 +38,6 @@@ import javax.swing.JTable
  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;
   */
  package jalview.io;
  
- import jalview.bin.Cache;
- import jalview.gui.Desktop;
- import jalview.gui.JvOptionPane;
- import jalview.util.MessageManager;
- import jalview.util.Platform;
 +
  import java.io.File;
  import java.io.IOException;
+ import java.nio.file.Files;
+ import java.nio.file.Path;
+ import java.nio.file.Paths;
+ import java.nio.file.StandardCopyOption;
  import java.text.SimpleDateFormat;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.Map;
  import java.util.TreeMap;
  
+ import jalview.bin.Cache;
+ import jalview.gui.Desktop;
+ import jalview.gui.JvOptionPane;
+ import jalview.util.MessageManager;
+ import jalview.util.Platform;
 -
  /*
   * BackupFiles used for manipulating (naming rolling/deleting) backup/version files when an alignment or project file is saved.
   * User configurable options are:
@@@ -96,6 -100,10 +100,9 @@@ public class BackupFile
    private static final SimpleDateFormat sdf = new SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss");
  
+   private static final String newTempFileSuffix = "_newfile";
+   private static final String oldTempFileSuffix = "_oldfile_tobedeleted";
 -
    public BackupFiles(String filename)
    {
      this(new File(filename));
        return true;
      }
  
+     Cache.trace("BACKUPFILES rollBackupFiles starting");
 -
      String dir = "";
      File dirFile;
      try
        if (reverseOrder)
        {
          // backup style numbering
 +
+         Cache.trace("BACKUPFILES rolling files in reverse order");
  
          int tempMax = noMax ? -1 : max;
          // noMax == true means no limits
              tempMax = i;
            }
          }
-         
 -
          File previousFile = null;
          File fileToBeDeleted = null;
          for (int n = tempMax; n > 0; n--)
              {
                File oldestTempFile = nextTempFile(fileToBeDeleted.getName(),
                        dirFile);
-               
 -
                if (fileToBeDeletedLMT > replacementFileLMT)
                {
                  String fileToBeDeletedLMTString = sdf
              File fileToBeDeleted = backupFiles[i];
              boolean delete = true;
  
+             Cache.trace("BACKUPFILES fileToBeDeleted: " + fileToBeDeleted);
 -
              boolean newer = false;
              if (replacementFile != null)
              {
      String latestBackupFilename = dir + File.separatorChar
              + BackupFilenameParts.getBackupFilename(nextIndexNum, basename,
                      suffix, digits);
-     ret |= file.renameTo(new File(latestBackupFilename));
+     Cache.trace("BACKUPFILES Moving old file [" + file
+             + "] to latestBackupFilename [" + latestBackupFilename + "]");
+     // using boolean '&' instead of '&&' as don't want moveFileToFile attempt to
+     // be conditional (short-circuit)
+     ret = ret & moveFileToFile(file, new File(latestBackupFilename));
+     Cache.debug("BACKUPFILES moving " + file + " to " + latestBackupFilename
+             + " was " + (ret ? "" : "NOT ") + "successful");
 +
      if (tidyUp)
      {
+       Cache.debug("BACKUPFILES tidying up files");
        tidyUpFiles();
      }
  
            MessageManager.getString("label.delete"),
            MessageManager.getString("label.rename") };
  
-       confirmButton = JvOptionPane.showOptionDialog(Desktop.getDesktopPane(),
-               messageSB.toString(),
-               MessageManager.getString("label.backupfiles_confirm_delete"),
-               JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE,
-               null, options, options[0]);
++      // TODO enable JvOptionPane to behave appropriately when batch/headless
+       confirmButton = Platform.isHeadless() ? JvOptionPane.YES_OPTION
 -              : JvOptionPane.showOptionDialog(Desktop.desktop,
++              : JvOptionPane.showOptionDialog(Desktop.getDesktopPane(),
+                       messageSB.toString(),
+                       MessageManager.getString(
+                               "label.backupfiles_confirm_delete"),
 -                      // "Confirm delete"
+                       JvOptionPane.YES_NO_OPTION,
+                       JvOptionPane.WARNING_MESSAGE, null, options,
+                       options[0]);
      }
      else
      {
            MessageManager.getString("label.delete"),
            MessageManager.getString("label.keep") };
  
-       confirmButton = JvOptionPane.showOptionDialog(Desktop.getDesktopPane(),
+       confirmButton = Platform.isHeadless() ? JvOptionPane.YES_OPTION
 -              : JvOptionPane.showOptionDialog(Desktop.desktop,
 -                      messageSB.toString(),
 -                      MessageManager.getString(
 -                              "label.backupfiles_confirm_delete"),
 -                      // "Confirm delete"
 -                      JvOptionPane.YES_NO_OPTION,
 -                      JvOptionPane.WARNING_MESSAGE, null, options,
 -                      options[0]);
++              : JvOptionPane.showOptionDialog(Desktop.getDesktopPane(),
 +              messageSB.toString(),
 +              MessageManager.getString("label.backupfiles_confirm_delete"),
 +              JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE,
 +              null, options, options[0]);
      }
  
 +
      // return should be TRUE if file is to be deleted
      return (confirmButton == JvOptionPane.YES_OPTION);
    }
                    new String[]
                    { sdf.format(df.lastModified()),
                        Long.toString(df.length()) }));
+           // "(modified {0}, size {1})"
          }
  
-         int confirmButton = JvOptionPane.showConfirmDialog(Desktop.getDesktopPane(),
-                 messageSB.toString(),
-                 MessageManager
-                         .getString("label.backupfiles_confirm_delete"),
-                 JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
+         int confirmButton = Platform.isHeadless() ? JvOptionPane.YES_OPTION
 -                : JvOptionPane.showConfirmDialog(Desktop.desktop,
++                : JvOptionPane.showConfirmDialog(Desktop.getDesktopPane(),
+                         messageSB.toString(),
+                         MessageManager.getString(
+                                 "label.backupfiles_confirm_delete"),
+                         // "Confirm delete"
+                         JvOptionPane.YES_NO_OPTION,
+                         JvOptionPane.WARNING_MESSAGE);
  
          doDelete = (confirmButton == JvOptionPane.YES_OPTION);
        }
          }
          messageSB.append(MessageManager.getString(
                  "label.backupfiles_confirm_save_new_saved_file_not_ok"));
+         // "The new saved file might not be okay."
        }
-       int confirmButton = JvOptionPane.showConfirmDialog(Desktop.getDesktopPane(),
-               messageSB.toString(),
-               MessageManager
-                       .getString("label.backupfiles_confirm_save_file"),
-               JvOptionPane.OK_OPTION, JvOptionPane.WARNING_MESSAGE);
+       if (messageSB.length() > 0)
+       {
+         messageSB.append("\n");
+       }
+       messageSB
+               .append(MessageManager.getString("label.continue_operation"));
+       int confirmButton = Platform.isHeadless() ? JvOptionPane.OK_OPTION
 -              : JvOptionPane.showConfirmDialog(Desktop.desktop,
++              : JvOptionPane.showConfirmDialog(Desktop.getDesktopPane(),
+                       messageSB.toString(),
+                       MessageManager.getString(
+                               "label.backupfiles_confirm_save_file"),
+                       // "Confirm save file"
+                       JvOptionPane.OK_OPTION, JvOptionPane.WARNING_MESSAGE);
        okay = confirmButton == JvOptionPane.OK_OPTION;
      }
      if (okay)
Simple merge
Simple merge
@@@ -47,16 -46,10 +47,14 @@@ 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;
    {
      Vector<String> recent = new Vector<>();
      if (protocol == DataSourceType.PASTE)
 +    this.viewport = viewport;
 +    this.file = file;
 +    this.protocol = sourceType;
 +    this.format = format;
 +    _LoadFileWaitTillLoaded();
 +  }
 +
 +
 +  /**
 +   * Updates (or creates) the tab-separated list of recently opened files held
 +   * under the given property name by inserting the filePath at the front of the
 +   * list. Duplicates are removed, and the list is limited to 11 entries. The
 +   * method returns the updated value of the property.
 +   * 
 +   * @param filePath
 +   * @param sourceType
 +   */
 +  public static String updateRecentlyOpened(String filePath,
 +          DataSourceType sourceType)
 +  {
 +    if (sourceType != DataSourceType.FILE
 +            && sourceType != DataSourceType.URL)
      {
 -      // do nothing if the file was pasted in as text... there is no filename to
 -      // refer to it as.
 -      return;
 +      return null;
      }
 -    if (file != null
 -            && file.indexOf(System.getProperty("java.io.tmpdir")) > -1)
 +    String propertyName = sourceType == DataSourceType.FILE ? "RECENT_FILE"
 +            : "RECENT_URL";
 +    String historyItems = Cache.getProperty(propertyName);
 +    if (filePath != null
 +            && filePath.indexOf(System.getProperty("java.io.tmpdir")) > -1)
      {
        // ignore files loaded from the system's temporary directory
 -      return;
 +      return null;
      }
 -    String type = protocol == DataSourceType.FILE ? "RECENT_FILE"
 -            : "RECENT_URL";
  
 -    String historyItems = Cache.getProperty(type);
 -
 -    StringTokenizer st;
 +    List<String> recent = new ArrayList<>();
  
      if (historyItems != null)
      {
                    .getFeatureColourScheme();
            if (viewport != null)
            {
++            // TODO: test if this needs to be done after addAlignment ? (like in 2.11.2)  
 +            if (proxyColourScheme != null)
 +            {
 +              viewport.applyFeaturesStyle(proxyColourScheme);
 +            }
              // append to existing alignment
              viewport.addAlignment(al, title);
 -            viewport.applyFeaturesStyle(proxyColourScheme);
 +            if (source instanceof HMMFile)
 +            {
 +              AlignmentI alignment = viewport.getAlignment();
 +              SequenceI seq = alignment
 +                      .getSequenceAt(alignment.getHeight() - 1);
 +              if (seq.hasHMMProfile())
 +              {
 +                /* 
 +                 * fudge: move HMM consensus sequence from last to first
 +                 */
 +                alignment.deleteSequence(alignment.getAbsoluteHeight() - 1);
 +                alignment.insertSequenceAt(0, seq);
 +              }
 +              viewport.getAlignPanel().adjustAnnotationHeight();
 +              viewport.updateSequenceIdColours();
 +            }
            }
            else
            {
@@@ -20,6 -20,8 +20,7 @@@
   */
  package jalview.io;
  
+ import java.util.Locale;
 -
  import java.io.File;
  import java.io.IOException;
  
@@@ -185,11 -187,19 +186,24 @@@ public class IdentifyFil
            reply = FileFormat.ScoreMatrix;
            break;
          }
 +        if (data.startsWith("HMMER3"))
 +        {
 +          reply = FileFormat.HMMER3;
 +          break;
 +        }
+         if (data.startsWith("LOCUS"))
+         {
+           reply = FileFormat.GenBank;
+           break;
+         }
+         if (data.startsWith("ID "))
+         {
+           if (data.substring(2).trim().split(";").length == 7)
+           {
+             reply = FileFormat.Embl;
+             break;
+           }
+         }
          if (data.startsWith("H ") && !aaIndexHeaderRead)
          {
            aaIndexHeaderRead = true;
  // TODO: Extended SequenceNodeI to hold parsed NHX strings
  package jalview.io;
  
+ import java.util.Locale;
  import jalview.datamodel.SequenceNode;
  import jalview.util.MessageManager;
 +import jalview.util.Platform;
  
  import java.io.BufferedReader;
  import java.io.File;
@@@ -274,12 -275,10 +276,12 @@@ public class SequenceAnnotationRepor
           * truncate overlong descriptions unless they contain an href
           * before the truncation point (as truncation could leave corrupted html)
           */
-         int linkindex = description.toLowerCase().indexOf("<a ");
+         int linkindex = description.toLowerCase(Locale.ROOT).indexOf("<a ");
          boolean hasLink = linkindex > -1
                  && linkindex < MAX_DESCRIPTION_LENGTH;
 -        if (description.length() > MAX_DESCRIPTION_LENGTH && !hasLink)
 +        if (
 +                // BH suggestion maxlength == 0 && 
 +                description.length() > MAX_DESCRIPTION_LENGTH && !hasLink)
          {
            description = description.substring(0, MAX_DESCRIPTION_LENGTH)
                    + ELLIPSIS;
                        + "\" target=\""
                        + urllink.get(0)
                        + "\">"
-                       + (urllink.get(0).toLowerCase()
-                               .equals(urllink.get(1).toLowerCase()) ? urllink
+                       + (urllink.get(0).toLowerCase(Locale.ROOT)
+                               .equals(urllink.get(1).toLowerCase(Locale.ROOT)) ? urllink
                                .get(0) : (urllink.get(0) + ":" + urllink
                                                .get(1)))
 -                      + "</a><br/>");
 +                      + "</a><br>");
              }
            } catch (Exception x)
            {
@@@ -48,6 -33,6 +48,7 @@@ import java.util.Enumeration
  import java.util.Hashtable;
  import java.util.LinkedHashMap;
  import java.util.List;
++import java.util.Locale;
  import java.util.Map;
  import java.util.Vector;
  
@@@ -79,106 -79,20 +80,105 @@@ public class StockholmFile extends Alig
  {
    private static final String ANNOTATION = "annotation";
  
 -//  private static final Regex OPEN_PAREN = new Regex("(<|\\[)", "(");
 -//
 -//  private static final Regex CLOSE_PAREN = new Regex("(>|\\])", ")");
 -
 -  public static final Regex DETECT_BRACKETS = new Regex(
 -          "(<|>|\\[|\\]|\\(|\\)|\\{|\\})");
 -
 +  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
  
      for (int i = 0; i < annots.length(); i++)
      {
        String pos = annots.substring(i, i + 1);
++      // TODO 2.12 release: verify this Stockholm IO behaviour change in release notes
 +      if (UNDERSCORE == pos.charAt(0))
 +      {
 +        pos = " ";
 +      }
        Annotation ann;
        ann = new Annotation(pos, "", ' ', 0f); // 0f is 'valid' null - will not
        // be written out
        if (alAnot != null)
        {
          Annotation[] ann;
 -        for (int j = 0, nj = alAnot.length; j < nj; j++)
 +        for (int j = 0; j < alAnot.length; j++)
          {
 -          String key = type2id(alAnot[j].label);
 -          boolean isrna = alAnot[j].isValidStruc();
 -
 -          if (isrna)
 -          {
 -            // hardwire to secondary structure if there is RNA secondary
 -            // structure on the annotation
 -            key = "SS";
 -          }
 -          if (key == null)
 +          if (alAnot[j].annotations != null)
            {
 +            String key = type2id(alAnot[j].label);
 +            boolean isrna = alAnot[j].isValidStruc();
  
 -            continue;
 -          }
 +            if (isrna)
 +            {
 +              // hardwire to secondary structure if there is RNA secondary
 +              // structure on the annotation
 +              key = "SS";
 +            }
 +            if (key == null)
 +            {
 +              continue;
 +            }
  
 -          // out.append("#=GR ");
 -          out.append(new Format("%-" + maxid + "s").form(
 -                  "#=GR " + printId(seq, jvSuffix) + " " + key + " "));
 -          ann = alAnot[j].annotations;
 -          String sseq = "";
 -          for (int k = 0, nk = ann.length; k < nk; k++)
 -          {
 -            sseq += outputCharacter(key, k, isrna, ann, seq);
 -          }
 -          out.append(sseq);
 -          out.append(newline);
 +            // out.append("#=GR ");
 +            out.append(new Format("%-" + maxid + "s").form(
 +                    "#=GR " + printId(s[i], jvSuffix) + " " + key + " "));
 +            ann = alAnot[j].annotations;
 +            String sseq = "";
 +            for (int k = 0; k < ann.length; k++)
 +            {
 +              sseq += outputCharacter(key, k, isrna, ann, s[i]);
 +            }
 +            out.append(sseq);
 +            out.append(newline);
 +        }
          }
        }
  
        out.append(new Format("%-" + maxid + "s")
  
      }
    }
 +  
    protected static String id2type(String id)
    {
      if (typeIds.containsKey(id))
   */
  package jalview.io.packed;
  
 +import java.io.BufferedReader;
 +import java.io.IOException;
 +import java.util.ArrayList;
 +import java.util.HashMap;
 +import java.util.List;
+ import java.util.Locale;
  
+ import jalview.api.FeatureColourI;
  import jalview.datamodel.AlignmentI;
  import jalview.io.AppletFormatAdapter;
  import jalview.io.FileFormatI;
@@@ -34,6 -31,12 +36,7 @@@ import jalview.io.FormatAdapter
  import jalview.io.IdentifyFile;
  import jalview.io.packed.DataProvider.JvDataType;
  
 -import java.io.BufferedReader;
 -import java.io.IOException;
 -import java.util.ArrayList;
 -import java.util.HashMap;
 -import java.util.List;
  public class ParsePackedSet
  {
  
@@@ -89,8 -74,22 +93,6 @@@ import javax.swing.event.DocumentListen
  import javax.swing.table.TableCellEditor;
  import javax.swing.table.TableCellRenderer;
  
- import net.miginfocom.swing.MigLayout;
 -import jalview.bin.Cache;
 -import jalview.fts.core.FTSDataColumnPreferences;
 -import jalview.fts.core.FTSDataColumnPreferences.PreferenceSource;
 -import jalview.fts.service.pdb.PDBFTSRestClient;
 -import jalview.gui.Desktop;
 -import jalview.gui.JalviewBooleanRadioButtons;
 -import jalview.gui.JvOptionPane;
 -import jalview.gui.JvSwingUtils;
 -import jalview.gui.StructureViewer.ViewerType;
 -import jalview.io.BackupFilenameParts;
 -import jalview.io.BackupFiles;
 -import jalview.io.BackupFilesPresetEntry;
 -import jalview.io.IntKeyStringValueEntry;
 -import jalview.util.MessageManager;
 -import jalview.util.Platform;
 -
  /**
   * Base class for the Preferences panel.
   * 
@@@ -165,10 -164,6 +167,9 @@@ public class GPreferences extends JPane
  
    protected JCheckBox showConsensLogo = new JCheckBox();
  
 +  protected JCheckBox showInformationHistogram = new JCheckBox();
 +
 +  protected JCheckBox showHMMLogo = new JCheckBox();
    protected JCheckBox showDbRefTooltip = new JCheckBox();
  
    protected JCheckBox showNpTooltip = new JCheckBox();
  
    protected JCheckBox structFromPdb = new JCheckBox();
  
-   protected JCheckBox useRnaView = new JCheckBox();
 +
    protected JCheckBox addSecondaryStructure = new JCheckBox();
  
    protected JCheckBox addTempFactor = new JCheckBox();
     */
    protected JPanel wsTab = new JPanel();
  
 +  protected JPanel slivkaTab = new JPanel();
    /*
     * Backups tab components
     * a lot of these are member variables instead of local variables only so that they
  
    protected JTextArea backupfilesExampleLabel = new JTextArea();
  
+   private final JTabbedPane tabbedPane = new JTabbedPane();
+   private JLabel messageLabel = new JLabel("", JLabel.CENTER);
 -
    /**
     * Creates a new GPreferences object.
     */
     */
    private void jbInit() throws Exception
    {
-     final JTabbedPane tabbedPane = new JTabbedPane();
+     // final JTabbedPane tabbedPane = new JTabbedPane();
      this.setLayout(new BorderLayout());
 -
+     // message label at top
+     this.add(messageLabel, BorderLayout.NORTH);
 -
      JPanel okCancelPanel = initOkCancelPanel();
      this.add(tabbedPane, BorderLayout.CENTER);
      this.add(okCancelPanel, BorderLayout.SOUTH);
      tabbedPane.add(initEditingTab(),
              MessageManager.getString("label.editing"));
  
 +    tabbedPane.add(initHMMERTab(), MessageManager.getString("label.hmmer"));
      /*
       * See WsPreferences for the real work of configuring this tab.
       */
            }
          }
          lastTab = tabbedPane.getSelectedComponent();
 -
+         clearMessage();
        }
  
      });
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         useProxy_actionPerformed();
+         proxyType_actionPerformed();
+       }
+     };
+     noProxy.addActionListener(al);
+     systemProxy.addActionListener(al);
+     customProxy.addActionListener(al);
+     proxyType.add(noProxy);
+     proxyType.add(systemProxy);
+     proxyType.add(customProxy);
+     proxyAuth.setFont(LABEL_FONT);
+     proxyAuth.setHorizontalAlignment(SwingConstants.LEFT);
+     proxyAuth.setText(MessageManager.getString("label.auth_required"));
+     proxyAuth.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         proxyAuth_actionPerformed();
        }
      });
 -
+     setCustomProxyEnabled();
  
      // Make proxy server panel
      JPanel proxyPanel = new JPanel();
      structureTab.add(structFromPdb);
  
      // indent checkboxes that are conditional on the first one
-     ypos += lineSpacing;
-     useRnaView.setFont(LABEL_FONT);
-     useRnaView.setText(MessageManager.getString("label.use_rnaview"));
-     useRnaView.setBounds(new Rectangle(25, ypos, width, height));
-     structureTab.add(useRnaView);
 +
      ypos += lineSpacing;
      addSecondaryStructure.setFont(LABEL_FONT);
      addSecondaryStructure
        @Override
        public void mouseClicked(MouseEvent e)
        {
-         if (e.getClickCount() == 2)
+         if (structureViewerPath.isEnabled() && e.getClickCount() == 2)
          {
-           String chosen = openFileChooser(false);
++          structureViewer_actionPerformed(
++                  (String) structViewer.getSelectedItem());
++        }
++      });
++      structureTab.add(structViewer);
++      ypos += lineSpacing;
++      structureViewerPathLabel = new JLabel();
++      structureViewerPathLabel.setFont(LABEL_FONT);// new Font("SansSerif", 0,
++                                                   // 11));
++      structureViewerPathLabel.setHorizontalAlignment(SwingConstants.LEFT);
++      structureViewerPathLabel.setText(MessageManager
++              .formatMessage("label.viewer_path", "Chimera(X)"));
++      structureViewerPathLabel
++              .setBounds(new Rectangle(10, ypos, 170, height));
++      structureViewerPathLabel.setEnabled(false);
++      structureTab.add(structureViewerPathLabel);
++ 
++      structureViewerPath.setFont(LABEL_FONT);
++      structureViewerPath.setText("");
++      structureViewerPath.setEnabled(false);
++      final String tooltip = JvSwingUtils.wrapTooltip(true,
++              MessageManager.getString("label.viewer_path_tip"));
++      structureViewerPath.setToolTipText(tooltip);
++      structureViewerPath.setBounds(new Rectangle(190, ypos, 290, height));
++      structureViewerPath.addMouseListener(new MouseAdapter()
++      {
++        if (structureViewerPath.isEnabled() && e.getClickCount() == 2)
++        {
+           String chosen = openFileChooser();
            if (chosen != null)
            {
-             chimeraPath.setText(chosen);
+             structureViewerPath.setText(chosen);
            }
          }
        }
        viewerLabel.setVisible(false);
        structViewer.setVisible(false);
      }
--
      return structureTab;
    }
  
    {
      String choice = null;
      JFileChooser chooser = new JFileChooser();
+     // Enable appBundleIsTraversable in macOS FileChooser to allow selecting
+     // hidden executables within .app dirs
+     if (Platform.isMac())
+     {
+       chooser.putClientProperty("JFileChooser.appBundleIsTraversable",
+               true);
+     }
 +    if (forFolder)
 +    {
 +      chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
 +    }
 +
      // chooser.setFileView(new JalviewFileView());
      chooser.setDialogTitle(
              MessageManager.getString("label.open_local_file"));
@@@ -228,8 -226,6 +230,7 @@@ public class Jalview2XM
  
    private static final String RNA_PREFIX = "rna_";
  
 +  private static final String HMMER_PREFIX = "hmmer_";
    private static final String UTF_8 = "UTF-8";
  
    /**
  
          String shortName = makeFilename(af, shortNames);
  
 -        int apSize = af.getAlignPanels().size();
 +        AlignmentI alignment = vp.getAlignment();
 +        List<? extends AlignmentViewPanel> panels = af.getAlignPanels();
 +        int apSize = panels.size();
          for (int ap = 0; ap < apSize; ap++)
 -        {
 -          AlignmentPanel apanel = (AlignmentPanel) af.getAlignPanels()
 -                  .get(ap);
 +          {
 +          AlignmentPanel apanel = (AlignmentPanel) panels.get(ap);
            String fileName = apSize == 1 ? shortName : ap + shortName;
            if (!fileName.endsWith(".xml"))
            {
            }
  
            saveState(apanel, fileName, jout, viewIds);
 -          String dssid = getDatasetIdRef(
 -                  af.getViewport().getAlignment().getDataset());
 +        }
 +        if (apSize > 0)
 +        {
 +          // BH moved next bit out of inner loop, not that it really matters.
 +          // so we are testing to make sure we actually have an alignment,
 +          // apparently.
 +          String dssid = getDatasetIdRef(alignment.getDataset());
            if (!dsses.containsKey(dssid))
            {
 +            // We have not already covered this data by reference from another
 +            // frame.
              dsses.put(dssid, af);
            }
          }
            String fileName, JarOutputStream jout)
    {
  
 +    // Note that in saveAllFrames we have associated each specific dataset to
 +    // ONE of its associated frames.
      for (String dssids : dsses.keySet())
      {
        AlignFrame _af = dsses.get(dssids);
  
        saveRnaViewers(jout, jseq, jds, viewIds, ap, storeDS);
  
 +      if (jds.hasHMMProfile())
 +      {
 +        saveHmmerProfile(jout, jseq, jds);
 +      }
        // jms.addJSeq(jseq);
        object.getJSeq().add(jseq);
      }
  
      return ucs;
    }
 -
+   /**
+    * contains last error message (if any) encountered by XML loader.
+    */
+   String errorMessage = null;
+   /**
+    * flag to control whether the Jalview2XML_V1 parser should be deferred to if
+    * exceptions are raised during project XML parsing
+    */
+   public boolean attemptversion1parse = false;
  
    /**
     * Load a jalview project archive from a jar file
                : ofile.toString());
        byte[] bytes = Platform.isJS() ? Platform.getFileBytes((File) ofile)
                : null;
 +      if (bytes != null)
 +      {
 +        this.jarFile = (File) ofile;
 +      }
+       URL url = null;
        errorMessage = null;
        uniqueSetSuffix = null;
        seqRefIds = null;
        viewportsAdded.clear();
        frefedSequence = null;
  
-       URL url = file.startsWith("http://") ? new URL(file) : null;
+       if (HttpUtils.startsWithHttpOrHttps(file))
+       {
+         url = new URL(file);
+       }
 -      final URL _url = url;
        return new jarInputStreamProvider()
        {
 -
          @Override
          public JarInputStream getJarInputStream() throws IOException
          {
      AlignFrame af = null, _af = null;
      IdentityHashMap<AlignmentI, AlignmentI> importedDatasets = new IdentityHashMap<>();
      Map<String, AlignFrame> gatherToThisFrame = new HashMap<>();
 -    final String file = jprovider.getFilename();
 +    String fileName = jprovider.getFilename();
 +    File file = jprovider.getFile();
 +    List<AlignFrame> alignFrames = new ArrayList<>();
      try
      {
        JarInputStream jin = null;
        JarEntry jarentry = null;
        int entryCount = 1;
  
 +      // Look for all the entry names ending with ".xml"
 +      // This includes all panels and at least one frame.
 +//      Platform.timeCheck(null, Platform.TIME_MARK);
        do
        {
          jin = jprovider.getJarInputStream();
          {
            jarentry = jin.getNextJarEntry();
          }
 -
 -        if (jarentry != null && jarentry.getName().endsWith(".xml"))
 -        {
 +        String name = (jarentry == null ? null : jarentry.getName());
 +
 +//        System.out.println("Jalview2XML opening " + name);
 +        if (name != null && name.endsWith(".xml"))
 +        {
 +          // DataSet for.... is read last.
 +          
 +          
 +          // The question here is what to do with the two
 +          // .xml files in the jvp file.
 +          // Some number of them, "...Dataset for...", will be the
 +          // Only AlignPanels and will have Viewport.
 +          // One or more will be the source data, with the DBRefs.
 +          //
 +          // JVP file writing (above) ensures tha the AlignPanels are written
 +          // first, then all relevant datasets (which are
 +          // Jalview.datamodel.Alignment).
 +          //
 +
 +//          Platform.timeCheck("Jalview2XML JAXB " + name, Platform.TIME_MARK);
            JAXBContext jc = JAXBContext
                    .newInstance("jalview.xml.binding.jalview");
            XMLStreamReader streamReader = XMLInputFactory.newInstance()
        errorMessage = "Out of memory loading jalview XML file";
        System.err.println("Out of memory whilst loading jalview XML file");
        e.printStackTrace();
 +    } finally
 +    {
 +      for (AlignFrame alf : alignFrames)
 +      {
 +        alf.alignPanel.setHoldRepaint(false);
 +      }
      }
  
      /*
     * @return
     */
    protected String copyJarEntry(jarInputStreamProvider jprovider,
 -          String jarEntryName, String prefix, String suffixModel)
 +          String jarEntryName, String prefix, String origFile)
    {
 +    BufferedReader in = null;
 +    PrintWriter out = null;
      String suffix = ".tmp";
 -    if (suffixModel == null)
 +    if (origFile == null)
      {
 -      suffixModel = jarEntryName;
 +      origFile = jarEntryName;
      }
 -    int sfpos = suffixModel.lastIndexOf(".");
 -    if (sfpos > -1 && sfpos < (suffixModel.length() - 1))
 +    int sfpos = origFile.lastIndexOf(".");
 +    if (sfpos > -1 && sfpos < (origFile.length() - 3))
      {
 -      suffix = "." + suffixModel.substring(sfpos + 1);
 +      suffix = "." + origFile.substring(sfpos + 1);
      }
-     try
 -
+     try (JarInputStream jin = jprovider.getJarInputStream())
      {
-       JarInputStream jin = jprovider.getJarInputStream();
-       /*
-        * if (jprovider.startsWith("http://")) { jin = new JarInputStream(new
-        * URL(jprovider).openStream()); } else { jin = new JarInputStream(new
-        * FileInputStream(jprovider)); }
-        */
 +
        JarEntry entry = null;
        do
        {
          entry = jin.getNextJarEntry();
        } while (entry != null && !entry.getName().equals(jarEntryName));
 -
        if (entry != null)
        {
-         in = new BufferedReader(new InputStreamReader(jin, UTF_8));
+         // in = new BufferedReader(new InputStreamReader(jin, UTF_8));
          File outFile = File.createTempFile(prefix, suffix);
          outFile.deleteOnExit();
-         out = new PrintWriter(new FileOutputStream(outFile));
-         String data;
-         while ((data = in.readLine()) != null)
+         try (OutputStream os = new FileOutputStream(outFile))
          {
-           out.println(data);
+           copyAll(jin, os);
          }
-         out.flush();
          String t = outFile.getAbsolutePath();
          return t;
        }
     *          data source provider
     * @return alignment frame created from view stored in DOM
     */
 -  AlignFrame loadFromObject(JalviewModel jalviewModel, String file,
 -          boolean loadTreesAndStructures, jarInputStreamProvider jprovider)
 +  AlignFrame loadFromObject(JalviewModel jalviewModel, String fileName,
 +          File file, boolean loadTreesAndStructures, jarInputStreamProvider jprovider)
    {
 -    SequenceSet vamsasSet = jalviewModel.getVamsasModel().getSequenceSet()
 -            .get(0);
 +    SequenceSet vamsasSet = jalviewModel.getVamsasModel().getSequenceSet().get(0);
      List<Sequence> vamsasSeqs = vamsasSet.getSequence();
  
      // JalviewModelSequence jms = object.getJalviewModelSequence();
  
      // Viewport view = (jms.getViewportCount() > 0) ? jms.getViewport(0)
              }
            }
          }
 +        /*
 +         * 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
  
            tp.getTreeCanvas().setAssociatedPanel(ap); // af.alignPanel;
          }
          tp.getTreeCanvas().setApplyToAllViews(tree.isLinkToAllViews());
 -        if (tp == null)
 -        {
 -          warn("There was a problem recovering stored Newick tree: \n"
 -                  + tree.getNewick());
 -          continue;
 -        }
          tp.fitToWindow.setState(safeBoolean(tree.isFitToWindow()));
          tp.fitToWindow_actionPerformed(null);
  
        return;
      }
  
-     /*
-      * From 2.9: stateData.type contains JMOL or CHIMERA, data is in jar entry
-      * "viewer_"+stateData.viewId
-      */
-     if (ViewerType.CHIMERA.toString().equals(stateData.getType()))
+     String type = stateData.getType();
+     try
      {
-       createChimeraViewer(viewerData, af, jprovider);
-     }
-     else
+       ViewerType viewerType = ViewerType.valueOf(type);
+       createStructureViewer(viewerType, viewerData, af, jprovider);
+     } catch (IllegalArgumentException | NullPointerException e)
      {
-       /*
-        * else Jmol (if pre-2.9, stateData contains JMOL state string)
-        */
-       createJmolViewer(viewerData, af, jprovider);
+       // TODO JAL-3619 show error dialog / offer an alternative viewer
+       Cache.log.error("Invalid structure viewer type: " + type);
      }
 -  }
    }
  
    /**
-    * Create a new Chimera viewer.
 -   * Returns any open frame that matches given structure viewer data. The match
 -   * is based on the unique viewId, or (for older project versions) the frame's
 -   * geometry.
++   * Creates a new structure viewer window
     * 
-    * @param data
++   * @param viewerType
+    * @param viewerData
 -   * @return
 +   * @param af
 +   * @param jprovider
     */
-   protected void createChimeraViewer(
-           Entry<String, StructureViewerModel> viewerData, AlignFrame af,
-           jarInputStreamProvider jprovider)
 -  protected StructureViewerBase findMatchingViewer(
 -          Entry<String, StructureViewerModel> viewerData)
++  protected void createStructureViewer(ViewerType viewerType,
++          final Entry<String, StructureViewerModel> viewerData,
++          AlignFrame af, jarInputStreamProvider jprovider)
    {
-     StructureViewerModel data = viewerData.getValue();
-     String chimeraSessionFile = data.getStateData();
 -    final String sviewid = viewerData.getKey();
 -    final StructureViewerModel svattrib = viewerData.getValue();
 -    StructureViewerBase comp = null;
 -    JInternalFrame[] frames = getAllFrames();
 -    for (JInternalFrame frame : frames)
 -    {
 -      if (frame instanceof StructureViewerBase)
++    final StructureViewerModel viewerModel = viewerData.getValue();
++    String sessionFilePath = null;
 +
-     /*
-      * Copy Chimera session from jar entry "viewer_"+viewId to a temporary file
-      * 
-      * NB this is the 'saved' viewId as in the project file XML, _not_ the
-      * 'uniquified' sviewid used to reconstruct the viewer here
-      */
-     String viewerJarEntryName = getViewerJarEntryName(data.getViewId());
-     chimeraSessionFile = copyJarEntry(jprovider, viewerJarEntryName,
-             "chimera", ".py");
-     Set<Entry<File, StructureData>> fileData = data.getFileData()
-             .entrySet();
-     List<PDBEntry> pdbs = new ArrayList<>();
-     List<SequenceI[]> allseqs = new ArrayList<>();
-     for (Entry<File, StructureData> pdb : fileData)
-     {
-       String filePath = pdb.getValue().getFilePath();
-       String pdbId = pdb.getValue().getPdbId();
-       // pdbs.add(new PDBEntry(filePath, pdbId));
-       pdbs.add(new PDBEntry(pdbId, null, PDBEntry.Type.PDB, filePath));
-       final List<SequenceI> seqList = pdb.getValue().getSeqList();
-       SequenceI[] seqs = seqList.toArray(new SequenceI[seqList.size()]);
-       allseqs.add(seqs);
-     }
-     boolean colourByChimera = data.isColourByViewer();
-     boolean colourBySequence = data.isColourWithAlignPanel();
-     // TODO use StructureViewer as a factory here, see JAL-1761
-     final PDBEntry[] pdbArray = pdbs.toArray(new PDBEntry[pdbs.size()]);
-     final SequenceI[][] seqsArray = allseqs
-             .toArray(new SequenceI[allseqs.size()][]);
-     String newViewId = viewerData.getKey();
-     ChimeraViewFrame cvf = new ChimeraViewFrame(chimeraSessionFile,
-             af.alignPanel, pdbArray, seqsArray, colourByChimera,
-             colourBySequence, newViewId);
-     cvf.setSize(data.getWidth(), data.getHeight());
-     cvf.setLocation(data.getX(), data.getY());
++    if (viewerType == ViewerType.JMOL)
++    {
++      sessionFilePath = rewriteJmolSession(viewerModel, jprovider);
++    }
++    else
++    {
++      String viewerJarEntryName = getViewerJarEntryName(
++              viewerModel.getViewId());
++      sessionFilePath = copyJarEntry(jprovider, viewerJarEntryName,
++              "viewerSession", ".tmp");
++    }
++    final String sessionPath = sessionFilePath;
++    final String sviewid = viewerData.getKey();
++// BH again was invokeAndWait
++    // try
++    // {
++      javax.swing.SwingUtilities.invokeLater(new Runnable()
++      {
++        @Override
++        public void run()
++        {
++          JalviewStructureDisplayI sview = null;
++          try
++          {
++            sview = StructureViewer.createView(viewerType, af.alignPanel,
++                    viewerModel, sessionPath, sviewid);
++            addNewStructureViewer(sview);
++          } catch (OutOfMemoryError ex)
++          {
++            new OOMWarning("Restoring structure view for " + viewerType,
++                    (OutOfMemoryError) ex.getCause());
++            if (sview != null && sview.isVisible())
++            {
++              sview.closeViewer(false);
++              sview.setVisible(false);
++              sview.dispose();
++            }
++          }
++        }
++      });
++//    } catch (InvocationTargetException | InterruptedException ex)
++//    {
++//      warn("Unexpected error when opening " + viewerType
++//              + " structure viewer", ex);
++//    }
 +  }
 +
 +  /**
-    * Create a new Jmol window. First parse the Jmol state to translate filenames
-    * loaded into the view, and record the order in which files are shown in the
-    * Jmol view, so we can add the sequence mappings in same order.
++   * Rewrites a Jmol session script, saves it to a temporary file, and returns
++   * the path of the file. "load file" commands are rewritten to change the
++   * original PDB file names to those created as the Jalview project is loaded.
 +   * 
-    * @param viewerData
-    * @param af
++   * @param svattrib
 +   * @param jprovider
++   * @return
 +   */
-   protected void createJmolViewer(
-           final Entry<String, StructureViewerModel> viewerData,
-           AlignFrame af, jarInputStreamProvider jprovider)
++  private String rewriteJmolSession(StructureViewerModel svattrib,
++          jarInputStreamProvider jprovider)
 +  {
-     final StructureViewerModel svattrib = viewerData.getValue();
-     String state = svattrib.getStateData();
-     /*
-      * Pre-2.9: state element value is the Jmol state string
-      * 
-      * 2.9+: @type is "JMOL", state data is in a Jar file member named "viewer_"
-      * + viewId
-      */
-     if (ViewerType.JMOL.toString().equals(svattrib.getType()))
++    String state = svattrib.getStateData(); // Jalview < 2.9
++    if (state == null || state.isEmpty()) // Jalview >= 2.9
 +    {
-       state = readJarEntry(jprovider,
-               getViewerJarEntryName(svattrib.getViewId()));
++      String jarEntryName = getViewerJarEntryName(svattrib.getViewId());
++      state = readJarEntry(jprovider, jarEntryName);
 +    }
-     List<String> pdbfilenames = new ArrayList<>();
-     List<SequenceI[]> seqmaps = new ArrayList<>();
-     List<String> pdbids = new ArrayList<>();
-     StringBuilder newFileLoc = new StringBuilder(64);
++    // TODO or simpler? for each key in oldFiles,
++    // replace key.getPath() in state with oldFiles.get(key).getFilePath()
++    // (allowing for different path escapings)
++    StringBuilder rewritten = new StringBuilder(state.length());
 +    int cp = 0, ncp, ecp;
 +    Map<File, StructureData> oldFiles = svattrib.getFileData();
 +    while ((ncp = state.indexOf("load ", cp)) > -1)
 +    {
 +      do
 +      {
 +        // look for next filename in load statement
-         newFileLoc.append(state.substring(cp,
++        rewritten.append(state.substring(cp,
 +                ncp = (state.indexOf("\"", ncp + 1) + 1)));
 +        String oldfilenam = state.substring(ncp,
 +                ecp = state.indexOf("\"", ncp));
 +        // recover the new mapping data for this old filename
 +        // have to normalize filename - since Jmol and jalview do
-         // filename
-         // translation differently.
++        // filename translation differently.
 +        StructureData filedat = oldFiles.get(new File(oldfilenam));
 +        if (filedat == null)
 +        {
 +          String reformatedOldFilename = oldfilenam.replaceAll("/", "\\\\");
 +          filedat = oldFiles.get(new File(reformatedOldFilename));
 +        }
-         newFileLoc
-                 .append(Platform.escapeBackslashes(filedat.getFilePath()));
-         pdbfilenames.add(filedat.getFilePath());
-         pdbids.add(filedat.getPdbId());
-         seqmaps.add(filedat.getSeqList().toArray(new SequenceI[0]));
-         newFileLoc.append("\"");
++        rewritten.append(Platform.escapeBackslashes(filedat.getFilePath()));
++        rewritten.append("\"");
 +        cp = ecp + 1; // advance beyond last \" and set cursor so we can
 +                      // look for next file statement.
 +      } while ((ncp = state.indexOf("/*file*/", cp)) > -1);
 +    }
 +    if (cp > 0)
 +    {
 +      // just append rest of state
-       newFileLoc.append(state.substring(cp));
++      rewritten.append(state.substring(cp));
 +    }
 +    else
 +    {
 +      System.err.print("Ignoring incomplete Jmol state for PDB ids: ");
-       newFileLoc = new StringBuilder(state);
-       newFileLoc.append("; load append ");
++      rewritten = new StringBuilder(state);
++      rewritten.append("; load append ");
 +      for (File id : oldFiles.keySet())
 +      {
-         // add this and any other pdb files that should be present in
-         // the viewer
++        // add pdb files that should be present in the viewer
 +        StructureData filedat = oldFiles.get(id);
-         newFileLoc.append(filedat.getFilePath());
-         pdbfilenames.add(filedat.getFilePath());
-         pdbids.add(filedat.getPdbId());
-         seqmaps.add(filedat.getSeqList().toArray(new SequenceI[0]));
-         newFileLoc.append(" \"");
-         newFileLoc.append(filedat.getFilePath());
-         newFileLoc.append("\"");
++        rewritten.append(" \"").append(filedat.getFilePath()).append("\"");
 +      }
-       newFileLoc.append(";");
++      rewritten.append(";");
 +    }
 +
-     if (newFileLoc.length() == 0)
++    if (rewritten.length() == 0)
 +    {
-       return;
++      return null;
 +    }
-     int histbug = newFileLoc.indexOf("history = ");
-     if (histbug > -1)
++    final String history = "history = ";
++    int historyIndex = rewritten.indexOf(history);
++    if (historyIndex > -1)
 +    {
 +      /*
 +       * change "history = [true|false];" to "history = [1|0];"
 +       */
-       histbug += 10;
-       int diff = histbug == -1 ? -1 : newFileLoc.indexOf(";", histbug);
-       String val = (diff == -1) ? null
-               : newFileLoc.substring(histbug, diff);
-       if (val != null && val.length() >= 4)
++      historyIndex += history.length();
++      String val = rewritten.substring(historyIndex, historyIndex + 5);
++      if (val.startsWith("true"))
 +      {
-         if (val.contains("e")) // eh? what can it be?
-         {
-           if (val.trim().equals("true"))
-           {
-             val = "1";
-           }
-           else
-           {
-             val = "0";
-           }
-           newFileLoc.replace(histbug, diff, val);
-         }
++        rewritten.replace(historyIndex, historyIndex + 4, "1");
++      }
++      else if (val.startsWith("false"))
++      {
++        rewritten.replace(historyIndex, historyIndex + 5, "0");
 +      }
 +    }
 +
-     final String[] pdbf = pdbfilenames
-             .toArray(new String[pdbfilenames.size()]);
-     final String[] id = pdbids.toArray(new String[pdbids.size()]);
-     final SequenceI[][] sq = seqmaps
-             .toArray(new SequenceI[seqmaps.size()][]);
-     final String fileloc = newFileLoc.toString();
-     final String sviewid = viewerData.getKey();
-     final AlignFrame alf = af;
-     final Rectangle rect = new Rectangle(svattrib.getX(), svattrib.getY(),
-             svattrib.getWidth(), svattrib.getHeight());
-     
-     // BH again was invokeAndWait
-     // try
-     // {
-       javax.swing.SwingUtilities.invokeLater(new Runnable()
++    try
++    {
++      File tmp = File.createTempFile("viewerSession", ".tmp");
++      try (OutputStream os = new FileOutputStream(tmp))
 +      {
-         @Override
-         public void run()
-         {
-           JalviewStructureDisplayI sview = null;
-           try
-           {
-             sview = new StructureViewer(
-                     alf.alignPanel.getStructureSelectionManager())
-                             .createView(StructureViewer.ViewerType.JMOL,
-                                     pdbf, id, sq, alf.alignPanel, svattrib,
-                                     fileloc, rect, sviewid);
-             addNewStructureViewer(sview);
-           } catch (OutOfMemoryError ex)
-           {
-             new OOMWarning("restoring structure view for PDB id " + id,
-                     (OutOfMemoryError) ex.getCause());
-             if (sview != null && sview.isVisible())
-             {
-               sview.closeViewer(false);
-               sview.setVisible(false);
-               sview.dispose();
-             }
-           }
-         }
-       });
-     // } catch (InvocationTargetException ex)
-     // {
-     // warn("Unexpected error when opening Jmol view.", ex);
-     //
-     // } catch (InterruptedException e)
-     // {
-     // // e.printStackTrace();
-     // }
++        InputStream is = new ByteArrayInputStream(
++                rewritten.toString().getBytes());
++        copyAll(is, os);
++        return tmp.getAbsolutePath();
++      }
++    } catch (IOException e)
++    {
++      Cache.log.error("Error restoring Jmol session: " + e.toString());
++    }
++    return null;
 +  }
 +
 +  /**
 +   * Generates a name for the entry in the project jar file to hold state
 +   * information for a structure viewer
 +   * 
 +   * @param viewId
 +   * @return
 +   */
 +  protected String getViewerJarEntryName(String viewId)
 +  {
 +    return VIEWER_PREFIX + viewId;
 +  }
 +
 +  /**
 +   * Returns any open frame that matches given structure viewer data. The match
 +   * is based on the unique viewId, or (for older project versions) the frame's
 +   * geometry.
 +   * 
 +   * @param viewerData
 +   * @return
 +   */
 +  protected StructureViewerBase findMatchingViewer(
 +          Entry<String, StructureViewerModel> viewerData)
 +  {
 +    final String sviewid = viewerData.getKey();
 +    final StructureViewerModel svattrib = viewerData.getValue();
 +    StructureViewerBase comp = null;
 +    JInternalFrame[] frames = getAllFrames();
 +    for (JInternalFrame frame : frames)
 +    {
 +      if (frame instanceof StructureViewerBase)
        {
          /*
           * Post jalview 2.4 schema includes structure view id
      //
      // }
      ;
 -    af.setFileName(file, FileFormat.Jalview);
 +    af.alignPanel.setHoldRepaint(true);
 +    af.setFile(fileName, file, null, FileFormat.Jalview);
 +    af.setFileObject(jarFile); // BH 2019 JAL-3436
  
      final AlignViewport viewport = af.getViewport();
      for (int i = 0; i < JSEQ.size(); i++)
            boolean and)
    {
      jalview.xml.binding.jalview.FeatureMatcherSet result = new jalview.xml.binding.jalview.FeatureMatcherSet();
-   
 -
      if (filters.hasNext())
      {
        /*
        }
        result.setMatchCondition(matcherModel);
      }
-   
 -
      return result;
    }
  
                        featureType, e.getMessage()));
        // return as much as was parsed up to the error
      }
-   
 -
      return result;
    }
  
        else if (filterBy == FilterBy.BY_SCORE)
        {
          matchCondition = FeatureMatcher.byScore(cond, pattern);
-   
 -
        }
        else if (filterBy == FilterBy.BY_ATTRIBUTE)
        {
          matchCondition = FeatureMatcher.byAttribute(cond, pattern,
                  attNames);
        }
-   
 -
        /*
         * note this throws IllegalStateException if AND-ing to a 
         * previously OR-ed compound condition, or vice versa
    public static FeatureColourI parseColour(Colour colourModel)
    {
      FeatureColourI colour = null;
-   
 -
      if (colourModel.getMax() != null)
      {
        Color mincol = null;
        Color maxcol = null;
        Color noValueColour = null;
-   
 -
        try
        {
          mincol = new Color(Integer.parseInt(colourModel.getMinRGB(), 16));
          maxcol = new Color(Integer.parseInt(colourModel.getRGB(), 16));
        } catch (Exception e)
        {
 -        Cache.log.warn("Couldn't parse out graduated feature color.", e);
 +          Cache.log.warn("Couldn't parse out graduated feature color.", e);
        }
-   
 -
        NoValueColour noCol = colourModel.getNoValueColour();
        if (noCol == NoValueColour.MIN)
        {
        {
          noValueColour = maxcol;
        }
-   
 -
        colour = new FeatureColour(maxcol, mincol, maxcol, noValueColour,
                safeFloat(colourModel.getMin()),
                safeFloat(colourModel.getMax()));
        Color color = new Color(Integer.parseInt(colourModel.getRGB(), 16));
        colour = new FeatureColour(color);
      }
-   
 -
      return colour;
    }
  }
   */
  package jalview.schemes;
  
+ import java.util.Locale;
  import jalview.api.AlignViewportI;
 +import jalview.bin.ApplicationSingletonProvider;
 +import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
  import jalview.datamodel.AnnotatedCollectionI;
  import jalview.datamodel.SequenceCollectionI;
  import jalview.datamodel.SequenceI;
   */
  package jalview.structure;
  
++
+ import java.util.Locale;
 +import jalview.bin.ApplicationSingletonProvider;
 +import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
++
++
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.PDBEntry.Type;
  
   * @author tcofoegbu
   *
   */
 -public class StructureImportSettings
 +public class StructureImportSettings implements ApplicationSingletonI
  {
 +  private StructureImportSettings()
 +  {
 +    // private singleton
 +  }
 +
 +  private static StructureImportSettings getInstance()
 +  {
 +    return (StructureImportSettings) ApplicationSingletonProvider
 +            .getInstance(StructureImportSettings.class);
 +  }
    /**
     * set to true to add derived sequence annotations (temp factor read from
     * file, or computed secondary structure) to the alignment
@@@ -62,9 -50,9 +65,9 @@@
     * Set true (with predictSecondaryStructure=true) to predict secondary
     * structure using an external service (currently Annotate3D for RNA only)
     */
 -  private static boolean externalSecondaryStructure = false;
 +  private boolean externalSecondaryStructure = false;
  
-   private boolean showSeqFeatures = true;
+   private static boolean showSeqFeatures = true;
  
    public enum StructureParser
    {
@@@ -75,7 -63,7 +78,7 @@@
     * Determines the default file format for structure files to be downloaded
     * from the PDB sequence fetcher. Possible options include: PDB|mmCIF
     */
-   private PDBEntry.Type defaultStructureFileFormat = Type.PDB;
 -  private static PDBEntry.Type defaultStructureFileFormat = Type.PDB;
++  private PDBEntry.Type defaultStructureFileFormat = Type.PDB; // TODO 2.12 should be mmCIF now ?
  
    /**
     * Determines the parser used for parsing PDB format file. Possible options
    public static void setDefaultStructureFileFormat(
            String defaultStructureFileFormat)
    {
 -    StructureImportSettings.defaultStructureFileFormat = PDBEntry.Type
 +    getInstance().defaultStructureFileFormat = PDBEntry.Type
-             .valueOf(defaultStructureFileFormat.toUpperCase());
+             .valueOf(defaultStructureFileFormat.toUpperCase(Locale.ROOT));
    }
  
    public static String getDefaultPDBFileParser()
  
    public static void setDefaultPDBFileParser(String defaultPDBFileParser)
    {
 -    StructureImportSettings.defaultPDBFileParser = StructureParser
 +    getInstance().defaultPDBFileParser = StructureParser
-             .valueOf(defaultPDBFileParser.toUpperCase());
+             .valueOf(defaultPDBFileParser.toUpperCase(Locale.ROOT));
    }
  
  }
Simple merge
Simple merge
Simple merge
   */
  package jalview.util;
  
 +import java.io.BufferedInputStream;
 +import java.io.File;
 +import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
+ import java.net.HttpURLConnection;
+ import java.net.ProtocolException;
  import java.net.URL;
+ import java.util.List;
+ import javax.ws.rs.HttpMethod;
  
  public class HttpUtils
  {
      }
      return false;
    }
 -
+   public static boolean startsWithHttpOrHttps(String file)
+   {
+     return file.startsWith("http://") || file.startsWith("https://");
+   }
+   
 -
+   /**
+    * wrapper to get/post to a URL or check headers
+    * @param url
+    * @param ids
+    * @param readTimeout
+    * @return
+    * @throws IOException
+    * @throws ProtocolException
+    */
+   public static boolean checkUrlAvailable(URL url,
+           int readTimeout) throws IOException, ProtocolException
+   {
+     // System.out.println(System.currentTimeMillis() + " " + url);
+     HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+     connection.setRequestMethod(HttpMethod.HEAD);
+     connection.setDoInput(true);
+     connection.setUseCaches(false);
+     connection.setConnectTimeout(300);
+     connection.setReadTimeout(readTimeout);
+     return connection.getResponseCode() == 200;
+   }
  
 +  /**
 +   * download from given URL and return a pointer to temporary file
 +   */
 +  public static File fetchURLToTemp(String url) throws OutOfMemoryError,
 +          IOException
 +  {
 +    long time = System.currentTimeMillis();
 +    URL rcall = new URL(url);
 +
 +    InputStream is = new BufferedInputStream(rcall.openStream());
 +    File outFile = null;
 +    try
 +    {
 +      outFile = File.createTempFile("jalview", ".xml");
 +      outFile.deleteOnExit();
 +      if (outFile.length() == 0)
 +      {
 +        outFile.delete();
 +        return null;
 +      }
 +    } catch (Exception ex)
 +    {
 +    }
 +
 +    if (outFile != null)
 +    {
 +      FileOutputStream fio = new FileOutputStream(outFile);
 +      byte[] bb = new byte[32 * 1024];
 +      int l;
 +      while ((l = is.read(bb)) > 0)
 +      {
 +        fio.write(bb, 0, l);
 +      }
 +      fio.close();
 +      is.close();
 +      return outFile;
 +    }
 +    else
 +    {
 +      return null;
 +    }
 +  }
  }
Simple merge
@@@ -1020,59 -1018,25 +1020,79 @@@ public final class MappingUtil
        }
      }
    }
 +  /**
 +   * Adds the given range to a list of ranges. If the new range just extends
 +   * existing ranges, the current endpoint is updated instead.
 +   * 
 +   * @param range
 +   * @param addTo
 +   */
 +  public static void addRange(int[] range, List<int[]> addTo)
 +  {
 +    /*
 +     * list is empty - add to it!
 +     */
 +    if (addTo.size() == 0)
 +    {
 +      addTo.add(range);
 +      return;
 +    }
 +  
 +    int[] last = addTo.get(addTo.size() - 1);
 +    boolean lastForward = last[1] >= last[0];
 +    boolean newForward = range[1] >= range[0];
 +  
 +    /*
 +     * contiguous range in the same direction - just update endpoint
 +     */
 +    if (lastForward == newForward && last[1] == range[0])
 +    {
 +      last[1] = range[1];
 +      return;
 +    }
 +  
 +    /*
 +     * next range starts at +1 in forward sense - update endpoint
 +     */
 +    if (lastForward && newForward && range[0] == last[1] + 1)
 +    {
 +      last[1] = range[1];
 +      return;
 +    }
 +  
 +    /*
 +     * next range starts at -1 in reverse sense - update endpoint
 +     */
 +    if (!lastForward && !newForward && range[0] == last[1] - 1)
 +    {
 +      last[1] = range[1];
 +      return;
 +    }
 +  
 +    /*
 +     * just add the new range
 +     */
 +    addTo.add(range);
 +  }
+   /**
+    * Converts a list of {@code start-end} ranges to a single array of
+    * {@code start1, end1, start2, ... } ranges
+    * 
+    * @param ranges
+    * @return
+    */
+   public static int[] rangeListToArray(List<int[]> ranges)
+   {
+     int rangeCount = ranges.size();
+     int[] result = new int[rangeCount * 2];
+     int j = 0;
+     for (int i = 0; i < rangeCount; i++)
+     {
+       int[] range = ranges.get(i);
+       result[j++] = range[0];
+       result[j++] = range[1];
+     }
+     return result;
+   }
  }
Simple merge
   */
  package jalview.util;
  
 -import jalview.javascript.json.JSON;
 +import java.awt.Component;
 +import java.awt.Dimension;
 +import java.awt.GraphicsEnvironment;
  import java.awt.Toolkit;
 +import java.awt.event.KeyEvent;
  import java.awt.event.MouseEvent;
  import java.io.BufferedReader;
  import java.io.File;
@@@ -56,12 -40,6 +57,11 @@@ import javax.swing.SwingUtilities
  import org.json.simple.parser.JSONParser;
  import org.json.simple.parser.ParseException;
  
 +import com.stevesoft.pat.Regex;
 +
 +import jalview.bin.Jalview;
 +import jalview.javascript.json.JSON;
 +import swingjs.api.JSUtilI;
  /**
   * System platform information used by Applet and Application
   * 
@@@ -74,28 -52,10 +74,31 @@@ public class Platfor
            false;
  
    private static Boolean isNoJSMac = null, isNoJSWin = null, isMac = null,
-           isWin = null;
+           isWin = null, isLinux = null;
++
+   private static Boolean isHeadless = null;
  
 +  private static swingjs.api.JSUtilI jsutil;
 +
 +  static
 +  {
 +    if (isJS)
 +    {
 +      try
 +      {
 +        // this is ok - it's a highly embedded method in Java; the deprecation
 +        // is
 +        // really a recommended best practice.
 +        jsutil = ((JSUtilI) Class.forName("swingjs.JSUtil").newInstance());
 +      } catch (InstantiationException | IllegalAccessException
 +              | ClassNotFoundException e)
 +      {
 +        e.printStackTrace();
 +      }
 +    }
 +  }
 +
    /**
     * added to group mouse events into Windows and nonWindows (mac, unix, linux)
     * 
  
    public static String getUniqueAppletID()
    {
 -    /**
 -     * @j2sNative return swingjs.JSUtil.getApplet$()._uniqueId;
 -     *
 -     */
 -    return null;
 +    return (isJS ? (String) jsutil.getAppletAttribute("_uniqueId") : null);
    }
  
    /**
    public static void streamToFile(InputStream is, File outFile)
            throws IOException
    {
 -    if (isJS() && /**
 -                   * @j2sNative outFile.setBytes$O && outFile.setBytes$O(is) &&
 -                   */
 -            true)
 +    if (isJS)
      {
 +      jsutil.setFileBytes(outFile, is);
        return;
      }
      FileOutputStream fio = new FileOutputStream(outFile);
      try
      {
    public static void addJ2SDirectDatabaseCall(String domain)
    {
  
 -    if (isJS())
 +    if (isJS)
      {
 +      jsutil.addDirectDatabaseCall(domain);
        System.out.println(
 -            "Platform adding known access-control-allow-origin * for domain "
 -                    + domain);
 -      /**
 -       * @j2sNative
 -       * 
 -       *            J2S.addDirectDatabaseCall(domain);
 -       */
 +              "Platform adding known access-control-allow-origin * for domain "
 +                      + domain);
      }
  
    }
  
 +  /**
 +   * Allow for URL-line command arguments. Untested.
 +   * 
 +   */
    public static void getURLCommandArguments()
    {
 +
-     try
-     {
+       try {
        /**
         * Retrieve the first query field as command arguments to Jalview. Include
         * only if prior to "?j2s" or "&j2s" or "#". Assign the applet's
      } catch (Throwable t)
      {
      }
++
    }
  
    /**
      String p2 = path2.replace('\\', '/');
      return p1.equals(p2);
    }
 +  ///////////// JAL-3253 Applet additions //////////////
 +
 +  /**
 +   * Retrieve the object's embedded size from a div's style on a page if
 +   * embedded in SwingJS.
 +   * 
 +   * @param frame
 +   *          JFrame or JInternalFrame
 +   * @param defaultWidth
 +   *          use -1 to return null (no default size)
 +   * @param defaultHeight
 +   * @return the embedded dimensions or null (no default size or not embedded)
 +   */
 +  public static Dimension getDimIfEmbedded(Component frame,
 +          int defaultWidth, int defaultHeight)
 +  {
 +    Dimension d = null;
 +    if (isJS)
 +    {
 +      d = (Dimension) getEmbeddedAttribute(frame, "dim");
 +    }
 +    return (d == null && defaultWidth >= 0
 +            ? new Dimension(defaultWidth, defaultHeight)
 +            : d);
 +
 +  }
 +
 +  public static Regex newRegex(String regex)
 +  {
 +    return newRegex(regex, null);
 +  }
 +
 +  public static Regex newRegex(String searchString, String replaceString)
 +  {
 +    ensureRegex();
 +    return (replaceString == null ? new Regex(searchString)
 +            : new Regex(searchString, replaceString));
 +  }
 +
 +  public static Regex newRegexPerl(String code)
 +  {
 +    ensureRegex();
 +    return Regex.perlCode(code);
 +  }
 +
 +  /**
 +   * Initialize Java debug logging. A representative sample -- adapt as desired.
 +   */
 +  public static void startJavaLogging()
 +  {
 +    /**
 +     * @j2sIgnore
 +     */
 +    {
 +      logClass("java.awt.EventDispatchThread", "java.awt.EventQueue",
 +              "java.awt.Component", "java.awt.focus.Component",
 +              "java.awt.event.Component",
 +              "java.awt.focus.DefaultKeyboardFocusManager");
 +    }
 +  }
 +
 +  /**
 +   * Initiate Java logging for a given class. Only for Java, not JavaScript;
 +   * Allows debugging of complex event processing.
 +   * 
 +   * @param className
 +   */
 +  public static void logClass(String... classNames)
 +  {
 +    /**
 +     * @j2sIgnore
 +     * 
 +     * 
 +     */
 +    {
 +      Logger rootLogger = Logger.getLogger("");
 +      rootLogger.setLevel(Level.ALL);
 +      ConsoleHandler consoleHandler = new ConsoleHandler();
 +      consoleHandler.setLevel(Level.ALL);
 +      for (int i = classNames.length; --i >= 0;)
 +      {
 +        Logger logger = Logger.getLogger(classNames[i]);
 +        logger.setLevel(Level.ALL);
 +        logger.addHandler(consoleHandler);
 +      }
 +    }
 +  }
 +
 +  /**
 +   * load a resource -- probably a core file -- if and only if a particular
 +   * class has not been instantialized. We use a String here because if we used
 +   * a .class object, that reference itself would simply load the class, and we
 +   * want the core package to include that as well.
 +   * 
 +   * @param resourcePath
 +   * @param className
 +   */
 +  public static void loadStaticResource(String resourcePath,
 +          String className)
 +  {
 +    if (isJS)
 +    {
 +      jsutil.loadResourceIfClassUnknown(resourcePath, className);
 +    }
 +  }
 +
 +  public static void ensureRegex()
 +  {
 +    if (isJS)
 +    {
 +      loadStaticResource("core/core_stevesoft.z.js",
 +              "com.stevesoft.pat.Regex");
 +    }
 +  }
 +
 +  /**
 +   * Set the "app" property of the HTML5 applet object, for example,
 +   * "testApplet.app", to point to the Jalview instance. This will be the object
 +   * that page developers use that is similar to the original Java applet object
 +   * that was accessed via LiveConnect.
 +   * 
 +   * @param j
 +   */
 +  public static void setAppClass(Object j)
 +  {
 +    if (isJS)
 +    {
 +      jsutil.setAppClass(j);
 +    }
 +  }
 +
 +  /**
 +   *
 +   * If this frame is embedded in a web page, return a known type.
 +   * 
 +   * @param frame
 +   *          a JFrame or JInternalFrame
 +   * @param type
 +   *          "name", "node", "init", "dim", or any DOM attribute, such as "id"
 +   * @return null if frame is not embedded.
 +   */
 +  public static Object getEmbeddedAttribute(Component frame, String type)
 +  {
 +    return (isJS ? jsutil.getEmbeddedAttribute(frame, type) : null);
 +  }
 +
 +  public static void stackTrace()
 +  {
 +    try
 +    {
 +      throw new NullPointerException();
 +    } catch (Exception e)
 +    {
 +      e.printStackTrace();
 +    }
 +
 +  }
 +
 +  public static URL getDocumentBase()
 +  {
 +    return (isJS ? jsutil.getDocumentBase() : null);
 +  }
 +
 +  public static URL getCodeBase()
 +  {
 +    return (isJS ? jsutil.getCodeBase() : null);
 +  }
 +
 +  public static String getUserPath(String subpath)
 +  {
 +    char sep = File.separatorChar;
 +    return System.getProperty("user.home") + sep
 +            + subpath.replace('/', sep);
 +  }
 +
 +  /**
 +   * This method enables checking if a cached file has exceeded a certain
 +   * threshold(in days)
 +   * 
 +   * @param file
 +   *          the cached file
 +   * @param noOfDays
 +   *          the threshold in days
 +   * @return
 +   */
 +  public static boolean isFileOlderThanThreshold(File file, int noOfDays)
 +  {
 +    if (isJS())
 +    {
 +      // not meaningful in SwingJS -- this is a session-specific temp file. It
 +      // doesn't have a timestamp.
 +      return false;
 +    }
 +    Path filePath = file.toPath();
 +    BasicFileAttributes attr;
 +    int diffInDays = 0;
 +    try
 +    {
 +      attr = Files.readAttributes(filePath, BasicFileAttributes.class);
 +      diffInDays = (int) ((new Date().getTime()
 +              - attr.lastModifiedTime().toMillis())
 +              / (1000 * 60 * 60 * 24));
 +      // System.out.println("Diff in days : " + diffInDays);
 +    } catch (IOException e)
 +    {
 +      e.printStackTrace();
 +    }
 +    return noOfDays <= diffInDays;
 +  }
 +
 +  /**
 +   * Get the leading integer part of a string that begins with an integer.
 +   * 
 +   * @param input
 +   *          - the string input to process
 +   * @param failValue
 +   *          - value returned if unsuccessful
 +   * @return
 +   */
 +  public static int getLeadingIntegerValue(String input, int failValue)
 +  {
 +    if (input == null)
 +    {
 +      return failValue;
 +    }
 +    if (isJS)
 +    {
 +      int val = /** @j2sNative 1 ? parseInt(input) : */
 +              0;
 +      return (val == val + 0 ? val : failValue);
 +    }
 +    // JavaScript does not support Regex ? lookahead
 +    String[] parts = input.split("(?=\\D)(?<=\\d)");
 +    if (parts != null && parts.length > 0 && parts[0].matches("[0-9]+"))
 +    {
 +      return Integer.valueOf(parts[0]);
 +    }
 +    return failValue;
 +  }
 +
 +  public static Map<String, Object> getAppletInfoAsMap()
 +  {
 +    return (isJS ? jsutil.getAppletInfoAsMap() : null);
 +  }
 +
 +  /**
 +   * Get the SwingJS applet ID and combine that with the frameType
 +   * 
 +   * @param frameType
 +   *          "alignment", "desktop", etc., or null
 +   * @return
 +   */
 +  public static String getAppID(String frameType)
 +  {
 +
 +    String id = Jalview.getInstance().j2sAppletID;
 +    if (id == null)
 +    {
 +      Jalview.getInstance().j2sAppletID = id = (isJS
 +              ? (String) jsutil.getAppletAttribute("_id")
 +              : "jalview");
 +    }
 +    return id + (frameType == null ? "" : "-" + frameType);
 +  }
 +
 +  /**
 +   * Option to avoid unnecessary seeking of nonexistent resources in JavaScript.
 +   * Works in Java as well.
 +   * 
 +   * @param loc
 +   * @return
 +   */
 +  public static Locale getLocaleOrNone(Locale loc)
 +  {
 +    return (isJS && loc.getLanguage() == "en" ? new Locale("") : loc);
 +  }
 +
 +  /**
 +   * From UrlDownloadClient; trivial in JavaScript; painful in Java.
 +   * 
 +   * @param urlstring
 +   * @param outfile
 +   * @throws IOException
 +   */
 +  public static void download(String urlstring, String outfile)
 +          throws IOException
 +  {
 +    Path temp = null;
 +    try (InputStream is = new URL(urlstring).openStream())
 +    {
 +      if (isJS)
 +      { // so much easier!
 +        streamToFile(is, new File(outfile));
 +        return;
 +      }
 +      temp = Files.createTempFile(".jalview_", ".tmp");
 +      try (FileOutputStream fos = new FileOutputStream(temp.toString());
 +              ReadableByteChannel rbc = Channels.newChannel(is))
 +      {
 +        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
 +        // copy tempfile to outfile once our download completes
 +        // incase something goes wrong
 +        Files.copy(temp, Paths.get(outfile),
 +                StandardCopyOption.REPLACE_EXISTING);
 +      }
 +    } catch (IOException e)
 +    {
 +      throw e;
 +    } finally
 +    {
 +      try
 +      {
 +        if (temp != null)
 +        {
 +          Files.deleteIfExists(temp);
 +        }
 +      } catch (IOException e)
 +      {
 +        System.out.println("Exception while deleting download temp file: "
 +                + e.getMessage());
 +      }
 +    }
 +  }
  }
Simple merge
@@@ -87,9 -83,6 +87,8 @@@ import java.util.Map
  public abstract class AlignmentViewport
          implements AlignViewportI, CommandListener, VamsasSource
  {
 +  public static final String PROPERTY_ALIGNMENT = "alignment";
 +  public static final String PROPERTY_SEQUENCE = "sequence";
    protected ViewportRanges ranges;
  
    protected ViewStyleI viewStyle = new ViewStyle();
     * alignment
     */
    protected boolean isDataset = false;
 +  
    public void setDataset(boolean b)
    {
      isDataset = b;
  
    protected boolean autoCalculateStrucConsensus = true;
  
 +  public boolean getAutoCalculateStrucConsensus()
 +  { // BH 2019.07.24
 +    return autoCalculateStrucConsensus;
 +  }
 +
 +  public void setAutoCalculateStrucConsensus(boolean b)
 +  {
 +    autoCalculateStrucConsensus = b;
 +  }
    protected boolean ignoreGapsInConsensusCalculation = false;
  
    protected ResidueShaderI residueShading = new ResidueShader();
 +  
    @Override
    public void setGlobalColourScheme(ColourSchemeI cs)
    {
    {
      return residueShading;
    }
 +  
    protected AlignmentAnnotation consensus;
  
    protected AlignmentAnnotation complementConsensus;
    protected Hashtable<String, Object>[] hStrucConsensus = null;
  
    protected Conservation hconservation = null;
 +  
    @Override
    public void setConservation(Conservation cons)
    {
      }
    }
  
 +  @Override
 +  public void initInformationWorker(final AlignmentViewPanel ap)
 +  {
 +    if (calculator.getWorkersOfClass(InformationThread.class).isEmpty())
 +    {
 +      calculator.registerWorker(new InformationThread(this, ap));
 +    }
 +  }
    // --------START Structure Conservation
    public void updateStrucConsensus(final AlignmentViewPanel ap)
    {
      this.showSequenceLogo = showSequenceLogo;
    }
  
 +  public void setShowHMMSequenceLogo(boolean showHMMSequenceLogo)
 +  {
 +    if (showHMMSequenceLogo != this.hmmShowSequenceLogo)
 +    {
 +      this.hmmShowSequenceLogo = showHMMSequenceLogo;
 +      // TODO: updateAnnotation if description (tooltip) will show
 +      // profile in place of information content?
 +      // calculator.updateAnnotationFor(InformationThread.class);
 +    }
 +    this.hmmShowSequenceLogo = showHMMSequenceLogo;
 +  }
    /**
     * @param showConsensusHistogram
     *          the showConsensusHistogram to set
      return ignoreGapsInConsensusCalculation;
    }
  
 +  @Override
 +  public boolean isIgnoreBelowBackground()
 +  {
 +    return ignoreBelowBackGroundFrequencyCalculation;
 +  }
 +
 +  @Override
 +  public boolean isInfoLetterHeight()
 +  {
 +    return infoLetterHeight;
 +  }
    // property change stuff
    // JBPNote Prolly only need this in the applet version.
    private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
      }
    }
  
 -  /**
 -   * Property change listener for changes in alignment
 -   * 
 -   * @param prop
 -   *          DOCUMENT ME!
 -   * @param oldvalue
 -   *          DOCUMENT ME!
 -   * @param newvalue
 -   *          DOCUMENT ME!
 -   */
 -  public void firePropertyChange(String prop, Object oldvalue,
 -          Object newvalue)
 -  {
 -    changeSupport.firePropertyChange(prop, oldvalue, newvalue);
 -  }
    // common hide/show column stuff
  
    public void hideSelectedColumns()
        }
  
        ranges.setStartEndSeq(startSeq, endSeq + tmp.size());
 -      firePropertyChange("alignment", null, alignment.getSequences());
 +      notifyAlignment();
        sendSelection();
      }
    }
      return sq;
    }
  
 +  public boolean hasReferenceAnnotation()
 +  {
 +    AlignmentAnnotation[] annots = this.alignment.getAlignmentAnnotation();
 +    for (AlignmentAnnotation annot : annots)
 +    {
 +      if ("RF".equals(annot.label) || annot.label.contains("Reference"))
 +      {
 +        return true;
 +      }
 +    }
 +    return false;
 +  }
    @Override
    public void setCurrentTree(TreeModel tree)
    {
        codingComplement.setUpdateStructures(needToUpdateStructureViews);
      }
    }
+   @Override
+   public Iterator<int[]> getViewAsVisibleContigs(boolean selectedRegionOnly)
+   {
+     int start = 0;
+     int end = 0;
+     if (selectedRegionOnly && selectionGroup != null)
+     {
+       start = selectionGroup.getStartRes();
+       end = selectionGroup.getEndRes() + 1;
+     }
+     else
+     {
+       end = alignment.getWidth();
+     }
+     return (alignment.getHiddenColumns().getVisContigsIterator(start, end,
+             false));
+   }
 +  /**
 +   * 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);
 +  }
  }
Simple merge
@@@ -75,15 -47,6 +47,7 @@@ public class EmblCdsSource extends Embl
      return getEmblSequenceRecords(DBRefSource.EMBLCDS, queries);
    }
  
-   @Override
-   public boolean isValidReference(String accession)
-   {
-     // most embl CDS refs look like ..
-     // TODO: improve EMBLCDS regex
-     return (accession == null || accession.length() < 2) ? false
-             : getAccessionValidator().search(accession);
-   }
 +
    /**
     * cDNA for LDHA_CHICK swissprot sequence
     */
index 0000000,2058800..6694a52
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,122 +1,126 @@@
+ package jalview.ws.dbsources;
+ import java.util.Locale;
+ import java.io.File;
+ import java.io.IOException;
+ import com.stevesoft.pat.Regex;
+ import jalview.bin.Cache;
+ import jalview.datamodel.Alignment;
+ import jalview.datamodel.AlignmentI;
+ import jalview.datamodel.SequenceI;
+ import jalview.io.DataSourceType;
+ import jalview.io.EmblFlatFile;
+ import jalview.io.FileParse;
+ import jalview.ws.ebi.EBIFetchClient;
+ /**
+  * A class that does partial parsing of an EMBL flatfile.
+  * 
+  * @author gmcarstairs
+  *
+  */
+ public abstract class EmblFlatfileSource extends EbiFileRetrievedProxy
+ {
 -  private static final Regex ACCESSION_REGEX = new Regex("^[A-Z]+[0-9]+");
++  private static final Regex ACCESSION_REGEX = null;
+   @Override
+   public String getDbVersion()
+   {
+     return "0";
+   }
+   @Override
+   public String getAccessionSeparator()
+   {
+     return null;
+   }
+   @Override
+   public Regex getAccessionValidator()
+   {
++    if (ACCESSION_REGEX == null)
++    {
++      ACCESSION_REGEX = Platform.newRegex("^[A-Z]+[0-9]+");
++    }
+     return ACCESSION_REGEX;
+   }
+   @Override
+   public boolean isValidReference(String accession)
+   {
+     if (accession == null || accession.length() < 2)
+     {
+       return false;
+     }
+     return getAccessionValidator().search(accession);
+   }
+   @Override
+   public AlignmentI getSequenceRecords(String queries) throws Exception
+   {
+     return null;
+   }
+   @Override
+   public int getTier()
+   {
+     return 0;
+   }
+   protected AlignmentI getEmblSequenceRecords(String dbName, String query)
+           throws Exception
+   {
+     startQuery();
+     EBIFetchClient dbFetch = new EBIFetchClient();
+     File reply;
+     try
+     {
+       reply = dbFetch.fetchDataAsFile(
+               dbName.toLowerCase(Locale.ROOT) + ":" + query.trim(), null, "gz");
+     } catch (Exception e)
+     {
+       stopQuery();
+       throw new Exception(
+               String.format("EBI EMBL retrieval failed for %s:%s",
+                       dbName.toLowerCase(Locale.ROOT), query.trim()),
+               e);
+     }
+     return getEmblSequenceRecords(dbName, query, reply);
+   }
+   private AlignmentI getEmblSequenceRecords(String dbName, String query,
+           File reply) throws IOException
+   {
+     AlignmentI al = null;
+     if (reply != null && reply.exists())
+     {
+       file = reply.getAbsolutePath();
+       FileParse fp = new FileParse(file, DataSourceType.FILE);
+       EmblFlatFile emblParser = new EmblFlatFile(fp, getDbSource());
+       SequenceI[] seqs = emblParser.getSeqsAsArray();
+       if (seqs.length > 0)
+       {
+         al = new Alignment(seqs);
+       }
+       if (al == null)
+       {
+         Cache.log.error(
+                 "No record found for '" + dbName + ":" + query + "'");
+       }
+     }
+     stopQuery();
+     return al;
+   }
+   @Override
+   public boolean isDnaCoding()
+   {
+     return true;
+   }
+ }
@@@ -22,10 -22,7 +22,8 @@@ package jalview.ws.dbsources
  
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.DBRefSource;
- import jalview.util.Platform;
  
- import com.stevesoft.pat.Regex;
 +
  /**
   * @author JimP
   * 
@@@ -99,21 -55,6 +56,7 @@@ public class EmblSource extends EmblFla
    public AlignmentI getSequenceRecords(String queries) throws Exception
    {
      return getEmblSequenceRecords(DBRefSource.EMBL, queries);
-   }
-   /*
-    * (non-Javadoc)
-    * 
-    * @see jalview.ws.DbSourceProxy#isValidReference(java.lang.String)
-    */
-   @Override
-   public boolean isValidReference(String accession)
-   {
-     // most embl refs look like ..
-     return (accession == null || accession.length() < 2) ? false
-             : getAccessionValidator().search(accession);
 +
    }
  
    /**
@@@ -62,8 -65,16 +64,17 @@@ import jalview.xml.binding.embl.EntryTy
  import jalview.xml.binding.embl.ROOT;
  import jalview.xml.binding.embl.XrefType;
  
+ /**
+  * Provides XML binding and parsing of EMBL or EMBLCDS records retrieved from
+  * (e.g.) {@code https://www.ebi.ac.uk/ena/data/view/x53828&display=xml}.
+  * 
+  * @deprecated endpoint withdrawn August 2020 (JAL-3692), use EmblFlatfileSource
+  */
++
  public abstract class EmblXmlSource extends EbiFileRetrievedProxy
  {
++  // TODO: delete class or update tyhis validator for 2.12 style Platform.regex
+   private static final Regex ACCESSION_REGEX = new Regex("^[A-Z]+[0-9]+");
 -
    /*
     * JAL-1856 Embl returns this text for query not found
     */
      {
        return new int[] {};
      }
-   
 -
      try
      {
        List<int[]> ranges = DnaUtils.parseLocation(location);
      return sf;
    }
  
+   @Override
+   public String getAccessionSeparator()
+   {
+     return null;
+   }
+   @Override
+   public Regex getAccessionValidator()
+   {
+     return ACCESSION_REGEX;
+   }
+   @Override
+   public String getDbVersion()
+   {
+     return "0";
+   }
+   @Override
+   public int getTier()
+   {
+     return 0;
+   }
+   @Override
+   public boolean isValidReference(String accession)
+   {
+     if (accession == null || accession.length() < 2)
+     {
+       return false;
+     }
+     return getAccessionValidator().search(accession);
+   }
 -
    /**
     * Truncates (if necessary) the exon intervals to match 3 times the length of
     * the protein; also accepts 3 bases longer (for stop codon not included in
      }
      int expectedCdsLength = proteinLength * 3;
      int exonLength = MappingUtils.getLength(Arrays.asList(exon));
-   
 -
      /*
       * if exon length matches protein, or is shorter, or longer by the 
       * length of a stop codon (3 bases), then leave it unchanged
      {
        return exon;
      }
-   
 -
      int origxon[];
      int sxpos = -1;
      int endxon = 0;
            // .println("Truncating final exon interval on region by "
            // + (cdspos - cdslength));
          }
-   
 -
          /*
           * shrink the final exon - reduce end position if forward
           * strand, increase it if reverse
          break;
        }
      }
-   
 -
      if (sxpos != -1)
      {
        // and trim the exon interval set if necessary
   */
  package jalview.ws.dbsources;
  
- import java.io.InputStream;
- import java.net.URL;
- import java.net.URLConnection;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Vector;
- import javax.xml.bind.JAXBContext;
- import javax.xml.bind.JAXBElement;
- import javax.xml.bind.JAXBException;
- import javax.xml.stream.FactoryConfigurationError;
- import javax.xml.stream.XMLInputFactory;
- import javax.xml.stream.XMLStreamException;
- import javax.xml.stream.XMLStreamReader;
- import com.stevesoft.pat.Regex;
+ import java.util.Locale;
 -
  import jalview.bin.Cache;
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentI;
@@@ -70,8 -71,6 +70,7 @@@ public class Uniprot extends DbSourcePr
    private static final String DEFAULT_UNIPROT_DOMAIN = "https://www.uniprot.org";
  
    private static final String BAR_DELIMITER = "|";
 +  private static Regex ACCESSION_REGEX;
  
    /**
     * Constructor
                + ".xml";
  
        URL url = new URL(downloadstring);
-       URLConnection urlconn = url.openConnection();
-       InputStream istr = urlconn.getInputStream();
-       List<Entry> entries = getUniprotEntries(istr);
-       if (entries != null)
+       HttpURLConnection urlconn = (HttpURLConnection)url.openConnection();
+       // anything other than 200 means we don't have data
+       // TODO: JAL-3882 reuse the EnsemblRestClient's fair 
+       // use/backoff logic to retry when the server tells us to go away
+       if (urlconn.getResponseCode() == 200)
        {
 -        InputStream istr = urlconn.getInputStream();
 -        List<Entry> entries = getUniprotEntries(istr);
 -        if (entries != null)
 +        List<SequenceI> seqs = new ArrayList<>();
 +        for (Entry entry : entries)
          {
 -          List<SequenceI> seqs = new ArrayList<>();
 -          for (Entry entry : entries)
 -          {
 -            seqs.add(uniprotEntryToSequence(entry));
 -          }
 -          al = new Alignment(seqs.toArray(new SequenceI[seqs.size()]));
 +          seqs.add(uniprotEntryToSequence(entry));
          }
 +        al = new Alignment(seqs.toArray(new SequenceI[seqs.size()]));
        }
 +
        stopQuery();
        return al;
+       
      } catch (Exception e)
      {
        throw (e);
Simple merge
Simple merge
@@@ -37,18 -53,88 +37,20 @@@ import javax.swing.JMenu
   */
  public abstract class Jws2Client extends jalview.ws.WSClient
  {
 -  protected AlignFrame alignFrame;
 -
 -  protected WsParamSetI preset;
 -
 -  protected List<Argument> paramset;
 +  /**
 +   * instantiate a new service client. preset and arguments are assumed to be
 +   * valid for the service
 +   * 
 +   * @param _alignFrame
 +   * @param preset
 +   * @param arguments
 +   */
    public Jws2Client(AlignFrame _alignFrame, WsParamSetI preset,
 -          List<Argument> arguments)
 +          List<ArgumentI> arguments)
    {
 -    alignFrame = _alignFrame;
 -    this.preset = preset;
 -    if (preset != null)
 -    {
 -      if (!((preset instanceof JabaPreset)
 -              || preset instanceof JabaWsParamSet))
 -      {
 -        /*
 -         * { this.preset = ((JabaPreset) preset).p; } else if (preset instanceof
 -         * JabaWsParamSet) { List<Argument> newargs = new ArrayList<Argument>();
 -         * JabaWsParamSet pset = ((JabaWsParamSet) preset); for (Option opt :
 -         * pset.getjabaArguments()) { newargs.add(opt); } if (arguments != null
 -         * && arguments.size() > 0) { // merge arguments with preset's own
 -         * arguments. for (Argument opt : arguments) { newargs.add(opt); } }
 -         * paramset = newargs; } else {
 -         */
 -        throw new Error(MessageManager.getString(
 -                "error.implementation_error_can_only_instantiate_jaba_param_sets"));
 -      }
 -    }
 -    else
 -    {
 -      // just provided with a bunch of arguments
 -      this.paramset = arguments;
 -    }
 -  }
 -
 -  boolean processParams(Jws2Instance sh, boolean editParams)
 -  {
 -    return processParams(sh, editParams, false);
 -  }
 -
 -  protected boolean processParams(Jws2Instance sh, boolean editParams,
 -          boolean adjustingExisting)
 -  {
 -
 -    if (editParams)
 -    {
 -      if (sh.paramStore == null)
 -      {
 -        sh.paramStore = new JabaParamStore(sh,
 -                Desktop.getUserParameterStore());
 -      }
 -      WsJobParameters jobParams = (preset == null && paramset != null
 -              && paramset.size() > 0)
 -                      ? new WsJobParameters(null, sh, null, paramset)
 -                      : new WsJobParameters(sh, preset);
 -      if (adjustingExisting)
 -      {
 -        jobParams.setName(MessageManager
 -                .getString("label.adjusting_parameters_for_calculation"));
 -      }
 -      if (!jobParams.showRunDialog())
 -      {
 -        return false;
 -      }
 -      WsParamSetI prset = jobParams.getPreset();
 -      if (prset == null)
 -      {
 -        paramset =
 -                /* JAL-3739 always take values from input form */
 -                /* jobParams.isServiceDefaults() ? null : */
 -                JabaParamStore.getJabafromJwsArgs(jobParams.getJobParams());
 -        this.preset = null;
 -      }
 -      else
 -      {
 -        this.preset = prset; // ((JabaPreset) prset).p;
 -        paramset = null; // no user supplied parameters.
 -      }
 -    }
 -    return true;
 +    super(_alignFrame, preset, arguments);
    }
  
    public Jws2Client()
      // anonymous constructor - used for headless method calls only
    }
  
 -  protected WebserviceInfo setWebService(Jws2Instance serv, boolean b)
 -  {
 -    // serviceHandle = serv;
 -    String serviceInstance = serv.action; // serv.service.getClass().getName();
 -    WebServiceName = serv.serviceType;
 -    WebServiceJobTitle = serv.getActionText();
 -    WsURL = serv.hosturl;
 -    if (!b)
 -    {
 -      return new WebserviceInfo(WebServiceJobTitle,
 -              WebServiceJobTitle + " using service hosted at "
 -                      + serv.hosturl + "\n"
 -                      + (serv.description != null ? serv.description : ""),
 -              false);
 -    }
 -    return null;
 -  }
    /*
     * Jws2Instance serviceHandle; (non-Javadoc)
     * 
     * @param service
     * @param alignFrame
     */
 -  abstract void attachWSMenuEntry(JMenu wsmenu, final Jws2Instance service,
 +  abstract void attachWSMenuEntry(JMenu wsmenu,
 +          final ServiceWithParameters service,
            final AlignFrame alignFrame);
 -  protected boolean registerAAConWSInstance(final JMenu wsmenu,
 -          final Jws2Instance service, final AlignFrame alignFrame)
 -  {
 -    final AlignAnalysisUIText aaui = service.getAlignAnalysisUI(); // null ; //
 -                                                                   // AlignAnalysisUIText.aaConGUI.get(service.serviceType.toString());
 -    if (aaui == null)
 -    {
 -      // not an instantaneous calculation GUI type service
 -      return false;
 -    }
 -    // create the instaneous calculation GUI bits and update state if existing
 -    // GUI elements already present
 -
 -    JCheckBoxMenuItem _aaConEnabled = null;
 -    for (int i = 0; i < wsmenu.getItemCount(); i++)
 -    {
 -      JMenuItem item = wsmenu.getItem(i);
 -      if (item instanceof JCheckBoxMenuItem
 -              && item.getText().equals(aaui.getAAconToggle()))
 -      {
 -        _aaConEnabled = (JCheckBoxMenuItem) item;
 -      }
 -    }
 -    // is there an aaCon worker already present - if so, set it to use the
 -    // given service handle
 -    {
 -      List<AlignCalcWorkerI> aaconClient = alignFrame.getViewport()
 -              .getCalcManager()
 -              .getRegisteredWorkersOfClass(aaui.getClient());
 -      if (aaconClient != null && aaconClient.size() > 0)
 -      {
 -        AbstractJabaCalcWorker worker = (AbstractJabaCalcWorker) aaconClient
 -                .get(0);
 -        if (!worker.service.hosturl.equals(service.hosturl))
 -        {
 -          // javax.swing.SwingUtilities.invokeLater(new Runnable()
 -          {
 -            // @Override
 -            // public void run()
 -            {
 -              removeCurrentAAConWorkerFor(aaui, alignFrame);
 -              buildCurrentAAConWorkerFor(aaui, alignFrame, service);
 -            }
 -          } // );
 -        }
 -      }
 -    }
 -
 -    // is there a service already registered ? there shouldn't be if we are
 -    // being called correctly
 -    if (_aaConEnabled == null)
 -    {
 -      final JCheckBoxMenuItem aaConEnabled = new JCheckBoxMenuItem(
 -              aaui.getAAconToggle());
 -
 -      aaConEnabled.setToolTipText(
 -              JvSwingUtils.wrapTooltip(true, aaui.getAAconToggleTooltip()));
 -      aaConEnabled.addActionListener(new ActionListener()
 -      {
 -        @Override
 -        public void actionPerformed(ActionEvent arg0)
 -        {
 -          List<AlignCalcWorkerI> aaconClient = alignFrame.getViewport()
 -                  .getCalcManager()
 -                  .getRegisteredWorkersOfClass(aaui.getClient());
 -          if (aaconClient != null && aaconClient.size() > 0)
 -          {
 -            removeCurrentAAConWorkerFor(aaui, alignFrame);
 -          }
 -          else
 -          {
 -            buildCurrentAAConWorkerFor(aaui, alignFrame);
 -
 -          }
 -        }
 -
 -      });
 -      wsmenu.add(aaConEnabled);
 -      final JMenuItem modifyParams = new JMenuItem(
 -              aaui.getAAeditSettings());
 -      modifyParams.setToolTipText(JvSwingUtils.wrapTooltip(true,
 -              aaui.getAAeditSettingsTooltip()));
 -      modifyParams.addActionListener(new ActionListener()
 -      {
 -
 -        @Override
 -        public void actionPerformed(ActionEvent arg0)
 -        {
 -          showAAConAnnotationSettingsFor(aaui, alignFrame);
 -        }
 -      });
 -      wsmenu.add(modifyParams);
 -      wsmenu.addMenuListener(new MenuListener()
 -      {
 -
 -        @Override
 -        public void menuSelected(MenuEvent arg0)
 -        {
 -          // TODO: refactor to the implementing class.
 -          if (alignFrame.getViewport().getAlignment().isNucleotide()
 -                  ? aaui.isNa()
 -                  : aaui.isPr())
 -          {
 -            aaConEnabled.setEnabled(true);
 -            modifyParams.setEnabled(true);
 -          }
 -          else
 -          {
 -            aaConEnabled.setEnabled(false);
 -            modifyParams.setEnabled(false);
 -          }
 -          List<AlignCalcWorkerI> aaconClient = alignFrame.getViewport()
 -                  .getCalcManager()
 -                  .getRegisteredWorkersOfClass(aaui.getClient());
 -          if (aaconClient != null && aaconClient.size() > 0)
 -          {
 -            aaConEnabled.setSelected(true);
 -          }
 -          else
 -          {
 -            aaConEnabled.setSelected(false);
 -          }
 -        }
 -
 -        @Override
 -        public void menuDeselected(MenuEvent arg0)
 -        {
 -          // TODO Auto-generated method stub
 -
 -        }
 -
 -        @Override
 -        public void menuCanceled(MenuEvent arg0)
 -        {
 -          // TODO Auto-generated method stub
 -
 -        }
 -      });
 -
 -    }
 -    return true;
 -  }
 -
 -  private static void showAAConAnnotationSettingsFor(
 -          final AlignAnalysisUIText aaui, AlignFrame alignFrame)
 -  {
 -    /*
 -     * preferred settings Whether AACon is automatically recalculated Which
 -     * AACon server to use What parameters to use
 -     */
 -    // could actually do a class search for this too
 -    AAConSettings fave = (AAConSettings) alignFrame.getViewport()
 -            .getCalcIdSettingsFor(aaui.getCalcId());
 -    if (fave == null)
 -    {
 -      fave = createDefaultAAConSettings(aaui);
 -    }
 -    new SequenceAnnotationWSClient(fave, alignFrame, true);
 -
 -  }
 -
 -  private static void buildCurrentAAConWorkerFor(
 -          final AlignAnalysisUIText aaui, AlignFrame alignFrame)
 -  {
 -    buildCurrentAAConWorkerFor(aaui, alignFrame, null);
 -  }
 -
 -  private static void buildCurrentAAConWorkerFor(
 -          final AlignAnalysisUIText aaui, AlignFrame alignFrame,
 -          Jws2Instance service)
 -  {
 -    /*
 -     * preferred settings Whether AACon is automatically recalculated Which
 -     * AACon server to use What parameters to use
 -     */
 -    AAConSettings fave = (AAConSettings) alignFrame.getViewport()
 -            .getCalcIdSettingsFor(aaui.getCalcId());
 -    if (fave == null)
 -    {
 -      fave = createDefaultAAConSettings(aaui, service);
 -    }
 -    else
 -    {
 -      if (service != null
 -              && !fave.getService().hosturl.equals(service.hosturl))
 -      {
 -        Cache.log.debug("Changing AACon service to " + service.hosturl
 -                + " from " + fave.getService().hosturl);
 -        fave.setService(service);
 -      }
 -    }
 -    new SequenceAnnotationWSClient(fave, alignFrame, false);
 -  }
 -
 -  private static AAConSettings createDefaultAAConSettings(
 -          AlignAnalysisUIText aaui)
 -  {
 -    return createDefaultAAConSettings(aaui, null);
 -  }
 -
 -  private static AAConSettings createDefaultAAConSettings(
 -          AlignAnalysisUIText aaui, Jws2Instance service)
 -  {
 -    if (service != null)
 -    {
 -      if (!service.serviceType.toString()
 -              .equals(compbio.ws.client.Services.AAConWS.toString()))
 -      {
 -        Cache.log.warn(
 -                "Ignoring invalid preferred service for AACon calculations (service type was "
 -                        + service.serviceType + ")");
 -        service = null;
 -      }
 -      else
 -      {
 -        // check service is actually in the list of currently avaialable
 -        // services
 -        if (!Jws2Discoverer.getDiscoverer().getServices().contains(service))
 -        {
 -          // it isn't ..
 -          service = null;
 -        }
 -      }
 -    }
 -    if (service == null)
 -    {
 -      // get the default service for AACon
 -      service = Jws2Discoverer.getDiscoverer().getPreferredServiceFor(null,
 -              aaui.getServiceType());
 -    }
 -    if (service == null)
 -    {
 -      // TODO raise dialog box explaining error, and/or open the JABA
 -      // preferences menu.
 -      throw new Error(
 -              MessageManager.getString("error.no_aacon_service_found"));
 -    }
 -    return new AAConSettings(true, service, null, null);
 -  }
 -
 -  private static void removeCurrentAAConWorkerFor(AlignAnalysisUIText aaui,
 -          AlignFrame alignFrame)
 -  {
 -    alignFrame.getViewport().getCalcManager()
 -            .removeRegisteredWorkersOfClass(aaui.getClient());
 -  }
  }
@@@ -40,33 -27,37 +40,35 @@@ import java.awt.event.ActionListener
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
  import java.util.List;
++import java.util.Locale;
  
  import javax.swing.JMenu;
  import javax.swing.JMenuItem;
  import javax.swing.ToolTipManager;
  
 -import compbio.data.msa.MsaWS;
 -import compbio.metadata.Argument;
 -import jalview.datamodel.AlignmentI;
 -import jalview.datamodel.AlignmentView;
 -import jalview.gui.AlignFrame;
 -import jalview.gui.Desktop;
 -import jalview.gui.JvOptionPane;
 -import jalview.gui.JvSwingUtils;
 -import jalview.util.MessageManager;
 -import jalview.ws.jws2.jabaws2.Jws2Instance;
 -import jalview.ws.params.WsParamSetI;
  /**
 - * DOCUMENT ME!
 + * MsaWSClient
   * 
 - * @author $author$
 + * Instantiates web service menu items for multiple alignment services, and
 + * holds logic for constructing a web service thread.
 + * 
 + * TODO remove dependency on Jws2Client methods for creating AACon service UI
 + * elements.
 + * 
 + * @author Jim Procter et al
   * @version $Revision$
   */
 -public class MsaWSClient extends Jws2Client
 +public class MsaWSClient extends Jws2Client implements WSMenuEntryProviderI
  {
    /**
 -   * server is a WSDL2Java generated stub for an archetypal MsaWSI service.
 +   * server is a proxy class implementing the core methods for submitting,
 +   * monitoring and retrieving results from a multiple sequence alignment
 +   * service
     */
 -  MsaWS server;
 +  MultipleSequenceAlignmentI server;
  
 -  public MsaWSClient(Jws2Instance sh, String altitle,
 +  public MsaWSClient(ServiceWithParameters sh, String altitle,
            jalview.datamodel.AlignmentView msa, boolean submitGaps,
            boolean preserveOrder, AlignmentI seqdataset,
            AlignFrame _alignFrame)
            AlignFrame _alignFrame)
    {
      super(_alignFrame, preset, arguments);
 -    if (!processParams(sh, editParams))
 -    {
 -      return;
 -    }
 -
 -    if (!(sh.service instanceof MsaWS))
 -    {
 -      // redundant at mo - but may change
 -      JvOptionPane.showMessageDialog(Desktop.desktop,
 -              MessageManager.formatMessage(
 -                      "label.service_called_is_not_msa_service",
 -                      new String[]
 -                      { sh.serviceType }),
 -              MessageManager.getString("label.internal_jalview_error"),
 -              JvOptionPane.WARNING_MESSAGE);
 -
 -      return;
 -    }
 -    server = (MsaWS) sh.service;
 -    if ((wsInfo = setWebService(sh, false)) == null)
 -    {
 -      JvOptionPane.showMessageDialog(Desktop.desktop, MessageManager
 -              .formatMessage("label.msa_service_is_unknown", new String[]
 -              { sh.serviceType }),
 -              MessageManager.getString("label.internal_jalview_error"),
 -              JvOptionPane.WARNING_MESSAGE);
 -
 -      return;
 -    }
 -
 -    startMsaWSClient(altitle, msa, submitGaps, preserveOrder, seqdataset);
 +    processParams(sh, editParams).thenAccept((startJob) -> {
 +      if (!startJob)
 +        return;
 +      
 +      if (!(sh instanceof JalviewServiceEndpointProviderI
 +              && ((JalviewServiceEndpointProviderI) sh)
 +                      .getEndpoint() instanceof MultipleSequenceAlignmentI))
 +      {
 +        // redundant at mo - but may change
 +        JvOptionPane.showMessageDialog(Desktop.getDesktopPane(),
 +                MessageManager.formatMessage(
 +                        "label.service_called_is_not_msa_service",
 +                        new String[]
 +                        { sh.getName() }),
 +                MessageManager.getString("label.internal_jalview_error"),
 +                JvOptionPane.WARNING_MESSAGE);
 +  
 +        return;
 +      }
 +      serviceHandle = sh;
 +      server = (MultipleSequenceAlignmentI) ((JalviewServiceEndpointProviderI) sh)
 +              .getEndpoint();
 +      if ((wsInfo = setWebService(sh, false)) == null)
 +      {
 +        JvOptionPane.showMessageDialog(Desktop.getDesktopPane(), MessageManager
 +                .formatMessage("label.msa_service_is_unknown", new String[]
 +                { sh.getName() }),
 +                MessageManager.getString("label.internal_jalview_error"),
 +                JvOptionPane.WARNING_MESSAGE);
 +  
 +        return;
 +      }
 +  
 +      startMsaWSClient(altitle, msa, submitGaps, preserveOrder, seqdataset);
 +    });
    }
  
    public MsaWSClient()
  
    @Override
    public void attachWSMenuEntry(JMenu rmsawsmenu,
-           final ServiceWithParameters service, final AlignFrame alignFrame)
 -          final Jws2Instance service, final AlignFrame af)
++          final Jws2Instance service, final AlignFrame alignFrame)
    {
-     if (Jws2ClientFactory.registerAAConWSInstance(rmsawsmenu,
-                     service, alignFrame))
 -    if (registerAAConWSInstance(rmsawsmenu, service, af))
++    if (registerAAConWSInstance(rmsawsmenu, service, alignFrame))
      {
        // Alignment dependent analysis calculation WS gui
        return;
@@@ -33,6 -35,6 +33,8 @@@ import jalview.ws.uimodel.AlignAnalysis
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.util.List;
++import java.util.Locale;
++
  
  import javax.swing.JMenu;
  import javax.swing.JMenuItem;
@@@ -82,46 -84,32 +84,47 @@@ public class SequenceAnnotationWSClien
        // columns
  
        List<AlignCalcWorkerI> clnts = alignFrame.getViewport()
 -              .getCalcManager().getRegisteredWorkersOfClass(clientClass);
 -      AbstractJabaCalcWorker worker;
 -      if (clnts == null || clnts.size() == 0)
 +          .getCalcManager()
 +          .getWorkersOfClass(SeqAnnotationServiceCalcWorker.class);
 +
 +      SeqAnnotationServiceCalcWorker tmpworker = null;
 +      if (clnts != null)
        {
 -        if (!processParams(sh, editParams))
 +        for (AlignCalcWorkerI _worker : clnts)
          {
 -          return;
 +          tmpworker = (SeqAnnotationServiceCalcWorker) _worker;
 +          if (tmpworker.hasService()
 +              && tmpworker.getService().getClass().equals(clientClass))
 +          {
 +            break;
 +          }
 +          tmpworker = null;
          }
 -        try
 -        {
 -          worker = (AbstractJabaCalcWorker) (clientClass
 -                  .getConstructor(new Class[]
 -                  { Jws2Instance.class, AlignFrame.class, WsParamSetI.class,
 -                      List.class })
 -                  .newInstance(new Object[]
 -                  { sh, alignFrame, this.preset, paramset }));
 -        } catch (Exception x)
 -        {
 -          x.printStackTrace();
 -          throw new Error(
 +      }
 +      final var worker = tmpworker;
 +      if (worker == null)
 +      {
 +        processParams(sh, editParams).thenAccept((startJob) -> {
 +          if (startJob)
 +          {
 +            final SeqAnnotationServiceCalcWorker worker_;
 +            try
 +            {
 +              worker_ = new SeqAnnotationServiceCalcWorker(sh, alignFrame, this.preset,
 +                  paramset);
 +            } catch (Exception x)
 +            {
 +              x.printStackTrace();
 +              throw new Error(
                    MessageManager.getString("error.implementation_error"),
                    x);
 -        }
 -        alignFrame.getViewport().getCalcManager().registerWorker(worker);
 -        alignFrame.getViewport().getCalcManager().startWorker(worker);
 +            }
 +            alignFrame.getViewport().getCalcManager().registerWorker(worker_);
 +                // also starts the worker
 +            startSeqAnnotationWorker(sh, alignFrame, preset, editParams);
 +          }
 +        });
        }
        else
        {
          {
            return;
          }
 -        // reinstate worker if it was blacklisted (might have happened due to
 -        // invalid parameters)
 -        alignFrame.getViewport().getCalcManager().enableWorker(worker);
 -        worker.updateParameters(this.preset, paramset);
        }
      }
-     else
-     {
-       startSeqAnnotationWorker(sh, alignFrame, preset, editParams);
-     }
-   }
-   private void startSeqAnnotationWorker(ServiceWithParameters sh,
-       AlignFrame alignFrame, WsParamSetI preset, boolean editParams)
-   {
-     if (!sh.isInteractiveUpdate())
+     if (sh.action.toLowerCase(Locale.ROOT).contains("disorder"))
      {
        // build IUPred style client. take sequences, returns annotation per
        // sequence.
Simple merge
   */
  package jalview.ws.sifts;
  
 -import java.util.Locale;
 -
  import java.io.File;
  import java.io.FileInputStream;
+ import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.PrintStream;
@@@ -34,6 -41,6 +39,7 @@@ import java.util.Date
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
++import java.util.Locale;
  import java.util.Map;
  import java.util.Set;
  import java.util.TreeMap;
@@@ -110,14 -120,9 +119,12 @@@ public class SiftsClient implements Sif
  
    private final static String NEWLINE = System.lineSeparator();
  
 +  private static final boolean GET_STREAM = false;
 +  private static final boolean CACHE_FILE = true;
    private String curSourceDBRef;
  
    private HashSet<String> curDBRefAccessionIdsString;
 +  private boolean doCache = false;
  
    private enum CoordinateSys
    {
     */
    public static File downloadSiftsFile(String pdbId)
            throws SiftsException, IOException
 +  {    
 +    return (File) downloadSifts(pdbId, CACHE_FILE);
 +  }
 +
 +  /**
 +   * Download SIFTs XML with the option to cache a file or to get a stream.
 +   * 
 +   * @param pdbId
 +   * @param asFile 
 +   * @return
 +   * @throws IOException
 +   */
 +  private static Object downloadSifts(String pdbId, boolean asFile) throws IOException
    {
-     pdbId = pdbId.toLowerCase();
++    pdbId = pdbId.toLowerCase(Locale.ROOT);
      if (pdbId.contains(".cif"))
      {
        pdbId = pdbId.replace(".cif", "");
      }
      String siftFile = pdbId + ".xml.gz";
 -    String siftsFileFTPURL = SIFTS_FTP_BASE_URL + siftFile;
 -
 -    /*
 -     * Download the file from URL to either
 -     * Java: directory of cached downloaded SIFTS files
 -     * Javascript: temporary 'file' (in-memory cache)
 -     */
      File downloadTo = null;
 -    if (Platform.isJS())
 -    {
 -      downloadTo = File.createTempFile(siftFile, ".xml.gz");
 -    }
 -    else
 +    if (asFile)
      {
        downloadTo = new File(
                SiftsSettings.getSiftDownloadDirectory() + siftFile);
          siftsDownloadDir.mkdirs();
        }
      }
 -    // System.out.println(">> Download ftp url : " + siftsFileFTPURL);
 -    // long now = System.currentTimeMillis();
 +    String siftsFileFTPURL = SIFTS_FTP_BASE_URL + siftFile;
      URL url = new URL(siftsFileFTPURL);
      URLConnection conn = url.openConnection();
 -    InputStream inputStream = conn.getInputStream();
 -    FileOutputStream outputStream = new FileOutputStream(downloadTo);
 -    byte[] buffer = new byte[BUFFER_SIZE];
 -    int bytesRead = -1;
 -    while ((bytesRead = inputStream.read(buffer)) != -1)
 -    {
 -      outputStream.write(buffer, 0, bytesRead);
 -    }
 -    outputStream.close();
 -    inputStream.close();
 -    // System.out.println(">>> File downloaded : " + downloadedSiftsFile
 -    // + " took " + (System.currentTimeMillis() - now) + "ms");
 +    InputStream is = conn.getInputStream();
 +    if (!asFile)
 +      return is;
 +    // This is MUCH more efficent in JavaScript, as we already have the bytes
 +    Platform.streamToFile(is, downloadTo);
 +    is.close();
      return downloadTo;
    }
  
@@@ -1,8 -1,8 +1,8 @@@
  //
 -// This file was generated by the Eclipse Implementation of JAXB, v2.3.3 
 -// See https://eclipse-ee4j.github.io/jaxb-ri 
 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 
 +// See <a href="http://java.sun.com/xml/jaxb">http://java.sun.com/xml/jaxb</a> 
  // Any modifications to this file will be lost upon recompilation of the source schema. 
- // Generated on: 2019.07.10 at 12:10:00 PM BST 
+ // Generated on: 2021.08.30 at 11:05:22 AM BST 
  //
  
  
   */
  package jalview.analysis;
  
+ import java.util.Locale;
 -import jalview.datamodel.Alignment;
 -import jalview.datamodel.AlignmentI;
 -import jalview.datamodel.Sequence;
 -import jalview.datamodel.SequenceI;
 -import jalview.gui.JvOptionPane;
 -import jalview.io.FastaFile;
 -
  import java.io.File;
  import java.io.FileNotFoundException;
  import java.io.PrintStream;
@@@ -68,11 -68,16 +68,16 @@@ public class FinderTes
    public void setUp()
    {
      Cache.loadProperties("test/jalview/io/testProps.jvprops");
 -    Cache.applicationProperties.setProperty("PAD_GAPS",
 +    Cache.setPropertyNoSave("PAD_GAPS",
              Boolean.FALSE.toString());
  
-     String seqData = "seq1seq1/8-18 ABCD--EF-GHIJI\n" + "seq2 A--BCDefHI\n"
-             + "seq3 --bcdEFH\n" + "seq4 aa---aMMMMMaaa\n";
+     //@formatter:off
+     String seqData = 
+         "seq1/8-18 ABCD--EF-GHIJI\n" + 
+         "seq2      A--BCDefHI\n" + 
+         "seq3      --bcdEFH\n" + 
+         "seq4      aa---aMMMMMaaa\n";
+     //@formatter:on
      af = new FileLoader().LoadFileWaitTillLoaded(seqData,
              DataSourceType.PASTE);
      av = af.getViewport();
@@@ -93,9 -92,20 +95,21 @@@ public class ResidueCountTes
      assertEquals(rc.getCount(' '), 4);
      assertEquals(rc.getCount('-'), 4);
      assertEquals(rc.getCount('.'), 4);
 +    assertEquals(rc.getTotalResidueCount(), 0);
      assertFalse(rc.isUsingOtherData());
      assertFalse(rc.isCountingInts());
+     
+     rc.set(ResidueCount.GAP_COUNT, Short.MAX_VALUE-2);
+     assertEquals(rc.getGapCount(), Short.MAX_VALUE-2);
+     assertFalse(rc.isCountingInts());
+     rc.addGap();
+     assertEquals(rc.getGapCount(), Short.MAX_VALUE-1);
+     assertFalse(rc.isCountingInts());
+     rc.addGap();
+     assertEquals(rc.getGapCount(), Short.MAX_VALUE);
+     rc.addGap();
+     assertTrue(rc.isCountingInts());
+     assertEquals(rc.getGapCount(), Short.MAX_VALUE+1);
    }
  
    @Test(groups = "Functional")
@@@ -23,50 -23,36 +23,36 @@@ package jalview.ext.jmol
  import static org.testng.Assert.assertEquals;
  import static org.testng.Assert.assertTrue;
  
+ import java.awt.Color;
+ import java.util.HashMap;
+ import java.util.LinkedHashMap;
+ import java.util.List;
+ import java.util.Map;
+ import org.testng.annotations.BeforeClass;
+ import org.testng.annotations.Test;
 -
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.Sequence;
  import jalview.datamodel.SequenceI;
  import jalview.gui.AlignFrame;
- import jalview.gui.JvOptionPane;
  import jalview.gui.SequenceRenderer;
  import jalview.schemes.JalviewColourScheme;
+ import jalview.structure.AtomSpecModel;
+ import jalview.structure.StructureCommandI;
  import jalview.structure.StructureMapping;
- import jalview.structure.StructureMappingcommandSet;
  import jalview.structure.StructureSelectionManager;
  
- import java.util.HashMap;
- import org.testng.annotations.BeforeClass;
- import org.testng.annotations.Test;
 +
  public class JmolCommandsTest
  {
+   private JmolCommands testee;
  
    @BeforeClass(alwaysRun = true)
-   public void setUpJvOptionPane()
+   public void setUp()
    {
-     JvOptionPane.setInteractiveMode(false);
-     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
-   }
-   @Test(groups = { "Functional" })
-   public void testGetColourBySequenceCommand_noFeatures()
-   {
-     SequenceI seq1 = new Sequence("seq1", "MHRSQTRALK");
-     SequenceI seq2 = new Sequence("seq2", "MRLEITQSGD");
-     AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
-     AlignFrame af = new AlignFrame(al, 800, 500);
-     SequenceRenderer sr = new SequenceRenderer(af.getViewport());
-     SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } };
-     String[] files = new String[] { "seq1.pdb", "seq2.pdb" };
-     StructureSelectionManager ssm = StructureSelectionManager.getStructureSelectionManager(null);
-     // need some mappings!
-     StructureMappingcommandSet[] commands = JmolCommands
-             .getColourBySequenceCommand(ssm, files, seqs, sr, af.alignPanel);
+     testee = new JmolCommands();
    }
  
    @Test(groups = { "Functional" })
@@@ -90,8 -76,8 +76,7 @@@
      SequenceRenderer sr = new SequenceRenderer(af.getViewport());
      SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } };
      String[] files = new String[] { "seq1.pdb", "seq2.pdb" };
-     StructureSelectionManager ssm = StructureSelectionManager.getStructureSelectionManager(null);
-   
+     StructureSelectionManager ssm = new StructureSelectionManager();
 -
      /*
       * map residues 1-10 to residues 21-30 (atoms 105-150) in structures
       */
      StructureMapping sm2 = new StructureMapping(seq2, "seq2.pdb", "pdb2",
              "B", map, null);
      ssm.addStructureMapping(sm2);
-   
-     StructureMappingcommandSet[] commands = JmolCommands
-             .getColourBySequenceCommand(ssm, files, seqs, sr, af.alignPanel);
 -
+     String[] commands = testee.colourBySequence(ssm,
+             files,
+             seqs, sr, af.alignPanel);
      assertEquals(commands.length, 2);
-     assertEquals(commands[0].commands.length, 1);
++    assertEquals(commands[0].commands.length, 1); // from 2.12 merge from 2.11.2
  
-     String chainACommand = commands[0].commands[0];
+     String chainACommand = commands[0];
      // M colour is #82827d == (130, 130, 125) (see strand.html help page)
-     assertTrue(chainACommand
-             .contains("select 21:A/1.1;color[130,130,125]")); // first one
+     assertTrue(
+             chainACommand.contains("select 21:A/1.1;color[130,130,125]")); // first
+                                                                            // one
      // H colour is #60609f == (96, 96, 159)
      assertTrue(chainACommand.contains(";select 22:A/1.1;color[96,96,159]"));
      // hidden columns are Gray (128, 128, 128)
@@@ -23,19 -23,6 +23,7 @@@ package jalview.ext.rbvi.chimera
  import static org.testng.Assert.assertEquals;
  import static org.testng.Assert.assertTrue;
  
- import jalview.datamodel.Alignment;
- import jalview.datamodel.AlignmentI;
- import jalview.datamodel.ColumnSelection;
- import jalview.datamodel.Sequence;
- import jalview.datamodel.SequenceI;
- import jalview.gui.AlignFrame;
- import jalview.gui.JvOptionPane;
- import jalview.gui.SequenceRenderer;
- import jalview.schemes.JalviewColourScheme;
- import jalview.structure.StructureMapping;
- import jalview.structure.StructureMappingcommandSet;
- import jalview.structure.StructureSelectionManager;
 +
  import java.awt.Color;
  import java.util.HashMap;
  import java.util.LinkedHashMap;
@@@ -45,14 -32,18 +33,17 @@@ import java.util.Map
  import org.testng.annotations.BeforeClass;
  import org.testng.annotations.Test;
  
+ import jalview.structure.AtomSpecModel;
+ import jalview.structure.StructureCommand;
+ import jalview.structure.StructureCommandI;
 -
  public class ChimeraCommandsTest
  {
+   private ChimeraCommands testee;
  
    @BeforeClass(alwaysRun = true)
-   public void setUpJvOptionPane()
+   public void setUp()
    {
-     JvOptionPane.setInteractiveMode(false);
-     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
+     testee = new ChimeraCommands();
    }
  
    @Test(groups = { "Functional" })
@@@ -84,9 -75,9 +75,8 @@@
      /*
       * make a map of { featureType, {featureValue, {residue range specification } } }
       */
-     Map<String, Map<Object, AtomSpecModel>> featuresMap = new LinkedHashMap<String, Map<Object, AtomSpecModel>>();
-     Map<Object, AtomSpecModel> featureValues = new HashMap<Object, AtomSpecModel>();
-     
+     Map<String, Map<Object, AtomSpecModel>> featuresMap = new LinkedHashMap<>();
+     Map<Object, AtomSpecModel> featureValues = new HashMap<>();
 -
      /*
       * start with just one feature/value...
       */
    @Test(groups = { "Functional" })
    public void testMakeAttributeName()
    {
-     assertEquals(ChimeraCommands.makeAttributeName(null), "jv_");
-     assertEquals(ChimeraCommands.makeAttributeName(""), "jv_");
-     assertEquals(ChimeraCommands.makeAttributeName("helix"), "jv_helix");
-     assertEquals(ChimeraCommands.makeAttributeName("Hello World 24"),
+     assertEquals(testee.makeAttributeName(null), "jv_");
+     assertEquals(testee.makeAttributeName(""), "jv_");
+     assertEquals(testee.makeAttributeName("helix"), "jv_helix");
+     assertEquals(testee.makeAttributeName(
+             "Hello World 24"),
              "jv_Hello_World_24");
-     assertEquals(
-             ChimeraCommands.makeAttributeName("!this is-a_very*{odd(name"),
+     assertEquals(testee.makeAttributeName(
+             "!this is-a_very*{odd(name"),
              "jv__this_is_a_very__odd_name");
      // name ending in color gets underscore appended
-     assertEquals(ChimeraCommands.makeAttributeName("helixColor"),
-             "jv_helixColor_");
+     assertEquals(testee.makeAttributeName("helixColor"), "jv_helixColor_");
+   }
+   @Test(groups = "Functional")
+   public void testGetAtomSpec()
+   {
+     AtomSpecModel model = new AtomSpecModel();
+     assertEquals(testee.getAtomSpec(model, false), "");
+     model.addRange("1", 2, 4, "A");
+     assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A");
+     model.addRange("1", 8, 8, "A");
+     assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A,8.A");
+     model.addRange("1", 5, 7, "B");
+     assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A,8.A,5-7.B");
+     model.addRange("1", 3, 5, "A");
+     assertEquals(testee.getAtomSpec(model, false), "#1:2-5.A,8.A,5-7.B");
+     model.addRange("0", 1, 4, "B");
+     assertEquals(testee.getAtomSpec(model, false),
+             "#0:1-4.B|#1:2-5.A,8.A,5-7.B");
+     model.addRange("0", 5, 9, "C");
+     assertEquals(testee.getAtomSpec(model, false),
+             "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-7.B");
+     model.addRange("1", 8, 10, "B");
+     assertEquals(testee.getAtomSpec(model, false),
+             "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B");
+     model.addRange("1", 8, 9, "B");
+     assertEquals(testee.getAtomSpec(model, false),
+             "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B");
+     model.addRange("0", 3, 10, "C"); // subsumes 5-9
+     assertEquals(testee.getAtomSpec(model, false),
+             "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B");
+     model.addRange("5", 25, 35, " ");
+     assertEquals(testee.getAtomSpec(model, false),
+             "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B|#5:25-35.");
 -
    }
  
    @Test(groups = { "Functional" })
@@@ -3,15 -3,6 +3,7 @@@ package jalview.gui
  import static org.testng.Assert.assertNotNull;
  import static org.testng.Assert.assertTrue;
  
- import jalview.analysis.AlignmentGenerator;
- import jalview.bin.Cache;
- import jalview.bin.Jalview;
- import jalview.datamodel.AlignmentI;
- import jalview.datamodel.SequenceGroup;
- import jalview.io.DataSourceType;
- import jalview.io.FileLoader;
 +
  import java.awt.event.MouseEvent;
  import java.io.File;
  import java.io.IOException;
@@@ -26,69 -87,56 +88,80 @@@ public class FreeUpMemoryTes
  {
    private static final int ONE_MB = 1000 * 1000;
  
+   /*
+    * maximum retained heap usage (in MB) for a passing test
+    */
+   private static int MAX_RESIDUAL_HEAP = 45;
 -
    /**
     * Configure (read-only) Jalview property settings for test
     */
    @BeforeClass(alwaysRun = true)
    public void setUp()
    {
-     Jalview.main(new String[] { "-nonews", "-props",
-         "test/jalview/testProps.jvprops" });
+     Jalview.main(
+             new String[]
+             { "-nonews", "-props", "test/jalview/testProps.jvprops" });
      String True = Boolean.TRUE.toString();
 -    Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS", True);
 -    Cache.applicationProperties.setProperty("SHOW_QUALITY", True);
 -    Cache.applicationProperties.setProperty("SHOW_CONSERVATION", True);
 -    Cache.applicationProperties.setProperty("SHOW_OCCUPANCY", True);
 -    Cache.applicationProperties.setProperty("SHOW_IDENTITY", True);
 +    Cache.setPropertyNoSave("SHOW_ANNOTATIONS", True);
 +    Cache.setPropertyNoSave("SHOW_QUALITY", True);
 +    Cache.setPropertyNoSave("SHOW_CONSERVATION", True);
 +    Cache.setPropertyNoSave("SHOW_OCCUPANCY", True);
 +    Cache.setPropertyNoSave("SHOW_IDENTITY", True);
    }
  
 +  /**
 +   * A simple test that memory is released when all windows are closed.
 +   * <ul>
 +   * <li>generates a reasonably large alignment and loads it</li>
 +   * <li>performs various operations on the alignment</li>
 +   * <li>closes all windows</li>
 +   * <li>requests garbage collection</li>
 +   * <li>asserts that the remaining memory footprint (heap usage) is 'not large'
 +   * </li>
 +   * </ul>
 +   * If the test fails, this suggests that a reference to some large object
 +   * (perhaps the alignment data, or some annotation / Tree / PCA data) has
 +   * failed to be garbage collected. If this is the case, the heap will need to
 +   * be inspected manually (suggest using jvisualvm) in order to track down
 +   * where large objects are still referenced. The code (for example
 +   * AlignmentViewport.dispose()) should then be updated to ensure references to
 +   * large objects are set to null when they are no longer required.
 +   * 
 +   * @throws IOException
 +   */
    @Test(groups = "Memory")
    public void testFreeMemoryOnClose() throws IOException
    {
      File f = generateAlignment();
      f.deleteOnExit();
  
-     long expectedMin = 35L;
++    long expectedMin = MAX_RESIDUAL_HEAP;
 +    long usedMemoryAtStart=getUsedMemory();
 +    if (usedMemoryAtStart>expectedMin)
 +    {
 +      System.err.println("used memory before test is "+usedMemoryAtStart+" > "+expectedMin+"MB .. adjusting minimum.");
 +      expectedMin = usedMemoryAtStart;
 +    }
      doStuffInJalview(f);
  
 -    Desktop.instance.closeAll_actionPerformed(null);
 +    Desktop.getInstance().closeAll_actionPerformed(null);
  
 -    checkUsedMemory(MAX_RESIDUAL_HEAP);
 +    checkUsedMemory(expectedMin);
    }
  
-   private static long getUsedMemory()
+   /**
+    * Returns the current total used memory (available memory - free memory),
+    * rounded down to the nearest MB
+    * 
+    * @return
+    */
+   private static int getUsedMemory()
    {
-     long availableMemory = Runtime.getRuntime().totalMemory() / ONE_MB;
-     long freeMemory = Runtime.getRuntime().freeMemory() / ONE_MB;
+     long availableMemory = Runtime.getRuntime().totalMemory();
+     long freeMemory = Runtime.getRuntime().freeMemory();
      long usedMemory = availableMemory - freeMemory;
-     return usedMemory;
 -
+     return (int) (usedMemory / ONE_MB);
    }
 -
    /**
     * Requests garbage collection and then checks whether remaining memory in use
     * is less than the expected value (in Megabytes)
@@@ -26,12 -31,7 +26,13 @@@ import static org.testng.Assert.assertN
  import static org.testng.Assert.assertNull;
  import static org.testng.Assert.assertTrue;
  
 +import java.awt.Font;
 +import java.awt.FontMetrics;
 +
- import org.testng.annotations.BeforeClass;
++import org.testng.annotations.BeforeMethod;
 +import org.testng.annotations.Test;
 +
  import jalview.bin.Cache;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.SearchResults;
@@@ -243,10 -231,9 +239,8 @@@ public class SeqCanvasTes
      av.setFont(new Font("SansSerif", Font.PLAIN, 14), true);
      int charHeight = av.getCharHeight();
      int charWidth = av.getCharWidth();
 -    assertEquals(charHeight, 17);
 -    assertEquals(charWidth, 12);
 -  
 +    assertEquals(charHeight, !Platform.isWin() ? 17 : 19);
 +    assertEquals(charWidth, !Platform.isWin() ? 12 : 11);
      SeqCanvas testee = af.alignPanel.getSeqPanel().seqCanvas;
    
      /*
      av.setScaleAboveWrapped(true);
      av.setScaleLeftWrapped(true);
      av.setScaleRightWrapped(true);
      FontMetrics fm = testee.getFontMetrics(av.getFont());
      int labelWidth = fm.stringWidth("000") + charWidth;
 -    assertEquals(labelWidth, 39); // 3 x 9 + charWidth
 +    assertEquals(labelWidth,
 +            !Platform.isWin() ? 3 * 9 + charWidth : 3 * 8 + charWidth);
      int annotationHeight = testee.getAnnotationHeight();
  
      /*
@@@ -24,6 -24,17 +24,16 @@@ import static org.testng.Assert.assertE
  import static org.testng.Assert.assertNull;
  import static org.testng.Assert.assertTrue;
  
+ import java.awt.EventQueue;
+ import java.awt.FontMetrics;
+ import java.awt.event.MouseEvent;
+ import java.lang.reflect.InvocationTargetException;
+ import javax.swing.JLabel;
+ import org.testng.annotations.AfterMethod;
+ import org.testng.annotations.BeforeClass;
+ import org.testng.annotations.Test;
 -
  import jalview.api.AlignViewportI;
  import jalview.bin.Cache;
  import jalview.bin.Jalview;
@@@ -40,17 -53,6 +52,8 @@@ import jalview.io.DataSourceType
  import jalview.io.FileLoader;
  import jalview.util.MessageManager;
  import jalview.viewmodel.ViewportRanges;
 +
- import java.awt.EventQueue;
- import java.awt.event.MouseEvent;
- import java.lang.reflect.InvocationTargetException;
- import javax.swing.JLabel;
- import org.testng.annotations.AfterMethod;
- import org.testng.annotations.BeforeClass;
- import org.testng.annotations.Test;
 +
  import junit.extensions.PA;
  
  public class SeqPanelTest
@@@ -24,6 -24,6 +24,7 @@@ import static org.testng.Assert.assertE
  import static org.testng.AssertJUnit.assertNotNull;
  import static org.testng.AssertJUnit.assertTrue;
  
++
  import java.util.Collection;
  import java.util.Vector;
  
@@@ -73,6 -80,46 +81,45 @@@ public class StructureChooserTes
      pdbIds.add(dbRef);
  
      seq.setPDBId(pdbIds);
+     
+     // Uniprot sequence for 3D-Beacons mocks
+     upSeq = new Sequence("P38398", 
+             "MDLSALRVEEVQNVINAMQKILECPICLELIKEPVSTKCDHIFCKFCMLKLLNQKKGPSQCPLCKNDITKRS\n"
+             + "LQESTRFSQLVEELLKIICAFQLDTGLEYANSYNFAKKENNSPEHLKDEVSIIQSMGYRNRAKRLLQSEPEN\n"
+             + "PSLQETSLSVQLSNLGTVRTLRTKQRIQPQKTSVYIELGSDSSEDTVNKATYCSVGDQELLQITPQGTRDEI\n"
+             + "SLDSAKKAACEFSETDVTNTEHHQPSNNDLNTTEKRAAERHPEKYQGSSVSNLHVEPCGTNTHASSLQHENS\n"
+             + "SLLLTKDRMNVEKAEFCNKSKQPGLARSQHNRWAGSKETCNDRRTPSTEKKVDLNADPLCERKEWNKQKLPC\n"
+             + "SENPRDTEDVPWITLNSSIQKVNEWFSRSDELLGSDDSHDGESESNAKVADVLDVLNEVDEYSGSSEKIDLL\n"
+             + "ASDPHEALICKSERVHSKSVESNIEDKIFGKTYRKKASLPNLSHVTENLIIGAFVTEPQIIQERPLTNKLKR\n"
+             + "KRRPTSGLHPEDFIKKADLAVQKTPEMINQGTNQTEQNGQVMNITNSGHENKTKGDSIQNEKNPNPIESLEK\n"
+             + "ESAFKTKAEPISSSISNMELELNIHNSKAPKKNRLRRKSSTRHIHALELVVSRNLSPPNCTELQIDSCSSSE\n"
+             + "EIKKKKYNQMPVRHSRNLQLMEGKEPATGAKKSNKPNEQTSKRHDSDTFPELKLTNAPGSFTKCSNTSELKE\n"
+             + "FVNPSLPREEKEEKLETVKVSNNAEDPKDLMLSGERVLQTERSVESSSISLVPGTDYGTQESISLLEVSTLG\n"
+             + "KAKTEPNKCVSQCAAFENPKGLIHGCSKDNRNDTEGFKYPLGHEVNHSRETSIEMEESELDAQYLQNTFKVS\n"
+             + "KRQSFAPFSNPGNAEEECATFSAHSGSLKKQSPKVTFECEQKEENQGKNESNIKPVQTVNITAGFPVVGQKD\n"
+             + "KPVDNAKCSIKGGSRFCLSSQFRGNETGLITPNKHGLLQNPYRIPPLFPIKSFVKTKCKKNLLEENFEEHSM\n"
+             + "SPEREMGNENIPSTVSTISRNNIRENVFKEASSSNINEVGSSTNEVGSSINEIGSSDENIQAELGRNRGPKL\n"
+             + "NAMLRLGVLQPEVYKQSLPGSNCKHPEIKKQEYEEVVQTVNTDFSPYLISDNLEQPMGSSHASQVCSETPDD\n"
+             + "LLDDGEIKEDTSFAENDIKESSAVFSKSVQKGELSRSPSPFTHTHLAQGYRRGAKKLESSEENLSSEDEELP\n"
+             + "CFQHLLFGKVNNIPSQSTRHSTVATECLSKNTEENLLSLKNSLNDCSNQVILAKASQEHHLSEETKCSASLF\n"
+             + "SSQCSELEDLTANTNTQDPFLIGSSKQMRHQSESQGVGLSDKELVSDDEERGTGLEENNQEEQSMDSNLGEA\n"
+             + "ASGCESETSVSEDCSGLSSQSDILTTQQRDTMQHNLIKLQQEMAELEAVLEQHGSQPSNSYPSIISDSSALE\n"
+             + "DLRNPEQSTSEKAVLTSQKSSEYPISQNPEGLSADKFEVSADSSTSKNKEPGVERSSPSKCPSLDDRWYMHS\n"
+             + "CSGSLQNRNYPSQEELIKVVDVEEQQLEESGPHDLTETSYLPRQDLEGTPYLESGISLFSDDPESDPSEDRA\n"
+             + "PESARVGNIPSSTSALKVPQLKVAESAQSPAAAHTTDTAGYNAMEESVSREKPELTASTERVNKRMSMVVSG\n"
+             + "LTPEEFMLVYKFARKHHITLTNLITEETTHVVMKTDAEFVCERTLKYFLGIAGGKWVVSYFWVTQSIKERKM\n"
+             + "LNEHDFEVRGDVVNGRNHQGPKRARESQDRKIFRGLEICCYGPFTNMPTDQLEWMVQLCGASVVKELSSFTL\n"
+             + "GTGVHPIVVVQPDAWTEDNGFHAIGQMCEAPVVTREWVLDSVALYQCQELDTYLIPQIPHSHY\n"
+             + "", 1,
+ 1863);
+     upSeq.setDescription("Breast cancer type 1 susceptibility protein");
+     upSeq_nocanonical = new Sequence(upSeq);
+     upSeq.createDatasetSequence();
+     upSeq.addDBRef(new DBRefEntry("UNIPROT","0","P38398",null,true));
+     
+     upSeq_nocanonical.createDatasetSequence();
+     // not a canonical reference
+     upSeq_nocanonical.addDBRef(new DBRefEntry("UNIPROT","0","P38398",null,false));
 -
    }
  
    @AfterMethod(alwaysRun = true)
              "discoveredStructuresSet");
      assertNotNull(ss);
      assertTrue(ss.size() > 0);
+   }
  
+   @Test(groups = { "Functional" })
+   public void fetchStructuresInfoMockedTest()
+   {
+     TDBeaconsFTSRestClientTest.setMock();
+     PDBFTSRestClientTest.setMock();
+     SequenceI[] selectedSeqs = new SequenceI[] { upSeq };
+     StructureChooser sc = new StructureChooser(selectedSeqs, seq, null);
+     ThreadwaitFor(500, sc);
+     
+     sc.fetchStructuresMetaData();
+     Collection<FTSData> ss = (Collection<FTSData>) PA.getValue(sc,
+             "discoveredStructuresSet");
+     assertNotNull(ss);
+     assertTrue(ss.size() > 0);
    }
  
+   private void ThreadwaitFor(int i, StructureChooser sc)
+   {
+     long timeout = i+System.currentTimeMillis();
+     while (!sc.isDialogVisible() && timeout > System.currentTimeMillis())
+     {
+       try {
+         Thread.sleep(50);
+       } catch (InterruptedException x)
+       {
+         
+       }
+     }
+     
+   }
 -
 -
    @Test(groups = { "Functional" })
    public void sanitizeSeqNameTest()
    {
@@@ -208,28 -234,34 +234,34 @@@ public class BackupFilesTes
    private void setBackupFilesOptions(boolean enabled, boolean reverse,
            boolean noMax)
    {
-     Cache.loadProperties("test/jalview/io/testProps.jvprops");
      BackupFilesPresetEntry bfpe = new BackupFilesPresetEntry(suffix, digits,
              reverse, noMax, rollMax, false);
+     setBackupFilesOptions(enabled, reverse, noMax,
+             "test/jalview/io/testProps.jvprops", bfpe);
+   }
+   private void setBackupFilesOptions(boolean enabled, boolean reverse,
+           boolean noMax, String propsFile, BackupFilesPresetEntry bfpe)
+   {
+     Cache.loadProperties(propsFile);
  
 -    Cache.applicationProperties.setProperty(BackupFiles.ENABLED,
 +    Cache.setPropertyNoSave(BackupFiles.ENABLED,
              Boolean.toString(enabled));
 -    Cache.applicationProperties.setProperty(
 +    Cache.setPropertyNoSave(
              BackupFilesPresetEntry.SAVEDCONFIG, bfpe.toString());
      /*
 -    Cache.applicationProperties.setProperty(BackupFiles.ENABLED,
 +    Cache.setPropertyNoSave(BackupFiles.ENABLED,
              Boolean.toString(enabled));
 -    Cache.applicationProperties.setProperty(BackupFiles.SUFFIX, suffix);
 -    Cache.applicationProperties.setProperty(BackupFiles.SUFFIX_DIGITS,
 +    Cache.setPropertyNoSave(BackupFiles.SUFFIX, suffix);
 +    Cache.setPropertyNoSave(BackupFiles.SUFFIX_DIGITS,
              Integer.toString(digits));
 -    Cache.applicationProperties.setProperty(BackupFiles.REVERSE_ORDER,
 +    Cache.setPropertyNoSave(BackupFiles.REVERSE_ORDER,
              Boolean.toString(reverse));
 -    Cache.applicationProperties.setProperty(BackupFiles.NO_MAX,
 +    Cache.setPropertyNoSave(BackupFiles.NO_MAX,
              Boolean.toString(noMax));
 -    Cache.applicationProperties.setProperty(BackupFiles.ROLL_MAX,
 +    Cache.setPropertyNoSave(BackupFiles.ROLL_MAX,
              Integer.toString(rollMax));
 -    Cache.applicationProperties.setProperty(BackupFiles.CONFIRM_DELETE_OLD,
 +    Cache.setPropertyNoSave(BackupFiles.CONFIRM_DELETE_OLD,
              "false");
              */
    }
@@@ -73,16 -76,15 +73,16 @@@ public class FileFormatsTes
    @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, GenBank Flatfile, ENA Flatfile, GFF or Jalview features, PDB, mmCIF, Jalview]";
 +    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, BSML]";
++    String readable = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GenBank Flatfile, ENA Flatfile, 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, GenBank Flatfile, ENA Flatfile, GFF or Jalview features, PDB, mmCIF, Jalview]";
 +    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, BSML]";
++    readable = "[PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GenBank Flatfile, ENA Flatfile, GFF or Jalview features, PDB, mmCIF, Jalview, HMMER3, BSML]";
      assertEquals(formats.getWritableFormats(true).toString(), writable);
      assertEquals(formats.getReadableFormats().toString(), readable);
  
@@@ -90,8 -92,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, GenBank Flatfile, ENA Flatfile, GFF or Jalview features, PDB, mmCIF, Jalview, 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, BSML, Fasta]";
++    readable = "[PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GenBank Flatfile, ENA Flatfile, GFF or Jalview features, PDB, mmCIF, Jalview, HMMER3, BSML, Fasta]";
      assertEquals(formats.getWritableFormats(true).toString(), writable);
      assertEquals(formats.getReadableFormats().toString(), readable);
    }
Simple merge
@@@ -46,18 -58,13 +58,14 @@@ import jalview.io.FileFormat
  import jalview.io.FileFormatI;
  import jalview.io.FormatAdapter;
  
- import java.awt.Color;
- import java.io.IOException;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Iterator;
- import java.util.List;
- import org.testng.annotations.BeforeClass;
- import org.testng.annotations.Test;
 +
  public class MappingUtilsTest
  {
+   @BeforeClass(alwaysRun = true)
+   public void setUp()
+   {
+     Cache.initLogger();
+   }
  
    @BeforeClass(alwaysRun = true)
    public void setUpJvOptionPane()
    }
  
    @Test(groups = "Functional")
+   public void testListToArray()
+   {
+     List<int[]> ranges = new ArrayList<>();
+     int[] result = MappingUtils.rangeListToArray(ranges);
+     assertEquals(result.length, 0);
+     ranges.add(new int[] { 24, 12 });
+     result = MappingUtils.rangeListToArray(ranges);
+     assertEquals(result.length, 2);
+     assertEquals(result[0], 24);
+     assertEquals(result[1], 12);
+     ranges.add(new int[] { -7, 30 });
+     result = MappingUtils.rangeListToArray(ranges);
+     assertEquals(result.length, 4);
+     assertEquals(result[0], 24);
+     assertEquals(result[1], 12);
+     assertEquals(result[2], -7);
+     assertEquals(result[3], 30);
+     try
+     {
+       MappingUtils.rangeListToArray(null);
+       fail("Expected exception");
+     } catch (NullPointerException e)
+     {
+       // expected
+     }
+   }
++
++  // new for 2.12
++  @Test(groups = "Functional")
 +  public void testAddRange()
 +  {
 +    int[] range = { 1, 5 };
 +    List<int[]> ranges = new ArrayList<>();
 +  
 +    // add to empty list:
 +    MappingUtils.addRange(range, ranges);
 +    assertEquals(1, ranges.size());
 +    assertSame(range, ranges.get(0));
 +  
 +    // extend contiguous (same position):
 +    MappingUtils.addRange(new int[] { 5, 10 }, ranges);
 +    assertEquals(1, ranges.size());
 +    assertEquals(1, ranges.get(0)[0]);
 +    assertEquals(10, ranges.get(0)[1]);
 +  
 +    // extend contiguous (next position):
 +    MappingUtils.addRange(new int[] { 11, 15 }, ranges);
 +    assertEquals(1, ranges.size());
 +    assertEquals(1, ranges.get(0)[0]);
 +    assertEquals(15, ranges.get(0)[1]);
 +  
 +    // change direction: range is not merged:
 +    MappingUtils.addRange(new int[] { 16, 10 }, ranges);
 +    assertEquals(2, ranges.size());
 +    assertEquals(16, ranges.get(1)[0]);
 +    assertEquals(10, ranges.get(1)[1]);
 +  
 +    // extend reverse contiguous (same position):
 +    MappingUtils.addRange(new int[] { 10, 8 }, ranges);
 +    assertEquals(2, ranges.size());
 +    assertEquals(16, ranges.get(1)[0]);
 +    assertEquals(8, ranges.get(1)[1]);
 +  
 +    // extend reverse contiguous (next position):
 +    MappingUtils.addRange(new int[] { 7, 6 }, ranges);
 +    assertEquals(2, ranges.size());
 +    assertEquals(16, ranges.get(1)[0]);
 +    assertEquals(6, ranges.get(1)[1]);
 +  
 +    // change direction: range is not merged:
 +    MappingUtils.addRange(new int[] { 6, 9 }, ranges);
 +    assertEquals(3, ranges.size());
 +    assertEquals(6, ranges.get(2)[0]);
 +    assertEquals(9, ranges.get(2)[1]);
 +  
 +    // not contiguous: not merged
 +    MappingUtils.addRange(new int[] { 11, 12 }, ranges);
 +    assertEquals(4, ranges.size());
 +    assertEquals(11, ranges.get(3)[0]);
 +    assertEquals(12, ranges.get(3)[1]);
 +  }
  }
@@@ -20,6 -20,8 +20,7 @@@
   */
  package jalview.ws.gui;
  
+ import java.util.Locale;
 -
  import jalview.bin.Cache;
  import jalview.gui.JvOptionPane;
  import jalview.gui.WsJobParameters;
@@@ -66,10 -65,10 +67,10 @@@ public class Jws2ParamVie
    /**
     * which presets to test for services
     */
 -  public static List<String> presetTests = new ArrayList<String>();
 +  public static List<String> presetTests = new ArrayList<>();
    static
    {
-     serviceTests.add("AAConWS".toLowerCase());
+     serviceTests.add("AAConWS".toLowerCase(Locale.ROOT));
    }
  
    public static Jws2Discoverer disc = null;
    public void testJws2Gui()
    {
      Iterator<String> presetEnum = presetTests.iterator();
 -    for (Jws2Instance service : disc.getServices())
 +    for (ServiceWithParameters _service : disc.getServices())
      {
 +      // This will fail for non-jabaws services
 +      Jws2Instance service = (Jws2Instance) _service;
        if (serviceTests.size() == 0
-               || serviceTests.contains(service.getName().toLowerCase()))
+               || serviceTests.contains(service.serviceType.toLowerCase(Locale.ROOT)))
        {
          List<Preset> prl = null;
          Preset pr = null;
@@@ -20,6 -20,8 +20,7 @@@
   */
  package jalview.ws.jabaws;
  
+ import java.util.Locale;
 -
  import static org.testng.AssertJUnit.assertNotNull;
  import static org.testng.AssertJUnit.assertTrue;
  
@@@ -82,16 -83,11 +83,12 @@@ public class DisorderAnnotExportImpor
        // don't get services until discoverer has finished
        Thread.sleep(100);
      }
 -    iupreds = new ArrayList<Jws2Instance>();
 -    for (Jws2Instance svc : disc.getServices())
 +    SlivkaWSDiscoverer disc2 = SlivkaWSDiscoverer.getInstance();
 +    disc2.startDiscoverer();
 +    while (disc2.isRunning())
      {
-       Thread.sleep(100);
-     }
-     iupreds = new ArrayList<>();
-     for (ServiceWithParameters svc : disc2.getServices())
-     {
-       if (svc.getNameURI().toLowerCase().contains("iupred"))
+       if (svc.getServiceTypeURI().toLowerCase(Locale.ROOT).contains("iupredws"))
        {
          iupreds.add(svc);
        }
@@@ -20,6 -20,8 +20,7 @@@
   */
  package jalview.ws.jabaws;
  
+ import java.util.Locale;
 -
  import static org.testng.AssertJUnit.assertNotNull;
  import static org.testng.AssertJUnit.assertTrue;
  
@@@ -97,12 -97,12 +98,12 @@@ public class RNAStructExportImpor
        Thread.sleep(100);
      }
  
 -    for (Jws2Instance svc : disc.getServices())
 +    for (ServiceWithParameters svc : disc.getServices())
      {
  
-       if (svc.getNameURI().toLowerCase().contains("rnaalifoldws"))
+       if (svc.getServiceTypeURI().toLowerCase(Locale.ROOT).contains("rnaalifoldws"))
        {
 -        rnaalifoldws = svc;
 +        rnaalifoldws = (Jws2Instance) svc;
        }
      }
  
@@@ -20,6 -20,8 +20,7 @@@
   */
  package jalview.ws.jws2;
  
+ import java.util.Locale;
 -
  import static org.testng.AssertJUnit.assertEquals;
  import static org.testng.AssertJUnit.assertFalse;
  import static org.testng.AssertJUnit.assertTrue;
@@@ -131,10 -130,10 +132,10 @@@ public class ParameterUtilsTes
     * @param service
     * @return
     */
 -  public boolean isForTesting(Jws2Instance service)
 +  public boolean isForTesting(UIinfo service)
    {
      return serviceTests.size() == 0
-             || serviceTests.contains(service.getName().toLowerCase());
+             || serviceTests.contains(service.serviceType.toLowerCase(Locale.ROOT));
    }
  
    @Test(groups = { "Network" })