Merge branch 'develop' into trialMerge
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Wed, 13 Mar 2019 09:26:03 +0000 (09:26 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Wed, 13 Mar 2019 09:26:03 +0000 (09:26 +0000)
Conflicts:
.classpath
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/api/AlignViewportI.java
src/jalview/fts/core/GFTSPanel.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AnnotationPanel.java
src/jalview/gui/Desktop.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/Finder.java
src/jalview/gui/IdPanel.java
src/jalview/gui/Jalview2XML.java
src/jalview/gui/PCAPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/Preferences.java
src/jalview/gui/ScalePanel.java
src/jalview/gui/SeqPanel.java
src/jalview/io/JalviewFileChooser.java
src/jalview/jbgui/GDesktop.java
src/jalview/jbgui/GFinder.java
src/jalview/jbgui/GPCAPanel.java
src/jalview/util/DBRefUtils.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
test/jalview/io/CrossRef2xmlTests.java
test/jalview/io/Jalview2xmlTests.java
test/jalview/ws/dbsources/UniprotTest.java

71 files changed:
1  2 
.ant-targets-build.xml
.classpath
.gitignore
build.xml
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/analysis/Dna.java
src/jalview/api/AlignViewportI.java
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/AlignViewport.java
src/jalview/appletgui/OverviewPanel.java
src/jalview/bin/Cache.java
src/jalview/bin/Jalview.java
src/jalview/commands/EditCommand.java
src/jalview/controller/AlignViewController.java
src/jalview/datamodel/Alignment.java
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceGroup.java
src/jalview/datamodel/SequenceI.java
src/jalview/ext/ensembl/EnsemblRestClient.java
src/jalview/fts/core/GFTSPanel.java
src/jalview/fts/service/pdb/PDBFTSPanel.java
src/jalview/fts/service/uniprot/UniprotFTSPanel.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/AlignmentPanel.java
src/jalview/gui/AnnotationColourChooser.java
src/jalview/gui/AnnotationExporter.java
src/jalview/gui/AnnotationLabels.java
src/jalview/gui/AnnotationPanel.java
src/jalview/gui/CalculationChooser.java
src/jalview/gui/Desktop.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/FeatureTypeSettings.java
src/jalview/gui/Finder.java
src/jalview/gui/IdCanvas.java
src/jalview/gui/IdPanel.java
src/jalview/gui/OverviewPanel.java
src/jalview/gui/PCAPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/Preferences.java
src/jalview/gui/ScalePanel.java
src/jalview/gui/SeqCanvas.java
src/jalview/gui/SeqPanel.java
src/jalview/gui/SliderPanel.java
src/jalview/gui/StructureViewerBase.java
src/jalview/gui/TreeCanvas.java
src/jalview/gui/TreePanel.java
src/jalview/io/AnnotationFile.java
src/jalview/io/FeaturesFile.java
src/jalview/io/JalviewFileChooser.java
src/jalview/io/vcf/VCFLoader.java
src/jalview/jbgui/GAlignFrame.java
src/jalview/jbgui/GDesktop.java
src/jalview/jbgui/GFinder.java
src/jalview/jbgui/GPCAPanel.java
src/jalview/jbgui/GPreferences.java
src/jalview/project/Jalview2XML.java
src/jalview/util/DBRefUtils.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
src/jalview/ws/DBRefFetcher.java
src/jalview/ws/dbsources/Uniprot.java
test/jalview/analysis/CrossRefTest.java
test/jalview/datamodel/AlignmentTest.java
test/jalview/datamodel/SequenceTest.java
test/jalview/io/CrossRef2xmlTests.java
test/jalview/io/vcf/VCFLoaderTest.java
test/jalview/project/Jalview2xmlTests.java
test/jalview/util/DBRefUtilsTest.java
test/jalview/ws/dbsources/UniprotTest.java

diff --combined .ant-targets-build.xml
@@@ -1,18 -1,14 +1,18 @@@
  build
 +build-site
  buildPropertiesFile
  buildTests
  buildextclients
  buildindices
- castorbinding
  clean
 +clean-site
 +compile-site
  compileApplet
  distclean
 +eclipse-install
  help
  init
+ jaxb-bindings
  linkcheck
  makeApplet
  makedist
@@@ -20,7 -16,6 +20,7 @@@ makefulldis
  obfuscate
  packageApplet
  prepare
 +prepare-site
  prepareTests
  preparejnlp
  prepubapplet_1
@@@ -32,6 -27,5 +32,6 @@@ sourcedo
  sourcescrub
  testclean
  testng
 +unzip-to-site
  usage
  writejnlpf
diff --combined .classpath
@@@ -1,9 -1,8 +1,9 @@@
  <?xml version="1.0" encoding="UTF-8"?>
  <classpath>
        <classpathentry kind="src" path="src"/>
 -      <classpathentry kind="src" path="utils"/>
 +      <classpathentry kind="src" path="src2"/>
        <classpathentry kind="src" path="test"/>
 +      <classpathentry kind="src" path="utils"/>
        <classpathentry kind="lib" path="lib/activation.jar"/>
        <classpathentry kind="lib" path="lib/axis.jar" sourcepath="D:/axis-1_2RC2-src/axis-1_2RC2"/>
        <classpathentry kind="lib" path="lib/commons-discovery.jar"/>
@@@ -14,7 -13,6 +14,6 @@@
        <classpathentry kind="lib" path="lib/saaj.jar"/>
        <classpathentry kind="lib" path="lib/wsdl4j.jar"/>
        <classpathentry kind="lib" path="lib/xercesImpl.jar"/>
-       <classpathentry kind="lib" path="lib/castor-1.1-cycle-xml.jar" sourcepath="C:/Documents and Settings/JimP/workspace-3.3/castor/src/main/java"/>
        <classpathentry kind="lib" path="lib/JGoogleAnalytics_0.3.jar" sourcepath="/JGoogleAnalytics/src/main/java"/>
        <classpathentry kind="lib" path="lib/vamsas-client.jar"/>
        <classpathentry kind="lib" path="lib/commons-logging-1.1.1.jar"/>
@@@ -37,8 -35,6 +36,6 @@@
        <classpathentry kind="lib" path="lib/miglayout-4.0-swing.jar"/>
        <classpathentry kind="lib" path="lib/jswingreader-0.3.jar" sourcepath="/jswingreader"/>
        <classpathentry kind="lib" path="lib/commons-codec-1.3.jar"/>
-       <classpathentry kind="lib" path="lib/spring-core-3.0.5.RELEASE.jar"/>
-       <classpathentry kind="lib" path="lib/spring-web-3.0.5.RELEASE.jar"/>
        <classpathentry kind="lib" path="lib/jabaws-min-client-2.2.0.jar" sourcepath="/clustengine"/>
        <classpathentry kind="lib" path="lib/json_simple-1.1.jar" sourcepath="/Users/jimp/Downloads/json_simple-1.1-all.zip"/>
        <classpathentry kind="lib" path="lib/slf4j-api-1.7.7.jar"/>
        <classpathentry kind="lib" path="lib/jetty-http-9.2.10.v20150310.jar"/>
        <classpathentry kind="lib" path="lib/jetty-io-9.2.10.v20150310.jar"/>
        <classpathentry kind="lib" path="lib/java-json.jar"/>
 -      <classpathentry kind="lib" path="lib/Jmol-14.6.4_2016.10.26.jar"/>
        <classpathentry kind="con" path="org.testng.TESTNG_CONTAINER"/>
        <classpathentry kind="lib" path="lib/biojava-core-4.1.0.jar"/>
        <classpathentry kind="lib" path="lib/biojava-ontology-4.1.0.jar"/>
        <classpathentry kind="lib" path="lib/htsjdk-2.12.0.jar"/>
        <classpathentry kind="lib" path="lib/groovy-all-2.4.12-indy.jar"/>
        <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
 +      <classpathentry exported="true" kind="con" path="GROOVY_DSL_SUPPORT"/>
 +      <classpathentry kind="lib" path="lib/Jmol-14.29.17.jar"/>
+       <classpathentry kind="lib" path="lib/intervalstore-v0.4.jar"/>
        <classpathentry kind="output" path="classes"/>
  </classpath>
diff --combined .gitignore
@@@ -1,11 -1,13 +1,13 @@@
- .project
+ /*.project
+ .classpath
  /dist
  /clover
  /classes
  /tests
  /test-reports
  /test-output
- .externalToolBuilders/Jalview Release indices [Builder].launch
+ .externalToolBuilders/*
+ .settings/*
  /.DS_Store
  .DS_Store
  /.com.apple.timemachine.supported
@@@ -14,12 -16,4 +16,12 @@@ TESTN
  /jalviewApplet.jar
  /benchmarking/lib
  *.class
 -/site
 +/site/
 +/.settings/
 +/site0/
 +/sitev/
 +/.classpath
 +*.class
 +/site1/
 +/site2/
 +/site3/
diff --combined build.xml
+++ b/build.xml
      <property name="reportDir" value="test-reports" />
      <property name="testDir" value="test" />
      <property name="testOutputDir" value="tests" />
 +      <property name="cloverOutputDir" value="clover" if:set="clover.jar" />
      <!-- switch to indicate if we should obfuscate jalviewLite -->
      <!-- <property name="donotobfuscate" value="true"/> -->
      <!-- switch to exclude associations from generated jnlp files -->
          <include name="**/*.*" />
        </fileset>
      </copy>
 +      <mkdir dir="${cloverOutputDir}" if:set="clover.jar"/>
    </target>
  
    <target name="build" depends="prepare">
  
    <target name="buildindices" depends="init, prepare" unless="help.uptodate">
      <replace value="${JALVIEW_VERSION}">
 -      <replacetoken><![CDATA[$$Version-Rel$$]]></replacetoken>
 +      <replacetoken>
 +        <![CDATA[$$Version-Rel$$]]>
 +      </replacetoken>
        <fileset dir="${outputDir}/${helpDir}">
          <include name="help.jhm" />
        </fileset>
          </resources>
          <resources os="Mac OS X">
            <fileset dir="${packageDir}">
 -               <include name="quaqua-filechooser-only-8.0.jar"/>
 +            <include name="quaqua-filechooser-only-8.0.jar"/>
              <include name="*quaqua64*.jnilib.jar" />
            </fileset>
          </resources>
      <jnlpf toFile="${jnlpFile}" />
      <!-- add the add-modules j2se attribute for java 9 -->
      <replace file="${jnlpFile}" value="j2se version=&quot;1.8+&quot; initial-heap-size=&quot;${inih}&quot; max-heap-size=&quot;${maxh}&quot; java-vm-args=&quot;--add-modules=java.se.ee --illegal-access=warn&quot;">
 -          <replacetoken>j2se version="1.8+"</replacetoken>
 +      <replacetoken>j2se version="1.8+"</replacetoken>
      </replace>
    </target>
  
    <delete file="in.jar" />
  </target>
  
- <target name="castorbinding" depends="init" description="Generate Java bindings to supported Jalview XML models.">
-   <taskdef name="castor-srcgen" classname="org.castor.anttask.CastorCodeGenTask" classpathref="build.classpath" />
+ <target name="jaxb-bindings" depends="init" description="Generates JAXB bindings for supported Jalview XML models (needs xjc on the path)">
    <delete>
-     <fileset dir="${sourceDir}/jalview/schemabinding/version2">
+     <fileset dir="${sourceDir}/jalview/xml/binding/jalview">
        <include name="*.java" />
-       <include name="descriptors/*.java" />
      </fileset>
    </delete>
-   <castor-srcgen file="${schemaDir}/vamsas.xsd" todir="${sourceDir}" package="jalview.schemabinding.version2" warnings="false" nodesc="false" verbose="true" properties="${schemaDir}/jalview.properties" />
-   <castor-srcgen file="${schemaDir}/JalviewUserColours.xsd" todir="${sourceDir}" package="jalview.schemabinding.version2" warnings="false" nodesc="false" verbose="true" properties="${schemaDir}/jalview.properties" />
-   <castor-srcgen file="${schemaDir}/JalviewWsParamSet.xsd" todir="${sourceDir}" package="jalview.schemabinding.version2" warnings="false" nodesc="false" verbose="true" properties="${schemaDir}/jalview.properties" />
-   <castor-srcgen file="${schemaDir}/jalview.xsd" todir="${sourceDir}" package="jalview.schemabinding.version2" warnings="false" nodesc="false" verbose="true" properties="${schemaDir}/jalview.properties" />
-   <!-- 
-               now build the jalview.binding package with the old schema set
-               -->
+   <exec executable="xjc">
+     <arg value="${schemaDir}/jalview.xsd"/>
+     <arg value="-d"/>
+     <arg value="${sourceDir}"/>
+     <arg value="-p"/>
+     <arg value="jalview.xml.binding.jalview"/>
+   </exec>
    <delete>
-     <fileset dir="${sourceDir}/jalview/binding/">
-       <include name="**" />
+     <fileset dir="${sourceDir}/jalview/xml/binding/embl">
+       <include name="*.java" />
      </fileset>
    </delete>
-   <castor-srcgen file="${schemaDir}/vamsasJvV1.xsd" todir="${sourceDir}" package="jalview.binding" warnings="false" nodesc="true" verbose="true" properties="${schemaDir}/jalview.nodesc.properties" />
-   <castor-srcgen file="${schemaDir}/JalviewUserColours.xsd" todir="${sourceDir}" package="jalview.binding" warnings="false" nodesc="true" verbose="true" properties="${schemaDir}/jalview.nodesc.properties" />
-   <castor-srcgen file="${schemaDir}/jalviewJvV1.xsd" todir="${sourceDir}" package="jalview.binding" warnings="false" nodesc="true" verbose="true" properties="${schemaDir}/jalview.nodesc.properties" />
+   <exec executable="xjc">
+     <arg value="${schemaDir}/embl.xsd"/>
+     <arg value="-d"/>
+     <arg value="${sourceDir}"/>
+     <arg value="-b"/>
+     <arg value="${schemaDir}/embl_bindings.xml"/>
+     <arg value="-p"/>
+     <arg value="jalview.xml.binding.embl"/>
+   </exec>
+   <delete>
+     <fileset dir="${sourceDir}/jalview/xml/binding/uniprot">
+       <include name="*.java" />
+     </fileset>
+   </delete>
+   <exec executable="xjc">
+     <arg value="${schemaDir}/uniprot.xsd"/>
+     <arg value="-d"/>
+     <arg value="${sourceDir}"/>
+     <arg value="-p"/>
+     <arg value="jalview.xml.binding.uniprot"/>
+   </exec>
  </target>
  <target name="sourcedist" description="create jalview source distribution" depends="init">
    <delete file="${source.dist.name}" />
    <!-- temporary copy of source to update timestamps -->
    <!-- each replacetoken CDATA body must be on one line - 
         otherwise the pattern doesn't match -->
    <replace value="${JALVIEW_VERSION}">
 -    <replacetoken><![CDATA[$$Version-Rel$$]]></replacetoken>
 +    <replacetoken>
 +      <![CDATA[$$Version-Rel$$]]>
 +    </replacetoken>
      <fileset dir="_sourcedist">
        <include name="**/*" />
      </fileset>
    </replace>
    <replace dir="_sourcedist" value="${build.year}">
 -    <replacetoken><![CDATA[$$Year-Rel$$]]></replacetoken>
 +    <replacetoken>
 +      <![CDATA[$$Year-Rel$$]]>
 +    </replacetoken>
      <fileset dir="_sourcedist">
        <include name="**/*" />
      </fileset>
    <javac srcdir="utils" destdir="utils" includes="HelpLinksChecker.java"/>
    <java fork="true" dir="${helpDir}" classpath="utils" classname="HelpLinksChecker" failonerror="true">
      <arg file="${helpDir}"/>
 -    <arg value="-nointernet"/>
 +    <arg value="-nointernet" />
    </java>
  </target>
 +
 +<target name="eclipse-install" depends="init,prepare">
 +
 +  <property name="eclipseTempFile" value="eclipse-jee-oxygen-R-linux-gtk-x86_64.tar.gz"/>
 +  <property name="eclipseInstallURL" value="http://mirror.csclub.uwaterloo.ca/eclipse/technology/epp/downloads/release/oxygen/R/eclipse-jee-oxygen-R-linux-gtk-x86_64.tar.gz"/>
 +  <property name="java2scriptURL" value="https://github.com/BobHanson/java2script/blob/master/sources/net.sf.j2s.core/dist/dropins/net.sf.j2s.core.jar?raw=true"/>
 +
 +  <get url="${eclipseInstallURL}" dest="${eclipseTempFile}"/>
 +  <untar compression="gzip" src="${eclipseTempFile}" dest="${eclipse-inst}"/>
 +
 +  <!-- not needed since we ship transpiler with source
 +    <get url="${java2scriptURL}" dest="eclipse-inst/dropins/net.sf.j2s.core.jar" /> -->
 +
 +</target>
 +
 +<target name="build-site" depends="init,prepare,compile-site,unzip-to-site,make-j2s-cores">
 +  <!-- tarball -->
 +  <tar compression="gzip" destfile="site.tar.gz">
 +    <tarfileset dir="site" />
 +  </tar>
 +</target>
 +<target name="prepare-site" depends="init,prepare">
 +  <property name="swingjsdir" value="swingjs"/>
 +  <property name="eclipse-inst" value="/home/bamboo/buildtools/eclipse/eclipse-js"/>
 +  <property name="eclipse-exec" value="${eclipse-inst}/eclipse"/>
 +  <property name="site" value="site"/>
 +  <!-- where the eclipse js workspace has been initialised -->
 +  <property name="eclipse-work" value="/home/bamboo/buildtools/eclipse/eclipse-js-workspace"/>
 +  <!-- git repository linked to project in workspace -->
 +  <property name="eclipse-workrepo" value="/home/bamboo/buildtools/eclipse/eclipse-js-workspace/jalview-js"/>
 +</target>
 +  <target name="clean-site" depends="prepare-site">
 +    <delete dir="${eclipse-workrepo}/${site}"/>
 +    <mkdir dir="${eclipse-workrepo}/${site}"/>
 +  </target>
 +<target name="compile-site" depends="prepare-site,clean-site">
 +  <!-- update transpiler -->
 +  <copy file="${swingjsdir}/net.sf.j2s.core.jar" todir="${eclipse-inst}/dropins" overwrite="true" failonerror="true"/>
 +  <!-- update the git repo linked to the eclipse workspace -->
 +  <exec executable="/usr/bin/git" outputproperty="git.commit" failifexecutionfails="true">
 +    <arg value="rev-parse" />
 +    <arg value="--short" />
 +    <arg value="HEAD" />
 +  </exec>
 +  <!-- update and checkout the same commit in the workspace project -->
 +  <exec executable="/usr/bin/git" failifexecutionfails="true" dir="${eclipse-workrepo}">
 +    <arg value="reset" />
 +    <arg value="--hard" />
 +  </exec>
 +  <exec executable="/usr/bin/git" failifexecutionfails="true" dir="${eclipse-workrepo}">
 +      <arg value="pull" />
 +  </exec>
 +  <exec executable="/usr/bin/git" failifexecutionfails="true" dir="${eclipse-workrepo}">
 +    <arg value="checkout" />
 +    <arg value="${git.commit}" />
 +  </exec>
 +  <!-- custom classpath for .js builds -->
 +  <copy file=".classpath.js" tofile="${eclipse-workrepo}/.classpath" overwrite="true"/>
 +  <!-- clean eclipse log -->
 +  <delete file="${eclipse-work}/.metadata/.log"/>
 +    
 +  <!-- execute the eclipse build - the build may fail but valid javascript may still be produced, so we ignore return codes -->
 +  <exec executable="${eclipse-exec}" failonerror="no">
 +    <arg value="-nosplash"/>
 +    <arg value="--launcher.suppressErrors"/>
 +    <arg value="-application"/>
 +    <arg value="org.eclipse.jdt.apt.core.aptBuild"/>
 +    <arg value="-data"/>
 +    <arg value="${eclipse-work}"/>
 +  </exec>
 +  <!-- report log -->
 +  <exec executable="/bin/cat">
 +    <arg value="${eclipse-work}/.metadata/.log"/>
 +  </exec>
 +  <!-- TODO: run jslint and something else here to check we have a complete set of .js files for java -->
 +  <!-- possibly compare timestamps between .js files and their mate in source - any newer or not present triggers a new build -->
 +  <!-- <mkdir dir="${packageDir}/${site}" /> -->
 +  <!--   <property name="swingjs.zipurl" value="https://github.com/BobHanson/java2script/blob/master/sources/net.sf.j2s.java.core/dist/SwingJS-site.zip?raw=true" /> -->
 +
 +  <!-- and reset the .classpath -->
 +  <exec executable="/usr/bin/git" failifexecutionfails="true" dir="${eclipse-workrepo}">
 +    <arg value="checkout" />
 +    <arg value="${git.commit}"/>
 +    <arg value="--"/>
 +    <arg value=".classpath" />
 +  </exec>
 +
 +  <!-- finally copy artefacts from eclipse project checkout to the build site -->
 +  <copy todir="${site}">
 +    <fileset dir="${eclipse-workrepo}/site"/>
 +  </copy>
 +</target>
 +<target name="unzip-to-site" depends="prepare-site">
 +  <property name="swingjs.zip" value="${swingjsdir}/SwingJS-site.zip" />
 +  <unzip dest="${site}/" overwrite="true">
 +    <fileset dir="libjs">
 +      <include name="*.zip" />
 +    </fileset>
 +  </unzip>
 +  <copy overwrite="true" todir="${site}/swingjs/j2s/">
 +    <fileset dir="${resourceDir}">
 +      <include name="**"/>
 +    </fileset>
 +  </copy>
 +
 +  <!-- copy test files into place in site -->
 +  <copy todir="${site}/examples">
 +    <fileset dir="examples">
 +      <include name="*.*"/>
 +    </fileset>
 +  </copy>
 +  <!-- lastly, update SwingJS -->
 +  <unzip dest="${site}/" overwrite="true">
 +    <fileset dir="${swingjsdir}">
 +      <include name="SwingJS-site.zip"/>
 +    </fileset>
 +  </unzip>
 +</target>
 +
 +  <target name="make-j2s-cores" depends="">
 +    <ant antfile="buildcore.xml" target="build-all-cores"/>
 +  </target>
  </project>
@@@ -30,6 -30,7 +30,7 @@@ action.minimize_associated_windows = Mi
  action.close_all = Close all
  action.load_project = Load Project
  action.save_project = Save Project
+ action.save_project_as = Save Project as...
  action.quit = Quit
  action.expand_views = Expand Views
  action.gather_views = Gather Views
@@@ -118,10 -119,8 +119,8 @@@ action.select = Selec
  action.new_view = New View
  action.close = Close
  action.add = Add
- action.save_as_default = Save as default
  action.save_as = Save as...
  action.save = Save
- action.cancel_fetch = Cancel Fetch
  action.change_font = Change Font
  action.change_font_tree_panel = Change Font (Tree Panel)
  action.colour = Colour
@@@ -140,7 -139,6 +139,6 @@@ action.fetch_db_references = Fetch DB R
  action.view_flanking_regions = Show flanking regions
  label.view_flanking_regions = Show sequence data either side of the subsequences involved in this alignment
  label.structures_manager = Structures Manager
- label.nickname = Nickname:
  label.url = URL
  label.url\: = URL:
  label.input_file_url = Enter URL or Input File
@@@ -162,7 -160,6 +160,6 @@@ label.current_parameter_set_name = Curr
  label.service_action = Service Action:
  label.post_url = POST URL:
  label.url_suffix = URL Suffix
- label.sequence_source = Sequence Source
  label.per_seq = per Sequence
  label.result_vertically_separable = Results are vertically separable
  label.amend = Amend
@@@ -172,10 -169,9 +169,9 @@@ label.principal_component_analysis = Pr
  label.average_distance_identity = Average Distance Using % Identity
  label.neighbour_joining_identity = Neighbour Joining Using % Identity
  label.choose_calculation = Choose Calculation
- label.treecalc_title = {0} Using {1}
+ label.calc_title = {0} Using {1}
  label.tree_calc_av = Average Distance
  label.tree_calc_nj = Neighbour Joining
- label.select_score_model = Select score model
  label.score_model_pid = % Identity
  label.score_model_blosum62 = BLOSUM62
  label.score_model_pam250 = PAM 250
@@@ -203,6 -199,7 +199,7 @@@ label.colourScheme_purine/pyrimidine = 
  label.colourScheme_nucleotide = Nucleotide
  label.colourScheme_t-coffee_scores = T-Coffee Scores
  label.colourScheme_rna_helices = By RNA Helices
+ label.colourScheme_sequence_id = Sequence ID Colour
  label.blc = BLC
  label.fasta = Fasta
  label.msf = MSF
@@@ -353,7 -350,6 +350,6 @@@ label.status = Statu
  label.channels = Channels
  label.channel_title_item_count = {0} ({1})
  label.blog_item_published_on_date = {0} {1} 
- label.select_das_service_from_table = Select a DAS service from the table to read a full description here.</font></html>
  label.session_update = Session Update
  label.new_vamsas_session = New Vamsas Session
  action.load_vamsas_session = Load Vamsas Session...
@@@ -363,8 -359,7 +359,8 @@@ label.open_saved_vamsas_session = Open 
  label.groovy_console = Groovy Console...
  label.lineart = Lineart
  label.dont_ask_me_again = Don't ask me again
 -label.select_eps_character_rendering_style = Select EPS character rendering style
 +label.select_character_rendering_style = {0} character rendering style
 +label.select_character_style_title = {0} Rendering options
  label.invert_selection = Invert Selection
  label.optimise_order = Optimise Order
  label.seq_sort_by_score = Sequence sort by Score
@@@ -372,7 -367,6 +368,6 @@@ label.load_colours = Load Colour
  label.save_colours = Save Colours
  label.load_colours_tooltip = Load feature colours and filters from file
  label.save_colours_tooltip = Save feature colours and filters to file
- label.fetch_das_features = Fetch DAS Features
  label.selected_database_to_fetch_from = Selected {0} database {1} to fetch from {2} 
  label.database_param = Database: {0}
  label.example = Example
@@@ -410,14 -404,11 +405,11 @@@ label.couldnt_find_pdb_id_in_file = Cou
  label.no_pdb_id_in_file = No PDB Id in File
  label.couldnt_read_pasted_text = Couldn't read the pasted text {0}
  label.error_parsing_text = Error parsing text
- label.enter_local_das_source = Enter Nickname & URL of Local DAS Source
- label.you_can_only_edit_or_remove_local_das_sources = You can only edit or remove local DAS Sources!
- label.public_das_source = Public DAS source - not editable
  label.input_alignment_from_url = Input Alignment From URL
  label.input_alignment = Input Alignment
  label.couldnt_import_as_vamsas_session = Couldn't import {0} as a new vamsas session.
  label.vamsas_document_import_failed = Vamsas Document Import Failed
 -label.couldnt_locate = Could not locate {0}
 +label.couldnt_locate = Couldn''t locate {0}
  label.url_not_found = URL not found
  label.new_sequence_url_link = New sequence URL link
  label.cannot_edit_annotations_in_wrapped_view = Cannot edit annotations in wrapped view
@@@ -429,8 -420,6 +421,6 @@@ label.invalid_url = Invalid URL 
  label.error_loading_file = Error loading file
  label.problems_opening_file = Encountered problems opening {0}!!
  label.file_open_error = File open error
- label.no_das_sources_selected_warn = No das sources were selected.\nPlease select some sources and\ntry again.
- label.no_das_sources_selected_title = No DAS Sources Selected
  label.colour_scheme_exists_overwrite = Colour scheme {0} exists.\nContinue saving colour scheme as {1}?"
  label.duplicate_scheme_name = Duplicate scheme name
  label.jalview_new_questionnaire = There is a new Questionnaire available. Would you like to complete it now ?\n
@@@ -503,6 -492,10 +493,10 @@@ label.edit_name_description = Edit Name
  label.create_sequence_feature = Create Sequence Feature...
  label.edit_sequence = Edit Sequence
  label.edit_sequences = Edit Sequences
+ label.insert_gap = Insert 1 gap
+ label.insert_gaps = Insert {0} gaps
+ label.delete_gap = Delete 1 gap
+ label.delete_gaps = Delete {0} gaps
  label.sequence_details = Sequence Details
  label.jmol_help = Jmol Help
  label.chimera_help = Chimera Help
@@@ -601,7 -594,7 +595,7 @@@ label.check_for_questionnaires = Check 
  label.check_for_latest_version = Check for latest version
  label.url_linkfrom_sequence_id = URL link from Sequence ID
  label.use_proxy_server = Use a proxy server
 -label.eps_rendering_style = EPS rendering style
 +label.rendering_style = {0} rendering style
  label.append_start_end = Append /start-end (/15-380)
  label.full_sequence_id = Full Sequence Id
  label.smooth_font = Smooth Font
@@@ -623,7 -616,6 +617,6 @@@ label.visual = Visua
  label.connections = Connections
  label.output = Output
  label.editing = Editing
- label.das_settings = DAS Settings
  label.web_services = Web Services
  label.right_click_to_edit_currently_selected_parameter = Right click to edit currently selected parameter.
  label.let_jmol_manage_structure_colours = Let Jmol manage structure colours
@@@ -639,10 -631,6 +632,6 @@@ label.delete_service_url = Delete Servi
  label.details = Details
  label.options = Options
  label.parameters = Parameters
- label.available_das_sources = Available DAS Sources
- label.full_details = Full Details
- label.authority = Authority
- label.type = Type
  label.proxy_server = Proxy Server
  label.file_output = File Output
  label.select_input_type = Select input type
@@@ -687,7 -675,7 +676,7 @@@ label.sequence_details_for = Sequence D
  label.sequence_name = Sequence Name
  label.sequence_description = Sequence Description
  label.edit_sequence_name_description = Edit Sequence Name/Description
 -label.spaces_converted_to_backslashes = Spaces have been converted to _
 +label.spaces_converted_to_underscores = Spaces have been converted to _
  label.no_spaces_allowed_sequence_name = No spaces allowed in Sequence Name
  label.select_outline_colour = Select Outline Colour
  label.web_browser_not_found_unix = Unixers\: Couldn't find default web browser.\nAdd the full path to your browser in Preferences."
@@@ -711,9 -699,6 +700,6 @@@ label.sort_alignment_new_tree = Sort Al
  label.add_sequences = Add Sequences
  label.new_window = New Window
  label.split_window = Split Window
- label.refresh_available_sources = Refresh Available Sources
- label.use_registry = Use Registry
- label.add_local_source = Add Local Source
  label.set_as_default = Set as Default
  label.show_labels = Show labels
  action.background_colour = Background Colour...
@@@ -772,7 -757,7 +758,7 @@@ label.run_with_preset_params = Run {0} 
  label.view_and_change_parameters_before_running_calculation = View and change parameters before running calculation
  label.view_documentation = View documentation
  label.select_return_type = Select return type
- label.translation_of_params = Translation of {0}
+ label.translation_of_params = Translation of {0} (Table {1})
  label.features_for_params = Features for - {0}
  label.annotations_for_params = Annotations for - {0}
  label.generating_features_for_params = Generating features for - {0}
@@@ -854,7 -839,6 +840,6 @@@ label.multiharmony = Multi-Harmon
  label.unable_start_web_service_analysis = Unable to start web service analysis
  label.job_couldnt_be_started_check_input = The Job couldn't be started. Please check your input, and the Jalview console for any warning messages.
  label.prompt_each_time = Prompt each time
- label.use_source = Use Source
  label.couldnt_save_project = Couldn't save project
  label.error_whilst_saving_current_state_to = Error whilst saving current state to {0}
  label.error_whilst_loading_project_from = Error whilst loading project from {0}
@@@ -880,7 -864,6 +865,6 @@@ label.error_unsupported_owwner_user_col
  label.save_alignment_to_file = Save Alignment to file
  label.save_features_to_file = Save Features to File
  label.save_annotation_to_file = Save Annotation to File
- label.no_features_on_alignment = No features found on alignment
  label.save_pdb_file = Save PDB File
  label.save_text_to_file = Save Text to File
  label.save_state = Save State
@@@ -894,6 -877,8 +878,6 @@@ label.save_feature_colours = Save Featu
  label.select_startup_file = Select startup file
  label.select_default_browser = Select default web browser
  label.save_tree_as_newick = Save tree as newick file
 -label.create_eps_from_tree = Create EPS file from tree
 -label.create_png_from_tree = Create PNG image from tree
  label.save_colour_scheme = Save colour scheme
  label.edit_params_for = Edit parameters for {0}
  label.choose_filename_for_param_file = Choose a filename for this parameter file
@@@ -949,6 -934,8 +933,6 @@@ error.call_setprogressbar_before_regist
  label.cancelled_params = Cancelled {0}
  error.implementation_error_cannot_show_view_alignment_frame = Implementation error: cannot show a view from another alignment in an AlignFrame.
  error.implementation_error_dont_know_about_threshold_setting = Implementation error: don't know about threshold setting for current AnnotationColourGradient.
 -error.eps_generation_not_implemented = EPS Generation not yet implemented
 -error.png_generation_not_implemented = PNG Generation not yet implemented
  error.try_join_vamsas_session_another = Trying to join a vamsas session when another is already connected
  error.invalid_vamsas_session_id = Invalid vamsas session id
  label.groovy_support_failed = Jalview Groovy Support Failed
@@@ -1019,7 -1006,7 +1003,7 @@@ label.pca_recalculating = Recalculatin
  label.pca_calculating = Calculating PCA
  label.select_foreground_colour = Choose foreground colour
  label.select_colour_for_text = Select Colour for Text
 -label.adjunst_foreground_text_colour_threshold = Adjust Foreground Text Colour Threshold
 +label.adjust_foreground_text_colour_threshold = Adjust Foreground Text Colour Threshold
  label.select_subtree_colour = Select Sub-Tree Colour
  label.create_new_sequence_features = Create New Sequence Feature(s)
  label.amend_delete_features = Amend/Delete Features for {0}
@@@ -1076,8 -1063,6 +1060,6 @@@ exception.unable_to_create_internet_con
  exception.invocation_target_calling_url = InvocationTargetException while calling openURL: {0}
  exception.illegal_access_calling_url = IllegalAccessException while calling openURL: {0}
  exception.interrupted_launching_browser = InterruptedException while launching browser: {0}
- exception.das_source_doesnt_support_sequence_command = Source {0} does not support the sequence command.
- exception.invalid_das_source = Invalid das source: {0}
  exception.ebiembl_retrieval_failed_on = EBI EMBL XML retrieval failed on {0}:{1}
  exception.no_pdb_records_for_chain = No PDB Records for {0} chain {1}
  exception.unexpected_handling_rnaml_translation_for_pdb = Unexpected exception when handling RNAML translation of PDB data
@@@ -1093,6 -1078,7 +1075,6 @@@ error.implementation_error_cannot_find_
  exception.jobsubmission_invalid_params_set = Invalid parameter set. Check Jalview implementation
  exception.notvaliddata_group_contains_less_than_min_seqs = Group contains less than {0} sequences.
  exception.outofmemory_loading_pdb_file = Out of memory loading PDB File
 -exception.eps_coudnt_write_output_file = Could not write to the output file: {0}
  exception.eps_method_not_supported = Method not currently supported by EpsGraphics2D version {0}
  exception.eps_unable_to_get_inverse_matrix = Unable to get inverse of matrix: {0}
  warn.job_cannot_be_cancelled_close_window = This job cannot be cancelled.\nJust close the window.
@@@ -1117,7 -1103,8 +1099,7 @@@ status.searching_for_sequences_from = S
  status.finished_searching_for_sequences_from = Finished searching for sequences from {0}
  label.eps_file = EPS file
  label.png_image = PNG image
 -status.saving_file = Saving {0}
 -status.export_complete = {0} Export completed.
 +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}
@@@ -1130,10 -1117,6 +1112,6 @@@ status.parsing_results = Parsing result
  status.processing = Processing...
  status.refreshing_web_service_menus = Refreshing Web Service Menus
  status.collecting_job_results = Collecting job results.
- status.fetching_das_sequence_features = Fetching DAS Sequence Features
- status.no_das_sources_active = No DAS Sources Active
- status.das_feature_fetching_cancelled = DAS Feature Fetching Cancelled
- status.das_feature_fetching_complete = DAS Feature Fetching Complete
  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
@@@ -1156,8 -1139,6 +1134,6 @@@ warn.urls_not_contacted = URLs that cou
  warn.urls_no_jaba = URLs without any JABA Services
  info.validate_jabaws_server = Validate JabaWS Server ?\n(Look in console output for results)
  label.test_server = Test Server?
- info.you_want_jalview_to_find_uniprot_accessions = Do you want Jalview to find\nUniprot Accession ids for given sequence names?
- label.find_uniprot_accession_ids = Find Uniprot Accession Ids
  label.new_sequence_fetcher = New Sequence Fetcher
  label.additional_sequence_fetcher = Additional Sequence Fetcher
  label.select_database_retrieval_source = Select Database Retrieval Source
@@@ -1250,6 -1231,7 +1226,6 @@@ exception.fts_server_unreachable = Jalv
  label.nw_mapping = Needleman & Wunsch Alignment
  label.sifts_mapping = SIFTs Mapping
  label.mapping_method = Sequence \u27f7 Structure mapping method
 -status.waiting_for_user_to_select_output_file = Waiting for user to select {0} file
  status.cancelled_image_export_operation = Cancelled {0} export operation
  info.error_creating_file = Error creating {0} file
  exception.outofmemory_loading_mmcif_file = Out of memory loading mmCIF File
@@@ -1292,7 -1274,6 +1268,6 @@@ label.edit_sequence_url_link = Edit seq
  warn.name_cannot_be_duplicate = User-defined URL names must be unique and cannot be MIRIAM ids
  label.output_seq_details = Output Sequence Details to list all database references
  label.urllinks = Links
- label.default_cache_size = Default Cache Size
  action.clear_cached_items = Clear Cached Items
  label.togglehidden = Show hidden regions
  label.quality_descr = Alignment Quality based on Blosum62 scores
@@@ -1334,7 -1315,7 +1309,7 @@@ label.join_conditions = Join condition
  label.score = Score
  label.colour_by_label = Colour by label
  label.variable_colour = Variable colour...
 -label.select_colour = Select colour
 +label.select_colour_for = Select colour for {0}
  option.enable_disable_autosearch = When ticked, search is performed automatically
  option.autosearch = Autosearch
  label.retrieve_ids = Retrieve IDs
@@@ -1355,12 -1336,60 +1330,67 @@@ label.most_bound_molecules = Most Boun
  label.most_polymer_residues = Most Polymer Residues
  label.cached_structures = Cached Structures
  label.free_text_search = Free Text Search
 +label.annotation_name = Annotation Name
 +label.annotation_description = Annotation Description 
 +label.edit_annotation_name_description = Edit Annotation Name/Description
 +label.alignment = alignment
 +label.pca = PCA
 +label.create_image_of = Create {0} image of {1}
 +label.click_to_edit = Click to edit, right-click for menu
+ label.backupfiles_confirm_delete = Confirm delete
+ label.backupfiles_confirm_delete_old_files = Delete the following older backup files? (see the Backups tab in Preferences for more options)
+ label.backupfiles_confirm_save_file = Confirm save file
+ label.backupfiles_confirm_save_file_backupfiles_roll_wrong = Something possibly went wrong with the backups of this file.
+ label.backupfiles_confirm_save_new_saved_file_ok = The new saved file seems okay.
+ label.backupfiles_confirm_save_new_saved_file_not_ok = The new saved file might not be okay.
+ label.backups = Backups
+ label.backup = Backup
+ label.backup_files = Backup Files
+ label.enable_backupfiles = Enable backup files
+ label.backup_filename_strategy = Backup filename strategy
+ label.append_to_filename = Append to filename (%n is replaced by the backup number)
+ label.append_to_filename_tooltip = %n in the text will be replaced by the backup number. The text will appear after the filename. See the summary box above.
+ label.index_digits = Number of digits to use for the backup number (%n)
+ label.summary_of_backups_scheme = Summary of backup scheme
+ label.increment_index = Increase appended text numbers - newest file has largest number.
+ label.reverse_roll = "Roll" appended text numbers - newest backup file is always number 1.
+ label.keep_files = Deleting old backup files
+ label.keep_all_backup_files = Do not delete old backup files
+ label.keep_only_this_number_of_backup_files = Keep only this number of most recent backup files
+ label.autodelete_old_backup_files = Autodelete old backup files:
+ label.always_ask = Always ask
+ label.auto_delete = Automatically delete
+ label.filename = filename
+ label.braced_oldest = (oldest)
+ label.braced_newest = (most recent)
  label.configuration = Configuration
  label.configure_feature_tooltip = Click to configure variable colour or filters
+ label.schemes = Schemes
+ label.customise = Customise
+ label.default = Default
+ label.single_file = Single backup
+ label.keep_all_versions = Keep all versions
+ label.rolled_backups = Rolled backup files
+ label.previously_saved_scheme = Previously saved scheme
+ label.no_backup_files = NO BACKUP FILES
+ label.include_backup_files = Include backup files
+ label.cancel_changes = Cancel changes
+ label.warning_confirm_change_reverse = Warning!\nIf you change the increment/decrement of the backup filename number, without changing the suffix or number of digits,\nthis may cause loss of backup files created with the previous backup filename scheme.\nAre you sure you wish to do this?
+ label.change_increment_decrement = Change increment/decrement?
+ label.was_previous = was {0}
+ label.newerdelete_replacement_line = Backup file\n''{0}''\t(modified {2}, size {4})\nis to be deleted and replaced by apparently older file\n''{1}''\t(modified {3}, size {5}).
+ label.confirm_deletion_or_rename = Confirm deletion of ''{0}'' or rename to ''{1}''?
+ label.newerdelete_line = Backup file\n''{0}''\t(modified {2}, size {4})\nis to be deleted but is newer than the oldest remaining backup file\n''{1}''\t(modified {3}, size {5}).
+ label.confirm_deletion = Confirm deletion of ''{0}''?
+ label.delete = Delete
+ label.rename = Rename
+ label.keep = Keep
+ label.file_info = (modified {0}, size {1})
+ label.annotation_name = Annotation Name
+ label.annotation_description = Annotation Description 
+ label.edit_annotation_name_description = Edit Annotation Name/Description
+ label.alignment = alignment
+ label.pca = PCA
+ label.create_image_of = Create {0} image of {1}
+ label.click_to_edit = Click to edit, right-click for menu
+ label.by_annotation_tooltip = Annotation Colour is configured from the main Colour menu
@@@ -30,6 -30,7 +30,7 @@@ action.minimize_associated_windows = Mi
  action.close_all = Cerrar todo
  action.load_project = Cargar proyecto
  action.save_project = Guardar proyecto
+ action.save_project_as = Guardar proyecto como...
  action.quit = Salir
  action.expand_views = Expandir vistas
  action.gather_views = Capturar vistas
@@@ -115,10 -116,8 +116,8 @@@ action.select = Selecciona
  action.new_view = Nueva vista
  action.close = Cerrar
  action.add = Añadir
- action.save_as_default = Guardar como por defecto
  action.save_as = Guardar como
  action.save = Guardar
- action.cancel_fetch = Cancelar búsqueda
  action.change_font = Cambiar Fuente
  action.change_font_tree_panel = Cambiar fuente (panel del Ã¡rbol)
  action.colour = Color
@@@ -137,7 -136,6 +136,6 @@@ action.fetch_db_references = Recuperar 
  action.view_flanking_regions = Mostrar flancos
  label.view_flanking_regions = Mostrar los datos de la secuencia a ambos lados de las subsecuencias implicadas en este alineamiento
  label.structures_manager = Administrar estructuras
- label.nickname = Sobrenombre:
  label.url\: = URL:
  label.url = URL 
  label.input_file_url = Introducir URL en el fichero de entrada
@@@ -159,7 -157,6 +157,6 @@@ label.current_parameter_set_name = Nomb
  label.service_action = Acción de servicio:
  label.post_url = POST URL: 
  label.url_suffix = URL Sufijo
- label.sequence_source = Fuente de la secuencia
  label.per_seq = por secuencia
  label.result_vertically_separable = Los resultados son separables verticalmente
  label.amend = Modificar
@@@ -169,10 -166,9 +166,9 @@@ label.principal_component_analysis = An
  label.average_distance_identity = Distancia Media Usando % de Identidad
  label.neighbour_joining_identity = Unir vecinos utilizando % de Identidad
  label.choose_calculation = Elegir el cálculo
- label.treecalc_title = {0} utilizando {1}
+ label.calc_title = {0} utilizando {1}
  label.tree_calc_av = Distancia media
  label.tree_calc_nj = Unir vecinos
- label.select_score_model = Selecciones modelo de puntuación
  label.score_model_pid = % Identidad
  label.score_model_blosum62 = BLOSUM62
  label.score_model_pam250 = PAM 250
@@@ -199,6 -195,7 +195,7 @@@ label.colourScheme_purine/pyrimidine = 
  label.colourScheme_nucleotide = Nucleótido
  label.colourScheme_t-coffee_scores = Puntuación del T-Coffee
  label.colourScheme_rna_helices = Por hélices de RNA
+ label.colourScheme_sequence_id = Color de ID de secuencia
  label.blc = BLC
  label.fasta = Fasta
  label.msf = MSF
@@@ -322,7 -319,6 +319,6 @@@ label.status =  [Estado
  label.channels = Canales
  label.channel_title_item_count = {0} ({1})
  label.blog_item_published_on_date = {0} {1} 
- label.select_das_service_from_table = Seleccionar servicio DAS de la tabla para leer una descripción completa aquí.
  label.session_update = Actualizar sesión
  label.new_vamsas_session = Nueva sesión Vamsas
  action.save_vamsas_session = Guardar Sesión Vamsas
@@@ -331,8 -327,7 +327,8 @@@ label.open_saved_vamsas_session = Abri
  label.groovy_console = Consola Groovy 
  label.lineart = Lineart
  label.dont_ask_me_again = No volver a preguntar
 -label.select_eps_character_rendering_style = Seleccionar el carácter EPS como estilo de visualización 
 +label.select_character_rendering_style = Estilo de visualización para carácter {0} 
 +label.select_character_style_title = Opciones de visualización {0}
  label.invert_selection = Invertir selección
  label.optimise_order = Optimizar orden
  label.seq_sort_by_score = Ordenar las secuencias por puntuación
@@@ -340,7 -335,6 +336,6 @@@ label.load_colours = Cargar colore
  label.save_colours = Guardar colores
  label.load_colours_tooltip = Cargar colores y filtros desde fichero
  label.save_colours_tooltip = Guardar colores y filtros en fichero
- label.fetch_das_features = Recuperar funciones DAS
  label.selected_database_to_fetch_from = Seleccionada {0} Base de datos {1} para buscar de {2} 
  label.database_param = Base de datos: {0}
  label.example = Ejemplo
@@@ -377,9 -371,6 +372,6 @@@ label.couldnt_find_pdb_id_in_file = No 
  label.no_pdb_id_in_file = No hay un Id PDB en el fichero
  label.couldnt_read_pasted_text = No se pudo leer el texto pegado {0}
  label.error_parsing_text = Error analizando el texto
- label.enter_local_das_source = Intruduzca el Nickname & URL de la fuente DAS local
- label.you_can_only_edit_or_remove_local_das_sources = Sólo puedes editar o eliminar fuentes DAS locales!
- label.public_das_source = Fuente pública DAS - no editable
  label.input_alignment_from_url = Alineamiento de entrada desde URL
  label.input_alignment = Alineamiento de entrada
  label.couldnt_import_as_vamsas_session = No se pudo importar {0} como una nueva sesión Vamsas.
@@@ -396,8 -387,6 +388,6 @@@ label.invalid_url = URL Invalido
  label.error_loading_file = Error al cargar el fichero
  label.problems_opening_file = Encontrados problemas al abrir el fichero {0}!!
  label.file_open_error = Error al abrir el fichero
- label.no_das_sources_selected_warn = No han sido seleccionadas fuentes DAS.\nPor favor, seleccione algunas fuentes y\npruebe de nuevo.
- label.no_das_sources_selected_title = No han sido seleccionadas fuentes DAS
  label.colour_scheme_exists_overwrite = El esquema de colores {0} ya existe.\nContinuar guardando el esquema de colores como {1}?
  label.duplicate_scheme_name = Duplicar nombre de esquema
  label.jalview_new_questionnaire = Hay un nuevo cuestionario disponible. Querr\u00EDa completarlo ahora ?\n
@@@ -469,6 -458,10 +459,10 @@@ label.edit_name_description = Editar no
  label.create_sequence_feature = Crear función de secuencia
  label.edit_sequence = Editar secuencia
  label.edit_sequences = Editar secuencias
+ label.insert_gap = Insertar 1 hueco
+ label.insert_gaps = Insertar {0} huecos
+ label.delete_gap = Borrar 1 hueco
+ label.delete_gaps = Borrar {0} huecos
  label.sequence_details = Detalles de la secuencia
  label.jmol_help = Ayuda de Jmol
  # Todos/Todas is gender-sensitive, but currently only used for feminine (cadena / anotación)! 
@@@ -556,7 -549,7 +550,7 @@@ label.check_for_questionnaires = Compro
  label.check_for_latest_version = Comprobar la Ãºltima versión
  label.url_linkfrom_sequence_id = URL del enlace del ID de la secuencia
  label.use_proxy_server = Utilizar un servidor proxy
 -label.eps_rendering_style = Estilo de visualización EPS
 +label.rendering_style = Estilo de visualización {0}
  label.append_start_end = Añadir /inicio-fin (/15-380)
  label.full_sequence_id = ID de la secuencia completo
  label.smooth_font = Fuente alargada
@@@ -578,7 -571,6 +572,6 @@@ label.visual = Visua
  label.connections = Conexiones
  label.output = Salida
  label.editing = Edición
- label.das_settings = Configuración DAS
  label.web_services = Servicios web
  label.right_click_to_edit_currently_selected_parameter = Haga clic en el botón derecho para editar el parámetro seleccionado actualmente.
  label.let_jmol_manage_structure_colours = Permitir que Jmol gestione la estructuras cromáticas
@@@ -591,10 -583,6 +584,6 @@@ label.delete_service_url = Borrar la UR
  label.details = Detalles
  label.options = Opciones
  label.parameters = Paramétros
- label.available_das_sources = Fuentes DAS disponibles
- label.full_details = Detalles completos
- label.authority = Autoridad
- label.type = Tipo
  label.proxy_server = Servidor proxy
  label.file_output = Fichero de salida
  label.select_input_type = Seleccionar el tipo de entrada
@@@ -637,7 -625,7 +626,7 @@@ label.sequence_details_for = Detalles d
  label.sequence_name = Nombre de la secuencia
  label.sequence_description = Descripción de la secuencia
  label.edit_sequence_name_description = Editar el nombre/descripción de la secuencia
 -label.spaces_converted_to_backslashes = Los espacios se han convertido en _
 +label.spaces_converted_to_underscores = Los espacios se han convertido en _
  label.no_spaces_allowed_sequence_name = No se permiten espacios en el nombre de la secuencia
  label.select_outline_colour = Seleccionar el color del límite
  label.web_browser_not_found_unix = Unixers\: No es posible encontrar el navegador web por defecto.\nA\u00F1ada la ruta completa de su navegador en la pesta\u00F1a de Preferencias.
@@@ -657,9 -645,6 +646,6 @@@ label.get_cross_refs = Obtener referenc
  label.sort_alignment_new_tree = Alinear el alineamiento con el nuevo Ã¡rbol
  label.add_sequences = Añadir secuencias
  label.new_window = Nueva ventana
- label.refresh_available_sources = Refrescar las fuentes disponibles
- label.use_registry = Utilizar el registro
- label.add_local_source = Añadir fuente local
  label.set_as_default = Establecer por defecto
  label.show_labels = Mostrar etiquetas
  label.associate_nodes_with = Asociar nodos con
@@@ -701,7 -686,7 +687,7 @@@ label.run_with_preset_params = Ejecuta
  label.view_and_change_parameters_before_running_calculation = Ver y cambiar los parámetros antes de lanzar el cálculo
  label.view_documentation = Ver documentación
  label.select_return_type = Seleccionar el tipo de retorno
- label.translation_of_params = Traducción de {0}
+ label.translation_of_params = Traducción de {0} (Tabla {1})
  label.features_for_params = Características de - {0}
  label.annotations_for_params = Anotaciones de - {0}
  label.generating_features_for_params = Generando características de - {0}
@@@ -779,7 -764,6 +765,6 @@@ label.multiharmony = Multi-Harmon
  label.unable_start_web_service_analysis = No es posible iniciar el servicio web de análisis
  label.job_couldnt_be_started_check_input = El trabajo no puede arrancarse. Por favor, compruebe los parámetros de entrada y los mensajes de advertencia de la consola de Jalview.
  label.prompt_each_time = Preguntar siempre
- label.use_source = Fuente
  label.couldnt_save_project = No es posible guardar el proyecto
  label.error_whilst_saving_current_state_to = Error mientras se guardaba el estado a {0}
  label.error_whilst_loading_project_from = Error cargando el proyecto desde  {0}
@@@ -805,7 -789,6 +790,6 @@@ label.error_unsupported_owwner_user_col
  label.save_alignment_to_file = Guardar Alineamiento en fichero
  label.save_features_to_file = Guardar Características en un fichero
  label.save_annotation_to_file = Guardar Anotación en un fichero
- label.no_features_on_alignment = No se han encontrado características en el alineamiento
  label.save_pdb_file = Guardar fichero PDB 
  label.save_text_to_file = Guardar Texto en un fichero
  label.save_state = Guardar estado
@@@ -819,6 -802,8 +803,6 @@@ label.save_feature_colours = Guardar es
  label.select_startup_file = Seleccionar fichero de arranque
  label.select_default_browser = Seleccionar navegador web por defecto
  label.save_tree_as_newick = Guardar Ã¡rbol como fichero newick
 -label.create_eps_from_tree = Crear un fichero EPS a partir de un Ã¡rbol
 -label.create_png_from_tree = Crear una imagen PNG a partir de un Ã¡rbol
  label.save_colour_scheme = Guardar esquema cromático
  label.edit_params_for = Editar los parámetros de {0}
  label.choose_filename_for_param_file = Escoja un nombre de fichero para este fichero de parámetros
@@@ -874,6 -859,8 +858,6 @@@ error.call_setprogressbar_before_regist
  label.cancelled_params = {0} cancelado
  error.implementation_error_cannot_show_view_alignment_frame = Error de implementación: no es posible mostrar una vista de otro alineamiento en un AlignFrame.
  error.implementation_error_dont_know_about_threshold_setting = Error de implementación: no se conoce la configuración del umbral para el AnnotationColourGradient actual.
 -error.eps_generation_not_implemented = La generación de EPS no se ha implementado todavía
 -error.png_generation_not_implemented = La generación de PNG no se ha implementado todavía
  error.try_join_vamsas_session_another = Tratando de establecer una sesión VAMSAS cuando ya había otra conectada
  error.invalid_vamsas_session_id = Identificador de sesión VAMSAS no válido
  label.groovy_support_failed = El soporte Groovy de Jalview ha fallado
@@@ -944,7 -931,7 +928,7 @@@ label.pca_recalculating = Recalculando 
  label.pca_calculating = Calculando ACP
  label.select_foreground_colour = Escoger color del primer plano
  label.select_colour_for_text = Seleccione el color del texto
 -label.adjunst_foreground_text_colour_threshold = Ajustar el umbral del color del texto en primer plano
 +label.adjust_foreground_text_colour_threshold = Ajustar el umbral del color del texto en primer plano
  label.select_subtree_colour = Seleccioanr el color del sub-árbol
  label.create_new_sequence_features = Crear nueva(s) característica(s) de secuencia
  label.amend_delete_features = Arrelgar/Borrar características de {0}
@@@ -1001,8 -988,6 +985,6 @@@ exception.unable_to_create_internet_con
  exception.invocation_target_calling_url = InvocationTargetException mientras se invocaba openURL: {0}
  exception.illegal_access_calling_url = IllegalAccessException mientras se invocaba openURL: {0}
  exception.interrupted_launching_browser = InterruptedException mientras se lanzaba el navegador: {0}
- exception.das_source_doesnt_support_sequence_command = La fuente {0} no soporta el comando sequence.
- exception.invalid_das_source = Fuente DAS no válida: {0}
  exception.ebiembl_retrieval_failed_on = La recuperación de datos EBI EMBL XML ha fallado en {0}:{1}
  exception.no_pdb_records_for_chain = No se han encontrado registros {0} para la cadena {1}
  exception.unexpected_handling_rnaml_translation_for_pdb = Excepcion inesperada cuando se traducían a RNAML los datos PDB
@@@ -1018,6 -1003,7 +1000,6 @@@ error.implementation_error_cannot_find_
  exception.jobsubmission_invalid_params_set = Conjunto de parámetros no válido. Comprueba la implementación de Jalview
  exception.notvaliddata_group_contains_less_than_min_seqs = El grupo contiene menos de {0} secuencias.
  exception.outofmemory_loading_pdb_file = Sin memoria al cargar el fichero PDB
 -exception.eps_coudnt_write_output_file = No es posible escribir el fichero de salida: {0}
  exception.eps_method_not_supported = Método actualmente no suportado por la versión {0} de EpsGraphics2D
  exception.eps_unable_to_get_inverse_matrix = Imposible obtener la inversa de la matrix: {0}
  warn.job_cannot_be_cancelled_close_window = Este trabajo no se puede cancelar.\nSimplemente, cierre la ventana.
@@@ -1039,7 -1025,8 +1021,7 @@@ status.searching_for_sequences_from = B
  status.finished_searching_for_sequences_from = Finalizada la búsqueda de secuencias en {0}
  label.eps_file = Fichero EPS
  label.png_image = Imagen PNG
 -status.saving_file = Guardando {0}
 -status.export_complete = Exportación completada.
 +status.export_complete = Exportación completada
  status.fetching_pdb = Recuperando PDB {0}
  status.refreshing_news = Refrescando noticias
  status.importing_vamsas_session_from = Importando sesión VAMSAS de {0}
@@@ -1052,10 -1039,6 +1034,6 @@@ status.parsing_results = Parseando resu
  status.processing = Procesando...
  status.refreshing_web_service_menus = Refrescando los menús de servicios web
  status.collecting_job_results = Recolectando los resultados de los trabajos.
- status.fetching_das_sequence_features = Recuperando las características DAS de las secuencias
- status.no_das_sources_active = No existe ninguna fuente DAS activa
- status.das_feature_fetching_cancelled = Recuperación de características DAS cancelada
- status.das_feature_fetching_complete = Recuperación de características DAS completada
  status.fetching_db_refs = Recuperando db refs
  label.font_doesnt_have_letters_defined = La fuente no tiene letras definidas\npor lo que no puede emplease\ncon datos de alineamientos
  label.font_too_small = Tamaño de la letra es demasiado pequeña
@@@ -1072,8 -1055,6 +1050,6 @@@ warn.server_didnt_pass_validation = El 
  warn.url_must_contain = La URL de la secuencia debe contener $SEQUENCE_ID$, $DB_ACCESSION$ o un regex
  info.validate_jabaws_server = \u00BFValidar el servidor JabaWS?\n(Consulte la consola de salida para obtener los resultados)
  label.test_server = Â¿Probar servidor?
- info.you_want_jalview_to_find_uniprot_accessions = \u00BFDesea que Jalview encuentre\nUniprot Accession ids para los nombres de secuencias dados?
- label.find_uniprot_accession_ids = Buscar Uniprot Accession Ids
  label.new_sequence_fetcher = Añadir recuperador de secuencias
  label.additional_sequence_fetcher = Recuperador de secuencia adicional
  label.select_database_retrieval_source = Seleccionar fuente de recuperación de bases de datos
@@@ -1244,6 -1225,7 +1220,6 @@@ label.hide_columns_not_containing=Ocult
  label.pdb_sequence_fetcher=Recuperador de secuencias PDB
  exception.fts_server_error=Parece que hay un error desde el servidor {0}
  exception.service_not_available=Servicio no disponible. El servidor se está actualizando, vuelva a intentarlo más tarde.
 -status.waiting_for_user_to_select_output_file=Esperando que el usuario seleccione el fichero {0}
  action.prev_page=<< 
  status.cancelled_image_export_operation=Operación de exportación {0} cancelada
  label.couldnt_run_groovy_script=No se ha podido ejecutar el script Groovy
@@@ -1293,7 -1275,6 +1269,6 @@@ label.edit_sequence_url_link = Editar l
  warn.name_cannot_be_duplicate = Los nombres URL definidos por el usuario deben ser Ãºnicos y no pueden ser ids de MIRIAM
  label.output_seq_details = Seleccionar Detalles de la secuencia para ver todas
  label.urllinks = Enlaces
- label.default_cache_size = Tamaño del caché por defecto
  action.clear_cached_items = Borrar elementos en caché
  label.quality_descr = Calidad de alineamiento basándose en puntuación Blosum62
  label.conservation_descr = Conservación del alineamiento total menos de {0}% huecos
@@@ -1335,7 -1316,7 +1310,7 @@@ label.join_conditions = Combinar condic
  label.score = Puntuación
  label.colour_by_label = Colorear por texto
  label.variable_colour = Color variable...
 -label.select_colour = Seleccionar color
 +label.select_colour_for = Seleccionar color para {0}
  option.enable_disable_autosearch = Marcar para buscar automáticamente
  option.autosearch = Auto búsqueda
  label.retrieve_ids = Recuperar IDs
@@@ -1356,12 -1337,60 +1331,67 @@@ label.most_bound_molecules = Más Molécu
  label.most_polymer_residues = Más Residuos de Polímeros
  label.cached_structures = Estructuras en Caché
  label.free_text_search = Búsqueda de texto libre
 +label.annotation_name = Nombre de la anotación
 +label.annotation_description = Descripción de la anotación 
 +label.edit_annotation_name_description = Editar el nombre/descripción de la anotación
 +label.alignment = alineamiento
 +label.pca = ACP
 +label.create_image_of = Crear imagen {0} de {1}
 +label.click_to_edit = Haga clic para editar, clic en el botón derecho para ver el menú  
+ label.backupfiles_confirm_delete = Confirmar borrar
+ label.backupfiles_confirm_delete_old_files = Â¿Borrar los siguientes archivos? (ver la pestaña 'Copias' de la ventana de Preferencias para más opciones)
+ label.backupfiles_confirm_save_file = Confirmar guardar archivo
+ label.backupfiles_confirm_save_file_backupfiles_roll_wrong = Posiblemente algo está mal con los archivos de respaldos.
+ label.backupfiles_confirm_save_new_saved_file_ok = El nuevo archivo guardado parece estar bien.
+ label.backupfiles_confirm_save_new_saved_file_not_ok = El nuevo archivo guardado podría no estar bien.
+ label.backups = Respaldos
+ label.backup = Respaldo
+ label.backup_files = Archivos de respaldos
+ label.enable_backupfiles = Habilitar archivos de respaldos
+ label.backup_filename_strategy = Estrategia de nombres de archivo de respaldos
+ label.append_to_filename = Adjuntar texto (%n es reemplazado por el número de respaldo)
+ label.append_to_filename_tooltip = %n en el texto será reemplazado por el número de respaldo. El texto será después del nombre del archivo. Vea el cuadro de resumen arriba.
+ label.index_digits = Número de dígitos a utilizar para el número de respaldo.
+ label.summary_of_backups_scheme = Resumen del esquema de copias de seguridad
+ label.increment_index = Aumente los números de texto adjuntos: el archivo más nuevo tiene el número más grande
+ label.reverse_roll = Ciclos de texto adjuntos: el respaldo más reciente es siempre el número 1
+ label.keep_files = Borrando los respaldos antiguos
+ label.keep_all_backup_files = No borrar respaldos antiguas
+ label.keep_only_this_number_of_backup_files = Mantenga solo este número de respaldos más recientes
+ label.autodelete_old_backup_files = Borrer automáticamente respaldos antiguos:
+ label.always_ask = Pregunta siempre
+ label.auto_delete = Borrer automáticamente
+ label.filename = nombre_de_archivo
+ label.braced_oldest = (mas antiguo)
+ label.braced_newest = (mas nuevo)
  label.configuration = Configuración
  label.configure_feature_tooltip = Haga clic para configurar el color o los filtros
+ label.schemes = Esquemas
+ label.customise = Personalizado
+ label.default = Defecto
+ label.single_file = Solo uno respaldo
+ label.keep_all_versions = Mantener todas las versiones
+ label.rolled_backups = Ciclos respaldos
+ label.previously_saved_scheme = Esquema previamente guardado
+ label.no_backup_files = NO ARCHIVOS DE RESPALDOS
+ label.include_backup_files = Incluir archivos de respaldos
+ label.cancel_changes = Cancelar cambios
+ label.warning_confirm_change_reverse = Â¡Advertencia!\nSi cambia el incremento/decremento del número de archivos de respaldos, sin cambiar el sufijo o número de dígitos,\nesto puede causar la pérdida de los archivos de respaldos creados con el esquema anterior de nombre de archivo de respaldos.\n¿Está seguro de que desea hacer esto?
+ label.change_increment_decrement = Â¿Cambiar de incremento/decremento?
+ label.was_previous = era {0}
+ label.newerdelete_replacement_line = El archivo de respaldo\n''{0}''\t(modificado {2}, tamaño {4})\nserá borrado y reemplazarse por un archivo aparentemente más antiguo\n''{1}''\t(modificado {3}, tamaño {5}).
+ label.confirm_deletion_or_rename = Confirmar borrar ''{0}'', o cambiar el nombre a ''{1}''?
+ label.newerdelete_line = El archivo de respaldo\n''{0}''\t(modificado {2}, tamaño {4})\nserá borrado pero es mas nuevo que el archivo de respaldo restante más antiguo\n''{1}''\t(modified {3}, size {5}).
+ label.confirm_deletion = Confirmar eliminar ''{0}''?
+ label.delete = Borrar
+ label.rename = Cambiar
+ label.keep = Mantener
+ label.file_info = (modificado {0}, tamaño {1})
+ label.annotation_name = Nombre de la anotación
+ label.annotation_description = Descripción de la anotación 
+ label.edit_annotation_name_description = Editar el nombre/descripción de la anotación
+ label.alignment = alineamiento
+ label.pca = ACP
+ label.create_image_of = Crear imagen {0} de {1}
+ label.click_to_edit = Haga clic para editar, clic en el botón derecho para ver el menú  
+ label.by_annotation_tooltip = El color de anotación se configura desde el menú principal de colores
@@@ -194,10 -194,11 +194,11 @@@ public class Dn
    }
  
    /**
+    * Translates cDNA using the specified code table
     * 
     * @return
     */
-   public AlignmentI translateCdna()
+   public AlignmentI translateCdna(GeneticCodeI codeTable)
    {
      AlignedCodonFrame acf = new AlignedCodonFrame();
  
      for (s = 0; s < sSize; s++)
      {
        SequenceI newseq = translateCodingRegion(selection.get(s),
-               seqstring[s], acf, pepseqs);
+               seqstring[s], acf, pepseqs, codeTable);
  
        if (newseq != null)
        {
      for (int gd = 0; gd < selection.length; gd++)
      {
        SequenceI dna = selection[gd];
 -      DBRefEntry[] dnarefs = DBRefUtils.selectRefs(dna.getDBRefs(),
 +      List<DBRefEntry> dnarefs = DBRefUtils.selectRefs(dna.getDBRefs(),
                jalview.datamodel.DBRefSource.DNACODINGDBS);
        if (dnarefs != null)
        {
          // intersect with pep
          List<DBRefEntry> mappedrefs = new ArrayList<>();
 -        DBRefEntry[] refs = dna.getDBRefs();
 -        for (int d = 0; d < refs.length; d++)
 +        List<DBRefEntry> refs = dna.getDBRefs();
 +        for (int d = 0, nd = refs.size(); d < nd; d++)
          {
 -          if (refs[d].getMap() != null && refs[d].getMap().getMap() != null
 -                  && refs[d].getMap().getMap().getFromRatio() == 3
 -                  && refs[d].getMap().getMap().getToRatio() == 1)
 +          DBRefEntry ref = refs.get(d);
 +          if (ref.getMap() != null && ref.getMap().getMap() != null
 +                  && ref.getMap().getMap().getFromRatio() == 3
 +                  && ref.getMap().getMap().getToRatio() == 1)
            {
 -            mappedrefs.add(refs[d]); // add translated protein maps
 +            mappedrefs.add(ref); // add translated protein maps
            }
          }
 -        dnarefs = mappedrefs.toArray(new DBRefEntry[mappedrefs.size()]);
 -        for (int d = 0; d < dnarefs.length; d++)
 +        dnarefs = mappedrefs;//.toArray(new DBRefEntry[mappedrefs.size()]);
 +        for (int d = 0, nd = dnarefs.size(); d < nd; d++)
          {
 -          Mapping mp = dnarefs[d].getMap();
 +          Mapping mp = dnarefs.get(d).getMap();
            if (mp != null)
            {
 -            for (int vc = 0; vc < viscontigs.length; vc += 2)
 +            for (int vc = 0, nv = viscontigs.length; vc < nv; vc += 2)
              {
                int[] mpr = mp.locateMappedRange(viscontigs[vc],
                        viscontigs[vc + 1]);
     * @param acf
     *          Definition of global ORF alignment reference frame
     * @param proteinSeqs
+    * @param codeTable
     * @return sequence ready to be added to alignment.
     */
    protected SequenceI translateCodingRegion(SequenceI selection,
            String seqstring, AlignedCodonFrame acf,
-           List<SequenceI> proteinSeqs)
+           List<SequenceI> proteinSeqs, GeneticCodeI codeTable)
    {
      List<int[]> skip = new ArrayList<>();
      int[] skipint = null;
          /*
           * Filled up a reading frame...
           */
-         AlignedCodon alignedCodon = new AlignedCodon(cdp[0], cdp[1],
-                 cdp[2]);
-         String aa = ResidueProperties.codonTranslate(new String(codon));
+         AlignedCodon alignedCodon = new AlignedCodon(cdp[0], cdp[1], cdp[2]);
+         String aa = codeTable.translate(new String(codon));
          rf = 0;
          final String gapString = String.valueOf(gapChar);
          if (aa == null)
    private static void transferCodedFeatures(SequenceI dna, SequenceI pep,
            MapList map)
    {
 -    DBRefEntry[] dnarefs = DBRefUtils.selectRefs(dna.getDBRefs(),
 -            DBRefSource.DNACODINGDBS);
 -    if (dnarefs != null)
 -    {
 -      // intersect with pep
 -      for (int d = 0; d < dnarefs.length; d++)
 -      {
 -        Mapping mp = dnarefs[d].getMap();
 -        if (mp != null)
 -        {
 -        }
 -      }
 -    }
 +      //  BH 2019.01.25 nop?
 +//    List<DBRefEntry> dnarefs = DBRefUtils.selectRefs(dna.getDBRefs(),
 +//            DBRefSource.DNACODINGDBS);
 +//    if (dnarefs != null)
 +//    {
 +//      // intersect with pep
 +//      for (int d = 0, nd = dnarefs.size(); d < nd; d++)
 +//      {
 +//        Mapping mp = dnarefs.get(d).getMap();
 +//        if (mp != null)
 +//        {
 +//        }
 +//      }
 +//    }
      for (SequenceFeature sf : dna.getFeatures().getAllFeatures())
      {
          if (FeatureProperties.isCodingFeature(null, sf.getType()))
@@@ -23,7 -23,6 +23,7 @@@ package jalview.api
  import jalview.analysis.Conservation;
  import jalview.analysis.TreeModel;
  import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.AlignmentExportData;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.AlignmentView;
  import jalview.datamodel.ColumnSelection;
@@@ -488,16 -487,35 +488,44 @@@ public interface AlignViewportI extend
    @Override
    void setProteinFontAsCdna(boolean b);
  
 -  public abstract TreeModel getCurrentTree();
 +  TreeModel getCurrentTree();
  
 -  public abstract void setCurrentTree(TreeModel tree);
 +  void setCurrentTree(TreeModel tree);
 +
 +  /**
 +   * Answers a data bean containing data for export as configured by the
 +   * supplied options
 +   * 
 +   * @param options
 +   * @return
 +   */
 +  AlignmentExportData getAlignExportData(AlignExportSettingsI options);
+   /**
+    * @param update
+    *          - set the flag for updating structures on next repaint
+    */
+   void setUpdateStructures(boolean update);
+   /**
+    *
+    * @return true if structure views will be updated on next refresh
+    */
+   boolean isUpdateStructures();
+   /**
+    * check if structure views need to be updated, and clear the flag afterwards.
+    * 
+    * @return if an update is needed
+    */
+   boolean needToUpdateStructureViews();
+   /**
+    * Adds sequencegroup to the alignment in the view. Also adds a group to the
+    * complement view if one is defined.
+    * 
+    * @param sequenceGroup
+    *          - a group defined on sequences in the alignment held by the view
+    */
+   void addSequenceGroup(SequenceGroup sequenceGroup);
  }
@@@ -416,7 -416,7 +416,7 @@@ public class AlignFrame extends Embmenu
          viewport.featureSettings.refreshTable();
        }
        alignPanel.paintAlignment(true, true);
 -      statusBar.setText(MessageManager
 +      setStatus(MessageManager
                .getString("label.successfully_added_features_alignment"));
      }
      return featuresFile;
  
      case KeyEvent.VK_F2:
        viewport.cursorMode = !viewport.cursorMode;
 -      statusBar.setText(MessageManager
 +      setStatus(MessageManager
                .formatMessage("label.keyboard_editing_mode", new String[]
                { (viewport.cursorMode ? "on" : "off") }));
        if (viewport.cursorMode)
      {
        features = formatter.printJalviewFormat(
                viewport.getAlignment().getSequencesArray(),
-               getDisplayedFeatureCols(), null, getDisplayedFeatureGroups(),
-               true);
+               alignPanel.getFeatureRenderer(), true);
      }
      else
      {
        features = formatter.printGffFormat(viewport.getAlignment()
-               .getSequencesArray(), getDisplayedFeatureCols(),
-               getDisplayedFeatureGroups(), true);
+               .getSequencesArray(), alignPanel.getFeatureRenderer(), true);
      }
  
      if (displayTextbox)
                  column, al);
        }
  
 -      statusBar.setText(MessageManager
 +      setStatus(MessageManager
                .formatMessage("label.removed_columns", new String[]
                { Integer.valueOf(trimRegion.getSize()).toString() }));
        addHistoryItem(trimRegion);
  
      addHistoryItem(removeGapCols);
  
 -    statusBar.setText(MessageManager
 +    setStatus(MessageManager
              .formatMessage("label.removed_empty_columns", new String[]
              { Integer.valueOf(removeGapCols.getSize()).toString() }));
  
       */
      statusBar.setBackground(Color.white);
      statusBar.setFont(new java.awt.Font("Verdana", 0, 11));
 -    statusBar.setText(MessageManager.getString("label.status_bar"));
 +    setStatus(MessageManager.getString("label.status_bar"));
      this.add(statusBar, BorderLayout.SOUTH);
    }
  
      }
      else
      {
 -      new MCview.AppletPDBViewer(pdb, seqs, chains, alignPanel, protocol);
 +      new mc_view.AppletPDBViewer(pdb, seqs, chains, alignPanel, protocol);
      }
  
    }
@@@ -25,14 -25,11 +25,11 @@@ import jalview.api.FeatureSettingsModel
  import jalview.bin.JalviewLite;
  import jalview.commands.CommandI;
  import jalview.datamodel.AlignmentI;
- import jalview.datamodel.Annotation;
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.SearchResults;
  import jalview.datamodel.SearchResultsI;
- import jalview.datamodel.Sequence;
  import jalview.datamodel.SequenceGroup;
- import jalview.datamodel.SequenceI;
  import jalview.renderer.ResidueShader;
  import jalview.schemes.ColourSchemeProperty;
  import jalview.schemes.UserColourScheme;
@@@ -55,6 -52,8 +52,6 @@@ public class AlignViewport extends Alig
  
    public jalview.bin.JalviewLite applet;
  
 -  boolean MAC = false;
 -
    private AnnotationColumnChooser annotationColumnSelectionState;
  
    public AlignViewport(AlignmentI al, JalviewLite applet)
      }
      setFont(font, true);
  
 -    MAC = new jalview.util.Platform().isAMac();
 -
      if (applet != null)
      {
        setShowJVSuffix(
        if (colour != null)
        {
          residueShading = new ResidueShader(
-                 ColourSchemeProperty.getColourScheme(alignment, colour));
+                 ColourSchemeProperty.getColourScheme(this, alignment,
+                         colour));
          if (residueShading != null)
          {
            residueShading.setConsensus(hconsensus);
@@@ -44,8 -44,6 +44,8 @@@ import java.awt.event.MouseListener
  import java.awt.event.MouseMotionListener;
  import java.beans.PropertyChangeEvent;
  
 +import javax.swing.SwingUtilities;
 +
  public class OverviewPanel extends Panel implements Runnable,
          MouseMotionListener, MouseListener, ViewportListenerI
  {
    {
      if (od.isPositionInBox(evt.getX(), evt.getY()))
      {
-       // display drag cursor at mouse position
-       setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+       this.getParent()
+               .setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
      }
      else
      {
-       // reset cursor
-       setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+       this.getParent()
+               .setCursor(
+                       Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
      }
    }
  
      if ((evt.getModifiers()
              & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
      {
 -      if (!Platform.isAMac())
 +      if (!Platform.isMac()) // BH was excluding JavaScript
        {
          showPopupMenu(evt);
        }
          od.updateViewportFromMouse(evt.getX(), evt.getY(),
                  av.getAlignment().getHiddenSequences(),
                  av.getAlignment().getHiddenColumns());
+         getParent()
+                 .setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        }
        else
        {
    @Override
    public void mouseDragged(MouseEvent evt)
    {
 -    if ((evt.getModifiers()
 -            & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
 +    if (Platform.isWinRightButton(evt)) 
      {
 -      if (!Platform.isAMac())
 -      {
 -        showPopupMenu(evt);
 -      }
 +      showPopupMenu(evt);
 +      return;
      }
 -    else
 -    {
 -      if (draggingBox)
 -      {
 -        // set the mouse position as a fixed point in the box
 -        // and drag relative to that position
 -        od.adjustViewportFromMouse(evt.getX(), evt.getY(),
 -                av.getAlignment().getHiddenSequences(),
 -                av.getAlignment().getHiddenColumns());
 -      }
 -      else
 -      {
 -        od.updateViewportFromMouse(evt.getX(), evt.getY(),
 -                av.getAlignment().getHiddenSequences(),
 -                av.getAlignment().getHiddenColumns());
 -      }
 -      ap.paintAlignment(false, false);
 +
 +    if (SwingUtilities.isRightMouseButton(evt))
 +    { 
 +      return;
      }
 +    
 +        if (draggingBox)
 +        {
 +          // set the mouse position as a fixed point in the box
 +          // and drag relative to that position
 +          od.adjustViewportFromMouse(evt.getX(), evt.getY(),
 +                  av.getAlignment().getHiddenSequences(),
 +                  av.getAlignment().getHiddenColumns());
 +        }
 +        else
 +        {
 +          od.updateViewportFromMouse(evt.getX(), evt.getY(),
 +                  av.getAlignment().getHiddenSequences(),
 +                  av.getAlignment().getHiddenColumns());
 +        }
 +        ap.paintAlignment(false, false);
    }
  
    /**
@@@ -28,7 -28,6 +28,7 @@@ import jalview.schemes.UserColourScheme
  import jalview.structure.StructureImportSettings;
  import jalview.urls.IdOrgSettings;
  import jalview.util.ColorUtils;
 +import jalview.util.Platform;
  import jalview.ws.sifts.SiftsSettings;
  
  import java.awt.Color;
@@@ -115,7 -114,6 +115,6 @@@ import org.apache.log4j.SimpleLayout
   * service</li>
   * <li>USAGESTATS (false - user prompted) Enable google analytics tracker for
   * collecting usage statistics</li>
-  * <li>DAS_LOCAL_SOURCE list of local das sources</li>
   * <li>SHOW_OVERVIEW boolean for overview window display</li>
   * <li>ANTI_ALIAS boolean for smooth fonts</li>
   * <li>RIGHT_ALIGN_IDS boolean</li>
   * sequence id (must be in SEQUENCE_LINKS or STORED_LINKS)
   * <li>GROUP_LINKS list of name|URL[|&lt;separator&gt;] tuples - see
   * jalview.utils.GroupURLLink for more info</li>
-  * <li>DAS_REGISTRY_URL the registry to query</li>
   * <li>DEFAULT_BROWSER for unix</li>
-  * <li>DAS_ACTIVE_SOURCE list of active sources</li>
   * <li>SHOW_MEMUSAGE boolean show memory usage and warning indicator on desktop
   * (false)</li>
   * <li>VERSION_CHECK (true) check for the latest release version from
@@@ -227,12 -223,6 +224,6 @@@ public class Cach
     */
    public static final String JALVIEWLOGLEVEL = "logs.Jalview.level";
  
-   public static final String DAS_LOCAL_SOURCE = "DAS_LOCAL_SOURCE";
-   public static final String DAS_REGISTRY_URL = "DAS_REGISTRY_URL";
-   public static final String DAS_ACTIVE_SOURCE = "DAS_ACTIVE_SOURCE";
    /**
     * Sifts settings
     */
    /** Default file is ~/.jalview_properties */
    static String propertiesFile;
  
 -  private static boolean propsAreReadOnly = false;
 +  private static boolean propsAreReadOnly = /** @j2sNative true || */
 +          false;
  
    public static void initLogger()
    {
      }
  
      // LOAD THE AUTHORS FROM THE authors.props file
 +    boolean ignore = Platform.isJS();
 +    if (!ignore)
      try
      {
        String authorDetails = "jar:"
        InputStream in = localJarFileURL.openStream();
        applicationProperties.load(in);
        in.close();
 +      
      } catch (Exception ex)
      {
        System.out.println("Error reading author details: " + ex);
 -      applicationProperties.remove("AUTHORS");
 -      applicationProperties.remove("AUTHORFNAMES");
 -      applicationProperties.remove("YEAR");
 +      ignore = true;
      }
  
 +    if (ignore) {
 +        applicationProperties.remove("AUTHORS");
 +        applicationProperties.remove("AUTHORFNAMES");
 +        applicationProperties.remove("YEAR");
 +    }
 +    
      // FIND THE VERSION NUMBER AND BUILD DATE FROM jalview.jar
      // MUST FOLLOW READING OF LOCAL PROPERTIES FILE AS THE
      // VERSION MAY HAVE CHANGED SINCE LAST USING JALVIEW
 +    // BH 2019.01.25 switching to Platform.isJS()
 +    ignore = Platform.isJS();
 +    if (!ignore)
      try
      {
        String buildDetails = "jar:".concat(Cache.class.getProtectionDomain()
      } catch (Exception ex)
      {
        System.out.println("Error reading build details: " + ex);
 -      applicationProperties.remove("VERSION");
 +      ignore = true;
 +    }
 +    
 +    if (ignore) {
 +        applicationProperties.remove("VERSION");
      }
  
      String jnlpVersion = System.getProperty("jalview.version");
      return def;
    }
  
+   public static int getDefault(String property, int def)
+   {
+     String string = getProperty(property);
+     if (string != null)
+     {
+       try
+       {
+         def = Integer.parseInt(string);
+       } catch (NumberFormatException e)
+       {
+         System.out.println("Error parsing int property '" + property
+                 + "' with value '" + string + "'");
+       }
+     }
+     return def;
+   }
    /**
 -   * These methods are used when checking if the saved preference is different
 -   * to the default setting
 +   * Answers the value of the given property, or the supplied default value if
 +   * the property is not set
     */
    public static String getDefault(String property, String def)
    {
 -    String string = getProperty(property);
 -    if (string != null)
 -    {
 -      return string;
 -    }
 -
 -    return def;
 +    String value = getProperty(property);
 +    return value == null ? def : value;
    }
  
    /**
        }
      }
    }
 +
 +      /**
 +       * Add a known domain that implements access-control-allow-origin:* bh 2018
 +       * 
 +       * @param defaultUniprotDomain
 +       */
 +      public static void addJ2SDirectDatabaseCall(String domain) 
 +      {
 +
 +              /**
 +               * @j2sNative
 +               * 
 +               *                      J2S.addDirectDatabaseCall(domain);
 +               */
 +      }
  }
@@@ -41,8 -41,6 +41,8 @@@ import jalview.util.MessageManager
  import jalview.util.Platform;
  import jalview.ws.jws2.Jws2Discoverer;
  
 +import java.awt.event.ActionEvent;
 +import java.awt.event.ActionListener;
  import java.io.BufferedReader;
  import java.io.File;
  import java.io.FileOutputStream;
@@@ -62,12 -60,8 +62,12 @@@ import java.security.Policy
  import java.util.HashMap;
  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.Timer;
  import javax.swing.UIManager;
  
  import groovy.lang.Binding;
@@@ -90,29 -84,6 +90,29 @@@ import groovy.util.GroovyScriptEngine
   */
  public class Jalview
  {
 +      /**
 +       * @j2sNative
 +       * 
 +       *  // find first query parameter (if any) that doesn't start with j2s
 +       *  // and set as space-delimited arguments to Jalview main
 +       *      hr = decodeURI(document.location.href);
 +       *  pos = hr.indexOf("?");
 +       *  if (pos > 0)
 +       *  {
 +       *     q = hr.substring(pos+1);
 +       *     args = q.split("&");
 +       *     for (i = 0 ; i < args.length; i++)
 +       *     {
 +       *      arg1 = args[i];
 +       *      if (!arg1.startsWith("j2s"))
 +       *      {
 +       *       thisApplet.__Info.args = arg1.split(" ");
 +       *       break;
 +       *      }
 +       *     }
 +       *  }
 +       */
 +
    /*
     * singleton instance of this class
     */
  
    static
    {
 -    // grab all the rights we can the JVM
 -    Policy.setPolicy(new Policy()
 -    {
 -      @Override
 -      public PermissionCollection getPermissions(CodeSource codesource)
 -      {
 -        Permissions perms = new Permissions();
 -        perms.add(new AllPermission());
 -        return (perms);
 -      }
 -
 -      @Override
 -      public void refresh()
 -      {
 -      }
 -    });
 +    if (!Platform.isJS())
 +    { // BH 2018
 +          // grab all the rights we can for the JVM
 +          Policy.setPolicy(new Policy()
 +          {
 +            @Override
 +            public PermissionCollection getPermissions(CodeSource codesource)
 +            {
 +              Permissions perms = new Permissions();
 +              perms.add(new AllPermission());
 +              return (perms);
 +            }
 +      
 +            @Override
 +            public void refresh()
 +            {
 +            }
 +          });
 +    }
    }
  
    /**
     */
    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() 
 +  {
 +        // BH - for event debugging in JavaScript (Java mode only)
 +      if (!(/** @j2sNative true ||*/false)) 
 +      {
 +              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"); 
 +      }       
 +
 +  }
 +  
 +
 +  
 +
    /**
     * @param args
     */
    void doMain(String[] args)
    {
 -    System.setSecurityManager(null);
 +
 +    if (!Platform.isJS())
 +    {
 +      System.setSecurityManager(null);
 +    }
 +
      System.out
              .println("Java version: " + System.getProperty("java.version"));
      System.out.println(System.getProperty("os.arch") + " "
      ArgsParser aparser = new ArgsParser(args);
      boolean headless = false;
  
 -    if (aparser.contains("help") || aparser.contains("h"))
 -    {
 -      showUsage();
 -      System.exit(0);
 -    }
 -    if (aparser.contains("nodisplay") || aparser.contains("nogui")
 -            || aparser.contains("headless"))
 -    {
 -      System.setProperty("java.awt.headless", "true");
 -      headless = true;
 -    }
      String usrPropsFile = aparser.getValue("props");
      Cache.loadProperties(usrPropsFile); // must do this before
      if (usrPropsFile != null)
                "CMD [-props " + usrPropsFile + "] executed successfully!");
      }
  
 -    // anything else!
 -
 -    final String jabawsUrl = aparser.getValue("jabaws");
 -    if (jabawsUrl != null)
 +    /**
 +     * BH 2018 ignoring this section for JS
 +     * 
 +     * @j2sNative
 +     */
      {
 -      try
 +      if (aparser.contains("help") || aparser.contains("h"))
        {
 -        Jws2Discoverer.getDiscoverer().setPreferredUrl(jabawsUrl);
 -        System.out.println(
 -                "CMD [-jabaws " + jabawsUrl + "] executed successfully!");
 -      } catch (MalformedURLException e)
 +        showUsage();
 +        System.exit(0);
 +      }
 +      if (aparser.contains("nodisplay") || aparser.contains("nogui")
 +              || aparser.contains("headless"))
        {
 -        System.err.println(
 -                "Invalid jabaws parameter: " + jabawsUrl + " ignored");
 +        System.setProperty("java.awt.headless", "true");
 +        headless = true;
        }
 -    }
 +      // anything else!
  
 +      final String jabawsUrl = aparser.getValue("jabaws");
 +      if (jabawsUrl != null)
 +      {
 +        try
 +        {
 +          Jws2Discoverer.getDiscoverer().setPreferredUrl(jabawsUrl);
 +          System.out.println(
 +                  "CMD [-jabaws " + jabawsUrl + "] executed successfully!");
 +        } catch (MalformedURLException e)
 +        {
 +          System.err.println(
 +                  "Invalid jabaws parameter: " + jabawsUrl + " ignored");
 +        }
 +      }
 +
 +    }
      String defs = aparser.getValue("setprop");
      while (defs != null)
      {
        else
        {
          System.out.println("Executing setprop argument: " + defs);
 -        // DISABLED FOR SECURITY REASONS
 -        // TODO: add a property to allow properties to be overriden by cli args
 -        // Cache.setProperty(defs.substring(0,p), defs.substring(p+1));
 +        if (Platform.isJS())
 +        {
 +          Cache.setProperty(defs.substring(0,p), defs.substring(p+1));
 +        }
        }
        defs = aparser.getValue("setprop");
      }
        System.err.println("Unexpected Look and Feel Exception");
        ex.printStackTrace();
      }
 -    if (Platform.isAMac())
 +    if (Platform.isAMacAndNotJS())
      {
  
        LookAndFeel lookAndFeel = ch.randelshofer.quaqua.QuaquaManager
  
      if (!headless)
      {
 -      desktop = new Desktop();
 +      desktop = new Desktop() 
 +//      {
 +// // BH testing
 +//              @Override
 +//              protected void processEvent(AWTEvent e) {
 +//                      System.out.println("Jalview.java " + e);
 +//                      super.processEvent(e);
 +//              }
 +//       }
 +      ;
        desktop.setInBatchMode(true); // indicate we are starting up
        desktop.setVisible(true);
 -      desktop.startServiceDiscovery();
 -      if (!aparser.contains("nousagestats"))
 -      {
 -        startUsageStats(desktop);
 -      }
 -      else
 -      {
 -        System.err.println("CMD [-nousagestats] executed successfully!");
 -      }
  
 -      if (!aparser.contains("noquestionnaire"))
 +      /**
 +       * BH 2018 JS bypass this section
 +       * 
 +       * @j2sNative
 +       * 
 +       */
        {
 -        String url = aparser.getValue("questionnaire");
 -        if (url != null)
 +        desktop.startServiceDiscovery();
 +        if (!aparser.contains("nousagestats"))
          {
 -          // Start the desktop questionnaire prompter with the specified
 -          // questionnaire
 -          Cache.log.debug("Starting questionnaire url at " + url);
 -          desktop.checkForQuestionnaire(url);
 -          System.out.println(
 -                  "CMD questionnaire[-" + url + "] executed successfully!");
 +          startUsageStats(desktop);
          }
          else
          {
 -          if (Cache.getProperty("NOQUESTIONNAIRES") == null)
 +          System.err.println("CMD [-nousagestats] executed successfully!");
 +        }
 +
 +        if (!aparser.contains("noquestionnaire"))
 +        {
 +          String url = aparser.getValue("questionnaire");
 +          if (url != null)
            {
              // Start the desktop questionnaire prompter with the specified
              // questionnaire
 -            // String defurl =
 -            // "http://anaplog.compbio.dundee.ac.uk/cgi-bin/questionnaire.pl";
 -            // //
 -            String defurl = "http://www.jalview.org/cgi-bin/questionnaire.pl";
 -            Cache.log.debug(
 -                    "Starting questionnaire with default url: " + defurl);
 -            desktop.checkForQuestionnaire(defurl);
 +            Cache.log.debug("Starting questionnaire url at " + url);
 +            desktop.checkForQuestionnaire(url);
 +            System.out.println("CMD questionnaire[-" + url
 +                    + "] executed successfully!");
 +          }
 +          else
 +          {
 +            if (Cache.getProperty("NOQUESTIONNAIRES") == null)
 +            {
 +              // Start the desktop questionnaire prompter with the specified
 +              // questionnaire
 +              // String defurl =
 +              // "http://anaplog.compbio.dundee.ac.uk/cgi-bin/questionnaire.pl";
 +              // //
 +              String defurl = "http://www.jalview.org/cgi-bin/questionnaire.pl";
 +              Cache.log.debug(
 +                      "Starting questionnaire with default url: " + defurl);
 +              desktop.checkForQuestionnaire(defurl);
 +            }
            }
          }
 -      }
 -      else
 -      {
 -        System.err.println("CMD [-noquestionnaire] executed successfully!");
 -      }
 +        else
 +        {
 +          System.err
 +                  .println("CMD [-noquestionnaire] executed successfully!");
 +        }
  
 -      if (!aparser.contains("nonews"))
 -      {
 -        desktop.checkForNews();
 -      }
 +        if (!aparser.contains("nonews"))
 +        {
 +          desktop.checkForNews();
 +        }
  
 -      BioJsHTMLOutput.updateBioJS();
 +        BioJsHTMLOutput.updateBioJS();
 +      }
      }
  
      String file = null, data = null;
      FileFormatI format = null;
      DataSourceType protocol = null;
      FileLoader fileLoader = new FileLoader(!headless);
-     Vector<String> getFeatures = null; // vector of das source nicknames to
-                                        // fetch
-     // features from
-     // loading is done.
      String groovyscript = null; // script to execute after all loading is
      // completed one way or another
      // extract groovy argument and execute if necessary
        }
        System.out.println("CMD [-open " + file + "] executed successfully!");
  
 -      if (!file.startsWith("http://"))
 +      if (!Platform.isJS() && !file.startsWith("http://"))
        {
          if (!(new File(file)).exists())
          {
          }
        }
  
 -      protocol = AppletFormatAdapter.checkProtocol(file);
 +        protocol = AppletFormatAdapter.checkProtocol(file);
  
        try
        {
            data.replaceAll("%20", " ");
  
            ColourSchemeI cs = ColourSchemeProperty
-                   .getColourScheme(af.getViewport().getAlignment(), data);
+                   .getColourScheme(af.getViewport(),
+                           af.getViewport().getAlignment(), data);
  
            if (cs != null)
            {
          // TODO - load PDB structure(s) to alignment JAL-629
          // (associate with identical sequence in alignment, or a specified
          // sequence)
-         getFeatures = checkDasArguments(aparser);
-         if (af != null && getFeatures != null)
-         {
-           FeatureFetcher ff = startFeatureFetching(getFeatures);
-           if (ff != null)
-           {
-             while (!ff.allFinished() || af.operationInProgress())
-             {
-               // wait around until fetching is finished.
-               try
-               {
-                 Thread.sleep(100);
-               } catch (Exception e)
-               {
-               }
-             }
-           }
-           getFeatures = null; // have retrieved features - forget them now.
-         }
          if (groovyscript != null)
          {
            // Execute the groovy script after we've done all the rendering stuff
              continue;
            }
  
 -          if (af.saveAlignment(file, format))
 +          af.saveAlignment(file, format);
 +          if (af.isSaveAlignmentSuccessful())
            {
              System.out.println("Written alignment in " + format
                      + " format to " + file);
      // And the user
      // ////////////////////
  
 -    if (!headless && file == null && vamsasImport == null
 +    if (/** @j2sNative false && */ // BH 2018
 +    !headless && file == null && vamsasImport == null
              && jalview.bin.Cache.getDefault("SHOW_STARTUP_FILE", true))
      {
        file = jalview.bin.Cache.getDefault("STARTUP_FILE",
  
        startUpAlframe = fileLoader.LoadFileWaitTillLoaded(file, protocol,
                format);
-       getFeatures = checkDasArguments(aparser);
        // extract groovy arguments before anything else.
      }
-     // If the user has specified features to be retrieved,
-     // or a groovy script to be executed, do them if they
-     // haven't been done already
-     // fetch features for the default alignment
-     if (getFeatures != null)
-     {
-       if (startUpAlframe != null)
-       {
-         startFeatureFetching(getFeatures);
-       }
-     }
      // Once all other stuff is done, execute any groovy scripts (in order)
      if (groovyscript != null)
      {
                      // (quote the 'PROPERTY=VALUE' pair to ensure spaces are
                      // passed in correctly)"
                      + "-jabaws URL\tSpecify URL for Jabaws services (e.g. for a local installation).\n"
-                     + "-dasserver nickname=URL\tAdd and enable a das server with given nickname\n\t\t\t(alphanumeric or underscores only) for retrieval of features for all alignments.\n"
-                     + "\t\t\tSources that also support the sequence command may be specified by prepending the URL with sequence:\n"
-                     + "\t\t\t e.g. sequence:http://localdas.somewhere.org/das/source)\n"
                      + "-fetchfrom nickname\tQuery nickname for features for the alignments and display them.\n"
                      // +
                      // "-vdoc vamsas-document\tImport vamsas document into new
      }
    }
  
-   /**
-    * Check commandline for any das server definitions or any fetchfrom switches
-    * 
-    * @return vector of DAS source nicknames to retrieve from
-    */
-   private static Vector<String> checkDasArguments(ArgsParser aparser)
-   {
-     Vector<String> source = null;
-     String data;
-     String locsources = Cache.getProperty(Cache.DAS_LOCAL_SOURCE);
-     while ((data = aparser.getValue("dasserver", true)) != null)
-     {
-       String nickname = null;
-       String url = null;
-       int pos = data.indexOf('=');
-       // determine capabilities
-       if (pos > 0)
-       {
-         nickname = data.substring(0, pos);
-       }
-       url = data.substring(pos + 1);
-       if (url != null && (url.startsWith("http:")
-               || url.startsWith("sequence:http:")))
-       {
-         if (nickname == null)
-         {
-           nickname = url;
-         }
-         if (locsources == null)
-         {
-           locsources = "";
-         }
-         else
-         {
-           locsources += "\t";
-         }
-         locsources = locsources + nickname + "|" + url;
-         System.err.println(
-                 "NOTE! dasserver parameter not yet really supported (got args of "
-                         + nickname + "|" + url);
-         if (source == null)
-         {
-           source = new Vector<>();
-         }
-         source.addElement(nickname);
-       }
-       System.out.println(
-               "CMD [-dasserver " + data + "] executed successfully!");
-     } // loop until no more server entries are found.
-     if (locsources != null && locsources.indexOf('|') > -1)
-     {
-       Cache.log.debug("Setting local source list in properties file to:\n"
-               + locsources);
-       Cache.setProperty(Cache.DAS_LOCAL_SOURCE, locsources);
-     }
-     while ((data = aparser.getValue("fetchfrom", true)) != null)
-     {
-       System.out.println("adding source '" + data + "'");
-       if (source == null)
-       {
-         source = new Vector<>();
-       }
-       source.addElement(data);
-     }
-     return source;
-   }
-   /**
-    * start a feature fetcher for every alignment frame
-    * 
-    * @param dasSources
-    */
-   private FeatureFetcher startFeatureFetching(
-           final Vector<String> dasSources)
-   {
-     FeatureFetcher ff = new FeatureFetcher();
-     AlignFrame afs[] = Desktop.getAlignFrames();
-     if (afs == null || afs.length == 0)
-     {
-       return null;
-     }
-     for (int i = 0; i < afs.length; i++)
-     {
-       ff.addFetcher(afs[i], dasSources);
-     }
-     return ff;
-   }
    public static boolean isHeadlessMode()
    {
      String isheadless = System.getProperty("java.awt.headless");
@@@ -24,6 -24,7 +24,7 @@@ import jalview.analysis.AlignSeq
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.Annotation;
+ import jalview.datamodel.ContiguousI;
  import jalview.datamodel.Range;
  import jalview.datamodel.Sequence;
  import jalview.datamodel.SequenceFeature;
@@@ -526,8 -527,8 +527,8 @@@ public class EditCommand implements Com
          command.string[i] = sequence.getSequence(command.position,
                  command.position + command.number);
          SequenceI oldds = sequence.getDatasetSequence();
-         Range cutPositions = sequence.findPositions(command.position + 1,
-                 command.position + command.number);
+         ContiguousI cutPositions = sequence.findPositions(
+                 command.position + 1, command.position + command.number);
          boolean cutIsInternal = cutPositions != null
                  && sequence.getStart() != cutPositions
                  .getBegin() && sequence.getEnd() != cutPositions.getEnd();
           */
          if (command.alIndex[i] < command.al.getHeight())
          {
 -          List<SequenceI> sequences;
 -          synchronized (sequences = command.al.getSequences())
 +          List<SequenceI> sequences = command.al.getSequences();
 +          synchronized (sequences)
            {
              if (!(command.alIndex[i] < 0))
              {
         * EditCommand.PASTE, sequences, 0, alignment.getWidth(), alignment) );
         * 
         */
-       Range beforeEditedPositions = command.seqs[i].findPositions(1, start);
-       Range afterEditedPositions = command.seqs[i]
+       ContiguousI beforeEditedPositions = command.seqs[i].findPositions(1,
+               start);
+       ContiguousI afterEditedPositions = command.seqs[i]
                .findPositions(end + 1, command.seqs[i].getLength());
        
        oldstring = command.seqs[i].getSequenceAsString();
  
    public class Edit
    {
 -    private SequenceI[] oldds;
 +    SequenceI[] oldds;
  
      /**
       * start and end of sequence prior to edit
       */
 -    private Range[] oldStartEnd;
 +    Range[] oldStartEnd;
  
 -    private boolean fullAlignmentHeight = false;
 +    boolean fullAlignmentHeight = false;
  
 -    private Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
 +    Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
  
 -    private Map<String, Annotation[]> deletedAnnotations;
 +    Map<String, Annotation[]> deletedAnnotations;
  
      /*
       * features deleted by the cut (re-add on Undo)
       * (including the original of any shortened features)
       */
 -    private Map<SequenceI, List<SequenceFeature>> deletedFeatures;
 +    Map<SequenceI, List<SequenceFeature>> deletedFeatures;
  
      /*
       * shortened features added by the cut (delete on Undo)
       */
 -    private Map<SequenceI, List<SequenceFeature>> truncatedFeatures;
 +    Map<SequenceI, List<SequenceFeature>> truncatedFeatures;
  
 -    private AlignmentI al;
 +    AlignmentI al;
  
 -    final private Action command;
 +    final Action command;
  
      char[][] string;
  
      SequenceI[] seqs;
  
 -    private int[] alIndex;
 +    int[] alIndex;
  
 -    private int position;
 +    int position;
  
 -    private int number;
 +    int number;
  
 -    private char gapChar;
 +    char gapChar;
  
+     /*
+      * flag that identifies edits inserted to balance 
+      * user edits in a 'locked editing' region
+      */
+     private boolean systemGenerated;
      public Edit(Action cmd, SequenceI[] sqs, int pos, int count,
              char gap)
      {
      {
        return gapChar;
      }
+     public void setSystemGenerated(boolean b)
+     {
+       systemGenerated = b;
+     }
+     public boolean isSystemGenerated()
+     {
+       return systemGenerated;
+     }
    }
  
    /**
@@@ -35,6 -35,7 +35,7 @@@ import jalview.datamodel.SequenceGroup
  import jalview.datamodel.SequenceI;
  import jalview.io.DataSourceType;
  import jalview.io.FeaturesFile;
+ import jalview.schemes.ColourSchemeI;
  import jalview.util.MessageManager;
  
  import java.awt.Color;
@@@ -97,19 -98,22 +98,22 @@@ public class AlignViewController implem
        viewport.getAlignment().deleteAllGroups();
        viewport.clearSequenceColours();
        viewport.setSelectionGroup(null);
+       ColourSchemeI colours = viewport.getGlobalColourScheme();
        // set view properties for each group
        for (int g = 0; g < gps.length; g++)
        {
          // gps[g].setShowunconserved(viewport.getShowUnconserved());
          gps[g].setshowSequenceLogo(viewport.isShowSequenceLogo());
          viewport.getAlignment().addGroup(gps[g]);
-         Color col = new Color((int) (Math.random() * 255),
-                 (int) (Math.random() * 255), (int) (Math.random() * 255));
-         col = col.brighter();
-         for (SequenceI sq : gps[g].getSequences(null))
+         if (colours != null)
          {
-           viewport.setSequenceColour(sq, col);
+           gps[g].setColourScheme(colours.getInstance(viewport, gps[g]));
          }
+         Color col = new Color((int) (Math.random() * 255),
+                 (int) (Math.random() * 255), (int) (Math.random() * 255));
+         gps[g].idColour = col;
+         viewport.setUpdateStructures(true);
+         viewport.addSequenceGroup(gps[g]);
        }
        return true;
      }
    }
  
    @Override
 -  public boolean parseFeaturesFile(String file, DataSourceType protocol,
 +  public boolean parseFeaturesFile(Object file, DataSourceType protocol,
            boolean relaxedIdMatching)
    {
      boolean featuresAdded = false;
@@@ -195,7 -195,6 +195,7 @@@ public class Alignment implements Align
    {
      synchronized (sequences)
      {
 +    
        if (i > -1 && i < sequences.size())
        {
          return sequences.get(i);
      int i = 0;
      SequenceI sq = null;
      String sqname = null;
 +    int nseq = sequences.size();
      if (startAfter != null)
      {
        // try to find the sequence in the alignment
        boolean matched = false;
 -      while (i < sequences.size())
 +      while (i < nseq)
        {
          if (getSequenceAt(i++) == startAfter)
          {
          i = 0;
        }
      }
 -    while (i < sequences.size())
 +    while (i < nseq)
      {
        sq = getSequenceAt(i);
        sqname = sq.getName();
      return maxLength;
    }
  
+   @Override
+   public int getVisibleWidth()
+   {
+     int w = getWidth();
+     if (hiddenCols != null)
+     {
+       w -= hiddenCols.getSize();
+     }
+     return w;
+   }
    /**
     * DOCUMENT ME!
     * 
    }
  
    @Override
-   public void setHiddenColumns(HiddenColumns cols)
+   public boolean setHiddenColumns(HiddenColumns cols)
    {
+     boolean changed = cols == null ? hiddenCols != null
+             : !cols.equals(hiddenCols);
      hiddenCols = cols;
+     return changed;
    }
  
    @Override
@@@ -21,6 -21,7 +21,6 @@@
  package jalview.datamodel;
  
  import jalview.analysis.AlignSeq;
 -import jalview.api.DBRefEntryI;
  import jalview.datamodel.features.SequenceFeatures;
  import jalview.datamodel.features.SequenceFeaturesI;
  import jalview.util.Comparison;
@@@ -46,57 -47,33 +46,57 @@@ import fr.orsay.lri.varna.models.rna.RN
   */
  public class Sequence extends ASequence implements SequenceI
  {
 -  SequenceI datasetSequence;
  
 -  String name;
 +      /**
 +       * A subclass that gives us access to modCount, which tracks 
 +       * whether there have been any changes. We use this to update 
 +       * @author hansonr
 +       *
 +       * @param <T>
 +       */
 +  @SuppressWarnings("serial")
 +  public class DBModList<T> extends ArrayList<DBRefEntry> {
 +
 +        protected int getModCount() {
 +                return modCount;
 +        }
 +        
 +  }
 +
 +SequenceI datasetSequence;
 +
 +  private String name;
  
    private char[] sequence;
  
 -  String description;
 +  private String description;
  
 -  int start;
 +  private int start;
  
 -  int end;
 +  private int end;
  
 -  Vector<PDBEntry> pdbIds;
 +  private Vector<PDBEntry> pdbIds;
  
 -  String vamsasId;
 +  private String vamsasId;
  
 -  DBRefEntry[] dbrefs;
 +  private DBModList<DBRefEntry> dbrefs; // controlled access
  
 -  RNA rna;
 +  /**
 +   * a flag to let us know that elements have changed in dbrefs
 +   * 
 +   * @author Bob Hanson
 +   */
 +  private int refModCount = 0;
  
 +  private RNA rna;
 +  
    /**
     * This annotation is displayed below the alignment but the positions are tied
     * to the residues of this sequence
     *
     * TODO: change to List<>
     */
 -  Vector<AlignmentAnnotation> annotation;
 +  private Vector<AlignmentAnnotation> annotation;
  
    private SequenceFeaturesI sequenceFeatureStore;
  
       */
      if (datasetSequence == null)
      {
 -      if (seq.getDBRefs() != null)
 +      List<DBRefEntry> dbr = seq.getDBRefs();
 +      if (dbr != null)
        {
 -        DBRefEntry[] dbr = seq.getDBRefs();
 -        for (int i = 0; i < dbr.length; i++)
 +        for (int i = 0, n = dbr.size(); i < n; i++)
          {
 -          addDBRef(new DBRefEntry(dbr[i]));
 +          addDBRef(new DBRefEntry(dbr.get(i)));
          }
        }
  
    @Override
    public GeneLociI getGeneLoci()
    {
 -    DBRefEntry[] refs = getDBRefs();
 +    List<DBRefEntry> refs = getDBRefs();
      if (refs != null)
      {
        for (final DBRefEntry ref : refs)
              @Override
              public String getAssemblyId()
              {
 +              // DEV NOTE: DBRefEntry is reused here to hold chromosomal locus of a gene sequence. 
 +              // source=species, version=assemblyId, accession=chromosome, map = positions.
 +              
                return ref.getVersion();
              }
  
     * {@inheritDoc}
     */
    @Override
-   public Range findPositions(int fromColumn, int toColumn)
+   public ContiguousI findPositions(int fromColumn, int toColumn)
    {
      if (toColumn < fromColumn || fromColumn < 1)
      {
      vamsasId = id;
    }
  
 +  @Deprecated
    @Override
 -  public void setDBRefs(DBRefEntry[] dbref)
 +  public void setDBRefs(DBModList<DBRefEntry> newDBrefs)
    {
      if (dbrefs == null && datasetSequence != null
              && this != datasetSequence)
      {
 -      datasetSequence.setDBRefs(dbref);
 +      datasetSequence.setDBRefs(newDBrefs);
        return;
      }
 -    dbrefs = dbref;
 -    if (dbrefs != null)
 -    {
 -      DBRefUtils.ensurePrimaries(this);
 -    }
 +    dbrefs = newDBrefs;
 +    refModCount = 0;
    }
  
    @Override
 -  public DBRefEntry[] getDBRefs()
 +  public DBModList<DBRefEntry> getDBRefs()
    {
      if (dbrefs == null && datasetSequence != null
              && this != datasetSequence)
      return dbrefs;
    }
  
 +
    @Override
    public void addDBRef(DBRefEntry entry)
    {
  
      if (dbrefs == null)
      {
 -      dbrefs = new DBRefEntry[0];
 +      dbrefs = new DBModList<DBRefEntry>();    
      }
  
 -    for (DBRefEntryI dbr : dbrefs)
 +    for (int ib = 0, nb= dbrefs.size(); ib < nb; ib++)
      {
 -      if (dbr.updateFrom(entry))
 +      if (dbrefs.get(ib).updateFrom(entry))
        {
          /*
           * found a dbref that either matched, or could be
        }
      }
  
 -    /*
 -     * extend the array to make room for one more
 -     */
 -    // TODO use an ArrayList instead
 -    int j = dbrefs.length;
 -    DBRefEntry[] temp = new DBRefEntry[j + 1];
 -    System.arraycopy(dbrefs, 0, temp, 0, j);
 -    temp[temp.length - 1] = entry;
 -
 -    dbrefs = temp;
  
 -    DBRefUtils.ensurePrimaries(this);
 +//    ///  BH OUCH!
 +//    /*
 +//     * extend the array to make room for one more
 +//     */
 +//    // TODO use an ArrayList instead
 +//    int j = dbrefs.length;
 +//    List<DBRefEntry> temp = new DBRefEntry[j + 1];
 +//    System.arraycopy(dbrefs, 0, temp, 0, j);
 +//    temp[temp.length - 1] = entry;
 +//
 +//    dbrefs = temp;
 +    
 +    dbrefs.add(entry);
    }
  
    @Override
  
    private int _seqhash = 0;
  
 +private List<DBRefEntry> primaryRefs;
 +
    /**
     * Answers false if the sequence is more than 85% nucleotide (ACGTU), else
     * true
        // TODO: could merge DBRefs
        return datasetSequence.updatePDBIds();
      }
 -    if (dbrefs == null || dbrefs.length == 0)
 +    if (dbrefs == null || dbrefs.size() == 0)
      {
        return false;
      }
      boolean added = false;
 -    for (DBRefEntry dbr : dbrefs)
 +    for (int ib = 0, nb = dbrefs.size(); ib < nb; ib++)
      {
 +      DBRefEntry dbr = dbrefs.get(ib);
        if (DBRefSource.PDB.equals(dbr.getSource()))
        {
          /*
        }
      }
      // transfer database references
 -    DBRefEntry[] entryRefs = entry.getDBRefs();
 +    List<DBRefEntry> entryRefs = entry.getDBRefs();
      if (entryRefs != null)
      {
 -      for (int r = 0; r < entryRefs.length; r++)
 +      for (int r = 0, n = entryRefs.size(); r < n; r++)
        {
 -        DBRefEntry newref = new DBRefEntry(entryRefs[r]);
 +        DBRefEntry newref = new DBRefEntry(entryRefs.get(r));
          if (newref.getMap() != null && mp != null)
          {
            // remap ref using our local mapping
      return null;
    }
  
 +  private List<DBRefEntry> tmpList;
 +  
    @Override
    public List<DBRefEntry> getPrimaryDBRefs()
    {
      {
        return datasetSequence.getPrimaryDBRefs();
      }
 -    if (dbrefs == null || dbrefs.length == 0)
 +    if (dbrefs == null || dbrefs.size() == 0)
      {
        return Collections.emptyList();
      }
      synchronized (dbrefs)
      {
 -      List<DBRefEntry> primaries = new ArrayList<>();
 -      DBRefEntry[] tmp = new DBRefEntry[1];
 -      for (DBRefEntry ref : dbrefs)
 +      if (refModCount == dbrefs.getModCount() && primaryRefs != null)
 +        return primaryRefs; // no changes
 +      refModCount = dbrefs.getModCount(); 
 +      List<DBRefEntry> primaries = (primaryRefs == null ? (primaryRefs = new ArrayList<>()) : primaryRefs);
 +      primaries.clear();
 +      if (tmpList == null) {
 +        tmpList = new ArrayList<>();
 +        tmpList.add(null); // for replacement 
 +      }
 +      for (int i = 0, n = dbrefs.size(); i < n; i++)
        {
 +      DBRefEntry ref = dbrefs.get(i);
          if (!ref.isPrimaryCandidate())
          {
            continue;
            }
          }
          // whilst it looks like it is a primary ref, we also sanity check type
 -        if (DBRefUtils.getCanonicalName(DBRefSource.PDB)
 -                .equals(DBRefUtils.getCanonicalName(ref.getSource())))
 +        if (DBRefSource.PDB_CANONICAL_NAME.equals(ref.getCanonicalSourceName()))
          {
            // PDB dbrefs imply there should be a PDBEntry associated
            // TODO: tighten PDB dbrefs
            // handle on the PDBEntry, and a real mapping between sequence and
            // extracted sequence from PDB file
            PDBEntry pdbentry = getPDBEntry(ref.getAccessionId());
 -          if (pdbentry != null && pdbentry.getFile() != null)
 +          if (pdbentry == null || pdbentry.getFile() == null)
            {
 -            primaries.add(ref);
 +            continue;
            }
 -          continue;
 -        }
 -        // check standard protein or dna sources
 -        tmp[0] = ref;
 -        DBRefEntry[] res = DBRefUtils.selectDbRefs(!isProtein(), tmp);
 -        if (res != null && res[0] == tmp[0])
 -        {
 -          primaries.add(ref);
 -          continue;
 -        }
 +        } else {
 +            // check standard protein or dna sources
 +            tmpList.set(0, ref);
 +            List<DBRefEntry> res = DBRefUtils.selectDbRefs(!isProtein(), tmpList);
 +            if (res == null || res.get(0) != tmpList.get(0))
 +            {
 +              continue;
 +            }
 +          }
 +        primaries.add(ref);
        }
 +      
 +      // version must be not null, as otherwise it will not be a candidate, above
 +      DBRefUtils.ensurePrimaries(this, primaries);
        return primaries;
      }
    }
@@@ -88,7 -88,7 +88,7 @@@ public class SequenceGroup implements A
    /**
     * group members
     */
-   private List<SequenceI> sequences = new ArrayList<>();
+   private List<SequenceI> sequences;
  
    /**
     * representative sequence for this group (if any)
    {
      groupName = "JGroup:" + this.hashCode();
      cs = new ResidueShader();
+     sequences = new ArrayList<>();
    }
  
    /**
      }
    }
  
+   /**
+    * Constructor that copies the given list of sequences
+    * 
+    * @param seqs
+    */
+   public SequenceGroup(List<SequenceI> seqs)
+   {
+     this();
+     this.sequences.addAll(seqs);
+   }
    public boolean isShowSequenceLogo()
    {
      return showSequenceLogo;
      for (int i = 0, ipos = 0; i < inorder.length; i++)
      {
        SequenceI seq = inorder[i];
 -
 -      seqs[ipos] = seq.getSubSequence(startRes, endRes + 1);
 -      if (seqs[ipos] != null)
 +      SequenceI seqipos = seqs[ipos] = seq.getSubSequence(startRes, endRes + 1);
 +      if (seqipos != null)
        {
 -        seqs[ipos].setDescription(seq.getDescription());
 -        seqs[ipos].setDBRefs(seq.getDBRefs());
 -        seqs[ipos].setSequenceFeatures(seq.getSequenceFeatures());
 +        seqipos.setDescription(seq.getDescription());
 +        seqipos.setDBRefs(seq.getDBRefs());
 +        seqipos.setSequenceFeatures(seq.getSequenceFeatures());
          if (seq.getDatasetSequence() != null)
          {
 -          seqs[ipos].setDatasetSequence(seq.getDatasetSequence());
 +          seqipos.setDatasetSequence(seq.getDatasetSequence());
          }
  
          if (seq.getAnnotation() != null)
              if (alann != null)
              {
                boolean found = false;
 -              for (int pos = 0; pos < alann.length; pos++)
 +              for (int pos = 0, np = alann.length; pos < np; pos++)
                {
                  if (alann[pos] == tocopy)
                  {
              newannot.restrict(startRes, endRes);
              newannot.setSequenceRef(seqs[ipos]);
              newannot.adjustForAlignment();
 -            seqs[ipos].addAlignmentAnnotation(newannot);
 +            seqipos.addAlignmentAnnotation(newannot);
            }
          }
          ipos++;
   */
  package jalview.datamodel;
  
 +import jalview.datamodel.Sequence.DBModList;
  import jalview.datamodel.features.SequenceFeaturesI;
  import jalview.util.MapList;
 +import jalview.ws.params.InvalidArgumentException;
  
  import java.util.BitSet;
  import java.util.Iterator;
@@@ -114,9 -112,11 +114,11 @@@ public interface SequenceI extends ASeq
     * get a range on the sequence as a string
     * 
     * @param start
-    *          position relative to start of sequence including gaps (from 0)
+    *          (inclusive) position relative to start of sequence including gaps
+    *          (from 0)
     * @param end
-    *          position relative to start of sequence including gaps (from 0)
+    *          (exclusive) position relative to start of sequence including gaps
+    *          (from 0)
     * 
     * @return String containing all gap and symbols in specified range
     */
     *          - last column, base 1
     * @return
     */
-   public Range findPositions(int fromColum, int toColumn);
+   public ContiguousI findPositions(int fromColum, int toColumn);
  
    /**
     * Returns an int array where indices correspond to each residue in the
    /**
     * set the array of Database references for the sequence.
     * 
 +   * BH 2019.02.04 changes param to DBModlist 
 +   * 
     * @param dbs
     * @deprecated - use is discouraged since side-effects may occur if DBRefEntry
     *             set are not normalised.
 +   * @throws InvalidArgumentException if the is not one created by Sequence itself
     */
    @Deprecated
 -  public void setDBRefs(DBRefEntry[] dbs);
 +  public void setDBRefs(DBModList<DBRefEntry> dbs);
  
 -  public DBRefEntry[] getDBRefs();
 +  public DBModList<DBRefEntry> getDBRefs();
  
    /**
     * add the given entry to the list of DBRefs for this sequence, or replace a
     * @return first residue not contained in regions
     */
    public int firstResidueOutsideIterator(Iterator<int[]> it);
 +
 +
  }
 +
   */
  package jalview.ext.ensembl;
  
 +import jalview.bin.Cache;
 +import jalview.javascript.json.JSON;
 +import jalview.util.JSONUtils;
 +import jalview.util.Platform;
  import jalview.util.StringUtils;
  
  import java.io.BufferedReader;
@@@ -31,7 -27,6 +31,7 @@@ import java.io.DataOutputStream
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
 +import java.io.Reader;
  import java.net.HttpURLConnection;
  import java.net.MalformedURLException;
  import java.net.ProtocolException;
@@@ -42,7 -37,9 +42,7 @@@ import java.util.Map
  
  import javax.ws.rs.HttpMethod;
  
 -import org.json.simple.JSONArray;
 -import org.json.simple.JSONObject;
 -import org.json.simple.parser.JSONParser;
 +import org.json.simple.parser.ParseException;
  
  /**
   * Base class for Ensembl REST service clients
   */
  abstract class EnsemblRestClient extends EnsemblSequenceFetcher
  {
 +      
 +  static {
 +        Cache.addJ2SDirectDatabaseCall("http://rest.ensembl");
 +        Cache.addJ2SDirectDatabaseCall("https://rest.ensembl");
 +  }
 +  
    private static final int DEFAULT_READ_TIMEOUT = 5 * 60 * 1000; // 5 minutes
  
    private static final int CONNECT_TIMEOUT_MS = 10 * 1000; // 10 seconds
  
    /*
     * update these constants when Jalview has been checked / updated for
-    * changes to Ensembl REST API (ref JAL-2105)
+    * changes to Ensembl REST API, and updated JAL-3018
     * @see https://github.com/Ensembl/ensembl-rest/wiki/Change-log
     * @see http://rest.ensembl.org/info/rest?content-type=application/json
     */
-   private static final String LATEST_ENSEMBLGENOMES_REST_VERSION = "7.0";
+   private static final String LATEST_ENSEMBLGENOMES_REST_VERSION = "9.0";
  
-   private static final String LATEST_ENSEMBL_REST_VERSION = "7.0";
+   private static final String LATEST_ENSEMBL_REST_VERSION = "9.0";
  
    private static final String REST_CHANGE_LOG = "https://github.com/Ensembl/ensembl-rest/wiki/Change-log";
  
     * @see http://rest.ensembl.org/documentation/info/ping
     * @return
     */
 -  boolean checkEnsembl()
 +  @SuppressWarnings("unchecked")
 +boolean checkEnsembl()
    {
      BufferedReader br = null;
      String pingUrl = getDomain() + "/info/ping" + CONTENT_TYPE_JSON;
      {
        // note this format works for both ensembl and ensemblgenomes
        // info/ping.json works for ensembl only (March 2016)
 -      URL ping = new URL(pingUrl);
 -
 +      
 +      
 +      
 +      
        /*
         * expect {"ping":1} if ok
         * if ping takes more than 2 seconds to respond, treat as if unavailable
         */
 -      br = getHttpResponse(ping, null, 2 * 1000);
 -      if (br == null)
 -      {
 -        // error reponse status
 -        return false;
 -      }
 -      JSONParser jp = new JSONParser();
 -      JSONObject val = (JSONObject) jp.parse(br);
 +      Map<String, Object> val = (Map<String, Object>) getJSON(new URL(pingUrl), null, 2 * 1000, MODE_MAP, null);
 +      if (val == null)
 +        return false;
        String pingString = val.get("ping").toString();
        return pingString != null;
      } catch (Throwable t)
      return false;
    }
  
 -  /**
 -   * Returns a reader to a (Json) response from the Ensembl sequence endpoint.
 -   * If the request failed the return value may be null.
 -   * 
 -   * @param ids
 -   * @return
 -   * @throws IOException
 -   */
 -  protected BufferedReader getSequenceReader(List<String> ids)
 -          throws IOException
 -  {
 -    URL url = getUrl(ids);
 -
 -    BufferedReader reader = getHttpResponse(url, ids);
 -    return reader;
 -  }
 -
 -  /**
 -   * Gets a reader to the HTTP response, using the default read timeout of 5
 -   * minutes
 -   * 
 -   * @param url
 -   * @param ids
 -   * @return
 -   * @throws IOException
 -   */
 -  protected BufferedReader getHttpResponse(URL url, List<String> ids)
 -          throws IOException
 -  {
 -    return getHttpResponse(url, ids, DEFAULT_READ_TIMEOUT);
 -  }
 +  
 +  protected final static int MODE_ARRAY    = 0;
 +  protected final static int MODE_MAP      = 1;
 +  protected final static int MODE_ITERATOR = 2;
 +  
 +//  /**
 +//   * Returns a reader to a (Json) response from the Ensembl sequence endpoint.
 +//   * If the request failed the return value may be null.
 +//   * 
 +//   * @param ids
 +//   * @return
 +//   * @throws IOException
 +// * @throws ParseException 
 +//   */
 +//  protected Object getSequenceJSON(List<String> ids, int mode)
 +//          throws IOException, ParseException
 +//  {
 +//    URL url = getUrl(ids);
 +//    return getJSON(url, ids, -1, mode);
 +//  }
 +//
 +//  /**
 +//   * Gets a reader to the HTTP response, using the default read timeout of 5
 +//   * minutes
 +//   * 
 +//   * @param url
 +//   * @param ids
 +//   * @return
 +//   * @throws IOException
 +//   */
 +//  protected BufferedReader getHttpResponse(URL url, List<String> ids)
 +//          throws IOException
 +//  {
 +//    return getHttpResponse(url, ids, DEFAULT_READ_TIMEOUT);
 +//  }
  
    /**
     * Sends the HTTP request and gets the response as a reader. Returns null if
     * @return
     * @throws IOException
     */
 -  protected BufferedReader getHttpResponse(URL url, List<String> ids,
 +  private BufferedReader getHttpResponse(URL url, List<String> ids,
            int readTimeout) throws IOException
    {
 +      if (readTimeout < 0)
 +              readTimeout = DEFAULT_READ_TIMEOUT;
      int retriesLeft = MAX_RETRIES;
      HttpURLConnection connection = null;
      int responseCode = 0;
 -
 +       
 +    if (Platform.isJS()) {
 +        JSON.setAjax(url);
 +    }
 +       
      while (retriesLeft > 0)
      {
        connection = tryConnection(url, ids, readTimeout);
         * note: a GET request for an invalid id returns an error code e.g. 415
         * but POST request returns 200 and an empty Fasta response 
         */
 -      System.err.println("Response code " + responseCode + " for " + url);
 +      System.err.println("Response code " + responseCode);// + " for " + url);
        return null;
      }
  
 + 
      InputStream response = connection.getInputStream();
 +    
 +    if (Platform.isJS()) {
 +      return JSON.getJSONReader(response);
 +    }
  
      // System.out.println(getClass().getName() + " took "
      // + (System.currentTimeMillis() - now) + "ms to fetch");
  
 -    BufferedReader reader = null;
 -    reader = new BufferedReader(new InputStreamReader(response, "UTF-8"));
 -    return reader;
 +    return new BufferedReader(new InputStreamReader(response, "UTF-8"));
    }
  
    /**
            int readTimeout) throws IOException, ProtocolException
    {
      // System.out.println(System.currentTimeMillis() + " " + url);
 +      
      HttpURLConnection connection = (HttpURLConnection) url.openConnection();
  
      /*
      connection.setRequestProperty("Content-Type", getRequestMimeType());
      connection.setRequestProperty("Accept", getResponseMimeType());
  
      connection.setDoInput(true);
      connection.setDoOutput(multipleIds);
  
 +    connection.setUseCaches(false);
      connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
      connection.setReadTimeout(readTimeout);
  
      StringBuilder postBody = new StringBuilder(64);
      postBody.append("{\"ids\":[");
      first = true;
 -    for (String id : ids)
 +    for (int i = 0, n = ids.size(); i < n; i++)
      {
 +      String id = ids.get(i);
        if (!first)
        {
          postBody.append(",");
      wr.close();
    }
  
 +      /**
 +       * Primary access point to parsed JSON data, including the call to retrieve and
 +       * parsing.
 +       * 
 +       * @param url     request url; if null, getUrl(ids) will be used
 +       * @param ids     optional; may be null
 +       * @param msDelay -1 for default delay
 +       * @param mode    map, array, or array iterator
 +       * @param mapKey  an optional key for an outer map 
 +       * @return                a Map, List, Iterator, or null
 +       * @throws IOException
 +       * @throws ParseException
 +       * 
 +       * @author Bob Hanson 2019
 +       */
 +  @SuppressWarnings("unchecked")
 +  protected Object getJSON(URL url, List<String> ids, int msDelay, int mode, String mapKey) throws IOException, ParseException {
 +
 +      if (url == null)
 +         url = getUrl(ids);
 +      
 +      Reader br = null;
 +      try {
 +
 +        Platform.timeCheck(null, Platform.TIME_MARK);
 +              
 +      br = (url == null ? null : getHttpResponse(url, ids, msDelay));
 +
 +      Object ret = (br == null ? null : JSONUtils.parse(br));
 +
 +        Platform.timeCheck("EnsembleRestClient.getJSON " + url, Platform.TIME_MARK);
 +      
 +
 +      if (ret != null && mapKey != null)
 +          ret = ((Map<String, Object>) ret).get(mapKey);
 +      if (ret == null) 
 +      {
 +       return null;
 +      }
 +      switch (mode) {
 +      case MODE_ARRAY:
 +      case MODE_MAP:
 +      break;
 +      case MODE_ITERATOR:
 +      ret = ((List<Object>) ret).iterator();
 +      break;
 +      }
 +      return ret;
 +        
 +    } finally
 +    {
 +      if (br != null)
 +      {
 +        try
 +        {
 +          br.close();
 +        } catch (IOException e)
 +        {
 +        // ignore
 +        }
 +      }    
 +    }
 +  }
 +
 +
 +
    /**
     * Fetches and checks Ensembl's REST version number
     * 
     * @return
     */
 +  @SuppressWarnings("unchecked")
    private void checkEnsemblRestVersion()
    {
      EnsemblData info = domainData.get(getDomain());
  
 -    JSONParser jp = new JSONParser();
 -    URL url = null;
      try
      {
 -      url = new URL(getDomain() + "/info/rest" + CONTENT_TYPE_JSON);
 -      BufferedReader br = getHttpResponse(url, null);
 -      if (br == null)
 -      {
 -        return;
 -      }
 -      JSONObject val = (JSONObject) jp.parse(br);
 +      Map<String, Object> val = (Map<String, Object>) getJSON(new URL(getDomain() + "/info/rest" + CONTENT_TYPE_JSON), null, -1, MODE_MAP, null);
 +      if (val == null)
 +        return;
        String version = val.get("release").toString();
        String majorVersion = version.substring(0, version.indexOf("."));
        String expected = info.expectedRestVersion;
     * 
     * @return
     */
 +  @SuppressWarnings("unchecked")
    private void checkEnsemblDataVersion()
    {
 -    JSONParser jp = new JSONParser();
 -    URL url = null;
 -    BufferedReader br = null;
 -
 -    try
 -    {
 -      url = new URL(getDomain() + "/info/data" + CONTENT_TYPE_JSON);
 -      br = getHttpResponse(url, null);
 -      if (br != null)
 -      {
 -        JSONObject val = (JSONObject) jp.parse(br);
 -        JSONArray versions = (JSONArray) val.get("releases");
 -        domainData.get(getDomain()).dataVersion = versions.get(0)
 -                .toString();
 -      }
 -    } catch (Throwable t)
 -    {
 -      System.err.println(
 -              "Error checking Ensembl data version: " + t.getMessage());
 -    } finally
 -    {
 -      if (br != null)
 -      {
 -        try
 -        {
 -          br.close();
 -        } catch (IOException e)
 -        {
 -          // ignore
 -        }
 -      }
 -    }
 +    Map<String, Object> val;
 +      try 
 +      {
 +        val = (Map<String, Object>) getJSON(
 +                                new URL(getDomain() + "/info/data" + CONTENT_TYPE_JSON), null, -1, MODE_MAP, null);
 +        if (val == null)
 +          return;
 +        List<Object> versions = (List<Object>) val.get("releases");
 +        domainData.get(getDomain()).dataVersion = versions.get(0).toString();
 +      } catch (Throwable e) {//could be IOException | ParseException e) {
 +        System.err.println("Error checking Ensembl data version: " + e.getMessage());
 +      }
    }
  
    public String getEnsemblDataVersion()
@@@ -21,7 -21,6 +21,7 @@@
  
  package jalview.fts.core;
  
 +import jalview.bin.Cache;
  import jalview.fts.api.FTSDataColumnI;
  import jalview.fts.api.GFTSPanelI;
  import jalview.fts.core.FTSDataColumnPreferences.PreferenceSource;
@@@ -31,11 -30,11 +31,12 @@@ import jalview.gui.JvSwingUtils
  import jalview.gui.SequenceFetcher;
  import jalview.io.cache.JvCacheableInputBox;
  import jalview.util.MessageManager;
 +import jalview.util.Platform;
  
  import java.awt.BorderLayout;
  import java.awt.CardLayout;
  import java.awt.Dimension;
+ import java.awt.Font;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.FocusAdapter;
@@@ -75,6 -74,7 +76,6 @@@ import javax.swing.event.DocumentListen
  import javax.swing.event.InternalFrameEvent;
  import javax.swing.table.DefaultTableModel;
  import javax.swing.table.TableColumn;
 -import javax.swing.text.JTextComponent;
  
  /**
   * This class provides the swing GUI layout for FTS Panel and implements most of
  @SuppressWarnings("serial")
  public abstract class GFTSPanel extends JPanel implements GFTSPanelI
  {
+   private static final Font VERDANA_12 = new Font("Verdana", 0, 12);
    protected JInternalFrame mainFrame = new JInternalFrame(
            getFTSFrameTitle());
  
 -  protected JTabbedPane tabs = new JTabbedPane();
 +  protected JTabbedPane tabs = jalview.jbgui.GDesktop.createTabbedPane();
 + 
    protected IProgressIndicator progressIndicator;
  
    protected JComboBox<FTSDataColumnI> cmb_searchTarget = new JComboBox<>();
  
    protected JLabel lbl_blank = new JLabel(balnkPlaceholderImage);
  
 -  private JTabbedPane tabbedPane = new JTabbedPane();
 +  JTabbedPane tabbedPane = jalview.jbgui.GDesktop.createTabbedPane();
  
    private JPanel pnl_actions = new JPanel();
  
        {
          tabs.addTab(MessageManager.getString("label.retrieve_ids"),
                  fetcher);
 -        fetcher.setDatabaseChooserVisible(false);
 -        fetcher.embedWithFTSPanel(this);
 +        fetcher.embedIn(this);
        }
        mainFrame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
        final JPanel ftsPanel = this;
            if (tabs != null
                    && tabs.getSelectedComponent() == ftsPanel)
            {
 -            txt_search.requestFocusInWindow();
 +            txt_search.getComponent().requestFocusInWindow();
            }
          }
        });
      Integer height = getTempUserPrefs().get("FTSPanel.height") == null ? 400
              : getTempUserPrefs().get("FTSPanel.height");
      lbl_warning.setVisible(false);
-     lbl_warning.setFont(new java.awt.Font("Verdana", 0, 12));
+     lbl_warning.setFont(VERDANA_12);
      lbl_loading.setVisible(false);
-     lbl_loading.setFont(new java.awt.Font("Verdana", 0, 12));
+     lbl_loading.setFont(VERDANA_12);
      lbl_blank.setVisible(true);
-     lbl_blank.setFont(new java.awt.Font("Verdana", 0, 12));
+     lbl_blank.setFont(VERDANA_12);
  
      tbl_summary.setAutoCreateRowSorter(true);
      tbl_summary.getTableHeader().setReorderingAllowed(false);
        }
      });
  
+     JButton txt_help = new JButton("?");
+     txt_help.setFont(VERDANA_12);
+     txt_help.setPreferredSize(new Dimension(15, 15));
+     txt_help.setToolTipText(MessageManager.getString("action.help"));
+     txt_help.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         showHelp();
+       }
+     });
      btn_autosearch.setText(MessageManager.getString("option.autosearch"));
      btn_autosearch.setToolTipText(
              MessageManager.getString("option.enable_disable_autosearch"));
 -    btn_autosearch.setSelected(
 -            jalview.bin.Cache.getDefault(getAutosearchPreference(), true));
 +      btn_autosearch.setSelected(Platform.isJS() ? false : 
 +              Cache.getDefault(getAutosearchPreference(), true));
      btn_autosearch.addActionListener(new java.awt.event.ActionListener()
      {
        @Override
                  Boolean.toString(btn_autosearch.isSelected()));
        }
      });
-     btn_back.setFont(new java.awt.Font("Verdana", 0, 12));
+     btn_back.setFont(VERDANA_12);
      btn_back.setText(MessageManager.getString("action.back"));
      btn_back.addActionListener(new java.awt.event.ActionListener()
      {
      });
  
      btn_ok.setEnabled(false);
-     btn_ok.setFont(new java.awt.Font("Verdana", 0, 12));
+     btn_ok.setFont(VERDANA_12);
      btn_ok.setText(MessageManager.getString("action.ok"));
      btn_ok.addActionListener(new java.awt.event.ActionListener()
      {
      btn_next_page.setEnabled(false);
      btn_next_page.setToolTipText(
              MessageManager.getString("label.next_page_tooltip"));
-     btn_next_page.setFont(new java.awt.Font("Verdana", 0, 12));
+     btn_next_page.setFont(VERDANA_12);
      btn_next_page.setText(MessageManager.getString("action.next_page"));
      btn_next_page.addActionListener(new java.awt.event.ActionListener()
      {
      btn_prev_page.setEnabled(false);
      btn_prev_page.setToolTipText(
              MessageManager.getString("label.prev_page_tooltip"));
-     btn_prev_page.setFont(new java.awt.Font("Verdana", 0, 12));
+     btn_prev_page.setFont(VERDANA_12);
      btn_prev_page.setText(MessageManager.getString("action.prev_page"));
      btn_prev_page.addActionListener(new java.awt.event.ActionListener()
      {
        btn_next_page.setVisible(false);
      }
  
-     btn_cancel.setFont(new java.awt.Font("Verdana", 0, 12));
+     btn_cancel.setFont(VERDANA_12);
      btn_cancel.setText(MessageManager.getString("action.cancel"));
      btn_cancel.addActionListener(new java.awt.event.ActionListener()
      {
      });
      scrl_searchResult.setPreferredSize(new Dimension(width, height));
  
-     cmb_searchTarget.setFont(new java.awt.Font("Verdana", 0, 12));
+     cmb_searchTarget.setFont(VERDANA_12);
      cmb_searchTarget.addItemListener(new ItemListener()
      {
        @Override
                      "label.separate_multiple_query_values", new Object[]
                      { getCmbSearchTarget().getSelectedItem().toString() });
            }
 -          txt_search.setToolTipText(
 +          txt_search.getComponent().setToolTipText(
                    JvSwingUtils.wrapTooltip(true, tooltipText));
            searchAction(true);
          }
        }
      });
  
-     txt_search.getComponent().setFont(new java.awt.Font("Verdana", 0, 12));
 -    txt_search.setFont(VERDANA_12);
++    txt_search.getComponent().setFont(VERDANA_12);
  
 -    txt_search.getEditor().getEditorComponent()
 -            .addKeyListener(new KeyAdapter()
 +    txt_search.addKeyListener(new KeyAdapter()
              {
                @Override
                public void keyPressed(KeyEvent e)
                  }
                }
              }, false);
 -    ((JTextComponent) txt_search.getEditor().getEditorComponent())
 -            .getDocument().addDocumentListener(listener);
 +    txt_search.addDocumentListener(listener);
  
      txt_search.addFocusListener(new FocusListener()
      {
  
      txt_search.addActionListener(new ActionListener()
      {
 -
        @Override
        public void actionPerformed(ActionEvent e)
        {
            btn_ok.setEnabled(false);
            btn_next_page.setEnabled(false);
            btn_prev_page.setEnabled(false);
 -          txt_search.setEnabled(false);
 +          txt_search.getComponent().setEnabled(false);
            cmb_searchTarget.setEnabled(false);
            previousWantedFields = getFTSRestClient()
                    .getAllDefaultDisplayedFTSDataColumns()
            btn_back.setEnabled(true);
            btn_cancel.setEnabled(true);
            refreshPaginatorState();
 -          txt_search.setEnabled(true);
 +          txt_search.getComponent().setEnabled(true);
            cmb_searchTarget.setEnabled(true);
            if (wantedFieldsUpdated())
            {
  
      pnl_results.add(tabbedPane);
      pnl_inputs.add(cmb_searchTarget);
 -    pnl_inputs.add(txt_search);
 +    pnl_inputs.add(txt_search.getComponent());
+     pnl_inputs.add(txt_help);
      pnl_inputs.add(btn_autosearch);
      pnl_inputs.add(lbl_loading);
      pnl_inputs.add(lbl_warning);
      Desktop.addInternalFrame(mainFrame, getFTSFrameTitle(), width, height);
    }
  
+   abstract protected void showHelp();
    protected void closeAction()
    {
      getTempUserPrefs().put("FTSPanel.width", this.getWidth());
      return cmb_searchTarget;
    }
  
 -  public JComboBox<String> getTxtSearch()
 -  {
 -    return txt_search;
 -  }
 -
    public JInternalFrame getMainFrame()
    {
      return mainFrame;
      }
    }
  
 +  /**
 +   * Action on Back button is to close this panel and open a new Sequence
 +   * Fetcher panel
 +   */
    public void btn_back_ActionPerformed()
    {
      closeAction();
  
    public void transferToSequenceFetcher(String ids)
    {
 -    seqFetcher.getTextArea().setText(ids);
 +    seqFetcher.setQuery(ids);
      Thread worker = new Thread(seqFetcher);
      worker.start();
    }
      lbl_blank.setVisible(true);
      btn_ok.setEnabled(false);
      mainFrame.setTitle(getFTSFrameTitle());
 -    referesh();
 +    refresh();
      tbl_summary.setModel(new DefaultTableModel());
      tbl_summary.setVisible(false);
    }
      }
    }
  
 -  public void referesh()
 +  public void refresh()
    {
      mainFrame.setTitle(getFTSFrameTitle());
    }
  
 +  @Override
 +  public abstract void okAction();
  }
@@@ -26,6 -26,8 +26,8 @@@ import jalview.fts.api.FTSRestClientI
  import jalview.fts.core.FTSRestRequest;
  import jalview.fts.core.FTSRestResponse;
  import jalview.fts.core.GFTSPanel;
+ import jalview.gui.Help;
+ import jalview.gui.Help.HelpId;
  import jalview.gui.SequenceFetcher;
  import jalview.util.MessageManager;
  
@@@ -33,13 -35,15 +35,15 @@@ import java.util.HashMap
  import java.util.HashSet;
  import java.util.Map;
  
+ import javax.help.HelpSetException;
  @SuppressWarnings("serial")
  public class PDBFTSPanel extends GFTSPanel
  {
    private static String defaultFTSFrameTitle = MessageManager
            .getString("label.pdb_sequence_fetcher");
  
-   private static Map<String, Integer> tempUserPrefs = new HashMap<String, Integer>();
+   private static Map<String, Integer> tempUserPrefs = new HashMap<>();
  
    private static final String PDB_FTS_CACHE_KEY = "CACHE.PDB_FTS";
  
  
            if (isPaginationEnabled() && resultSetCount > 0)
            {
 +            String f1 = totalNumberformatter.format(Integer.valueOf(offSet + 1));
 +            String f2 = totalNumberformatter
 +                    .format(Integer.valueOf(offSet + resultSetCount));
 +            String f3 = totalNumberformatter
 +                    .format(Integer.valueOf(totalResultSetCount));
              updateSearchFrameTitle(defaultFTSFrameTitle + " - " + result
 -                    + " "
 -                    + totalNumberformatter.format((Number) (offSet + 1))
 -                    + " to "
 -                    + totalNumberformatter
 -                            .format((Number) (offSet + resultSetCount))
 -                    + " of "
 -                    + totalNumberformatter
 -                            .format((Number) totalResultSetCount)
 -                    + " " + " (" + (endTime - startTime) + " milli secs)");
 +                    + " " + f1 + " to " + f2 + " of " + f3 + " " + " ("
 +                    + (endTime - startTime) + " milli secs)");
            }
            else
            {
      // mainFrame.dispose();
      disableActionButtons();
      StringBuilder selectedIds = new StringBuilder();
-     HashSet<String> selectedIdsSet = new HashSet<String>();
+     HashSet<String> selectedIdsSet = new HashSet<>();
      int primaryKeyColIndex = 0;
      try
      {
      }
  
      String ids = selectedIds.toString();
 -    // System.out.println(">>>>>>>>>>>>>>>> selected Ids: " + ids);
 -    seqFetcher.getTextArea().setText(ids);
 +    seqFetcher.setQuery(ids);
      Thread worker = new Thread(seqFetcher);
      worker.start();
      delayAndEnableActionButtons();
    {
      return PDB_AUTOSEARCH;
    }
+   @Override
+   protected void showHelp()
+   {
+     try
+     {
+       Help.showHelpWindow(HelpId.PdbFts);
+     } catch (HelpSetException e1)
+     {
+       e1.printStackTrace();
+     }
+  }
  }
@@@ -26,6 -26,8 +26,8 @@@ import jalview.fts.api.FTSRestClientI
  import jalview.fts.core.FTSRestRequest;
  import jalview.fts.core.FTSRestResponse;
  import jalview.fts.core.GFTSPanel;
+ import jalview.gui.Help;
+ import jalview.gui.Help.HelpId;
  import jalview.gui.SequenceFetcher;
  import jalview.util.MessageManager;
  
@@@ -33,6 -35,8 +35,8 @@@ import java.util.HashMap
  import java.util.HashSet;
  import java.util.Map;
  
+ import javax.help.HelpSetException;
  @SuppressWarnings("serial")
  public class UniprotFTSPanel extends GFTSPanel
  {
  
    private static final String UNIPROT_AUTOSEARCH = "FTS.UNIPROT.AUTOSEARCH";
  
 +  /**
 +   * Constructor given an (optional) sequence fetcher panel to revert to on
 +   * clicking the 'Back' button
 +   * 
 +   * @param fetcher
 +   */
    public UniprotFTSPanel(SequenceFetcher fetcher)
    {
      super(fetcher);
            {
              updateSearchFrameTitle(defaultFTSFrameTitle + " - " + result
                      + " "
 -                    + totalNumberformatter.format((Number) (offSet + 1))
 +                    + totalNumberformatter
 +                            .format(Integer.valueOf(offSet + 1))
                      + " to "
                      + totalNumberformatter
 -                            .format((Number) (offSet + resultSetCount))
 +                            .format(Integer
 +                                    .valueOf(offSet + resultSetCount))
                      + " of "
                      + totalNumberformatter
 -                            .format((Number) totalResultSetCount)
 +                            .format(Integer.valueOf(totalResultSetCount))
                      + " " + " (" + (endTime - startTime) + " milli secs)");
            }
            else
      }
  
      String ids = selectedIds.toString();
 -    // System.out.println(">>>>>>>>>>>>>>>> selected Ids: " + ids);
 -    seqFetcher.getTextArea().setText(ids);
 +    seqFetcher.setQuery(ids);
      Thread worker = new Thread(seqFetcher);
      worker.start();
      delayAndEnableActionButtons();
    {
      return UNIPROT_AUTOSEARCH;
    }
+   @Override
+   protected void showHelp()
+   {
+     try
+     {
+       Help.showHelpWindow(HelpId.UniprotFts);
+     } catch (HelpSetException e1)
+     {
+       e1.printStackTrace();
+     }
+   }
  }
@@@ -24,9 -24,10 +24,10 @@@ import jalview.analysis.AlignmentSorter
  import jalview.analysis.AlignmentUtils;
  import jalview.analysis.CrossRef;
  import jalview.analysis.Dna;
+ import jalview.analysis.GeneticCodeI;
  import jalview.analysis.ParseProperties;
  import jalview.analysis.SequenceIdMatcher;
 -import jalview.api.AlignExportSettingI;
 +import jalview.api.AlignExportSettingsI;
  import jalview.api.AlignViewControllerGuiI;
  import jalview.api.AlignViewControllerI;
  import jalview.api.AlignViewportI;
@@@ -45,7 -46,6 +46,7 @@@ import jalview.commands.RemoveGapColCom
  import jalview.commands.RemoveGapsCommand;
  import jalview.commands.SlideSequencesCommand;
  import jalview.commands.TrimRegionCommand;
 +import jalview.datamodel.AlignExportSettingsAdapter;
  import jalview.datamodel.AlignedCodonFrame;
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentAnnotation;
@@@ -55,6 -55,7 +56,6 @@@ import jalview.datamodel.AlignmentOrder
  import jalview.datamodel.AlignmentView;
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.HiddenColumns;
 -import jalview.datamodel.HiddenSequences;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.SeqCigar;
  import jalview.datamodel.Sequence;
@@@ -64,6 -65,7 +65,7 @@@ import jalview.gui.ColourMenuHelper.Col
  import jalview.gui.ViewSelectionMenu.ViewSetProvider;
  import jalview.io.AlignmentProperties;
  import jalview.io.AnnotationFile;
+ import jalview.io.BackupFiles;
  import jalview.io.BioJsHTMLOutput;
  import jalview.io.DataSourceType;
  import jalview.io.FileFormat;
@@@ -83,13 -85,11 +85,14 @@@ import jalview.io.ScoreMatrixFile
  import jalview.io.TCoffeeScoreFile;
  import jalview.io.vcf.VCFLoader;
  import jalview.jbgui.GAlignFrame;
++import jalview.project.Jalview2XML;
  import jalview.schemes.ColourSchemeI;
  import jalview.schemes.ColourSchemes;
  import jalview.schemes.ResidueColourScheme;
  import jalview.schemes.TCoffeeColourScheme;
 +import jalview.util.ImageMaker.TYPE;
  import jalview.util.MessageManager;
 +import jalview.util.Platform;
  import jalview.viewmodel.AlignmentViewport;
  import jalview.viewmodel.ViewportRanges;
  import jalview.ws.DBRefFetcher;
@@@ -100,7 -100,6 +103,7 @@@ import jalview.ws.jws2.jabaws2.Jws2Inst
  import jalview.ws.seqfetcher.DbSourceProxy;
  
  import java.awt.BorderLayout;
 +import java.awt.Color;
  import java.awt.Component;
  import java.awt.Rectangle;
  import java.awt.Toolkit;
@@@ -137,15 -136,13 +140,16 @@@ 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;
  
@@@ -184,8 -181,6 +188,8 @@@ public class AlignFrame extends GAlignF
     */
    String fileName = null;
  
 +  File fileObject;
 +
    /**
     * Creates a new AlignFrame object with specific width and height.
     * 
     */
    void init()
    {
 +//      setBackground(Color.white); // BH 2019
 +                
      if (!Jalview.isHeadlessMode())
      {
        progressBar = new ProgressBar(this.statusPanel, this.statusBar);
      if (Desktop.desktop != null)
      {
        this.setDropTarget(new java.awt.dnd.DropTarget(this, this));
 -      addServiceListeners();
 +      /**
 +       * BH 2018 ignore service listeners
 +       * 
 +       * @j2sNative
 +       * 
 +       */
 +      {
 +        addServiceListeners();
 +      }
        setGUINucleotide();
      }
  
    }
  
    /**
 +   * JavaScript will have this, maybe others. More dependable than a file name
 +   * and maintains a reference to the actual bytes loaded.
 +   * 
 +   * @param file
 +   */
 +  public void setFileObject(File file)
 +  {
 +    this.fileObject = file;
 +  }
 +
 +  /**
     * Add a KeyListener with handlers for various KeyPressed and KeyReleased
     * events
     */
          case KeyEvent.VK_BACK_SPACE:
            if (!viewport.cursorMode)
            {
 -            cut_actionPerformed(null);
 +            cut_actionPerformed();
            }
            else
            {
  
          case KeyEvent.VK_F2:
            viewport.cursorMode = !viewport.cursorMode;
 -          statusBar.setText(MessageManager
 +          setStatus(MessageManager
                    .formatMessage("label.keyboard_editing_mode", new String[]
                    { (viewport.cursorMode ? "on" : "off") }));
            if (viewport.cursorMode)
      return progressBar.operationInProgress();
    }
  
+   /**
+    * Sets the text of the status bar. Note that setting a null or empty value
+    * will cause the status bar to be hidden, with possibly undesirable flicker
+    * of the screen layout.
+    */
    @Override
    public void setStatus(String text)
    {
 -    statusBar.setText(text);
 +        // BH note: If text width and height are 0, then the layout manager
 +        // will dispense of it and change the frame height. 
 +        // In JavaScript, we use \u00A0  -- unicode "non-breaking space"
 +        // which is the unicode encoding of &nbsp;
 +        
 +    statusBar.setText(text == null || text.isEmpty() ? " " : text);
    }
  
    /*
    }
  
    @Override
 -  public void fetchSequence_actionPerformed(ActionEvent e)
 +  public void fetchSequence_actionPerformed()
    {
 -    new jalview.gui.SequenceFetcher(this);
 +    new SequenceFetcher(this);
    }
  
    @Override
          Rectangle bounds = this.getBounds();
  
          FileLoader loader = new FileLoader();
 -        DataSourceType protocol = fileName.startsWith("http:")
 -                ? DataSourceType.URL
 -                : DataSourceType.FILE;
 -        AlignFrame newframe = loader.LoadFileWaitTillLoaded(fileName,
 -                protocol, currentFileFormat);
 +
 +        AlignFrame newframe = null;
 +
 +        if (fileObject == null)
 +        {
 +
 +          DataSourceType protocol = (fileName.startsWith("http:")
 +                  ? DataSourceType.URL
 +                  : DataSourceType.FILE);
 +          newframe = loader.LoadFileWaitTillLoaded(fileName, protocol,
 +                  currentFileFormat);
 +        }
 +        else
 +        {
 +          newframe = loader.LoadFileWaitTillLoaded(fileObject,
 +                  DataSourceType.FILE, currentFileFormat);
 +        }
  
          newframe.setBounds(bounds);
          if (featureSettings != null && featureSettings.isShowing())
      if (fileName == null || (currentFileFormat == null)
              || fileName.startsWith("http"))
      {
 -      saveAs_actionPerformed(null);
 +      saveAs_actionPerformed();
      }
      else
      {
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Saves the alignment to a file with a name chosen by the user, if necessary
 +   * warning if a file would be overwritten
     */
    @Override
 -  public void saveAs_actionPerformed(ActionEvent e)
 +  public void saveAs_actionPerformed()
    {
      String format = currentFileFormat == null ? null
              : currentFileFormat.getName();
  
      int value = chooser.showSaveDialog(this);
  
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    if (value != JalviewFileChooser.APPROVE_OPTION)
 +    {
 +      return;
 +    }
 +    currentFileFormat = chooser.getSelectedFormat();
 +    // todo is this (2005) test now obsolete - value is never null?
 +    while (currentFileFormat == null)
      {
 +      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 +              MessageManager
 +                      .getString("label.select_file_format_before_saving"),
 +              MessageManager.getString("label.file_format_not_specified"),
 +              JvOptionPane.WARNING_MESSAGE);
        currentFileFormat = chooser.getSelectedFormat();
 -      while (currentFileFormat == null)
 +      value = chooser.showSaveDialog(this);
 +      if (value != JalviewFileChooser.APPROVE_OPTION)
        {
 -        JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 -                MessageManager.getString(
 -                        "label.select_file_format_before_saving"),
 -                MessageManager.getString("label.file_format_not_specified"),
 -                JvOptionPane.WARNING_MESSAGE);
 -        currentFileFormat = chooser.getSelectedFormat();
 -        value = chooser.showSaveDialog(this);
 -        if (value != JalviewFileChooser.APPROVE_OPTION)
 -        {
 -          return;
 -        }
 +        return;
        }
 +    }
  
 -      fileName = chooser.getSelectedFile().getPath();
 +    fileName = chooser.getSelectedFile().getPath();
  
 -      Cache.setProperty("DEFAULT_FILE_FORMAT", currentFileFormat.getName());
 +    Cache.setProperty("DEFAULT_FILE_FORMAT", currentFileFormat.getName());
 +    Cache.setProperty("LAST_DIRECTORY", fileName);
 +    saveAlignment(fileName, currentFileFormat);
 +  }
  
 -      Cache.setProperty("LAST_DIRECTORY", fileName);
 -      saveAlignment(fileName, currentFileFormat);
 +  boolean lastSaveSuccessful = false;
 +
 +  FileFormatI lastFormatSaved;
 +
 +  String lastFilenameSaved;
 +
 +  /**
 +   * Raise a dialog or status message for the last call to saveAlignment.
 +   *
 +   * @return true if last call to saveAlignment(file, format) was successful.
 +   */
 +  public boolean isSaveAlignmentSuccessful()
 +  {
 +
 +    if (!lastSaveSuccessful)
 +    {
 +      JvOptionPane.showInternalMessageDialog(this, MessageManager
 +              .formatMessage("label.couldnt_save_file", new Object[]
 +              { lastFilenameSaved }),
 +              MessageManager.getString("label.error_saving_file"),
 +              JvOptionPane.WARNING_MESSAGE);
      }
 +    else
 +    {
 +
 +      setStatus(MessageManager.formatMessage(
 +              "label.successfully_saved_to_file_in_format", new Object[]
 +              { lastFilenameSaved, lastFormatSaved }));
 +
 +    }
 +    return lastSaveSuccessful;
    }
  
 -  public boolean saveAlignment(String file, FileFormatI format)
 +  /**
 +   * Saves the alignment to the specified file path, in the specified format,
 +   * which may be an alignment format, or Jalview project format. If the
 +   * alignment has hidden regions, or the format is one capable of including
 +   * non-sequence data (features, annotations, groups), then the user may be
 +   * prompted to specify what to include in the output.
 +   * 
 +   * @param file
 +   * @param format
 +   */
 +  public void saveAlignment(String file, FileFormatI format)
    {
 -    boolean success = true;
 +    lastSaveSuccessful = false;
 +    lastFilenameSaved = file;
 +    lastFormatSaved = format;
  
      if (FileFormat.Jalview.equals(format))
      {
        String shortName = title;
 -
 -      if (shortName.indexOf(java.io.File.separatorChar) > -1)
 +      if (shortName.indexOf(File.separatorChar) > -1)
        {
          shortName = shortName.substring(
 -                shortName.lastIndexOf(java.io.File.separatorChar) + 1);
 +                shortName.lastIndexOf(File.separatorChar) + 1);
        }
-       lastSaveSuccessful = new jalview.project.Jalview2XML().saveAlignment(this, file,
-               shortName);
 -
 -      success = new jalview.project.Jalview2XML().saveAlignment(this, file,
 -              shortName);
 -
++      lastSaveSuccessful = new Jalview2XML().saveAlignment(this, file, shortName);
 +      
        statusBar.setText(MessageManager.formatMessage(
                "label.successfully_saved_to_file_in_format", new Object[]
                { fileName, format }));
 -
 +      
 +      return;
      }
 -    else
 +
 +    AlignExportSettingsI options = new AlignExportSettingsAdapter(false);
 +    Runnable cancelAction = new Runnable()
      {
 -      AlignmentExportData exportData = getAlignmentForExport(format,
 -              viewport, null);
 -      if (exportData.getSettings().isCancelled())
 -      {
 -        return false;
 -      }
 -      FormatAdapter f = new FormatAdapter(alignPanel,
 -              exportData.getSettings());
 -      String output = f.formatSequences(format, exportData.getAlignment(), // class
 -                                                                           // cast
 -                                                                           // exceptions
 -                                                                           // will
 -              // occur in the distant future
 -              exportData.getOmitHidden(), exportData.getStartEndPostions(),
 -              f.getCacheSuffixDefault(format),
 -              viewport.getAlignment().getHiddenColumns());
 -
 -      if (output == null)
 +      @Override
 +      public void run()
        {
 -        success = false;
 +        lastSaveSuccessful = false;
        }
 -      else
 +    };
 +    Runnable outputAction = new Runnable()
 +    {
 +      @Override
 +      public void run()
        {
 -        // create backupfiles object and get new temp filename destination
 -        BackupFiles backupfiles = new BackupFiles(file);
 -
 -        try
 +        // todo defer this to inside formatSequences (or later)
 +        AlignmentExportData exportData = viewport
 +                .getAlignExportData(options);
 +        String output = new FormatAdapter(alignPanel, options)
 +                .formatSequences(format, exportData.getAlignment(),
 +                        exportData.getOmitHidden(),
 +                        exportData.getStartEndPostions(),
 +                        viewport.getAlignment().getHiddenColumns());
 +        if (output == null)
 +        {
 +          lastSaveSuccessful = false;
 +        }
 +        else
          {
 -          PrintWriter out = new PrintWriter(
 -                  new FileWriter(backupfiles.getTempFilePath()));
++          // create backupfiles object and get new temp filename destination
++          BackupFiles backupfiles = new BackupFiles(file);
 +          try
 +          {
-             PrintWriter out = new PrintWriter(new FileWriter(file));
++            PrintWriter out = new PrintWriter(
++                    new FileWriter(backupfiles.getTempFilePath()));
 -          out.print(output);
 -          out.close();
 -          this.setTitle(file);
 -          statusBar.setText(MessageManager.formatMessage(
 +            out.print(output);
 +            out.close();
 +            AlignFrame.this.setTitle(file);
-             setStatus(MessageManager.formatMessage(
-                     "label.successfully_saved_to_file_in_format",
-                     new Object[]
-                     { fileName, format.getName() }));
++            statusBar.setText(MessageManager.formatMessage(
+                   "label.successfully_saved_to_file_in_format", new Object[]
+                   { fileName, format.getName() }));
 -        } catch (Exception ex)
 -        {
 -          success = false;
 -          ex.printStackTrace();
 -        }
 -
 -        backupfiles.setWriteSuccess(success);
 -        // do the backup file roll and rename the temp file to actual file
 -        success = backupfiles.rollBackupsAndRenameTempFile();
 +          } catch (Exception ex)
 +          {
 +            lastSaveSuccessful = false;
 +            ex.printStackTrace();
 +          }
++          backupfiles.setWriteSuccess(lastSaveSuccessful);
++          // do the backup file roll and rename the temp file to actual file
++          lastSaveSuccessful = backupfiles.rollBackupsAndRenameTempFile();
 +        }
        }
 -    }
 +    };
  
 -    if (!success)
 -    {
 -      JvOptionPane.showInternalMessageDialog(this, MessageManager
 -              .formatMessage("label.couldnt_save_file", new Object[]
 -              { fileName }),
 -              MessageManager.getString("label.error_saving_file"),
 -              JvOptionPane.WARNING_MESSAGE);
 -    }
 -
 -    return success;
 -  }
 -
 -  private void warningMessage(String warning, String title)
 -  {
 -    if (new jalview.util.Platform().isHeadless())
 +    /*
 +     * show dialog with export options if applicable; else just do it
 +     */
 +    if (AlignExportOptions.isNeeded(viewport, format))
      {
 -      System.err.println("Warning: " + title + "\nWarning: " + warning);
 -
 +      AlignExportOptions choices = new AlignExportOptions(
 +              alignPanel.getAlignViewport(), format, options);
 +      choices.setResponseAction(0, outputAction);
 +      choices.setResponseAction(1, cancelAction);
 +      choices.showDialog();
      }
      else
      {
 -      JvOptionPane.showInternalMessageDialog(this, warning, title,
 -              JvOptionPane.WARNING_MESSAGE);
 +      outputAction.run();
      }
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Outputs the alignment to textbox in the requested format, if necessary
 +   * first prompting the user for whether to include hidden regions or
 +   * non-sequence data
     * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * @param fileFormatName
     */
    @Override
 -  protected void outputText_actionPerformed(ActionEvent e)
 +  protected void outputText_actionPerformed(String fileFormatName)
    {
      FileFormatI fileFormat = FileFormats.getInstance()
 -            .forName(e.getActionCommand());
 -    AlignmentExportData exportData = getAlignmentForExport(fileFormat,
 -            viewport, null);
 -    if (exportData.getSettings().isCancelled())
 +            .forName(fileFormatName);
 +    AlignExportSettingsI options = new AlignExportSettingsAdapter(false);
 +    Runnable outputAction = new Runnable()
      {
 -      return;
 -    }
 -    CutAndPasteTransfer cap = new CutAndPasteTransfer();
 -    cap.setForInput(null);
 -    try
 -    {
 -      FileFormatI format = fileFormat;
 -      cap.setText(new FormatAdapter(alignPanel, exportData.getSettings())
 -              .formatSequences(format, exportData.getAlignment(),
 -                      exportData.getOmitHidden(),
 -                      exportData.getStartEndPostions(),
 -                      viewport.getAlignment().getHiddenColumns()));
 -      Desktop.addInternalFrame(cap, MessageManager
 -              .formatMessage("label.alignment_output_command", new Object[]
 -              { e.getActionCommand() }), 600, 500);
 -    } catch (OutOfMemoryError oom)
 -    {
 -      new OOMWarning("Outputting alignment as " + e.getActionCommand(),
 -              oom);
 -      cap.dispose();
 -    }
 -
 -  }
 -
 -  public static AlignmentExportData getAlignmentForExport(
 -          FileFormatI format, AlignViewportI viewport,
 -          AlignExportSettingI exportSettings)
 -  {
 -    AlignmentI alignmentToExport = null;
 -    AlignExportSettingI settings = exportSettings;
 -    String[] omitHidden = null;
 -
 -    HiddenSequences hiddenSeqs = viewport.getAlignment()
 -            .getHiddenSequences();
 -
 -    alignmentToExport = viewport.getAlignment();
 -
 -    boolean hasHiddenSeqs = hiddenSeqs.getSize() > 0;
 -    if (settings == null)
 -    {
 -      settings = new AlignExportSettings(hasHiddenSeqs,
 -              viewport.hasHiddenColumns(), format);
 -    }
 -    // settings.isExportAnnotations();
 -
 -    if (viewport.hasHiddenColumns() && !settings.isExportHiddenColumns())
 -    {
 -      omitHidden = viewport.getViewAsString(false,
 -              settings.isExportHiddenSequences());
 -    }
 +      @Override
 +      public void run()
 +      {
 +        // todo defer this to inside formatSequences (or later)
 +        AlignmentExportData exportData = viewport
 +                .getAlignExportData(options);
 +        CutAndPasteTransfer cap = new CutAndPasteTransfer();
 +        cap.setForInput(null);
 +        try
 +        {
 +          FileFormatI format = fileFormat;
 +          cap.setText(new FormatAdapter(alignPanel, options)
 +                  .formatSequences(format, exportData.getAlignment(),
 +                          exportData.getOmitHidden(),
 +                          exportData.getStartEndPostions(),
 +                          viewport.getAlignment().getHiddenColumns()));
 +          Desktop.addInternalFrame(cap, MessageManager.formatMessage(
 +                  "label.alignment_output_command", new Object[]
 +                  { fileFormat.getName() }), 600, 500);
 +        } catch (OutOfMemoryError oom)
 +        {
 +          new OOMWarning("Outputting alignment as " + fileFormat.getName(),
 +                  oom);
 +          cap.dispose();
 +        }
 +      }
 +    };
  
 -    int[] alignmentStartEnd = new int[2];
 -    if (hasHiddenSeqs && settings.isExportHiddenSequences())
 +    /*
 +     * show dialog with export options if applicable; else just do it
 +     */
 +    if (AlignExportOptions.isNeeded(viewport, fileFormat))
      {
 -      alignmentToExport = hiddenSeqs.getFullAlignment();
 +      AlignExportOptions choices = new AlignExportOptions(
 +              alignPanel.getAlignViewport(), fileFormat, options);
 +      choices.setResponseAction(0, outputAction);
 +      choices.showDialog();
      }
      else
      {
 -      alignmentToExport = viewport.getAlignment();
 +      outputAction.run();
      }
 -    alignmentStartEnd = viewport.getAlignment().getHiddenColumns()
 -            .getVisibleStartAndEndIndex(alignmentToExport.getWidth());
 -    AlignmentExportData ed = new AlignmentExportData(alignmentToExport,
 -            omitHidden, alignmentStartEnd, settings);
 -    return ed;
    }
  
    /**
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Creates a PNG image of the alignment and writes it to the given file. If
 +   * the file is null, the user is prompted to choose a file.
     * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * @param f
     */
    @Override
    public void createPNG(File f)
    {
 -    alignPanel.makePNG(f);
 +    alignPanel.makeAlignmentImage(TYPE.PNG, f);
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Creates an EPS image of the alignment and writes it to the given file. If
 +   * the file is null, the user is prompted to choose a file.
     * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * @param f
     */
    @Override
    public void createEPS(File f)
    {
 -    alignPanel.makeEPS(f);
 +    alignPanel.makeAlignmentImage(TYPE.EPS, f);
    }
  
 +  /**
 +   * Creates an SVG image of the alignment and writes it to the given file. If
 +   * the file is null, the user is prompted to choose a file.
 +   * 
 +   * @param f
 +   */
    @Override
    public void createSVG(File f)
    {
 -    alignPanel.makeSVG(f);
 +    alignPanel.makeAlignmentImage(TYPE.SVG, f);
    }
  
    @Override
    @Override
    public void associatedData_actionPerformed(ActionEvent e)
    {
 -    // Pick the tree file
 -    JalviewFileChooser chooser = new JalviewFileChooser(
 +    final JalviewFileChooser chooser = new JalviewFileChooser(
              jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
      chooser.setFileView(new JalviewFileView());
      chooser.setDialogTitle(
              MessageManager.getString("label.load_jalview_annotations"));
      chooser.setToolTipText(
              MessageManager.getString("label.load_jalview_annotations"));
 -
 -    int value = chooser.showOpenDialog(null);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    chooser.setResponseHandler(0, new Runnable()
      {
 -      String choice = chooser.getSelectedFile().getPath();
 -      jalview.bin.Cache.setProperty("LAST_DIRECTORY", choice);
 -      loadJalviewDataFile(choice, null, null, null);
 -    }
 +      @Override
 +      public void run()
 +      {
 +        String choice = chooser.getSelectedFile().getPath();
 +        jalview.bin.Cache.setProperty("LAST_DIRECTORY", choice);
 +        loadJalviewDataFile(chooser.getSelectedFile(), null, null, null);
 +      }
 +    });
  
 +    chooser.showOpenDialog(this);
    }
  
    /**
     *          DOCUMENT ME!
     */
    @Override
 -  protected void copy_actionPerformed(ActionEvent e)
 +  protected void copy_actionPerformed()
    {
      if (viewport.getSelectionGroup() == null)
      {
  
      Desktop.jalviewClipboard = new Object[] { seqs,
          viewport.getAlignment().getDataset(), hiddenColumns };
 -    statusBar.setText(MessageManager.formatMessage(
 +    setStatus(MessageManager.formatMessage(
              "label.copied_sequences_to_clipboard", new Object[]
              { Integer.valueOf(seqs.length).toString() }));
    }
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Action Cut (delete and copy) the selected region
     */
    @Override
 -  protected void cut_actionPerformed(ActionEvent e)
 +  protected void cut_actionPerformed()
    {
 -    copy_actionPerformed(null);
 -    delete_actionPerformed(null);
 +    copy_actionPerformed();
 +    delete_actionPerformed();
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Performs menu option to Delete the currently selected region
     */
    @Override
 -  protected void delete_actionPerformed(ActionEvent evt)
 +  protected void delete_actionPerformed()
    {
  
      SequenceGroup sg = viewport.getSelectionGroup();
        return;
      }
  
 +    Runnable okAction = new Runnable() 
 +    {
 +              @Override
 +              public void run() 
 +              {
 +                  SequenceI[] cut = sg.getSequences()
 +                          .toArray(new SequenceI[sg.getSize()]);
 +
 +                  addHistoryItem(new EditCommand(
 +                          MessageManager.getString("label.cut_sequences"), Action.CUT,
 +                          cut, sg.getStartRes(), sg.getEndRes() - sg.getStartRes() + 1,
 +                          viewport.getAlignment()));
 +
 +                  viewport.setSelectionGroup(null);
 +                  viewport.sendSelection();
 +                  viewport.getAlignment().deleteGroup(sg);
 +
 +                  viewport.firePropertyChange("alignment", null,
 +                          viewport.getAlignment().getSequences());
 +                  if (viewport.getAlignment().getHeight() < 1)
 +                  {
 +                    try
 +                    {
 +                      AlignFrame.this.setClosed(true);
 +                    } catch (Exception ex)
 +                    {
 +                    }
 +                  }
 +              }};
 +
      /*
 -     * If the cut affects all sequences, warn, remove highlighted columns
 +     * If the cut affects all sequences, prompt for confirmation
       */
 -    if (sg.getSize() == viewport.getAlignment().getHeight())
 -    {
 -      boolean isEntireAlignWidth = (((sg.getEndRes() - sg.getStartRes())
 -              + 1) == viewport.getAlignment().getWidth()) ? true : false;
 -      if (isEntireAlignWidth)
 -      {
 -        int confirm = JvOptionPane.showConfirmDialog(this,
 -                MessageManager.getString("warn.delete_all"), // $NON-NLS-1$
 -                MessageManager.getString("label.delete_all"), // $NON-NLS-1$
 -                JvOptionPane.OK_CANCEL_OPTION);
 -
 -        if (confirm == JvOptionPane.CANCEL_OPTION
 -                || confirm == JvOptionPane.CLOSED_OPTION)
 -        {
 -          return;
 -        }
 -      }
 -      viewport.getColumnSelection().removeElements(sg.getStartRes(),
 -              sg.getEndRes() + 1);
 -    }
 -    SequenceI[] cut = sg.getSequences()
 -            .toArray(new SequenceI[sg.getSize()]);
 -
 -    addHistoryItem(new EditCommand(
 -            MessageManager.getString("label.cut_sequences"), Action.CUT,
 -            cut, sg.getStartRes(), sg.getEndRes() - sg.getStartRes() + 1,
 -            viewport.getAlignment()));
 -
 -    viewport.setSelectionGroup(null);
 -    viewport.sendSelection();
 -    viewport.getAlignment().deleteGroup(sg);
 -
 -    viewport.firePropertyChange("alignment", null,
 -            viewport.getAlignment().getSequences());
 -    if (viewport.getAlignment().getHeight() < 1)
 -    {
 -      try
 -      {
 -        this.setClosed(true);
 -      } catch (Exception ex)
 -      {
 -      }
 -    }
 +    boolean wholeHeight = sg.getSize() == viewport.getAlignment().getHeight();
 +    boolean wholeWidth = (((sg.getEndRes() - sg.getStartRes())
 +            + 1) == viewport.getAlignment().getWidth()) ? true : false;
 +      if (wholeHeight && wholeWidth)
 +      {
 +          JvOptionPane dialog = JvOptionPane.newOptionDialog(Desktop.desktop);
 +              dialog.setResponseHandler(0, okAction); // 0 = OK_OPTION
 +          Object[] options = new Object[] { MessageManager.getString("action.ok"),
 +                  MessageManager.getString("action.cancel") };
 +              dialog.showDialog(MessageManager.getString("warn.delete_all"),
 +                  MessageManager.getString("label.delete_all"),
 +                  JvOptionPane.DEFAULT_OPTION, JvOptionPane.PLAIN_MESSAGE, null,
 +                  options, options[0]);
 +      } else 
 +      {
 +              okAction.run();
 +      }
    }
  
    /**
    @Override
    public void selectAllSequenceMenuItem_actionPerformed(ActionEvent e)
    {
-     SequenceGroup sg = new SequenceGroup();
-     for (int i = 0; i < viewport.getAlignment().getSequences().size(); i++)
-     {
-       sg.addSequence(viewport.getAlignment().getSequenceAt(i), false);
-     }
+     SequenceGroup sg = new SequenceGroup(
+             viewport.getAlignment().getSequences());
  
      sg.setEndRes(viewport.getAlignment().getWidth() - 1);
      viewport.setSelectionGroup(sg);
+     viewport.isSelectionGroupChanged(true);
      viewport.sendSelection();
      // JAL-2034 - should delegate to
      // alignPanel to decide if overview needs
                  column, viewport.getAlignment());
        }
  
 -      statusBar.setText(MessageManager
 +      setStatus(MessageManager
                .formatMessage("label.removed_columns", new String[]
                { Integer.valueOf(trimRegion.getSize()).toString() }));
  
  
      addHistoryItem(removeGapCols);
  
 -    statusBar.setText(MessageManager
 +    setStatus(MessageManager
              .formatMessage("label.removed_empty_columns", new Object[]
              { Integer.valueOf(removeGapCols.getSize()).toString() }));
  
      newap.av.setRedoList(viewport.getRedoList());
  
      /*
+      * copy any visualisation settings that are not saved in the project
+      */
+     newap.av.setColourAppliesToAllGroups(
+             viewport.getColourAppliesToAllGroups());
+     /*
       * Views share the same mappings; need to deregister any new mappings
       * created by copyAlignPanel, and register the new reference to the shared
       * mappings
      viewport.setFollowHighlight(state);
      if (state)
      {
-       alignPanel.scrollToPosition(viewport.getSearchResults(), false);
+       alignPanel.scrollToPosition(viewport.getSearchResults());
      }
    }
  
      viewport.expandColSelection(sg, false);
      viewport.hideAllSelectedSeqs();
      viewport.hideSelectedColumns();
+     alignPanel.updateLayout();
      alignPanel.paintAlignment(true, true);
      viewport.sendSelection();
    }
    public void hideSelColumns_actionPerformed(ActionEvent e)
    {
      viewport.hideSelectedColumns();
+     alignPanel.updateLayout();
      alignPanel.paintAlignment(true, true);
      viewport.sendSelection();
    }
    @Override
    public void alignmentProperties()
    {
 -    JEditorPane editPane = new JEditorPane("text/html", "");
 -    editPane.setEditable(false);
 +    JComponent pane;
      StringBuffer contents = new AlignmentProperties(viewport.getAlignment())
 +
              .formatAsHtml();
 -    editPane.setText(
 -            MessageManager.formatMessage("label.html_content", new Object[]
 -            { contents.toString() }));
 +    String content = MessageManager.formatMessage("label.html_content",
 +            new Object[]
 +            { contents.toString() });
 +    contents = null;
 +
 +    if (Platform.isJS())
 +    {
 +      JLabel textLabel = new JLabel();
 +      textLabel.setText(content);
 +      textLabel.setBackground(Color.WHITE);
 +      
 +      pane = new JPanel(new BorderLayout());
 +      ((JPanel) pane).setOpaque(true);
 +      pane.setBackground(Color.WHITE);
 +      ((JPanel) pane).add(textLabel, BorderLayout.NORTH);
 +    }
 +    else
 +    {
 +      JEditorPane editPane = new JEditorPane("text/html", "");
 +      editPane.setEditable(false);
 +      editPane.setText(content);
 +      pane = editPane;
 +    }
 +
      JInternalFrame frame = new JInternalFrame();
 -    frame.getContentPane().add(new JScrollPane(editPane));
 +
 +    frame.getContentPane().add(new JScrollPane(pane));
  
      Desktop.addInternalFrame(frame, MessageManager
              .formatMessage("label.alignment_properties", new Object[]
       * otherwise set the chosen colour scheme (or null for 'None')
       */
      ColourSchemeI cs = ColourSchemes.getInstance().getColourScheme(name,
+             viewport,
              viewport.getAlignment(), viewport.getHiddenRepSequences());
      changeColour(cs);
    }
      chooser.setToolTipText(
              MessageManager.getString("label.load_tree_file"));
  
 -    int value = chooser.showOpenDialog(null);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    chooser.setResponseHandler(0,new Runnable()
      {
 -      String filePath = chooser.getSelectedFile().getPath();
 -      Cache.setProperty("LAST_DIRECTORY", filePath);
 -      NewickFile fin = null;
 -      try
 -      {
 -        fin = new NewickFile(filePath, DataSourceType.FILE);
 -        viewport.setCurrentTree(showNewickTree(fin, filePath).getTree());
 -      } catch (Exception ex)
 -      {
 -        JvOptionPane.showMessageDialog(Desktop.desktop, ex.getMessage(),
 -                MessageManager.getString("label.problem_reading_tree_file"),
 -                JvOptionPane.WARNING_MESSAGE);
 -        ex.printStackTrace();
 -      }
 -      if (fin != null && fin.hasWarningMessage())
 +      @Override
 +      public void run()
        {
 -        JvOptionPane.showMessageDialog(Desktop.desktop,
 -                fin.getWarningMessage(),
 -                MessageManager
 -                        .getString("label.possible_problem_with_tree_file"),
 -                JvOptionPane.WARNING_MESSAGE);
 +        String filePath = chooser.getSelectedFile().getPath();
 +        Cache.setProperty("LAST_DIRECTORY", filePath);
 +        NewickFile fin = null;
 +        try
 +        {
 +          fin = new NewickFile(new FileParse(chooser.getSelectedFile(),
 +                  DataSourceType.FILE));
 +          viewport.setCurrentTree(showNewickTree(fin, filePath).getTree());
 +        } catch (Exception ex)
 +        {
 +          JvOptionPane.showMessageDialog(Desktop.desktop, ex.getMessage(),
 +                  MessageManager
 +                          .getString("label.problem_reading_tree_file"),
 +                  JvOptionPane.WARNING_MESSAGE);
 +          ex.printStackTrace();
 +        }
 +        if (fin != null && fin.hasWarningMessage())
 +        {
 +          JvOptionPane.showMessageDialog(Desktop.desktop,
 +                  fin.getWarningMessage(),
 +                  MessageManager.getString(
 +                          "label.possible_problem_with_tree_file"),
 +                  JvOptionPane.WARNING_MESSAGE);
 +        }
        }
 -    }
 +    });
 +    chooser.showOpenDialog(this);
    }
  
    public TreePanel showNewickTree(NewickFile nf, String treeTitle)
     * frame's DNA sequences to their aligned protein (amino acid) equivalents.
     */
    @Override
-   public void showTranslation_actionPerformed(ActionEvent e)
+   public void showTranslation_actionPerformed(GeneticCodeI codeTable)
    {
      AlignmentI al = null;
      try
      {
        Dna dna = new Dna(viewport, viewport.getViewAsVisibleContigs(true));
  
-       al = dna.translateCdna();
+       al = dna.translateCdna(codeTable);
      } catch (Exception ex)
      {
        jalview.bin.Cache.log.error(
        af.setFileFormat(this.currentFileFormat);
        final String newTitle = MessageManager
                .formatMessage("label.translation_of_params", new Object[]
-               { this.getTitle() });
+               { this.getTitle(), codeTable.getId() });
        af.setTitle(newTitle);
        if (Cache.getDefault(Preferences.ENABLE_SPLIT_FRAME, true))
        {
     * Try to load a features file onto the alignment.
     * 
     * @param file
 -   *          contents or path to retrieve file
 +   *          contents or path to retrieve file or a File object
     * @param sourceType
     *          access mode of file (see jalview.io.AlignFile)
     * @return true if features file was parsed correctly.
     */
 -  public boolean parseFeaturesFile(String file, DataSourceType sourceType)
 +  public boolean parseFeaturesFile(Object file, DataSourceType sourceType)
    {
 +    // BH 2018
      return avc.parseFeaturesFile(file, sourceType,
              Cache.getDefault("RELAXEDSEQIDMATCHING", false));
  
      // Java's Transferable for native dnd
      evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
      Transferable t = evt.getTransferable();
 +
      final AlignFrame thisaf = this;
 -    final List<String> files = new ArrayList<>();
 +    final List<Object> files = new ArrayList<>();
      List<DataSourceType> protocols = new ArrayList<>();
  
      try
               * Object[] { String,SequenceI}
               */
              ArrayList<Object[]> filesmatched = new ArrayList<>();
 -            ArrayList<String> filesnotmatched = new ArrayList<>();
 +            ArrayList<Object> filesnotmatched = new ArrayList<>();
              for (int i = 0; i < files.size(); i++)
              {
 -              String file = files.get(i).toString();
 +              // BH 2018
 +              Object file = files.get(i);
 +              String fileName = file.toString();
                String pdbfn = "";
 -              DataSourceType protocol = FormatAdapter.checkProtocol(file);
 +              DataSourceType protocol = (file instanceof File
 +                      ? DataSourceType.FILE
 +                      : FormatAdapter.checkProtocol(fileName));
                if (protocol == DataSourceType.FILE)
                {
 -                File fl = new File(file);
 +                File fl = (file instanceof File ? (File) file
 +                        : new File(fileName));
                  pdbfn = fl.getName();
                }
                else if (protocol == DataSourceType.URL)
                {
 -                URL url = new URL(file);
 +                URL url = new URL(fileName);
                  pdbfn = url.getFile();
                }
                if (pdbfn.length() > 0)
                  }
                  if (mtch != null)
                  {
 -                  FileFormatI type = null;
 +                  FileFormatI type;
                    try
                    {
                      type = new IdentifyFile().identify(file, protocol);
              int assocfiles = 0;
              if (filesmatched.size() > 0)
              {
 -              boolean autoAssociate = Cache.getDefault("AUTOASSOCIATE_PDBANDSEQS", false);
 +              boolean autoAssociate = Cache
 +                      .getDefault("AUTOASSOCIATE_PDBANDSEQS", false);
                if (!autoAssociate)
                {
                  String msg = MessageManager.formatMessage(
                    for (SequenceI toassoc : (SequenceI[]) fm[2])
                    {
                      PDBEntry pe = new AssociatePdbFileWithSeq()
 -                            .associatePdbWithSeq((String) fm[0],
 +                            .associatePdbWithSeq(fm[0].toString(),
                                      (DataSourceType) fm[1], toassoc, false,
                                      Desktop.instance);
                      if (pe != null)
                      {
                        System.err.println("Associated file : "
 -                              + ((String) fm[0]) + " with "
 +                              + (fm[0].toString()) + " with "
                                + toassoc.getDisplayId(true));
                        assocfiles++;
                      }
                   */
                  for (Object[] o : filesmatched)
                  {
 -                  filesnotmatched.add((String) o[0]);
 +                  filesnotmatched.add(o[0]);
                  }
                }
              }
                {
                  return;
                }
 -              for (String fn : filesnotmatched)
 +              for (Object fn : filesnotmatched)
                {
                  loadJalviewDataFile(fn, null, null, null);
                }
     * @param file
     *          either a filename or a URL string.
     */
 -  public void loadJalviewDataFile(String file, DataSourceType sourceType,
 +  public void loadJalviewDataFile(Object file, DataSourceType sourceType,
            FileFormatI format, SequenceI assocSeq)
    {
 +    // BH 2018 was String file
      try
      {
        if (sourceType == null)
                changeColour(
                        new TCoffeeColourScheme(viewport.getAlignment()));
                isAnnotation = true;
 -              statusBar.setText(MessageManager.getString(
 +              setStatus(MessageManager.getString(
                        "label.successfully_pasted_tcoffee_scores_to_alignment"));
              }
              else
                      new FileParse(file, sourceType));
              sm.parse();
              // todo: i18n this message
 -            statusBar.setText(MessageManager.formatMessage(
 +            setStatus(MessageManager.formatMessage(
                      "label.successfully_loaded_matrix",
                      sm.getMatrixName()));
            }
      if (e.isPopupTrigger())
      {
        String msg = MessageManager.getString("label.enter_view_name");
 -      String reply = JvOptionPane.showInternalInputDialog(this, msg, msg,
 -              JvOptionPane.QUESTION_MESSAGE);
 +      String ttl = tabbedPane.getTitleAt(tabbedPane.getSelectedIndex());
 +      String reply = JvOptionPane.showInputDialog(msg, ttl);
  
        if (reply != null)
        {
  
      });
      rfetch.add(fetchr);
 -    final AlignFrame me = this;
      new Thread(new Runnable()
      {
        @Override
        public void run()
        {
          final jalview.ws.SequenceFetcher sf = jalview.gui.SequenceFetcher
 -                .getSequenceFetcherSingleton(me);
 +                .getSequenceFetcherSingleton();
          javax.swing.SwingUtilities.invokeLater(new Runnable()
          {
            @Override
            public void run()
            {
 -            String[] dbclasses = sf.getOrderedSupportedSources();
 -            // sf.getDbInstances(jalview.ws.dbsources.DasSequenceSource.class);
 -            // jalview.util.QuickSort.sort(otherdb, otherdb);
 +            String[] dbclasses = sf.getNonAlignmentSources();
              List<DbSourceProxy> otherdb;
              JMenu dfetch = new JMenu();
              JMenu ifetch = new JMenu();
                {
                  continue;
                }
 -              // List<DbSourceProxy> dbs=otherdb;
 -              // otherdb=new ArrayList<DbSourceProxy>();
 -              // for (DbSourceProxy db:dbs)
 -              // {
 -              // if (!db.isA(DBRefSource.ALIGNMENTDB)
 -              // }
                if (mname == null)
                {
                  mname = "From " + dbclass;
      {
        PaintRefresher.Refresh(this, viewport.getSequenceSetId());
        alignPanel.updateAnnotation();
-       alignPanel.paintAlignment(true, true);
+       alignPanel.paintAlignment(true,
+               viewport.needToUpdateStructureViews());
      }
    }
  
      colourMenu.add(textColour);
      colourMenu.addSeparator();
  
-     ColourMenuHelper.addMenuItems(colourMenu, this, viewport.getAlignment(),
-             false);
+     ButtonGroup bg = ColourMenuHelper.addMenuItems(colourMenu, this,
+             viewport.getAlignment(), false);
  
+     colourMenu.add(annotationColour);
+     bg.add(annotationColour);
      colourMenu.addSeparator();
      colourMenu.add(conservationMenuItem);
      colourMenu.add(modifyConservation);
      colourMenu.add(abovePIDThreshold);
      colourMenu.add(modifyPID);
-     colourMenu.add(annotationColour);
  
      ColourSchemeI colourScheme = viewport.getGlobalColourScheme();
      ColourMenuHelper.setColourSelected(colourMenu, colourScheme);
      chooser.setFileView(new JalviewFileView());
      chooser.setDialogTitle(MessageManager.getString("label.load_vcf_file"));
      chooser.setToolTipText(MessageManager.getString("label.load_vcf_file"));
 -
 -    int value = chooser.showOpenDialog(null);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    final AlignFrame us = this;
 +    chooser.setResponseHandler(0, new Runnable()
      {
 -      String choice = chooser.getSelectedFile().getPath();
 -      Cache.setProperty("LAST_DIRECTORY", choice);
 -      SequenceI[] seqs = viewport.getAlignment().getSequencesArray();
 -      new VCFLoader(choice).loadVCF(seqs, this);
 -    }
 +      @Override
 +      public void run()
 +      {
 +        String choice = chooser.getSelectedFile().getPath();
 +        Cache.setProperty("LAST_DIRECTORY", choice);
 +        SequenceI[] seqs = viewport.getAlignment().getSequencesArray();
 +        new VCFLoader(choice).loadVCF(seqs, us);
 +      };
 +    });
 +    chooser.showOpenDialog(null);
  
    }
 +
  }
  
  class PrintThread extends Thread
@@@ -290,7 -290,7 +290,7 @@@ public class AlignViewport extends Alig
                ResidueColourScheme.NONE);
      }
      ColourSchemeI colourScheme = ColourSchemeProperty
-             .getColourScheme(alignment, schemeName);
+             .getColourScheme(this, alignment, schemeName);
      residueShading = new ResidueShader(colourScheme);
  
      if (colourScheme instanceof UserColourScheme)
      {
        residueShading.setConsensus(hconsensus);
      }
+     setColourAppliesToAllGroups(true);
    }
  
    boolean validCharWidth;
      {
        if (AlignmentUtils.isMappable(toAdd, getAlignment()))
        {
 -        if (openLinkedAlignment(toAdd, title))
 -        {
 -          return;
 -        }
 +        openLinkedAlignment(toAdd, title);
 +        return;
        }
      }
 +    addDataToAlignment(toAdd);
 +  }
  
 -    /*
 -     * No mappings, or offer declined - add sequences to this alignment
 -     */
 +  /**
 +   * adds sequences to this alignment
 +   * 
 +   * @param toAdd
 +   */
 +  void addDataToAlignment(AlignmentI toAdd)
 +  {
      // TODO: JAL-407 regardless of above - identical sequences (based on ID and
      // provenance) should share the same dataset sequence
  
     * @param al
     * @param title
     */
 -  protected boolean openLinkedAlignment(AlignmentI al, String title)
 +  protected void openLinkedAlignment(AlignmentI al, String title)
    {
      String[] options = new String[] { MessageManager.getString("action.no"),
          MessageManager.getString("label.split_window"),
          MessageManager.getString("label.new_window"), };
      final String question = JvSwingUtils.wrapTooltip(true,
              MessageManager.getString("label.open_split_window?"));
 -    int response = JvOptionPane.showOptionDialog(Desktop.desktop, question,
 +    final AlignViewport us = this;
 +    
 +    /*
 +     * options No, Split Window, New Window correspond to
 +     * dialog responses 0, 1, 2 (even though JOptionPane shows them
 +     * in reverse order)
 +     */
 +    JvOptionPane dialog = JvOptionPane.newOptionDialog(Desktop.desktop)
 +            .setResponseHandler(0, new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                  addDataToAlignment(al);
 +              }
 +            }).setResponseHandler(1, new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                us.openLinkedAlignmentAs(al, title, true);
 +              }
 +            }).setResponseHandler(2, new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                us.openLinkedAlignmentAs(al, title, false);
 +              }
 +            });
 +      dialog.showDialog(question,
              MessageManager.getString("label.open_split_window"),
              JvOptionPane.DEFAULT_OPTION, JvOptionPane.PLAIN_MESSAGE, null,
              options, options[0]);
 +  }
  
 -    if (response != 1 && response != 2)
 +  protected void openLinkedAlignmentAs(AlignmentI al, String title,
 +          boolean newWindowOrSplitPane)
      {
 -      return false;
 -    }
 -    final boolean openSplitPane = (response == 1);
 -    final boolean openInNewWindow = (response == 2);
 -
      /*
       * Identify protein and dna alignments. Make a copy of this one if opening
       * in a new split pane.
       */
 -    AlignmentI thisAlignment = openSplitPane ? new Alignment(getAlignment())
 +    AlignmentI thisAlignment = newWindowOrSplitPane
 +            ? new Alignment(getAlignment())
              : getAlignment();
      AlignmentI protein = al.isNucleotide() ? thisAlignment : al;
      final AlignmentI cdna = al.isNucleotide() ? al : thisAlignment;
      // alignFrame.setFileName(file, format);
      // }
  
 -    if (openInNewWindow)
 +    if (!newWindowOrSplitPane)
      {
        Desktop.addInternalFrame(newAlignFrame, title,
                AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
      {
      }
  
 -    if (openSplitPane)
 +    if (newWindowOrSplitPane)
      {
        al.alignAs(thisAlignment);
        protein = openSplitFrame(newAlignFrame, thisAlignment);
      }
 -
 -    return true;
    }
  
    /**
@@@ -24,21 -24,18 +24,21 @@@ import jalview.analysis.AnnotationSorte
  import jalview.api.AlignViewportI;
  import jalview.api.AlignmentViewPanel;
  import jalview.bin.Cache;
 +import jalview.bin.Jalview;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.SearchResultsI;
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
 +import jalview.gui.ImageExporter.ImageWriterI;
  import jalview.io.HTMLOutput;
  import jalview.jbgui.GAlignmentPanel;
  import jalview.math.AlignmentDimension;
  import jalview.schemes.ResidueProperties;
  import jalview.structure.StructureSelectionManager;
  import jalview.util.Comparison;
 +import jalview.util.ImageMaker;
  import jalview.util.MessageManager;
  import jalview.viewmodel.ViewportListenerI;
  import jalview.viewmodel.ViewportRanges;
@@@ -84,8 -81,11 +84,8 @@@ public class AlignmentPanel extends GAl
  
    private IdPanel idPanel;
  
 -  private boolean headless;
 -
    IdwidthAdjuster idwidthAdjuster;
  
 -  /** DOCUMENT ME!! */
    public AlignFrame alignFrame;
  
    private ScalePanel scalePanel;
     */
    public AlignmentPanel(AlignFrame af, final AlignViewport av)
    {
 +//    setBackground(Color.white);  // BH 2019
      alignFrame = af;
      this.av = av;
      setSeqPanel(new SeqPanel(av, this));
              new Dimension(10, av.getCharHeight() + fm.getDescent()));
      idwidthAdjuster.invalidate();
      scalePanelHolder.invalidate();
 -    getIdPanel().getIdCanvas().gg = null;
 +    // BH 2018 getIdPanel().getIdCanvas().gg = null;
      getSeqPanel().seqCanvas.img = null;
      getAnnotationPanel().adjustPanelHeight();
  
    }
  
    /**
-    * Highlight the given results on the alignment.
+    * Highlight the given results on the alignment
     * 
     */
    public void highlightSearchResults(SearchResultsI results)
    {
-     boolean scrolled = scrollToPosition(results, 0, true, false);
+     boolean scrolled = scrollToPosition(results, 0, false);
  
 -    boolean noFastPaint = scrolled && av.getWrapAlignment();
 +    boolean fastPaint = !(scrolled && av.getWrapAlignment());
  
 -    getSeqPanel().seqCanvas.highlightSearchResults(results, noFastPaint);
 +    getSeqPanel().seqCanvas.highlightSearchResults(results, fastPaint);
    }
  
    /**
     * (if any)
     * 
     * @param searchResults
-    * @param redrawOverview
     * @return
     */
-   public boolean scrollToPosition(SearchResultsI searchResults,
-           boolean redrawOverview)
+   public boolean scrollToPosition(SearchResultsI searchResults)
    {
-     return scrollToPosition(searchResults, 0, redrawOverview, false);
+     return scrollToPosition(searchResults, 0, false);
    }
  
    /**
     * @param verticalOffset
     *          if greater than zero, allows scrolling to a position below the
     *          first displayed sequence
-    * @param redrawOverview
-    *          - when set, the overview will be recalculated (takes longer)
     * @param centre
     *          if true, try to centre the search results horizontally in the view
     * @return
     */
    protected boolean scrollToPosition(SearchResultsI results,
-           int verticalOffset, boolean redrawOverview, boolean centre)
+           int verticalOffset, boolean centre)
    {
      int startv, endv, starts, ends;
      ViewportRanges ranges = av.getRanges();
        scrollNeeded = ranges.scrollToWrappedVisible(start);
      }
  
-     paintAlignment(redrawOverview, false);
+     paintAlignment(false, false);
  
      return scrollNeeded;
    }
      addNotify();
      // TODO: many places call this method and also paintAlignment with various
      // different settings. this means multiple redraws are triggered...
-     paintAlignment(true, false);
+     paintAlignment(true, av.needToUpdateStructureViews());
    }
  
    /**
      }
      else
      {
-       int width = av.getAlignment().getWidth();
+       int width = av.getAlignment().getVisibleWidth();
        int height = av.getAlignment().getHeight();
  
-       if (av.hasHiddenColumns())
-       {
-         // reset the width to exclude hidden columns
-         width = av.getAlignment().getHiddenColumns()
-                 .absoluteToVisibleColumn(width);
-       }
        hextent = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
        vextent = getSeqPanel().seqCanvas.getHeight() / av.getCharHeight();
  
       */
      ViewportRanges ranges = av.getRanges();
      setScrollValues(ranges.getStartRes(), ranges.getStartSeq());
 +    super.paintComponent(g);
    }
  
    /**
  
      final int totalSeq = (pageHeight - scaleHeight) / charHeight - 1;
  
-     final int alignmentWidth = av.getAlignment().getWidth();
-     final int pagesWide = (alignmentWidth / totalRes) + 1;
+     final int alignmentWidth = av.getAlignment().getVisibleWidth();
+     int pagesWide = (alignmentWidth / totalRes) + 1;
  
      final int startRes = (pageIndex % pagesWide) * totalRes;
      final int endRes = Math.min(startRes + totalRes - 1,
  
      int idWidth = getVisibleIdWidth(false);
  
-     int maxwidth = av.getAlignment().getWidth();
-     if (av.hasHiddenColumns())
-     {
-       maxwidth = av.getAlignment().getHiddenColumns()
-               .absoluteToVisibleColumn(maxwidth) - 1;
-     }
+     int maxwidth = av.getAlignment().getVisibleWidth();
  
      int resWidth = getSeqPanel().seqCanvas
              .getWrappedCanvasWidth(pageWidth - idWidth);
      return idwidth.intValue() + 4;
    }
  
 -  void makeAlignmentImage(jalview.util.ImageMaker.TYPE type, File file)
 +  /**
 +   * Builds an image of the alignment of the specified type (EPS/PNG/SVG) and
 +   * writes it to the specified file
 +   * 
 +   * @param type
 +   * @param file
 +   */
 +  void makeAlignmentImage(ImageMaker.TYPE type, File file)
    {
 -    int boarderBottomOffset = 5;
 -    long pSessionId = System.currentTimeMillis();
 -    headless = (System.getProperty("java.awt.headless") != null
 -            && System.getProperty("java.awt.headless").equals("true"));
 -    if (alignFrame != null && !headless)
 -    {
 -      if (file != null)
 -      {
 -        alignFrame.setProgressBar(MessageManager
 -                .formatMessage("status.saving_file", new Object[]
 -                { type.getLabel() }), pSessionId);
 -      }
 -    }
 -    try
 +    final int borderBottomOffset = 5;
 +
 +    AlignmentDimension aDimension = getAlignmentDimension();
 +    // todo use a lambda function in place of callback here?
 +    ImageWriterI writer = new ImageWriterI()
      {
 -      AlignmentDimension aDimension = getAlignmentDimension();
 -      try
 +      @Override
 +      public void exportImage(Graphics graphics) throws Exception
        {
 -        jalview.util.ImageMaker im;
 -        final String imageAction, imageTitle;
 -        if (type == jalview.util.ImageMaker.TYPE.PNG)
 -        {
 -          imageAction = "Create PNG image from alignment";
 -          imageTitle = null;
 -        }
 -        else if (type == jalview.util.ImageMaker.TYPE.EPS)
 -        {
 -          imageAction = "Create EPS file from alignment";
 -          imageTitle = alignFrame.getTitle();
 -        }
 -        else
 -        {
 -          imageAction = "Create SVG file from alignment";
 -          imageTitle = alignFrame.getTitle();
 -        }
 -
 -        im = new jalview.util.ImageMaker(this, type, imageAction,
 -                aDimension.getWidth(),
 -                aDimension.getHeight() + boarderBottomOffset, file,
 -                imageTitle, alignFrame, pSessionId, headless);
 -        Graphics graphics = im.getGraphics();
          if (av.getWrapAlignment())
          {
 -          if (graphics != null)
 -          {
 -            printWrappedAlignment(aDimension.getWidth(),
 -                    aDimension.getHeight() + boarderBottomOffset, 0,
 -                    graphics);
 -            im.writeImage();
 -          }
 +          printWrappedAlignment(aDimension.getWidth(),
 +                  aDimension.getHeight() + borderBottomOffset, 0, graphics);
          }
          else
          {
 -          if (graphics != null)
 -          {
 -            printUnwrapped(aDimension.getWidth(), aDimension.getHeight(), 0,
 -                    graphics, graphics);
 -            im.writeImage();
 -          }
 +          printUnwrapped(aDimension.getWidth(), aDimension.getHeight(), 0,
 +                  graphics, graphics);
          }
 -
 -      } catch (OutOfMemoryError err)
 -      {
 -        // Be noisy here.
 -        System.out.println("########################\n" + "OUT OF MEMORY "
 -                + file + "\n" + "########################");
 -        new OOMWarning("Creating Image for " + file, err);
 -        // System.out.println("Create IMAGE: " + err);
 -      } catch (Exception ex)
 -      {
 -        ex.printStackTrace();
        }
 -    } finally
 -    {
 +    };
  
 -    }
 +    String fileTitle = alignFrame.getTitle();
 +    ImageExporter exporter = new ImageExporter(writer, alignFrame, type,
 +            fileTitle);
 +    int imageWidth = aDimension.getWidth();
 +    int imageHeight = aDimension.getHeight() + borderBottomOffset;
 +    String of = MessageManager.getString("label.alignment");
 +    exporter.doExport(file, this, imageWidth, imageHeight, of);
    }
  
 +  /**
 +   * Calculates and returns a suitable width and height (in pixels) for an
 +   * exported image
 +   * 
 +   * @return
 +   */
    public AlignmentDimension getAlignmentDimension()
    {
-     int maxwidth = av.getAlignment().getWidth();
-     if (av.hasHiddenColumns())
-     {
-       maxwidth = av.getAlignment().getHiddenColumns()
-               .absoluteToVisibleColumn(maxwidth);
-     }
+     int maxwidth = av.getAlignment().getVisibleWidth();
  
      int height = ((av.getAlignment().getHeight() + 1) * av.getCharHeight())
              + getScalePanel().getHeight();
      if (av.getWrapAlignment())
      {
        height = getWrappedHeight();
 -      if (headless)
 +      if (Jalview.isHeadlessMode())
        {
          // need to obtain default alignment width and then add in any
          // additional allowance for id margin
  
    }
  
 -  /**
 -   * DOCUMENT ME!
 -   */
 -  public void makeEPS(File epsFile)
 -  {
 -    makeAlignmentImage(jalview.util.ImageMaker.TYPE.EPS, epsFile);
 -  }
 -
 -  /**
 -   * DOCUMENT ME!
 -   */
 -  public void makePNG(File pngFile)
 -  {
 -    makeAlignmentImage(jalview.util.ImageMaker.TYPE.PNG, pngFile);
 -  }
 -
 -  public void makeSVG(File svgFile)
 -  {
 -    makeAlignmentImage(jalview.util.ImageMaker.TYPE.SVG, svgFile);
 -  }
 -
    public void makePNGImageMap(File imgMapFile, String imageName)
    {
      // /////ONLY WORKS WITH NON WRAPPED ALIGNMENTS
     */
    protected void scrollToCentre(SearchResultsI sr, int verticalOffset)
    {
-     scrollToPosition(sr, verticalOffset, true, true);
+     scrollToPosition(sr, verticalOffset, true);
    }
  
    /**
@@@ -24,7 -24,6 +24,7 @@@ import jalview.bin.Cache
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.GraphLine;
  import jalview.datamodel.SequenceGroup;
 +import jalview.gui.JalviewColourChooser.ColourChooserListener;
  import jalview.schemes.AnnotationColourGradient;
  import jalview.schemes.ColourSchemeI;
  import jalview.util.MessageManager;
@@@ -43,6 -42,7 +43,6 @@@ import java.util.Vector
  import javax.swing.BorderFactory;
  import javax.swing.JButton;
  import javax.swing.JCheckBox;
 -import javax.swing.JColorChooser;
  import javax.swing.JComboBox;
  import javax.swing.JInternalFrame;
  import javax.swing.JLayeredPane;
@@@ -63,9 -63,9 +63,9 @@@ public class AnnotationColourChooser ex
  
    private JCheckBox useOriginalColours = new JCheckBox();
  
 -  private JPanel minColour = new JPanel();
 +  JPanel minColour = new JPanel();
  
 -  private JPanel maxColour = new JPanel();
 +  JPanel maxColour = new JPanel();
  
    private JCheckBox thresholdIsMin = new JCheckBox();
  
        {
          if (minColour.isEnabled())
          {
 -          minColour_actionPerformed();
 +          showColourChooser(minColour, "label.select_colour_minimum_value");
          }
        }
      });
        {
          if (maxColour.isEnabled())
          {
 -          maxColour_actionPerformed();
 +          showColourChooser(maxColour, "label.select_colour_maximum_value");
          }
        }
      });
              Cache.getDefaultColour("ANNOTATIONCOLOUR_MAX", Color.red));
    }
  
 -  public void minColour_actionPerformed()
 +  protected void showColourChooser(JPanel colourPanel, String titleKey)
    {
 -    Color col = JColorChooser.showDialog(this,
 -            MessageManager.getString("label.select_colour_minimum_value"),
 -            minColour.getBackground());
 -    if (col != null)
 +    String ttl = MessageManager.getString(titleKey);
 +    ColourChooserListener listener = new ColourChooserListener()
      {
 -      minColour.setBackground(col);
 -    }
 -    minColour.repaint();
 -    updateView();
 -  }
 -
 -  public void maxColour_actionPerformed()
 -  {
 -    Color col = JColorChooser.showDialog(this,
 -            MessageManager.getString("label.select_colour_maximum_value"),
 -            maxColour.getBackground());
 -    if (col != null)
 -    {
 -      maxColour.setBackground(col);
 -    }
 -    maxColour.repaint();
 -    updateView();
 +      @Override
 +      public void colourSelected(Color c)
 +      {
 +        colourPanel.setBackground(c);
 +        colourPanel.repaint();
 +        updateView();
 +      }
 +    };
 +    JalviewColourChooser.showColourChooser(Desktop.getDesktop(), ttl,
 +            colourPanel.getBackground(), listener);
    }
  
    @Override
    public void reset()
    {
-     av.setGlobalColourScheme(oldcs);
+     this.ap.alignFrame.changeColour(oldcs);
      if (av.getAlignment().getGroups() != null)
      {
  
  
      acg.setThresholdIsMinMax(thresholdIsMin.isSelected());
  
-     av.setGlobalColourScheme(acg);
+     this.ap.alignFrame.changeColour(acg);
  
      if (av.getAlignment().getGroups() != null)
      {
            continue;
          }
          sg.setColourScheme(
-                 acg.getInstance(sg, ap.av.getHiddenRepSequences()));
+                 acg.getInstance(av, sg));
        }
      }
    }
      super.sliderDragReleased();
      ap.paintAlignment(true, true);
    }
  }
   */
  package jalview.gui;
  
- import jalview.api.FeatureColourI;
+ import jalview.api.FeatureRenderer;
  import jalview.bin.Cache;
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.SequenceI;
- import jalview.datamodel.features.FeatureMatcherSetI;
  import jalview.io.AnnotationFile;
  import jalview.io.FeaturesFile;
  import jalview.io.JalviewFileChooser;
@@@ -38,8 -37,6 +37,6 @@@ import java.awt.event.ActionEvent
  import java.awt.event.ActionListener;
  import java.io.FileWriter;
  import java.io.PrintWriter;
- import java.util.List;
- import java.util.Map;
  
  import javax.swing.BorderFactory;
  import javax.swing.ButtonGroup;
@@@ -135,7 -132,6 +132,7 @@@ public class AnnotationExporter extend
  
    private void toFile_actionPerformed()
    {
 +    // TODO: JAL-3048 JalviewFileChooser - Save option
      JalviewFileChooser chooser = new JalviewFileChooser(
              Cache.getProperty("LAST_DIRECTORY"));
  
    {
      String text;
      SequenceI[] sequences = ap.av.getAlignment().getSequencesArray();
      boolean includeNonPositional = ap.av.isShowNPFeats();
  
      FeaturesFile formatter = new FeaturesFile();
+     final FeatureRenderer fr = ap.getFeatureRenderer();
      if (GFFFormat.isSelected())
      {
-       text = formatter.printGffFormat(sequences, featureColours,
-               featureGroups, includeNonPositional);
+       text = formatter.printGffFormat(sequences, fr, includeNonPositional);
      }
      else
      {
-       text = formatter.printJalviewFormat(sequences, featureColours,
-               featureFilters, featureGroups, includeNonPositional);
+       text = formatter.printJalviewFormat(sequences, fr,
+               includeNonPositional);
      }
      return text;
    }
@@@ -54,7 -54,6 +54,6 @@@ import java.awt.geom.AffineTransform
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Iterator;
- import java.util.regex.Pattern;
  
  import javax.swing.JCheckBoxMenuItem;
  import javax.swing.JMenuItem;
@@@ -71,6 -70,10 +70,10 @@@ import javax.swing.ToolTipManager
  public class AnnotationLabels extends JPanel
          implements MouseListener, MouseMotionListener, ActionListener
  {
+   private static final String HTML_END_TAG = "</html>";
+   private static final String HTML_START_TAG = "<html>";
    /**
     * width in pixels within which height adjuster arrows are shown and active
     */
@@@ -81,9 -84,6 +84,6 @@@
     */
    private static int HEIGHT_ADJUSTER_HEIGHT = 10;
  
-   private static final Pattern LEFT_ANGLE_BRACKET_PATTERN = Pattern
-           .compile("<");
    private static final Font font = new Font("Arial", Font.PLAIN, 11);
  
    private static final String TOGGLE_LABELSCALE = MessageManager
     */
    public AnnotationLabels(AlignmentPanel ap)
    {
 +        
      this.ap = ap;
      av = ap.av;
      ToolTipManager.sharedInstance().registerComponent(this);
      AlignmentAnnotation[] aa = ap.av.getAlignment()
              .getAlignmentAnnotation();
  
 -    boolean fullRepaint = false;
 -    if (evt.getActionCommand().equals(ADDNEW))
 +    String action = evt.getActionCommand();
 +    if (ADDNEW.equals(action))
      {
 +      /*
 +       * non-returning dialog
 +       */
        AlignmentAnnotation newAnnotation = new AlignmentAnnotation(null,
                null, new Annotation[ap.av.getAlignment().getWidth()]);
 -
 -      if (!editLabelDescription(newAnnotation))
 -      {
 -        return;
 -      }
 -
 -      ap.av.getAlignment().addAnnotation(newAnnotation);
 -      ap.av.getAlignment().setAnnotationIndex(newAnnotation, 0);
 -      fullRepaint = true;
 +      editLabelDescription(newAnnotation, true);
      }
 -    else if (evt.getActionCommand().equals(EDITNAME))
 +    else if (EDITNAME.equals(action))
      {
 -      String name = aa[selectedRow].label;
 -      editLabelDescription(aa[selectedRow]);
 -      if (!name.equalsIgnoreCase(aa[selectedRow].label))
 -      {
 -        fullRepaint = true;
 -      }
 +      /*
 +       * non-returning dialog
 +       */
 +      editLabelDescription(aa[selectedRow], false);
      }
 -    else if (evt.getActionCommand().equals(HIDE))
 +    else if (HIDE.equals(action))
      {
        aa[selectedRow].visible = false;
      }
 -    else if (evt.getActionCommand().equals(DELETE))
 +    else if (DELETE.equals(action))
      {
        ap.av.getAlignment().deleteAnnotation(aa[selectedRow]);
        ap.av.getCalcManager().removeWorkerForAnnotation(aa[selectedRow]);
 -      fullRepaint = true;
      }
 -    else if (evt.getActionCommand().equals(SHOWALL))
 +    else if (SHOWALL.equals(action))
      {
        for (int i = 0; i < aa.length; i++)
        {
            aa[i].visible = true;
          }
        }
 -      fullRepaint = true;
      }
 -    else if (evt.getActionCommand().equals(OUTPUT_TEXT))
 +    else if (OUTPUT_TEXT.equals(action))
      {
        new AnnotationExporter(ap).exportAnnotation(aa[selectedRow]);
      }
 -    else if (evt.getActionCommand().equals(COPYCONS_SEQ))
 +    else if (COPYCONS_SEQ.equals(action))
      {
        SequenceI cons = null;
        if (aa[selectedRow].groupRef != null)
        {
          copy_annotseqtoclipboard(cons);
        }
 -
      }
 -    else if (evt.getActionCommand().equals(TOGGLE_LABELSCALE))
 +    else if (TOGGLE_LABELSCALE.equals(action))
      {
        aa[selectedRow].scaleColLabel = !aa[selectedRow].scaleColLabel;
      }
  
 -    ap.refresh(fullRepaint);
 -
 +    ap.refresh(true);
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Shows a dialog where the annotation name and description may be edited. If
 +   * parameter addNew is true, then on confirmation, a new AlignmentAnnotation
 +   * is added, else an existing annotation is updated.
     * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * @param annotation
 +   * @param addNew
     */
 -  boolean editLabelDescription(AlignmentAnnotation annotation)
 +  void editLabelDescription(AlignmentAnnotation annotation, boolean addNew)
    {
 -    // TODO i18n
 +    String name = MessageManager.getString("label.annotation_name");
 +    String description = MessageManager
 +            .getString("label.annotation_description");
 +    String title = MessageManager
 +            .getString("label.edit_annotation_name_description");
      EditNameDialog dialog = new EditNameDialog(annotation.label,
 -            annotation.description, "       Annotation Name ",
 -            "Annotation Description ", "Edit Annotation Name/Description",
 -            ap.alignFrame);
 -
 -    if (!dialog.accept)
 -    {
 -      return false;
 -    }
 -
 -    annotation.label = dialog.getName();
 -
 -    String text = dialog.getDescription();
 -    if (text != null && text.length() == 0)
 -    {
 -      text = null;
 -    }
 -    annotation.description = text;
 -
 -    return true;
 +            annotation.description, name, description);
 +
 +    dialog.showDialog(ap.alignFrame, title,
 +            new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                annotation.label = dialog.getName();
 +                String text = dialog.getDescription();
 +                if (text != null && text.length() == 0)
 +                {
 +                  text = null;
 +                }
 +                annotation.description = text;
 +                if (addNew)
 +                {
 +                  ap.av.getAlignment().addAnnotation(annotation);
 +                  ap.av.getAlignment().setAnnotationIndex(annotation, 0);
 +                }
 +                ap.refresh(true);
 +              }
 +            });
    }
  
    @Override
              AlignmentUtils.showOrHideSequenceAnnotations(
                      ap.av.getAlignment(), Collections.singleton(label),
                      null, false, false);
-             // for (AlignmentAnnotation ann : ap.av.getAlignment()
-             // .getAlignmentAnnotation())
-             // {
-             // if (ann.sequenceRef != null && ann.label != null
-             // && ann.label.equals(label))
-             // {
-             // ann.visible = false;
-             // }
-             // }
              ap.refresh(true);
            }
          });
        }
        else if (label.indexOf("Consensus") > -1)
        {
-         pop.addSeparator();
-         // av and sequencegroup need to implement same interface for
-         final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(
-                 MessageManager.getString("label.ignore_gaps_consensus"),
-                 (aa[selectedRow].groupRef != null)
-                         ? aa[selectedRow].groupRef.getIgnoreGapsConsensus()
-                         : ap.av.isIgnoreGapsConsensus());
-         final AlignmentAnnotation aaa = aa[selectedRow];
-         cbmi.addActionListener(new ActionListener()
-         {
-           @Override
-           public void actionPerformed(ActionEvent e)
-           {
-             if (aaa.groupRef != null)
-             {
-               // TODO: pass on reference to ap so the view can be updated.
-               aaa.groupRef.setIgnoreGapsConsensus(cbmi.getState());
-               ap.getAnnotationPanel()
-                       .paint(ap.getAnnotationPanel().getGraphics());
-             }
-             else
-             {
-               ap.av.setIgnoreGapsConsensus(cbmi.getState(), ap);
-             }
-             ap.alignmentChanged();
-           }
-         });
-         pop.add(cbmi);
-         // av and sequencegroup need to implement same interface for
+         addConsensusMenuOptions(ap, aa[selectedRow], pop);
+         final JMenuItem consclipbrd = new JMenuItem(COPYCONS_SEQ);
+         consclipbrd.addActionListener(this);
+         pop.add(consclipbrd);
+       }
+     }
+     pop.show(this, evt.getX(), evt.getY());
+   }
+   /**
+    * A helper method that adds menu options for calculation and visualisation of
+    * group and/or alignment consensus annotation to a popup menu. This is
+    * designed to be reusable for either unwrapped mode (popup menu is shown on
+    * component AnnotationLabels), or wrapped mode (popup menu is shown on
+    * IdPanel when the mouse is over an annotation label).
+    * 
+    * @param ap
+    * @param ann
+    * @param pop
+    */
+   static void addConsensusMenuOptions(AlignmentPanel ap,
+           AlignmentAnnotation ann,
+           JPopupMenu pop)
+   {
+     pop.addSeparator();
+     final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(
+             MessageManager.getString("label.ignore_gaps_consensus"),
+             (ann.groupRef != null) ? ann.groupRef.getIgnoreGapsConsensus()
+                     : ap.av.isIgnoreGapsConsensus());
+     final AlignmentAnnotation aaa = ann;
+     cbmi.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
          if (aaa.groupRef != null)
          {
-           final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
-                   MessageManager.getString("label.show_group_histogram"),
-                   aa[selectedRow].groupRef.isShowConsensusHistogram());
-           chist.addActionListener(new ActionListener()
-           {
-             @Override
-             public void actionPerformed(ActionEvent e)
-             {
-               // TODO: pass on reference
-               // to ap
-               // so the
-               // view
-               // can be
-               // updated.
-               aaa.groupRef.setShowConsensusHistogram(chist.getState());
-               ap.repaint();
-               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
-             }
-           });
-           pop.add(chist);
-           final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem(
-                   MessageManager.getString("label.show_group_logo"),
-                   aa[selectedRow].groupRef.isShowSequenceLogo());
-           cprofl.addActionListener(new ActionListener()
-           {
-             @Override
-             public void actionPerformed(ActionEvent e)
-             {
-               // TODO: pass on reference
-               // to ap
-               // so the
-               // view
-               // can be
-               // updated.
-               aaa.groupRef.setshowSequenceLogo(cprofl.getState());
-               ap.repaint();
-               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
-             }
-           });
-           pop.add(cprofl);
-           final JCheckBoxMenuItem cproflnorm = new JCheckBoxMenuItem(
-                   MessageManager.getString("label.normalise_group_logo"),
-                   aa[selectedRow].groupRef.isNormaliseSequenceLogo());
-           cproflnorm.addActionListener(new ActionListener()
-           {
-             @Override
-             public void actionPerformed(ActionEvent e)
-             {
-               // TODO: pass on reference
-               // to ap
-               // so the
-               // view
-               // can be
-               // updated.
-               aaa.groupRef.setNormaliseSequenceLogo(cproflnorm.getState());
-               // automatically enable logo display if we're clicked
-               aaa.groupRef.setshowSequenceLogo(true);
-               ap.repaint();
-               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
-             }
-           });
-           pop.add(cproflnorm);
+           aaa.groupRef.setIgnoreGapsConsensus(cbmi.getState());
+           ap.getAnnotationPanel()
+                   .paint(ap.getAnnotationPanel().getGraphics());
          }
          else
          {
-           final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
-                   MessageManager.getString("label.show_histogram"),
-                   av.isShowConsensusHistogram());
-           chist.addActionListener(new ActionListener()
-           {
-             @Override
-             public void actionPerformed(ActionEvent e)
-             {
-               // TODO: pass on reference
-               // to ap
-               // so the
-               // view
-               // can be
-               // updated.
-               av.setShowConsensusHistogram(chist.getState());
-               ap.alignFrame.setMenusForViewport();
-               ap.repaint();
-               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
-             }
-           });
-           pop.add(chist);
-           final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
-                   MessageManager.getString("label.show_logo"),
-                   av.isShowSequenceLogo());
-           cprof.addActionListener(new ActionListener()
-           {
-             @Override
-             public void actionPerformed(ActionEvent e)
-             {
-               // TODO: pass on reference
-               // to ap
-               // so the
-               // view
-               // can be
-               // updated.
-               av.setShowSequenceLogo(cprof.getState());
-               ap.alignFrame.setMenusForViewport();
-               ap.repaint();
-               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
-             }
-           });
-           pop.add(cprof);
-           final JCheckBoxMenuItem cprofnorm = new JCheckBoxMenuItem(
-                   MessageManager.getString("label.normalise_logo"),
-                   av.isNormaliseSequenceLogo());
-           cprofnorm.addActionListener(new ActionListener()
-           {
-             @Override
-             public void actionPerformed(ActionEvent e)
-             {
-               // TODO: pass on reference
-               // to ap
-               // so the
-               // view
-               // can be
-               // updated.
-               av.setShowSequenceLogo(true);
-               av.setNormaliseSequenceLogo(cprofnorm.getState());
-               ap.alignFrame.setMenusForViewport();
-               ap.repaint();
-               // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
-             }
-           });
-           pop.add(cprofnorm);
+           ap.av.setIgnoreGapsConsensus(cbmi.getState(), ap);
          }
-         final JMenuItem consclipbrd = new JMenuItem(COPYCONS_SEQ);
-         consclipbrd.addActionListener(this);
-         pop.add(consclipbrd);
+         ap.alignmentChanged();
        }
+     });
+     pop.add(cbmi);
+     if (aaa.groupRef != null)
+     {
+       /*
+        * group consensus options
+        */
+       final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
+               MessageManager.getString("label.show_group_histogram"),
+               ann.groupRef.isShowConsensusHistogram());
+       chist.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           aaa.groupRef.setShowConsensusHistogram(chist.getState());
+           ap.repaint();
+         }
+       });
+       pop.add(chist);
+       final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem(
+               MessageManager.getString("label.show_group_logo"),
+               ann.groupRef.isShowSequenceLogo());
+       cprofl.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           aaa.groupRef.setshowSequenceLogo(cprofl.getState());
+           ap.repaint();
+         }
+       });
+       pop.add(cprofl);
+       final JCheckBoxMenuItem cproflnorm = new JCheckBoxMenuItem(
+               MessageManager.getString("label.normalise_group_logo"),
+               ann.groupRef.isNormaliseSequenceLogo());
+       cproflnorm.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           aaa.groupRef.setNormaliseSequenceLogo(cproflnorm.getState());
+           // automatically enable logo display if we're clicked
+           aaa.groupRef.setshowSequenceLogo(true);
+           ap.repaint();
+         }
+       });
+       pop.add(cproflnorm);
+     }
+     else
+     {
+       /*
+        * alignment consensus options
+        */
+       final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
+               MessageManager.getString("label.show_histogram"),
+               ap.av.isShowConsensusHistogram());
+       chist.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           ap.av.setShowConsensusHistogram(chist.getState());
+           ap.alignFrame.setMenusForViewport();
+           ap.repaint();
+         }
+       });
+       pop.add(chist);
+       final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
+               MessageManager.getString("label.show_logo"),
+               ap.av.isShowSequenceLogo());
+       cprof.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           ap.av.setShowSequenceLogo(cprof.getState());
+           ap.alignFrame.setMenusForViewport();
+           ap.repaint();
+         }
+       });
+       pop.add(cprof);
+       final JCheckBoxMenuItem cprofnorm = new JCheckBoxMenuItem(
+               MessageManager.getString("label.normalise_logo"),
+               ap.av.isNormaliseSequenceLogo());
+       cprofnorm.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           ap.av.setShowSequenceLogo(true);
+           ap.av.setNormaliseSequenceLogo(cprofnorm.getState());
+           ap.alignFrame.setMenusForViewport();
+           ap.repaint();
+         }
+       });
+       pop.add(cprofnorm);
      }
-     pop.show(this, evt.getX(), evt.getY());
    }
  
    /**
      if (selectedRow > -1 && ap.av.getAlignment()
              .getAlignmentAnnotation().length > selectedRow)
      {
-       AlignmentAnnotation aa = ap.av.getAlignment()
-               .getAlignmentAnnotation()[selectedRow];
+       AlignmentAnnotation[] anns = ap.av.getAlignment()
+               .getAlignmentAnnotation();
+       AlignmentAnnotation aa = anns[selectedRow];
+       String desc = getTooltip(aa);
+       this.setToolTipText(desc);
+       String msg = getStatusMessage(aa, anns);
+       ap.alignFrame.setStatus(msg);
+     }
+   }
  
-       StringBuffer desc = new StringBuffer();
-       if (aa.description != null
-               && !aa.description.equals("New description"))
+   /**
+    * Constructs suitable text to show in the status bar when over an annotation
+    * label, containing the associated sequence name (if any), and the annotation
+    * labels (or all labels for a graph group annotation)
+    * 
+    * @param aa
+    * @param anns
+    * @return
+    */
+   static String getStatusMessage(AlignmentAnnotation aa,
+           AlignmentAnnotation[] anns)
+   {
+     if (aa == null)
+     {
+       return null;
+     }
+     StringBuilder msg = new StringBuilder(32);
+     if (aa.sequenceRef != null)
+     {
+       msg.append(aa.sequenceRef.getName()).append(" : ");
+     }
+     if (aa.graphGroup == -1)
+     {
+       msg.append(aa.label);
+     }
+     else if (anns != null)
+     {
+       boolean first = true;
+       for (int i = anns.length - 1; i >= 0; i--)
        {
-         // TODO: we could refactor and merge this code with the code in
-         // jalview.gui.SeqPanel.mouseMoved(..) that formats sequence feature
-         // tooltips
-         desc.append(aa.getDescription(true).trim());
-         // check to see if the description is an html fragment.
-         if (desc.length() < 6 || (desc.substring(0, 6).toLowerCase()
-                 .indexOf("<html>") < 0))
-         {
-           // clean the description ready for embedding in html
-           desc = new StringBuffer(LEFT_ANGLE_BRACKET_PATTERN.matcher(desc)
-                   .replaceAll("&lt;"));
-           desc.insert(0, "<html>");
-         }
-         else
+         if (anns[i].graphGroup == aa.graphGroup)
          {
-           // remove terminating html if any
-           int i = desc.substring(desc.length() - 7).toLowerCase()
-                   .lastIndexOf("</html>");
-           if (i > -1)
+           if (!first)
            {
-             desc.setLength(desc.length() - 7 + i);
+             msg.append(", ");
            }
+           msg.append(anns[i].label);
+           first = false;
          }
-         if (aa.hasScore())
-         {
-           desc.append("<br/>");
-         }
-         // if (aa.hasProperties())
-         // {
-         // desc.append("<table>");
-         // for (String prop : aa.getProperties())
-         // {
-         // desc.append("<tr><td>" + prop + "</td><td>"
-         // + aa.getProperty(prop) + "</td><tr>");
-         // }
-         // desc.append("</table>");
-         // }
        }
-       else
+     }
+     return msg.toString();
+   }
+   /**
+    * Answers a tooltip, formatted as html, containing the annotation description
+    * (prefixed by associated sequence id if applicable), and the annotation
+    * (non-positional) score if it has one. Answers null if neither description
+    * nor score is found.
+    * 
+    * @param aa
+    * @return
+    */
+   static String getTooltip(AlignmentAnnotation aa)
+   {
+     if (aa == null)
+     {
+       return null;
+     }
+     StringBuilder tooltip = new StringBuilder();
+     if (aa.description != null && !aa.description.equals("New description"))
+     {
+       // TODO: we could refactor and merge this code with the code in
+       // jalview.gui.SeqPanel.mouseMoved(..) that formats sequence feature
+       // tooltips
+       String desc = aa.getDescription(true).trim();
+       if (!desc.toLowerCase().startsWith(HTML_START_TAG))
        {
-         // begin the tooltip's html fragment
-         desc.append("<html>");
-         if (aa.hasScore())
-         {
-           // TODO: limit precision of score to avoid noise from imprecise
-           // doubles
-           // (64.7 becomes 64.7+/some tiny value).
-           desc.append(" Score: " + aa.score);
-         }
+         tooltip.append(HTML_START_TAG);
+         desc = desc.replace("<", "&lt;");
        }
-       if (desc.length() > 6)
+       else if (desc.toLowerCase().endsWith(HTML_END_TAG))
        {
-         desc.append("</html>");
-         this.setToolTipText(desc.toString());
+         desc = desc.substring(0, desc.length() - HTML_END_TAG.length());
        }
-       else
+       tooltip.append(desc);
+     }
+     else
+     {
+       // begin the tooltip's html fragment
+       tooltip.append(HTML_START_TAG);
+     }
+     if (aa.hasScore())
+     {
+       if (tooltip.length() > HTML_START_TAG.length())
        {
-         this.setToolTipText(null);
+         tooltip.append("<br/>");
        }
+       // TODO: limit precision of score to avoid noise from imprecise
+       // doubles
+       // (64.7 becomes 64.7+/some tiny value).
+       tooltip.append(" Score: ").append(String.valueOf(aa.score));
+     }
+     if (tooltip.length() > HTML_START_TAG.length())
+     {
+       return tooltip.append(HTML_END_TAG).toString();
      }
+     /*
+      * nothing in the tooltip (except "<html>")
+      */
+     return null;
    }
  
    /**
    protected void showOrHideAdjuster(MouseEvent evt)
    {
      boolean was = resizePanel;
 -    resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
 +    resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT
 +            && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
  
      if (resizePanel != was)
      {
 -      setCursor(Cursor.getPredefinedCursor(
 -              resizePanel ? Cursor.S_RESIZE_CURSOR
 +      setCursor(Cursor
 +              .getPredefinedCursor(resizePanel ? Cursor.S_RESIZE_CURSOR
                        : Cursor.DEFAULT_CURSOR));
        repaint();
      }
  package jalview.gui;
  
  import jalview.datamodel.AlignmentAnnotation;
+ import jalview.datamodel.AlignmentI;
  import jalview.datamodel.Annotation;
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.SequenceI;
 +import jalview.gui.JalviewColourChooser.ColourChooserListener;
  import jalview.renderer.AnnotationRenderer;
  import jalview.renderer.AwtRenderPanelI;
  import jalview.schemes.ResidueProperties;
@@@ -59,6 -59,7 +60,6 @@@ import java.util.ArrayList
  import java.util.Collections;
  import java.util.List;
  
 -import javax.swing.JColorChooser;
  import javax.swing.JMenuItem;
  import javax.swing.JPanel;
  import javax.swing.JPopupMenu;
@@@ -113,7 -114,7 +114,7 @@@ public class AnnotationPanel extends JP
  
    public volatile BufferedImage fadedImage;
  
 -  Graphics2D gg;
 +  // private Graphics2D gg;
  
    public FontMetrics fm;
  
     */
    public AnnotationPanel(AlignmentPanel ap)
    {
 -    ToolTipManager.sharedInstance().registerComponent(this);
 +//        setBackground(Color.white);  // BH 2019
 +
 +          ToolTipManager.sharedInstance().registerComponent(this);
      ToolTipManager.sharedInstance().setInitialDelay(0);
      ToolTipManager.sharedInstance().setDismissDelay(10000);
      this.ap = ap;
      else if (action.equals(LABEL))
      {
        String exMesg = collectAnnotVals(anot, LABEL);
 -      String label = JvOptionPane.showInputDialog(this,
 +      String label = JvOptionPane.showInputDialog(
                MessageManager.getString("label.enter_label"), exMesg);
  
        if (label == null)
      }
      else if (action.equals(COLOUR))
      {
 -      Color col = JColorChooser.showDialog(this,
 -              MessageManager.getString("label.select_foreground_colour"),
 -              Color.black);
 -
 -      for (int index : av.getColumnSelection().getSelected())
 +      final Annotation[] fAnot = anot;
 +      String title = MessageManager
 +              .getString("label.select_foreground_colour");
 +      ColourChooserListener listener = new ColourChooserListener()
        {
 -        if (!av.getAlignment().getHiddenColumns().isVisible(index))
 -        {
 -          continue;
 -        }
 -
 -        if (anot[index] == null)
 +        @Override
 +        public void colourSelected(Color c)
          {
 -          anot[index] = new Annotation("", "", ' ', 0);
 -        }
 -
 -        anot[index].colour = col;
 -      }
 +          HiddenColumns hiddenColumns = av.getAlignment().getHiddenColumns();
 +          for (int index : av.getColumnSelection().getSelected())
 +          {
 +            if (hiddenColumns.isVisible(index))
 +            {
 +              if (fAnot[index] == null)
 +              {
 +                fAnot[index] = new Annotation("", "", ' ', 0);
 +              }
 +              fAnot[index].colour = c;
 +            }
 +        }};
 +      };
 +      JalviewColourChooser.showColourChooser(this,
 +              title, Color.black, listener);
      }
      else
      // HELIX, SHEET or STEM
    @Override
    public void mouseEntered(MouseEvent evt)
    {
 +    this.mouseDragging = false;
      ap.getScalePanel().mouseEntered(evt);
    }
  
    /**
 -   * DOCUMENT ME!
 +   * On leaving the panel, calls ScalePanel.mouseExited to deal with scrolling
 +   * with column selection on a mouse drag
     * 
     * @param evt
 -   *          DOCUMENT ME!
     */
    @Override
    public void mouseExited(MouseEvent evt)
    @Override
    public void mouseMoved(MouseEvent evt)
    {
+     int yPos = evt.getY();
      AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
  
-     if (aa == null)
-     {
-       this.setToolTipText(null);
-       return;
-     }
-     int row = -1;
-     int height = 0;
-     for (int i = 0; i < aa.length; i++)
-     {
-       if (aa[i].visible)
-       {
-         height += aa[i].height;
-       }
-       if (evt.getY() < height)
-       {
-         row = i;
-         break;
-       }
-     }
+     int row = getRowIndex(yPos, aa);
  
      if (row == -1)
      {
  
      int column = (evt.getX() / av.getCharWidth())
              + av.getRanges().getStartRes();
+     column = Math.min(column, av.getRanges().getEndRes());
  
      if (av.hasHiddenColumns())
      {
      if (row > -1 && ann.annotations != null
              && column < ann.annotations.length)
      {
-       buildToolTip(ann, column, aa);
-       setStatusMessage(column, ann);
+       setToolTipText(buildToolTip(ann, column, aa));
+       String msg = getStatusMessage(av.getAlignment(), column, ann);
+       ap.alignFrame.setStatus(msg);
      }
      else
      {
    }
  
    /**
-    * Builds a tooltip for the annotation at the current mouse position.
+    * Answers the index in the annotations array of the visible annotation at the
+    * given y position. This is done by adding the heights of visible annotations
+    * until the y position has been exceeded. Answers -1 if no annotations are
+    * visible, or the y position is below all annotations.
+    * 
+    * @param yPos
+    * @param aa
+    * @return
+    */
+   static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
+   {
+     if (aa == null)
+     {
+       return -1;
+     }
+     int row = -1;
+     int height = 0;
+     for (int i = 0; i < aa.length; i++)
+     {
+       if (aa[i].visible)
+       {
+         height += aa[i].height;
+       }
+       if (height > yPos)
+       {
+         row = i;
+         break;
+       }
+     }
+     return row;
+   }
+   /**
+    * Answers a tooltip for the annotation at the current mouse position
     * 
     * @param ann
     * @param column
     * @param anns
     */
-   void buildToolTip(AlignmentAnnotation ann, int column,
+   static String buildToolTip(AlignmentAnnotation ann, int column,
            AlignmentAnnotation[] anns)
    {
+     String tooltip = null;
      if (ann.graphGroup > -1)
      {
        StringBuilder tip = new StringBuilder(32);
        if (tip.length() != 6)
        {
          tip.setLength(tip.length() - 4);
-         this.setToolTipText(tip.toString() + "</html>");
+         tooltip = tip.toString() + "</html>";
        }
      }
-     else if (ann.annotations[column] != null)
+     else if (column < ann.annotations.length
+             && ann.annotations[column] != null)
      {
        String description = ann.annotations[column].description;
        if (description != null && description.length() > 0)
        {
-         this.setToolTipText(JvSwingUtils.wrapTooltip(true, description));
+         tooltip = JvSwingUtils.wrapTooltip(true, description);
        }
        else
        {
-         this.setToolTipText(null); // no tooltip if null or empty description
+         tooltip = null; // no tooltip if null or empty description
        }
      }
      else
      {
        // clear the tooltip.
-       this.setToolTipText(null);
+       tooltip = null;
      }
+     return tooltip;
    }
  
    /**
-    * Constructs and displays the status bar message
+    * Constructs and returns the status bar message
     * 
+    * @param al
     * @param column
     * @param ann
     */
-   void setStatusMessage(int column, AlignmentAnnotation ann)
+   static String getStatusMessage(AlignmentI al, int column,
+           AlignmentAnnotation ann)
    {
      /*
       * show alignment column and annotation description if any
      text.append(MessageManager.getString("label.column")).append(" ")
              .append(column + 1);
  
-     if (ann.annotations[column] != null)
+     if (column < ann.annotations.length && ann.annotations[column] != null)
      {
        String description = ann.annotations[column].description;
        if (description != null && description.trim().length() > 0)
      SequenceI seqref = ann.sequenceRef;
      if (seqref != null)
      {
-       int seqIndex = av.getAlignment().findIndex(seqref);
+       int seqIndex = al.findIndex(seqref);
        if (seqIndex != -1)
        {
          text.append(", ").append(MessageManager.getString("label.sequence"))
          {
            text.append(" ");
            String name;
-           if (av.getAlignment().isNucleotide())
+           if (al.isNucleotide())
            {
              name = ResidueProperties.nucleotideName
                      .get(String.valueOf(residue));
        }
      }
  
-     ap.alignFrame.setStatus(text.toString());
+     return text.toString();
    }
  
    /**
    }
  
    private volatile boolean imageFresh = false;
 +  private Rectangle visibleRect = new Rectangle(), clipBounds = new Rectangle();
  
    /**
     * DOCUMENT ME!
    @Override
    public void paintComponent(Graphics g)
    {
 -    super.paintComponent(g);
 -
 +        
 +        // BH: note that this method is generally recommended to 
 +        // call super.paintComponent(g). Otherwise, the children of this
 +        // component will not be rendered. That is not needed here 
 +        // because AnnotationPanel does not have any children. It is
 +        // just a JPanel contained in a JViewPort. 
 +
 +    computeVisibleRect(visibleRect);
 +    
      g.setColor(Color.white);
 -    g.fillRect(0, 0, getWidth(), getHeight());
 +    g.fillRect(0, 0, visibleRect.width, visibleRect.height);
  
      if (image != null)
      {
 -      if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
 -              || (getVisibleRect().height != g.getClipBounds().height))
 +      // BH 2018 optimizing generation of new Rectangle().
 +      if (fastPaint || (visibleRect.width != (clipBounds = g.getClipBounds(clipBounds)).width)
 +            || (visibleRect.height != clipBounds.height))
        {
 -        g.drawImage(image, 0, 0, this);
 +
 +        
 +        g.drawImage(image, 0, 0, this);
          fastPaint = false;
          return;
        }
      {
        return;
      }
 +    Graphics2D gg;
      if (image == null || imgWidth != image.getWidth(this)
              || image.getHeight(this) != getHeight())
      {
        gg.setColor(Color.white);
        gg.fillRect(0, 0, imgWidth, image.getHeight());
        imageFresh = true;
 +    } else {
 +        gg = (Graphics2D) image.getGraphics();
 +
      }
      
      drawComponent(gg, av.getRanges().getStartRes(),
              av.getRanges().getEndRes() + 1);
 +    gg.dispose();
      imageFresh = false;
      g.drawImage(image, 0, 0, this);
    }
     */
    public void fastPaint(int horizontal)
    {
 -    if ((horizontal == 0) || gg == null
 +    if ((horizontal == 0) || image == null
              || av.getAlignment().getAlignmentAnnotation() == null
              || av.getAlignment().getAlignmentAnnotation().length < 1
              || av.isCalcInProgress())
      int er = av.getRanges().getEndRes() + 1;
      int transX = 0;
  
 +    Graphics2D gg = (Graphics2D) image.getGraphics();
 +
      gg.copyArea(0, 0, imgWidth, getHeight(),
              -horizontal * av.getCharWidth(), 0);
  
  
      gg.translate(-transX, 0);
  
 +    gg.dispose();
 +    
      fastPaint = true;
  
      // Call repaint on alignment panel so that repaints from other alignment
      ap = null;
      image = null;
      fadedImage = null;
 -    gg = null;
 +//    gg = null;
      _mwl = null;
  
      /*
       * hscroll, status bar, insets. 
       */
      int stuff = (ap.getViewName() != null ? 30 : 0)
 -            + (Platform.isAMac() ? 120 : 140);
 +            + (Platform.isAMacAndNotJS() ? 120 : 140);
      int availableHeight = ap.alignFrame.getHeight() - stuff;
      int rowHeight = av.getCharHeight();
  
@@@ -25,6 -25,7 +25,7 @@@ import jalview.analysis.scoremodels.Sco
  import jalview.analysis.scoremodels.SimilarityParams;
  import jalview.api.analysis.ScoreModelI;
  import jalview.api.analysis.SimilarityParamsI;
+ import jalview.bin.Cache;
  import jalview.datamodel.SequenceGroup;
  import jalview.util.MessageManager;
  
@@@ -103,7 -104,7 +104,7 @@@ public class CalculationChooser extend
  
    final ComboBoxTooltipRenderer renderer = new ComboBoxTooltipRenderer();
  
-   List<String> tips = new ArrayList<String>();
+   List<String> tips = new ArrayList<>();
  
    /*
     * the most recently opened PCA results panel
      pca = new JRadioButton(
              MessageManager.getString("label.principal_component_analysis"));
      pca.setOpaque(false);
 +    
      neighbourJoining = new JRadioButton(
              MessageManager.getString("label.tree_calc_nj"));
      neighbourJoining.setSelected(true);
 +    neighbourJoining.setOpaque(false);
 +
      averageDistance = new JRadioButton(
              MessageManager.getString("label.tree_calc_av"));
 -    neighbourJoining.setOpaque(false);
 +    averageDistance.setOpaque(false);
  
      JPanel calcChoicePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
      calcChoicePanel.setOpaque(false);
        };
      });
  
 +    validateCalcTypes();
      frame.setLayer(JLayeredPane.PALETTE_LAYER);
    }
  
     */
    protected JComboBox<String> buildModelOptionsList()
    {
-     final JComboBox<String> scoreModelsCombo = new JComboBox<String>();
+     final JComboBox<String> scoreModelsCombo = new JComboBox<>();
      scoreModelsCombo.setRenderer(renderer);
  
      /*
    {
      Object curSel = comboBox.getSelectedItem();
      toolTips.clear();
-     DefaultComboBoxModel<String> model = new DefaultComboBoxModel<String>();
+     DefaultComboBoxModel<String> model = new DefaultComboBoxModel<>();
+     /*
+      * select the score models applicable to the alignment type
+      */
+     boolean nucleotide = af.getViewport().getAlignment().isNucleotide();
+     List<ScoreModelI> models = getApplicableScoreModels(nucleotide,
+             pca.isSelected());
  
      /*
       * now we can actually add entries to the combobox,
       * remembering their descriptions for tooltips
       */
-     ScoreModels scoreModels = ScoreModels.getInstance();
      boolean selectedIsPresent = false;
-     for (ScoreModelI sm : scoreModels.getModels())
+     for (ScoreModelI sm : models)
      {
-       boolean nucleotide = af.getViewport().getAlignment().isNucleotide();
-       if (sm.isDNA() && nucleotide || sm.isProtein() && !nucleotide)
+       if (curSel != null && sm.getName().equals(curSel))
+       {
+         selectedIsPresent = true;
+         curSel = sm.getName();
+       }
+       model.addElement(sm.getName());
+       /*
+        * tooltip is description if provided, else text lookup with
+        * fallback on the model name
+        */
+       String tooltip = sm.getDescription();
+       if (tooltip == null)
        {
-         if (curSel != null && sm.getName().equals(curSel))
-         {
-           selectedIsPresent = true;
-           curSel = sm.getName();
-         }
-         model.addElement(sm.getName());
-         /*
-          * tooltip is description if provided, else text lookup with
-          * fallback on the model name
-          */
-         String tooltip = sm.getDescription();
-         if (tooltip == null)
-         {
-           tooltip = MessageManager.getStringOrReturn("label.score_model_",
-                   sm.getName());
-         }
-         toolTips.add(tooltip);
+         tooltip = MessageManager.getStringOrReturn("label.score_model_",
+                 sm.getName());
        }
+       toolTips.add(tooltip);
      }
      if (selectedIsPresent)
      {
        model.setSelectedItem(curSel);
    }
  
    /**
+    * Builds a list of score models which are applicable for the alignment and
+    * calculation type (peptide or generic models for protein, nucleotide or
+    * generic models for nucleotide).
+    * <p>
+    * As a special case, includes BLOSUM62 as an extra option for nucleotide PCA.
+    * This is for backwards compatibility with Jalview prior to 2.8 when BLOSUM62
+    * was the only score matrix supported. This is included if property
+    * BLOSUM62_PCA_FOR_NUCLEOTIDE is set to true in the Jalview properties file.
+    * 
+    * @param nucleotide
+    * @param forPca
+    * @return
+    */
+   protected static List<ScoreModelI> getApplicableScoreModels(
+           boolean nucleotide, boolean forPca)
+   {
+     List<ScoreModelI> filtered = new ArrayList<>();
+     ScoreModels scoreModels = ScoreModels.getInstance();
+     for (ScoreModelI sm : scoreModels.getModels())
+     {
+       if (!nucleotide && sm.isProtein() || nucleotide && sm.isDNA())
+       {
+         filtered.add(sm);
+       }
+     }
+     /*
+      * special case: add BLOSUM62 as last option for nucleotide PCA, 
+      * for backwards compatibility with Jalview < 2.8 (JAL-2962)
+      */
+     if (nucleotide && forPca
+             && Cache.getDefault("BLOSUM62_PCA_FOR_NUCLEOTIDE", false))
+     {
+       filtered.add(scoreModels.getBlosum62());
+     }
+     return filtered;
+   }
+   /**
     * Open and calculate the selected tree or PCA on 'OK'
     */
    protected void calculate_actionPerformed()
                JvOptionPane.WARNING_MESSAGE);
        return;
      }
+     /*
+      * construct the panel and kick off its calculation thread
+      */
      pcaPanel = new PCAPanel(af.alignPanel, modelName, params);
+     new Thread(pcaPanel).start();
    }
  
    /**
@@@ -26,7 -26,7 +26,8 @@@ import jalview.api.AlignViewportI
  import jalview.api.AlignmentViewPanel;
  import jalview.bin.Cache;
  import jalview.bin.Jalview;
 +import jalview.gui.ImageExporter.ImageWriterI;
+ import jalview.io.BackupFiles;
  import jalview.io.DataSourceType;
  import jalview.io.FileFormat;
  import jalview.io.FileFormatException;
@@@ -39,10 -39,10 +40,11 @@@ import jalview.io.JalviewFileChooser
  import jalview.io.JalviewFileView;
  import jalview.jbgui.GSplitFrame;
  import jalview.jbgui.GStructureViewer;
+ import jalview.project.Jalview2XML;
  import jalview.structure.StructureSelectionManager;
  import jalview.urls.IdOrgSettings;
 -import jalview.util.ImageMaker;
 +import jalview.util.BrowserLauncher;
 +import jalview.util.ImageMaker.TYPE;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
  import jalview.util.UrlConstants;
@@@ -88,6 -88,7 +90,6 @@@ import java.util.ArrayList
  import java.util.Hashtable;
  import java.util.List;
  import java.util.ListIterator;
 -import java.util.StringTokenizer;
  import java.util.Vector;
  import java.util.concurrent.ExecutorService;
  import java.util.concurrent.Executors;
@@@ -113,7 -114,6 +115,7 @@@ import javax.swing.JMenuItem
  import javax.swing.JPanel;
  import javax.swing.JPopupMenu;
  import javax.swing.JProgressBar;
 +import javax.swing.JTextField;
  import javax.swing.KeyStroke;
  import javax.swing.SwingUtilities;
  import javax.swing.event.HyperlinkEvent;
@@@ -195,13 -195,6 +197,13 @@@ public class Desktop extends jalview.jb
  
    public static MyDesktopPane desktop;
  
 +  public static MyDesktopPane getDesktop()
 +  {
 +    // BH 2018 could use currentThread() here as a reference to a
 +    // Hashtable<Thread, MyDesktopPane> in JavaScript
 +    return desktop;
 +  }
 +
    static int openFrameCount = 0;
  
    static final int xOffset = 30;
     */
    public 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.
 +     * 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;
 -    doVamsasClientCheck();
 +    if (!Platform.isJS())
 +    {
 +      doVamsasClientCheck();
 +    }
  
      doConfigureStructurePrefs();
      setTitle("Jalview " + jalview.bin.Cache.getProperty("VERSION"));
      boolean showjconsole = jalview.bin.Cache.getDefault("SHOW_JAVA_CONSOLE",
              false);
      desktop = new MyDesktopPane(selmemusage);
 +    
 +    
      showMemusage.setSelected(selmemusage);
      desktop.setBackground(Color.white);
      getContentPane().setLayout(new BorderLayout());
      // 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");
 +      }
 +    
      getContentPane().add(desktop, BorderLayout.CENTER);
      desktop.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.isWindows() ? new DefaultDesktopManager()
 -                    : Platform.isAMac()
 +            (Platform.isWindowsAndNotJS() ? new DefaultDesktopManager()
 +                    : Platform.isAMacAndNotJS()
                              ? new AquaInternalFrameManager(
                                      desktop.getDesktopManager())
                              : desktop.getDesktopManager())));
      else
      {
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
 -      setBounds((screenSize.width - 900) / 2, (screenSize.height - 650) / 2,
 -              900, 650);
 +      int xPos = Math.max(5, (screenSize.width - 900) / 2);
 +      int yPos = Math.max(5, (screenSize.height - 650) / 2);
 +      setBounds(xPos, yPos, 900, 650);
      }
 -    jconsole = new Console(this, showjconsole);
 -    // add essential build information
 -    jconsole.setHeader(
 -            "Jalview Version: " + jalview.bin.Cache.getProperty("VERSION")
 -                    + "\n" + "Jalview Installation: "
 -                    + jalview.bin.Cache.getDefault("INSTALLATION",
 -                            "unknown")
 -                    + "\n" + "Build Date: "
 -                    + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown")
 -                    + "\n" + "Java version: "
 -                    + System.getProperty("java.version") + "\n"
 -                    + System.getProperty("os.arch") + " "
 -                    + System.getProperty("os.name") + " "
 -                    + System.getProperty("os.version"));
 +    
 +    boolean doFullLoad = /** @j2sNative ! */true;
 +    
 +    if (doFullLoad) {
 +      
 +      jconsole = new Console(this, showjconsole);
 +      // add essential build information
 +      jconsole.setHeader("Jalview Version: "
 +              + jalview.bin.Cache.getProperty("VERSION") + "\n"
 +              + "Jalview Installation: "
 +              + jalview.bin.Cache.getDefault("INSTALLATION", "unknown")
 +              + "\n" + "Build Date: "
 +              + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown") + "\n"
 +              + "Java version: " + System.getProperty("java.version") + "\n"
 +              + System.getProperty("os.arch") + " "
 +              + System.getProperty("os.name") + " "
 +              + System.getProperty("os.version"));
 +
 +      showConsole(showjconsole);
  
 -    showConsole(showjconsole);
 +      showNews.setVisible(false);
 +
 +      experimentalFeatures.setSelected(showExperimental());
 +
 +      getIdentifiersOrgData();
 +
 +      checkURLLinks();
 +
 +      // Spawn a thread that shows the splashscreen
 +
 +      SwingUtilities.invokeLater(new Runnable()
 +      {
 +        @Override
 +        public void run()
 +        {
 +          new SplashScreen();
 +        }
 +      });
 +
 +      // 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()
 +              {
  
 -    showNews.setVisible(false);
 +                @Override
 +                public void propertyChange(PropertyChangeEvent evt)
 +                {
 +                  Cache.log.debug("Firing service changed event for "
 +                          + evt.getNewValue());
 +                  JalviewServicesChanged(evt);
 +                }
  
 -    experimentalFeatures.setSelected(showExperimental());
 +              });
  
 -    getIdentifiersOrgData();
 +    } 
  
 -    checkURLLinks();
 +    this.setDropTarget(new java.awt.dnd.DropTarget(desktop, this));
  
      this.addWindowListener(new WindowAdapter()
      {
      });
      desktop.addMouseListener(ma);
  
 -    this.setDropTarget(new java.awt.dnd.DropTarget(desktop, this));
 -    // Spawn a thread that shows the splashscreen
 -    SwingUtilities.invokeLater(new Runnable()
 -    {
 -      @Override
 -      public void run()
 -      {
 -        new SplashScreen();
 -      }
 -    });
 -
 -    // 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);
 -              }
 -
 -            });
    }
  
    /**
        public void run()
        {
          Cache.log.debug("Starting news thread.");
 -
          jvnews = new BlogReader(me);
          showNews.setVisible(true);
          Cache.log.debug("Completed news thread.");
  
    void showNews(boolean visible)
    {
 +    Cache.log.debug((visible ? "Showing" : "Hiding") + " news.");
 +    showNews.setSelected(visible);
 +    if (visible && !jvnews.isVisible())
      {
 -      Cache.log.debug((visible ? "Showing" : "Hiding") + " news.");
 -      showNews.setSelected(visible);
 -      if (visible && !jvnews.isVisible())
 +      new Thread(new Runnable()
        {
 -        new Thread(new Runnable()
 +        @Override
 +        public void run()
          {
 -          @Override
 -          public void run()
 -          {
 -            long now = System.currentTimeMillis();
 -            Desktop.instance.setProgressBar(
 -                    MessageManager.getString("status.refreshing_news"),
 -                    now);
 -            jvnews.refreshNews();
 -            Desktop.instance.setProgressBar(null, now);
 -            jvnews.showNews();
 -          }
 -        }).start();
 -      }
 +          long now = System.currentTimeMillis();
 +          Desktop.instance.setProgressBar(
 +                  MessageManager.getString("status.refreshing_news"), now);
 +          jvnews.refreshNews();
 +          Desktop.instance.setProgressBar(null, now);
 +          jvnews.showNews();
 +        }
 +      }).start();
      }
    }
  
  
    private void doVamsasClientCheck()
    {
 -    if (jalview.bin.Cache.vamsasJarsPresent())
 +    if (Cache.vamsasJarsPresent())
      {
        setupVamsasDisconnectedGui();
        VamsasMenu.setVisible(true);
      frame.setResizable(resizable);
      frame.setMaximizable(resizable);
      frame.setIconifiable(resizable);
 -    frame.setOpaque(false);
 +    frame.setOpaque(/** @j2sNative true || */
 +            false);
  
      if (frame.getX() < 1 && frame.getY() < 1)
      {
            frame.setIcon(false);
          } catch (java.beans.PropertyVetoException ex)
          {
 -
 +          // System.err.println(ex.toString());
          }
        }
      });
    }
  
    /**
 -   * Add key bindings to a JInternalFrame so that Ctrl-W and Cmd-W will close
 -   * the window
 +   * Add key bindings to a JInternalFrame so that Ctrl-W and Cmd-W will close the
 +   * window
     * 
     * @param frame
     */
      // Java's Transferable for native dnd
      evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
      Transferable t = evt.getTransferable();
 -    List<String> files = new ArrayList<>();
 +    List<Object> files = new ArrayList<>();
      List<DataSourceType> protocols = new ArrayList<>();
  
      try
        {
          for (int i = 0; i < files.size(); i++)
          {
 -          String file = files.get(i).toString();
 +          // BH 2018 File or String
 +          Object file = files.get(i);
 +          String fileName = file.toString();
            DataSourceType protocol = (protocols == null)
                    ? DataSourceType.FILE
                    : protocols.get(i);
            FileFormatI format = null;
  
 -          if (file.endsWith(".jar"))
 +          if (fileName.endsWith(".jar"))
            {
              format = FileFormat.Jalview;
  
              format = new IdentifyFile().identify(file, protocol);
            }
  
 -          new FileLoader().LoadFile(file, protocol, format);
 +          new FileLoader().LoadFile(null, file, protocol, format);
  
          }
        } catch (Exception ex)
    {
      String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
      JalviewFileChooser chooser = JalviewFileChooser
-             .forRead(Cache.getProperty("LAST_DIRECTORY"), fileFormat);
+             .forRead(Cache.getProperty("LAST_DIRECTORY"), fileFormat, true);
  
      chooser.setFileView(new JalviewFileView());
      chooser.setDialogTitle(
              MessageManager.getString("label.open_local_file"));
      chooser.setToolTipText(MessageManager.getString("action.open"));
  
 -    int value = chooser.showOpenDialog(this);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    chooser.setResponseHandler(0, new Runnable()
      {
 -      String choice = chooser.getSelectedFile().getPath();
 -      Cache.setProperty("LAST_DIRECTORY",
 -              chooser.getSelectedFile().getParent());
 +      @Override
 +      public void run()
 +      {
 +        File selectedFile = chooser.getSelectedFile();
 +        Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
  
 -      FileFormatI format = chooser.getSelectedFormat();
 +        FileFormatI format = chooser.getSelectedFormat();
  
 -      /*
 -       * Call IdentifyFile to verify the file contains what its extension implies.
 -       * Skip this step for dynamically added file formats, because
 -       * IdentifyFile does not know how to recognise them.
 -       */
 -      if (FileFormats.getInstance().isIdentifiable(format))
 -      {
 -        try
 -        {
 -          format = new IdentifyFile().identify(choice, DataSourceType.FILE);
 -        } catch (FileFormatException e)
 +        /*
 +         * Call IdentifyFile to verify the file contains what its extension implies.
 +         * Skip this step for dynamically added file formats, because
 +         * IdentifyFile does not know how to recognise them.
 +         */
 +        if (FileFormats.getInstance().isIdentifiable(format))
          {
 -          // format = null; //??
 +          try
 +          {
 +            format = new IdentifyFile().identify(selectedFile,
 +                    DataSourceType.FILE);
 +          } catch (FileFormatException e)
 +          {
 +            // format = null; //??
 +          }
          }
 -      }
  
 -      if (viewport != null)
 -      {
 -        new FileLoader().LoadFile(viewport, choice, DataSourceType.FILE,
 -                format);
 +        new FileLoader().LoadFile(viewport, selectedFile,
 +                DataSourceType.FILE, format);
        }
 -      else
 -      {
 -        new FileLoader().LoadFile(choice, DataSourceType.FILE, format);
 -      }
 -    }
 +    });
 +    chooser.showOpenDialog(this);
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Shows a dialog for input of a URL at which to retrieve alignment data
     * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * @param viewport
     */
    @Override
    public void inputURLMenuItem_actionPerformed(AlignViewport viewport)
      // for viewing
      JLabel label = new JLabel(
              MessageManager.getString("label.input_file_url"));
  
      JPanel panel = new JPanel(new GridLayout(2, 1));
      panel.add(label);
 -    panel.add(history);
 -    history.setPreferredSize(new Dimension(400, 20));
 -    history.setEditable(true);
 -    history.addItem("http://www.");
 -
 -    String historyItems = jalview.bin.Cache.getProperty("RECENT_URL");
 -
 -    StringTokenizer st;
 -
 -    if (historyItems != null)
 -    {
 -      st = new StringTokenizer(historyItems, "\t");
 -
 -      while (st.hasMoreTokens())
 -      {
 -        history.addItem(st.nextElement());
 -      }
 -    }
 -
 -    int reply = JvOptionPane.showInternalConfirmDialog(desktop, panel,
 -            MessageManager.getString("label.input_alignment_from_url"),
 -            JvOptionPane.OK_CANCEL_OPTION);
 -
 -    if (reply != JvOptionPane.OK_OPTION)
 -    {
 -      return;
 -    }
 -
 -    String url = history.getSelectedItem().toString();
 -
 -    if (url.toLowerCase().endsWith(".jar"))
 +    
 +    /*
 +     * the URL to fetch is
 +     * Java: an editable combobox with history
 +     * JS: (pending JAL-3038) a plain text field
 +     */
 +    JComponent history;
 +    String urlBase = "http://www.";
 +    if (Platform.isJS())
      {
 -      if (viewport != null)
 -      {
 -        new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
 -                FileFormat.Jalview);
 -      }
 -      else
 -      {
 -        new FileLoader().LoadFile(url, DataSourceType.URL,
 -                FileFormat.Jalview);
 -      }
 +      history = new JTextField(urlBase, 35);
      }
      else
      {
 -      FileFormatI format = null;
 -      try
 -      {
 -        format = new IdentifyFile().identify(url, DataSourceType.URL);
 -      } catch (FileFormatException e)
 +      JComboBox<String> asCombo = new JComboBox<>();
 +      asCombo.setPreferredSize(new Dimension(400, 20));
 +      asCombo.setEditable(true);
 +      asCombo.addItem(urlBase);
 +      String historyItems = Cache.getProperty("RECENT_URL");
 +      if (historyItems != null)
        {
 -        // TODO revise error handling, distinguish between
 -        // URL not found and response not valid
 +        for (String token : historyItems.split("\\t"))
 +        {
 +          asCombo.addItem(token);
 +        }
        }
 +      history = asCombo;
 +    }
 +    panel.add(history);
  
 -      if (format == null)
 +    Object[] options = new Object[] { MessageManager.getString("action.ok"),
 +        MessageManager.getString("action.cancel") };
 +    Runnable action = new Runnable() {
 +      @Override
 +      public void run()
        {
 -        JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 -                MessageManager.formatMessage("label.couldnt_locate",
 -                        new Object[]
 -                        { url }),
 -                MessageManager.getString("label.url_not_found"),
 -                JvOptionPane.WARNING_MESSAGE);
 +        String url = Platform.isJS() ? ((JTextField) history).getText()
 +                : ((JComboBox<String>) history).getSelectedItem()
 +                        .toString();
  
 -        return;
 -      }
 +        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);
 +          }
 +        }
 +        else
 +        {
 +          FileFormatI format = null;
 +          try
 +          {
 +            format = new IdentifyFile().identify(url, DataSourceType.URL);
 +          } catch (FileFormatException e)
 +          {
 +            // TODO revise error handling, distinguish between
 +            // URL not found and response not valid
 +          }
  
 -      if (viewport != null)
 -      {
 -        new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
 -                format);
 -      }
 -      else
 -      {
 -        new FileLoader().LoadFile(url, DataSourceType.URL, format);
 -      }
 -    }
 +          if (format == null)
 +          {
 +            String msg = MessageManager.formatMessage("label.couldnt_locate", url);
 +            JvOptionPane.showInternalMessageDialog(Desktop.desktop, msg,
 +                    MessageManager.getString("label.url_not_found"),
 +                    JvOptionPane.WARNING_MESSAGE);
 +
 +            return;
 +          }
 +
 +          if (viewport != null)
 +          {
 +            new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
 +                    format);
 +          }
 +          else
 +          {
 +            new FileLoader().LoadFile(url, DataSourceType.URL, format);
 +          }
 +        }
 +      }};
 +    String dialogOption = MessageManager
 +            .getString("label.input_alignment_from_url");
 +    JvOptionPane.newOptionDialog(desktop).setResponseHandler(0, action)
 +            .showInternalDialog(panel, dialogOption,
 +                    JvOptionPane.YES_NO_CANCEL_OPTION,
 +                    JvOptionPane.PLAIN_MESSAGE, null, options,
 +                    MessageManager.getString("action.ok"));
    }
  
    /**
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Action on requesting Help documentation
     */
    @Override
 -  public void documentationMenuItem_actionPerformed(ActionEvent e)
 +  public void documentationMenuItem_actionPerformed()
    {
      try
      {
 -      Help.showHelpWindow();
 +      if (Platform.isJS())
 +      {
 +        BrowserLauncher.openURL("http://www.jalview.org/help.html");
 +      }
 +      else
 +      {
 +        Help.showHelpWindow();
 +      }
      } catch (Exception ex)
      {
 +      System.err.println("Error opening help: " + ex.getMessage());
      }
    }
  
     */
    void showConsole(boolean selected)
    {
 -    showConsole.setSelected(selected);
      // TODO: decide if we should update properties file
 -    Cache.setProperty("SHOW_JAVA_CONSOLE",
 -            Boolean.valueOf(selected).toString());
 -    jconsole.setVisible(selected);
 +    if (jconsole != null) // BH 2018
 +    {
 +      showConsole.setSelected(selected);
 +      Cache.setProperty("SHOW_JAVA_CONSOLE",
 +              Boolean.valueOf(selected).toString());
 +      jconsole.setVisible(selected);
 +    }
    }
  
    void reorderAssociatedWindows(boolean minimize, boolean close)
    }
  
    /**
 -   * Shows a file chooser dialog and writes out the current session as a Jalview
 -   * project file
 +   * Prompts the user to choose a file and then saves the Jalview state as a
 +   * Jalview project file
     */
    @Override
-   public void saveState_actionPerformed(boolean asCastor)
+   public void saveState_actionPerformed()
    {
-     JalviewFileChooser chooser = new JalviewFileChooser(
-             asCastor ? "jvp" : "jvx",
-             "Jalview Project");
+     saveState_actionPerformed(false);
+   }
  
-     chooser.setFileView(new JalviewFileView());
-     chooser.setDialogTitle(MessageManager.getString("label.save_state"));
-     int option = chooser.showSaveDialog(this);
-     if (option == JalviewFileChooser.APPROVE_OPTION)
+   public void saveState_actionPerformed(boolean saveAs)
+   {
+     java.io.File projectFile = getProjectFile();
+     // autoSave indicates we already have a file and don't need to ask
+     boolean autoSave = projectFile != null && !saveAs
+             && BackupFiles.getEnabled();
+     // System.out.println("autoSave="+autoSave+", projectFile='"+projectFile+"',
+     // saveAs="+saveAs+", Backups
+     // "+(BackupFiles.getEnabled()?"enabled":"disabled"));
+     boolean approveSave = false;
+     if (!autoSave)
      {
-       File choice = chooser.getSelectedFile();
-       setProjectFile(choice);
+       JalviewFileChooser chooser = new JalviewFileChooser("jvp",
+               "Jalview Project");
+       chooser.setFileView(new JalviewFileView());
+       chooser.setDialogTitle(MessageManager.getString("label.save_state"));
+       int value = chooser.showSaveDialog(this);
+       if (value == JalviewFileChooser.APPROVE_OPTION)
+       {
+         projectFile = chooser.getSelectedFile();
+         setProjectFile(projectFile);
+         approveSave = true;
+       }
+     }
  
+     if (approveSave || autoSave)
+     {
+       final Desktop me = this;
+       final java.io.File chosenFile = projectFile;
        new Thread(new Runnable()
        {
          @Override
            // TODO: refactor to Jalview desktop session controller action.
            setProgressBar(MessageManager.formatMessage(
                    "label.saving_jalview_project", new Object[]
-                   { choice.getName() }), choice.hashCode());
+                   { chosenFile.getName() }), chosenFile.hashCode());
            jalview.bin.Cache.setProperty("LAST_DIRECTORY",
-                   choice.getParent());
+                   chosenFile.getParent());
            // TODO catch and handle errors for savestate
            // TODO prevent user from messing with the Desktop whilst we're saving
            try
            {
-             if (asCastor)
-             {
-               new Jalview2XML().saveState(choice);
-             }
-             else
-             {
-               new jalview.project.Jalview2XML().saveState(choice);
-             }
+             BackupFiles backupfiles = new BackupFiles(chosenFile);
+             new Jalview2XML().saveState(backupfiles.getTempFile());
+             backupfiles.setWriteSuccess(true);
+             backupfiles.rollBackupsAndRenameTempFile();
            } catch (OutOfMemoryError oom)
            {
-             new OOMWarning(
-                     "Whilst saving current state to " + choice.getName(),
-                     oom);
+             new OOMWarning("Whilst saving current state to "
+                     + chosenFile.getName(), oom);
            } catch (Exception ex)
            {
-             Cache.log.error(
-                     "Problems whilst trying to save to " + choice.getName(),
-                     ex);
-             JvOptionPane.showMessageDialog(Desktop.this,
+             Cache.log.error("Problems whilst trying to save to "
+                     + chosenFile.getName(), ex);
+             JvOptionPane.showMessageDialog(me,
                      MessageManager.formatMessage(
                              "label.error_whilst_saving_current_state_to",
                              new Object[]
-                             { choice.getName() }),
+                             { chosenFile.getName() }),
                      MessageManager.getString("label.couldnt_save_project"),
                      JvOptionPane.WARNING_MESSAGE);
            }
-           setProgressBar(null, choice.hashCode());
+           setProgressBar(null, chosenFile.hashCode());
          }
        }).start();
 -    }
 +      }
    }
  
-   void setProjectFile(File choice)
+   @Override
+   public void saveAsState_actionPerformed(ActionEvent e)
+   {
+     saveState_actionPerformed(true);
+   }
+   private void setProjectFile(File choice)
    {
      this.projectFile = choice;
    }
    }
  
    /**
-    * Prompts the user to choose a file and loads in as a Jalview project file
+    * Shows a file chooser dialog and tries to read in the selected file as a
+    * Jalview project
     */
    @Override
-   public void loadState_actionPerformed(boolean asCastor)
+   public void loadState_actionPerformed()
    {
-     // TODO: GET RID OF .JVX BEFORE RELEASE JIM!
-     final String[] suffix = asCastor ? new String[] { "jvp", "jar" }
-             : new String[]
-             { "jvx" };
-     final String[] desc = asCastor
-             ? new String[]
-             { "Jalview Project", "Jalview Project (old)" }
-             : new String[]
-             { "Jalview Project" };
+     final String[] suffix = new String[] { "jvp", "jar" };
+     final String[] desc = new String[] { "Jalview Project",
+         "Jalview Project (old)" };
      JalviewFileChooser chooser = new JalviewFileChooser(
-             Cache.getProperty("LAST_DIRECTORY"), suffix,
-             desc,
-             "Jalview Project");
+             Cache.getProperty("LAST_DIRECTORY"), suffix, desc,
+             "Jalview Project", true, true); // last two booleans: allFiles,
+                                             // allowBackupFiles
      chooser.setFileView(new JalviewFileView());
      chooser.setDialogTitle(MessageManager.getString("label.restore_state"));
 -
 -    int value = chooser.showOpenDialog(this);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    chooser.setResponseHandler(0, new Runnable()
      {
 -      final File selectedFile = chooser.getSelectedFile();
 -      setProjectFile(selectedFile);
 -      final String choice = selectedFile.getAbsolutePath();
 -      Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
 -      new Thread(new Runnable()
 +      @Override
 +      public void run()
        {
 -        @Override
 -        public void run()
 +        File selectedFile = chooser.getSelectedFile();
 +        setProjectFile(selectedFile);
 +        String choice = selectedFile.getAbsolutePath();
 +        Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
 +        new Thread(new Runnable()
          {
 -          setProgressBar(MessageManager.formatMessage(
 -                  "label.loading_jalview_project", new Object[]
 -                  { choice }), choice.hashCode());
 -          try
 -          {
 -            new Jalview2XML().loadJalviewAlign(choice);
 -          } catch (OutOfMemoryError oom)
 -          {
 -            new OOMWarning("Whilst loading project from " + choice, oom);
 -          } catch (Exception ex)
 +          @Override
 +          public void run()
            {
-               try {
-               if (asCastor)
-               {
-                 new Jalview2XML().loadJalviewAlign(choice);
-               }
-               else
-               {
-                 new jalview.project.Jalview2XML().loadJalviewAlign(selectedFile);
-               }
-             } catch (OutOfMemoryError oom)
 -            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);
++              try 
 +            {
-             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);
-             }
++              new Jalview2XML().loadJalviewAlign(choice);
++            } 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);
++              }
            }
 -          setProgressBar(null, choice.hashCode());
 -        }
 -      }).start();
 -    }
 +        }).start();
 +      }
 +    });
 +    
 +    chooser.showOpenDialog(this);
    }
  
    @Override
  
    ArrayList<JPanel> fileLoadingPanels = new ArrayList<>();
  
 -  public void startLoading(final String fileName)
 +  public void startLoading(final Object fileName)
    {
      if (fileLoadingCount == 0)
      {
  
    /**
     * Gather expanded views (separate AlignFrame's) with the same sequence set
 -   * identifier back in to this frame as additional views, and close the
 -   * expanded views. Note the expanded frames may themselves have multiple
 -   * views. We take the lot.
 +   * identifier back in to this frame as additional views, and close the expanded
 +   * views. Note the expanded frames may themselves have multiple views. We take
 +   * the lot.
     * 
     * @param source
     */
    @Override
    public void vamsasImport_actionPerformed(ActionEvent e)
    {
 +    // TODO: JAL-3048 not needed for Jalview-JS
 +
      if (v_client == null)
      {
        // Load and try to start a session.
    @Override
    public void vamsasSave_actionPerformed(ActionEvent e)
    {
 +    // TODO: JAL-3048 not needed for Jalview-JS
 +
      if (v_client != null)
      {
        // TODO: VAMSAS DOCUMENT EXTENSION is VDJ
  
    /**
     * Proxy class for JDesktopPane which optionally displays the current memory
 -   * usage and highlights the desktop area with a red bar if free memory runs
 -   * low.
 +   * usage and highlights the desktop area with a red bar if free memory runs low.
     * 
     * @author AMW
     */
 -  public class MyDesktopPane extends JDesktopPane implements Runnable
 +  public class MyDesktopPane extends JDesktopPane
 +          implements Runnable
    {
 -
      private static final float ONE_MB = 1048576f;
  
      boolean showMemoryUsage = false;
    }
  
    /**
 -   * Bind Ctrl/Cmd-Q to Quit - for reset as Groovy Console takes over this
 -   * binding when opened
 +   * Bind Ctrl/Cmd-Q to Quit - for reset as Groovy Console takes over this binding
 +   * when opened
     */
    protected void addQuitHandler()
    {
    @Override
    public void setProgressBar(String message, long id)
    {
 +          Platform.timeCheck("Desktop " + message, Platform.TIME_MARK);     
 +
      if (progressBars == null)
      {
        progressBars = new Hashtable<>();
    }
  
    /**
 -   * This will return the first AlignFrame holding the given viewport instance.
 -   * It will break if there are more than one AlignFrames viewing a particular
 -   * av.
 +   * This will return the first AlignFrame holding the given viewport instance. It
 +   * will break if there are more than one AlignFrames viewing a particular av.
     * 
     * @param viewport
     * @return alignFrame for viewport
      block.release();
    }
  
 +  /**
 +   * Outputs an image of the desktop to file in EPS format, after prompting the
 +   * user for choice of Text or Lineart character rendering (unless a preference
 +   * has been set). The file name is generated as
 +   * 
 +   * <pre>
 +   * Jalview_snapshot_nnnnn.eps where nnnnn is the current timestamp in milliseconds
 +   * </pre>
 +   */
    @Override
    protected void snapShotWindow_actionPerformed(ActionEvent e)
    {
 +    // currently the menu option to do this is not shown
      invalidate();
 -    File of;
 -    ImageMaker im = new jalview.util.ImageMaker(
 -            this, ImageMaker.TYPE.EPS, "View of Desktop", getWidth(),
 -            getHeight(), of = new File("Jalview_snapshot"
 -                    + System.currentTimeMillis() + ".eps"),
 -            "View of desktop", null, 0, false);
 -    try
 -    {
 -      paintAll(im.getGraphics());
 -      im.writeImage();
 -    } catch (Exception q)
 +
 +    int width = getWidth();
 +    int height = getHeight();
 +    File of = new File(
 +            "Jalview_snapshot_" + System.currentTimeMillis() + ".eps");
 +    ImageWriterI writer = new ImageWriterI()
      {
 -      Cache.log.error("Couldn't write snapshot to " + of.getAbsolutePath(),
 -              q);
 -      return;
 -    }
 -    Cache.log.info("Successfully written snapshot to file "
 -            + of.getAbsolutePath());
 +      @Override
 +      public void exportImage(Graphics g) throws Exception
 +      {
 +        paintAll(g);
 +        Cache.log.info("Successfully written snapshot to file "
 +                + of.getAbsolutePath());
 +      }
 +    };
 +    String title = "View of desktop";
 +    ImageExporter exporter = new ImageExporter(writer, null, TYPE.EPS,
 +            title);
 +    exporter.doExport(of, this, width, height, title);
    }
  
    /**
     * Explode the views in the given SplitFrame into separate SplitFrame windows.
 -   * This respects (remembers) any previous 'exploded geometry' i.e. the size
 -   * and location last time the view was expanded (if any). However it does not
 +   * This respects (remembers) any previous 'exploded geometry' i.e. the size and
 +   * location last time the view was expanded (if any). However it does not
     * remember the split pane divider location - this is set to match the
     * 'exploding' frame.
     * 
     *          - the payload from the drop event
     * @throws Exception
     */
 -  public static void transferFromDropTarget(List<String> files,
 +  public static void transferFromDropTarget(List<Object> files,
            List<DataSourceType> protocols, DropTargetDropEvent evt,
            Transferable t) throws Exception
    {
  
 +    // BH 2018 changed List<String> to List<Object> to allow for File from SwingJS
 +
 +    // DataFlavor[] flavors = t.getTransferDataFlavors();
 +    // for (int i = 0; i < flavors.length; i++) {
 +    // if (flavors[i].isFlavorJavaFileListType()) {
 +    // evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
 +    // List<File> list = (List<File>) t.getTransferData(flavors[i]);
 +    // for (int j = 0; j < list.size(); j++) {
 +    // File file = (File) list.get(j);
 +    // byte[] data = getDroppedFileBytes(file);
 +    // fileName.setText(file.getName() + " - " + data.length + " " +
 +    // evt.getLocation());
 +    // JTextArea target = (JTextArea) ((DropTarget) evt.getSource()).getComponent();
 +    // target.setText(new String(data));
 +    // }
 +    // dtde.dropComplete(true);
 +    // return;
 +    // }
 +    //
 +
      DataFlavor uriListFlavor = new DataFlavor(
              "text/uri-list;class=java.lang.String"), urlFlavour = null;
      try
          }
          else
          {
 -          if (Platform.isAMac())
 +          if (Platform.isAMacAndNotJS())
            {
              System.err.println(
                      "Please ignore plist error - occurs due to problem with java 8 on OSX");
        for (Object file : (List) t
                .getTransferData(DataFlavor.javaFileListFlavor))
        {
 -        files.add(((File) file).toString());
 +        files.add(file);
          protocols.add(DataSourceType.FILE);
        }
      }
          }
        }
      }
 -    if (Platform.isWindows())
 -
 +    if (Platform.isWindowsAndNotJS())
      {
        Cache.log.debug("Scanning dropped content for Windows Link Files");
  
        // resolve any .lnk files in the file drop
        for (int f = 0; f < files.size(); f++)
        {
 -        String source = files.get(f).toLowerCase();
 +        String source = files.get(f).toString().toLowerCase();
          if (protocols.get(f).equals(DataSourceType.FILE)
                  && (source.endsWith(".lnk") || source.endsWith(".url")
                          || source.endsWith(".site")))
          {
            try
            {
 -            File lf = new File(files.get(f));
 +            Object obj = files.get(f);
 +            File lf = (obj instanceof File ? (File) obj
 +                    : new File((String) obj));
              // process link file to get a URL
              Cache.log.debug("Found potential link file: " + lf);
              WindowsShortcut wscfile = new WindowsShortcut(lf);
    }
  
    /**
 -   * Answers a (possibly empty) list of any structure viewer frames (currently
 -   * for either Jmol or Chimera) which are currently open. This may optionally
 -   * be restricted to viewers of a specified class, or viewers linked to a
 -   * specified alignment panel.
 +   * Answers a (possibly empty) list of any structure viewer frames (currently for
 +   * either Jmol or Chimera) which are currently open. This may optionally be
 +   * restricted to viewers of a specified class, or viewers linked to a specified
 +   * alignment panel.
     * 
     * @param apanel
     *          if not null, only return viewers linked to this panel
@@@ -29,7 -29,6 +29,7 @@@ import jalview.datamodel.features.Featu
  import jalview.datamodel.features.FeatureMatcherSet;
  import jalview.datamodel.features.FeatureMatcherSetI;
  import jalview.gui.Help.HelpId;
 +import jalview.gui.JalviewColourChooser.ColourChooserListener;
  import jalview.io.JalviewFileChooser;
  import jalview.io.JalviewFileView;
  import jalview.schemes.FeatureColour;
@@@ -81,7 -80,8 +81,7 @@@ import javax.swing.BorderFactory
  import javax.swing.Icon;
  import javax.swing.JButton;
  import javax.swing.JCheckBox;
 -import javax.swing.JColorChooser;
 -import javax.swing.JDialog;
 +import javax.swing.JCheckBoxMenuItem;
  import javax.swing.JInternalFrame;
  import javax.swing.JLabel;
  import javax.swing.JLayeredPane;
@@@ -93,11 -93,11 +93,12 @@@ import javax.swing.JSlider
  import javax.swing.JTable;
  import javax.swing.ListSelectionModel;
  import javax.swing.SwingConstants;
 +import javax.swing.ToolTipManager;
  import javax.swing.border.Border;
  import javax.swing.event.ChangeEvent;
  import javax.swing.event.ChangeListener;
  import javax.swing.table.AbstractTableModel;
+ import javax.swing.table.JTableHeader;
  import javax.swing.table.TableCellEditor;
  import javax.swing.table.TableCellRenderer;
  import javax.swing.table.TableColumn;
@@@ -141,9 -141,9 +142,9 @@@ public class FeatureSettings extends JP
     */
    Object[][] originalData;
  
 -  private float originalTransparency;
 +  float originalTransparency;
  
 -  private Map<String, FeatureMatcherSetI> originalFilters;
 +  Map<String, FeatureMatcherSetI> originalFilters;
  
    final JInternalFrame frame;
  
  
    int selectedRow = -1;
  
-   JButton fetchDAS = new JButton();
-   JButton saveDAS = new JButton();
-   JButton cancelDAS = new JButton();
    boolean resettingTable = false;
  
    /*
     * true when Feature Settings are updating from feature renderer
     */
 -  private boolean handlingUpdate = false;
 +  boolean handlingUpdate = false;
  
    /*
     * holds {featureCount, totalExtent} for each feature type
  
      table = new JTable()
      {
-       
- //            @Override
- //            public void repaint() {
- //              System.out.println("FS repaint");
- //              super.repaint();
- //              
- //            }
- //            
- //            @Override
- //        public void repaint(long tm, int x, int y, int width, int height) {
- //              System.out.println("FS repaint " + x  + " " + y + " " + width + " " + height);
- //              super.repaint(tm, x, y, width, height);
- //              
- //            }
- //            @Override
- //            public void invalidate() {
- //                    if (isValid())
- //               System.out.println("FS invalidating ");
- //            super.invalidate();
- //            }
- //            
- //            @Override
- //            public void validate() {
- //            System.out.println("FS validating "  + isValid());
- //            super.validate();
- //            }
        @Override
        public String getToolTipText(MouseEvent e)
        {
          
          return tip;
        }
 +      
  
        /**
         * Position the tooltip near the bottom edge of, and half way across, the
          return loc;
        }
      };
-     
-     // next line is needed to avoid (quiet) exceptions thrown
-     // when column ordering changes so that the above constants
-     // no longer apply.
-     table.getTableHeader().setReorderingAllowed(false); // BH 2018
-     
-     table.getTableHeader().setFont(new Font("Verdana", Font.PLAIN, 12));
-     ToolTipManager.sharedInstance().registerComponent(table);
+     JTableHeader tableHeader = table.getTableHeader();
+     tableHeader.setFont(new Font("Verdana", Font.PLAIN, 12));
+     tableHeader.setReorderingAllowed(false);
+     table.setFont(new Font("Verdana", Font.PLAIN, 12));
 -
 -    table.setDefaultEditor(FeatureColour.class, new ColorEditor(this));
 +    table.setDefaultEditor(FeatureColour.class, new ColorEditor());
      table.setDefaultRenderer(FeatureColour.class, new ColorRenderer());
  
 -    table.setDefaultEditor(FeatureMatcherSet.class, new FilterEditor(this));
 +    table.setDefaultEditor(FeatureMatcherSet.class, new FilterEditor());
      table.setDefaultRenderer(FeatureMatcherSet.class, new FilterRenderer());
 -
 +    
      TableColumn colourColumn = new TableColumn(COLOUR_COLUMN, 75,
 -            new ColorRenderer(), new ColorEditor(this));
 +            new ColorRenderer(), new ColorEditor());
      table.addColumn(colourColumn);
  
      TableColumn filterColumn = new TableColumn(FILTER_COLUMN, 75,
 -            new FilterRenderer(), new FilterEditor(this));
 +            new FilterRenderer(), new FilterEditor());
      table.addColumn(filterColumn);
  
      table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
          if (evt.isPopupTrigger())
          {
            Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
 -          popupSort(selectedRow, type, colour, fr.getMinMax(), evt.getX(),
 -                  evt.getY());
 +          showPopupMenu(selectedRow, type, colour, evt.getPoint());
          }
          else if (evt.getClickCount() == 2)
          {
          {
            String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
            Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
 -          popupSort(selectedRow, type, colour, fr.getMinMax(), evt.getX(),
 -                  evt.getY());
 +          showPopupMenu(selectedRow, type, colour, evt.getPoint());
          }
        }
      });
  
      frame = new JInternalFrame();
      frame.setContentPane(this);
 -    if (Platform.isAMac())
 -    {
 -      Desktop.addInternalFrame(frame,
 -              MessageManager.getString("label.sequence_feature_settings"),
 -              600, 480);
 -    }
 -    else
 -    {
 -      Desktop.addInternalFrame(frame,
 -              MessageManager.getString("label.sequence_feature_settings"),
 -              600, 450);
 -    }
 +    Desktop.addInternalFrame(frame,
 +            MessageManager.getString("label.sequence_feature_settings"),
 +            600, Platform.isAMacAndNotJS() ? 480 : 450);
      frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
  
      frame.addInternalFrameListener(
      inConstruction = false;
    }
  
 -  protected void popupSort(final int rowSelected, final String type,
 -          final Object typeCol, final Map<String, float[][]> minmax, int x,
 -          int y)
 +      /**
 +       * Constructs and shows a popup menu of possible actions on the selected row and
 +       * feature type
 +       * 
 +       * @param rowSelected
 +       * @param type
 +       * @param typeCol
 +       * @param pt
 +       */
 +  protected void showPopupMenu(final int rowSelected, final String type,
 +          final Object typeCol, final Point pt)
    {
      JPopupMenu men = new JPopupMenu(MessageManager
              .formatMessage("label.settings_for_param", new String[]
              { type }));
 +    final FeatureColourI featureColour = (FeatureColourI) typeCol;
 +
 +    /*
 +     * menu option to select (or deselect) variable colour
 +     */
 +    final JCheckBoxMenuItem variableColourCB = new JCheckBoxMenuItem(
 +            MessageManager.getString("label.variable_colour"));
 +    variableColourCB.setSelected(!featureColour.isSimpleColour());
 +    men.add(variableColourCB);
 +    
 +    /*
 +     * checkbox action listener doubles up as listener to OK
 +     * from the variable colour / filters dialog
 +     */
 +    variableColourCB.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        if (e.getSource() == variableColourCB)
 +        {
 +          men.setVisible(true); // BH 2018 for JavaScript because this is a checkbox
 +          men.setVisible(false); // BH 2018 for JavaScript because this is a checkbox
 +          if (featureColour.isSimpleColour())
 +          {
 +            /*
 +             * toggle simple colour to variable colour - show dialog
 +             */
 +            FeatureTypeSettings fc = new FeatureTypeSettings(fr, type);
 +            fc.addActionListener(this);
 +          }
 +          else
 +          {
 +            /*
 +             * toggle variable to simple colour - show colour chooser
 +             */
 +            String title = MessageManager.formatMessage("label.select_colour_for", type);
 +            ColourChooserListener listener = new ColourChooserListener()
 +            {
 +              @Override
 +              public void colourSelected(Color c)
 +              {
 +                table.setValueAt(new FeatureColour(c), rowSelected,
 +                        COLOUR_COLUMN);
 +                table.validate();
 +                updateFeatureRenderer(
 +                        ((FeatureTableModel) table.getModel()).getData(),
 +                        false);
 +              }
 +            };
 +            JalviewColourChooser.showColourChooser(FeatureSettings.this, title,
 +              featureColour.getMaxColour(), listener);
 +          }
 +        }
 +        else    
 +        {
 +          if (e.getSource() instanceof FeatureTypeSettings)
 +          {
 +            /*
 +             * update after OK in feature colour dialog; the updated
 +             * colour will have already been set in the FeatureRenderer
 +             */
 +            FeatureColourI fci = fr.getFeatureColours().get(type);
 +            table.setValueAt(fci, rowSelected, COLOUR_COLUMN);
 +            // BH 2018 setting a table value does not invalidate it.
 +//                System.out.println("FeatureSettings is valied" + table.isValid());
 +//                table.validate();
 +          }
 +        }
 +      }
 +    });
 +    
 +    men.addSeparator();
 +
      JMenuItem scr = new JMenuItem(
              MessageManager.getString("label.sort_by_score"));
      men.add(scr);
 -    final FeatureSettings me = this;
      scr.addActionListener(new ActionListener()
      {
  
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        me.af.avc
 -                .sortAlignmentByFeatureScore(Arrays.asList(new String[]
 +        af.avc.sortAlignmentByFeatureScore(Arrays.asList(new String[]
                  { type }));
        }
 -
      });
      JMenuItem dens = new JMenuItem(
              MessageManager.getString("label.sort_by_density"));
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        me.af.avc
 -                .sortAlignmentByFeatureDensity(Arrays.asList(new String[]
 +        af.avc.sortAlignmentByFeatureDensity(Arrays.asList(new String[]
                  { type }));
        }
 -
      });
      men.add(dens);
  
      men.add(clearCols);
      men.add(hideCols);
      men.add(hideOtherCols);
 -    men.show(table, x, y);
 +    men.show(table, pt.x, pt.y);
    }
  
    @Override
      chooser.setDialogTitle(
              MessageManager.getString("label.load_feature_colours"));
      chooser.setToolTipText(MessageManager.getString("action.load"));
 -
 -    int value = chooser.showOpenDialog(this);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 -    {
 -      File file = chooser.getSelectedFile();
 -      load(file);
 -    }
 +    chooser.setResponseHandler(0, new Runnable()
 +    {
 +        @Override
 +        public void run() 
 +        {
 +          File file = chooser.getSelectedFile();
 +              load(file);
 +        }
 +      });
 +    chooser.showOpenDialog(this);
    }
  
    /**
      chooser.setDialogTitle(
              MessageManager.getString("label.save_feature_colours"));
      chooser.setToolTipText(MessageManager.getString("action.save"));
 -
 -    int value = chooser.showSaveDialog(this);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 -    {
 -      save(chooser.getSelectedFile());
 -    }
 +    int option = chooser.showSaveDialog(this);
 +      if (option == JalviewFileChooser.APPROVE_OPTION) 
 +      {
 +        File file = chooser.getSelectedFile();
 +        save(file);
 +      }
    }
  
    /**
     * @param data
     * @param visibleNew
     */
 -  private void updateFeatureRenderer(Object[][] data, boolean visibleNew)
 +  void updateFeatureRenderer(Object[][] data, boolean visibleNew)
    {
      FeatureSettingsBean[] rowData = getTableAsBeans(data);
  
      JButton help = new JButton(MessageManager.getString("action.help"));
      help.setFont(JvSwingUtils.getLabelFont());
      help.addActionListener(new ActionListener()
-     {
-       @Override
-       public void actionPerformed(ActionEvent e)
-       {
-         try
-         {
-           Help.showHelpWindow(HelpId.SequenceFeatureSettings);
-         } catch (HelpSetException e1)
-         {
-           e1.printStackTrace();
-         }
-       }
-     });
-     help.setFont(JvSwingUtils.getLabelFont());
-     help.setText(MessageManager.getString("action.help"));
-     help.addActionListener(new ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent e)
      renderGraduatedColor(comp, gcol, w, h);
    }
  
 -  class ColorEditor extends AbstractCellEditor
 +  @SuppressWarnings("serial")
 +class ColorEditor extends AbstractCellEditor
            implements TableCellEditor, ActionListener
    {
 -    FeatureSettings me;
 -
      FeatureColourI currentColor;
  
      FeatureTypeSettings chooser;
  
      JButton button;
  
 -    JColorChooser colorChooser;
 -
 -    JDialog dialog;
 -
      protected static final String EDIT = "edit";
  
      int rowSelected = 0;
  
 -    public ColorEditor(FeatureSettings me)
 +    public ColorEditor()
      {
 -      this.me = me;
        // Set up the editor (from the table's point of view),
        // which is a button.
        // This button brings up the color chooser dialog,
        button.setActionCommand(EDIT);
        button.addActionListener(this);
        button.setBorderPainted(false);
      }
  
      /**
 -     * Handles events from the editor button and from the dialog's OK button.
 +     * Handles events from the editor button, and from the colour/filters
 +     * dialog's OK button
       */
      @Override
      public void actionPerformed(ActionEvent e)
      {
 -      // todo test e.getSource() instead here
 -      if (EDIT.equals(e.getActionCommand()))
 +      if (button == e.getSource())
        {
 -        // The user has clicked the cell, so
 -        // bring up the dialog.
          if (currentColor.isSimpleColour())
          {
 -          // bring up simple color chooser
 -          button.setBackground(currentColor.getColour());
 -          colorChooser.setColor(currentColor.getColour());
 -          dialog.setVisible(true);
 +          /*
 +           * simple colour chooser
 +           */
 +          String ttl = MessageManager.formatMessage("label.select_colour_for", type);
 +          ColourChooserListener listener = new ColourChooserListener() 
 +          {
 +            @Override
 +            public void colourSelected(Color c)
 +            {
 +              currentColor = new FeatureColour(c);
 +              table.setValueAt(currentColor, rowSelected, COLOUR_COLUMN);
 +              fireEditingStopped();
 +            }
 +                      @Override
 +                      public void cancel() 
 +                      {
 +                fireEditingStopped();
 +                      }
 +          };
 +          JalviewColourChooser.showColourChooser(button,  ttl,  currentColor.getColour(), listener);
          }
          else
          {
 -          // bring up graduated chooser.
 -          chooser = new FeatureTypeSettings(me.fr, type);
 -          /**
 -           * @j2sNative
 +          /*
 +           * variable colour and filters dialog
             */
 +          chooser = new FeatureTypeSettings(fr, type);
 +          if (!Platform.isJS())
            {
              chooser.setRequestFocusEnabled(true);
              chooser.requestFocus();
            }
            chooser.addActionListener(this);
 -          // Make the renderer reappear.
            fireEditingStopped();
          }
        }
        else
        {
 -        if (currentColor.isSimpleColour())
 -        {
 -          /*
 -           * read off colour picked in colour chooser after OK pressed
 -           */
 -          currentColor = new FeatureColour(colorChooser.getColor());
 -          me.table.setValueAt(currentColor, rowSelected, COLOUR_COLUMN);
 -        }
 -        else
 +        /*
 +         * after OK in variable colour dialog, any changes to colour 
 +         * (or filters!) are already set in FeatureRenderer, so just
 +         * update table data without triggering updateFeatureRenderer
 +         */
 +        currentColor = fr.getFeatureColours().get(type);
 +        FeatureMatcherSetI currentFilter = fr.getFeatureFilter(type);
 +        if (currentFilter == null)
          {
 -          /*
 -           * after OK in variable colour dialog, any changes to colour 
 -           * (or filters!) are already set in FeatureRenderer, so just
 -           * update table data without triggering updateFeatureRenderer
 -           */
 -          currentColor = fr.getFeatureColours().get(type);
 -          FeatureMatcherSetI currentFilter = me.fr.getFeatureFilter(type);
 -          if (currentFilter == null)
 -          {
 -            currentFilter = new FeatureMatcherSet();
 -          }
 -          Object[] data = ((FeatureTableModel) table.getModel())
 -                  .getData()[rowSelected];
 -          data[COLOUR_COLUMN] = currentColor;
 -          data[FILTER_COLUMN] = currentFilter;
 +          currentFilter = new FeatureMatcherSet();
          }
 +        Object[] data = ((FeatureTableModel) table.getModel())
 +                .getData()[rowSelected];
 +        data[COLOUR_COLUMN] = currentColor;
 +        data[FILTER_COLUMN] = currentFilter;
          fireEditingStopped();
 -        me.table.validate();
 +        // SwingJS needs an explicit repaint() here, 
 +        // rather than relying upon no validation having
 +        // occurred since the stopEditing call was made.
 +        // Its laying out has not been stopped by the modal frame
 +        table.validate();
 +        table.repaint();
        }
      }
  
 -    // Implement the one CellEditor method that AbstractCellEditor doesn't.
 +    /**
 +     * Override allows access to this method from anonymous inner classes 
 +     */
 +    @Override
 +      protected void fireEditingStopped() 
 +    {
 +          super.fireEditingStopped();
 +      }
 +
 +      // Implement the one CellEditor method that AbstractCellEditor doesn't.
      @Override
      public Object getCellEditorValue()
      {
      {
        currentColor = (FeatureColourI) value;
        this.rowSelected = row;
 -      type = me.table.getValueAt(row, TYPE_COLUMN).toString();
 +      type = table.getValueAt(row, TYPE_COLUMN).toString();
        button.setOpaque(true);
 -      button.setBackground(me.getBackground());
 +      button.setBackground(FeatureSettings.this.getBackground());
        if (!currentColor.isSimpleColour())
        {
          JLabel btn = new JLabel();
     * as display text). On click in the cell, opens the Feature Display Settings
     * dialog at the Filters tab.
     */
 -  class FilterEditor extends AbstractCellEditor
 +  @SuppressWarnings("serial")
 +class FilterEditor extends AbstractCellEditor
            implements TableCellEditor, ActionListener
    {
 -    FeatureSettings me;
  
      FeatureMatcherSetI currentFilter;
  
  
      int rowSelected = 0;
  
 -    public FilterEditor(FeatureSettings me)
 +    public FilterEditor()
      {
 -      this.me = me;
        button = new JButton();
        button.setActionCommand(EDIT);
        button.addActionListener(this);
      {
        if (button == e.getSource())
        {
 -        FeatureTypeSettings chooser = new FeatureTypeSettings(me.fr, type);
 +        FeatureTypeSettings chooser = new FeatureTypeSettings(fr, type);
          chooser.addActionListener(this);
          chooser.setRequestFocusEnabled(true);
          chooser.requestFocus();
           * update table data without triggering updateFeatureRenderer
           */
          FeatureColourI currentColor = fr.getFeatureColours().get(type);
 -        currentFilter = me.fr.getFeatureFilter(type);
 +        currentFilter = fr.getFeatureFilter(type);
          if (currentFilter == null)
          {
            currentFilter = new FeatureMatcherSet();
          }
 +        
          Object[] data = ((FeatureTableModel) table.getModel())
                  .getData()[rowSelected];
          data[COLOUR_COLUMN] = currentColor;
          data[FILTER_COLUMN] = currentFilter;
          fireEditingStopped();
 -        me.table.validate();
 +        // SwingJS needs an explicit repaint() here, 
 +        // rather than relying upon no validation having
 +        // occurred since the stopEditing call was made.
 +        // Its laying out has not been stopped by the modal frame
 +        table.validate();
 +        table.repaint();
        }
      }
  
      {
        currentFilter = (FeatureMatcherSetI) value;
        this.rowSelected = row;
 -      type = me.table.getValueAt(row, TYPE_COLUMN).toString();
 +      type = table.getValueAt(row, TYPE_COLUMN).toString();
        button.setOpaque(true);
 -      button.setBackground(me.getBackground());
 +      button.setBackground(FeatureSettings.this.getBackground());
        button.setText(currentFilter.toString());
        button.setIcon(null);
        return button;
@@@ -2025,8 -1897,7 +1972,8 @@@ class FeatureIcon implements Ico
          g.fillRect(s1, 0, e1 - s1, height);
        }
        g.setColor(gcol.getMaxColour());
 -      g.fillRect(0, e1, width - e1, height);
 +//      g.fillRect(0, e1, width - e1, height);  // BH 2018
 +      g.fillRect(e1, 0, width - e1, height);
      }
    }
  }
@@@ -29,7 -29,6 +29,7 @@@ import jalview.datamodel.features.Featu
  import jalview.datamodel.features.FeatureMatcherI;
  import jalview.datamodel.features.FeatureMatcherSet;
  import jalview.datamodel.features.FeatureMatcherSetI;
 +import jalview.gui.JalviewColourChooser.ColourChooserListener;
  import jalview.schemes.FeatureColour;
  import jalview.util.ColorUtils;
  import jalview.util.MessageManager;
@@@ -57,15 -56,18 +57,15 @@@ import javax.swing.BoxLayout
  import javax.swing.ButtonGroup;
  import javax.swing.JButton;
  import javax.swing.JCheckBox;
 -import javax.swing.JColorChooser;
  import javax.swing.JComboBox;
  import javax.swing.JLabel;
  import javax.swing.JPanel;
  import javax.swing.JRadioButton;
  import javax.swing.JSlider;
  import javax.swing.JTextField;
  import javax.swing.border.LineBorder;
  import javax.swing.event.ChangeEvent;
  import javax.swing.event.ChangeListener;
 -import javax.swing.plaf.basic.BasicArrowButton;
  
  /**
   * A dialog where the user can configure colour scheme, and any filters, for one
@@@ -110,9 -112,9 +110,9 @@@ public class FeatureTypeSettings extend
    /*
     * the view panel to update when settings change
     */
 -  private final AlignmentViewPanel ap;
 +  final AlignmentViewPanel ap;
  
 -  private final String featureType;
 +  final String featureType;
  
    /*
     * the colour and filters to reset to on Cancel
     * set flag to true when setting values programmatically,
     * to avoid invocation of action handlers
     */
 -  private boolean adjusting = false;
 +  boolean adjusting = false;
  
    /*
     * minimum of the value range for graduated colour
    /*
     * scale factor for conversion between absolute min-max and slider
     */
 -  private float scaleFactor;
 +  float scaleFactor;
  
    /*
     * radio button group, to select what to colour by:
     * simple colour, by category (text), or graduated
     */
 -  private JRadioButton simpleColour = new JRadioButton();
 +  JRadioButton simpleColour = new JRadioButton();
  
 -  private JRadioButton byCategory = new JRadioButton();
 +  JRadioButton byCategory = new JRadioButton();
  
 -  private JRadioButton graduatedColour = new JRadioButton();
 +  JRadioButton graduatedColour = new JRadioButton();
  
 -  /**
 -   * colours and filters are shown in tabbed view or single content pane
 -   */
 -  JPanel coloursPanel, filtersPanel;
 +  JPanel coloursPanel;
 +  
 +  JPanel filtersPanel;
  
    JPanel singleColour = new JPanel();
  
 -  private JPanel minColour = new JPanel();
 +  JPanel minColour = new JPanel();
  
 -  private JPanel maxColour = new JPanel();
 +  JPanel maxColour = new JPanel();
  
    private JComboBox<String> threshold = new JComboBox<>();
  
 -  private JSlider slider = new JSlider();
 +  JSlider slider = new JSlider();
  
 -  private JTextField thresholdValue = new JTextField(20);
 +  JTextField thresholdValue = new JTextField(20);
  
    private JCheckBox thresholdIsMin = new JCheckBox();
  
    /*
     * filters for the currently selected feature type
     */
 -  private List<FeatureMatcherI> filters;
 +  List<FeatureMatcherI> filters;
  
    private JPanel chooseFiltersPanel;
  
        return;
      }
      
 -    updateColoursTab();
 +    updateColoursPanel();
      
 -    updateFiltersTab();
 +    updateFiltersPanel();
      
      adjusting = false;
      
    }
  
    /**
 -   * Configures the widgets on the Colours tab according to the current feature
 +   * Configures the widgets on the Colours panel according to the current feature
     * colour scheme
     */
 -  private void updateColoursTab()
 +  private void updateColoursPanel()
    {
      FeatureColourI fc = fr.getFeatureColours().get(featureType);
  
      };
  
      /*
 -     * first panel/tab: colour options
 +     * first panel: colour options
       */
      JPanel coloursPanel = initialiseColoursPanel();
      this.add(coloursPanel, BorderLayout.NORTH);
  
      /*
 -     * second panel/tab: filter options
 +     * second panel: filter options
       */
      JPanel filtersPanel = initialiseFiltersPanel();
      this.add(filtersPanel, BorderLayout.CENTER);
      graduatedColour = new JRadioButton(
              MessageManager.getString("label.by_range_of") + COLON);
      graduatedColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
 +    graduatedColour.setOpaque(false);
      graduatedColour.addItemListener(new ItemListener()
      {
        @Override
        {
          if (minColour.isEnabled())
          {
 -          showColourChooser(minColour, "label.select_colour_minimum_value");
 +          String ttl = MessageManager.getString("label.select_colour_minimum_value");
 +          showColourChooser(minColour, ttl);
          }
        }
      });
        {
          if (maxColour.isEnabled())
          {
 -          showColourChooser(maxColour, "label.select_colour_maximum_value");
 +          String ttl = MessageManager.getString("label.select_colour_maximum_value");
 +          showColourChooser(maxColour, ttl);
          }
        }
      });
      maxColour.setBorder(new LineBorder(Color.black));
  
      /*
-      * default max colour to last plain colour;
-      * make min colour a pale version of max colour
+      * if not set, default max colour to last plain colour,
+      * and make min colour a pale version of max colour
       */
-     FeatureColourI fc = fr.getFeatureColours().get(featureType);
-     Color bg = fc.getColour() == null ? Color.BLACK : fc.getColour();
-     maxColour.setBackground(bg);
-     minColour.setBackground(ColorUtils.bleachColour(bg, 0.9f));
+     Color max = originalColour.getMaxColour();
+     if (max == null)
+     {
+       max = originalColour.getColour();
+       minColour.setBackground(ColorUtils.bleachColour(max, 0.9f));
+     }
+     else
+     {
+       maxColour.setBackground(max);
+       minColour.setBackground(originalColour.getMinColour());
+     }
  
      noValueCombo = new JComboBox<>();
      noValueCombo.addItem(MessageManager.getString("label.no_colour"));
      simpleColour = new JRadioButton(
              MessageManager.getString("label.simple_colour"));
      simpleColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
 +    simpleColour.setOpaque(false);
      simpleColour.addItemListener(new ItemListener()
      {
        @Override
      singleColour.setFont(JvSwingUtils.getLabelFont());
      singleColour.setBorder(BorderFactory.createLineBorder(Color.black));
      singleColour.setPreferredSize(new Dimension(40, 20));
-     if (originalColour.isGraduatedColour())
-     {
-       singleColour.setBackground(originalColour.getMaxColour());
-       singleColour.setForeground(originalColour.getMaxColour());
-     }
-     else
-     {
+     // if (originalColour.isGraduatedColour())
+     // {
+     // singleColour.setBackground(originalColour.getMaxColour());
+     // singleColour.setForeground(originalColour.getMaxColour());
+     // }
+     // else
+     // {
        singleColour.setBackground(originalColour.getColour());
        singleColour.setForeground(originalColour.getColour());
-     }
+     // }
      singleColour.addMouseListener(new MouseAdapter()
      {
        @Override
        {
          if (simpleColour.isSelected())
          {
 -          showColourChooser(singleColour, "label.select_colour");
 +          String ttl = MessageManager.formatMessage("label.select_colour_for",  featureType);
 +          showColourChooser(singleColour, ttl);
          }
        }
      });
      byCategory = new JRadioButton(
              MessageManager.getString("label.by_text_of") + COLON);
      byCategory.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
 +    byCategory.setOpaque(false);
      byCategory.addItemListener(new ItemListener()
      {
        @Override
      return colourByPanel;
    }
  
 -  private void showColourChooser(JPanel colourPanel, String key)
 +  /**
 +   * Shows a colour chooser dialog, and if a selection is made, updates the
 +   * colour of the given panel
 +   * 
 +   * @param colourPanel
 +   *          the panel whose background colour is being picked
 +   * @param title
 +   */
 +  void showColourChooser(JPanel colourPanel, String title)
    {
 -    Color col = JColorChooser.showDialog(this,
 -            MessageManager.getString(key), colourPanel.getBackground());
 -    if (col != null)
 +    ColourChooserListener listener = new ColourChooserListener()
      {
 -      colourPanel.setBackground(col);
 -      colourPanel.setForeground(col);
 -    }
 -    colourPanel.repaint();
 -    colourChanged(true);
 +      @Override
 +      public void colourSelected(Color col)
 +      {
 +        colourPanel.setBackground(col);
 +        colourPanel.setForeground(col);
 +        colourPanel.repaint();
 +        colourChanged(true);
 +      }
 +    };
 +      JalviewColourChooser.showColourChooser(this, title, 
 +        colourPanel.getBackground(), listener);
    }
  
    /**
      fr.setColour(featureType, acg);
      ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
  
 -    updateColoursTab();
 +    updateColoursPanel();
    }
  
    /**
    private FeatureColourI makeColourFromInputs()
    {
      /*
-      * easiest case - a single colour
+      * min-max range is to (or from) threshold value if 
+      * 'threshold is min/max' is selected 
       */
-     if (simpleColour.isSelected())
-     {
-       return new FeatureColour(singleColour.getBackground());
-     }
-     /*
-      * next easiest case - colour by Label, or attribute text
-      */
-     if (byCategory.isSelected())
-     {
-       Color c = singleColour.getBackground();
-       FeatureColourI fc = new FeatureColour(c);
-       fc.setColourByLabel(true);
-       String byWhat = (String) colourByTextCombo.getSelectedItem();
-       if (!LABEL_18N.equals(byWhat))
-       {
-         fc.setAttributeName(
-                 FeatureMatcher.fromAttributeDisplayName(byWhat));
-       }
-       return fc;
-     }
-     /*
-      * remaining case - graduated colour by score, or attribute value
-      */
-     Color noColour = null;
-     if (noValueCombo.getSelectedIndex() == MIN_COLOUR_OPTION)
-     {
-       noColour = minColour.getBackground();
-     }
-     else if (noValueCombo.getSelectedIndex() == MAX_COLOUR_OPTION)
-     {
-       noColour = maxColour.getBackground();
-     }
  
      float thresh = 0f;
      try
      {
        // invalid inputs are already handled on entry
      }
-     /*
-      * min-max range is to (or from) threshold value if 
-      * 'threshold is min/max' is selected 
-      */
      float minValue = min;
      float maxValue = max;
 -    final int thresholdOption = threshold.getSelectedIndex();
 +    int thresholdOption = threshold.getSelectedIndex();
      if (thresholdIsMin.isSelected()
              && thresholdOption == ABOVE_THRESHOLD_OPTION)
      {
      {
        maxValue = thresh;
      }
+     Color noColour = null;
+     if (noValueCombo.getSelectedIndex() == MIN_COLOUR_OPTION)
+     {
+       noColour = minColour.getBackground();
+     }
+     else if (noValueCombo.getSelectedIndex() == MAX_COLOUR_OPTION)
+     {
+       noColour = maxColour.getBackground();
+     }
+     /*
+      * construct a colour that 'remembers' all the options, including
+      * those not currently selected
+      */
+     FeatureColourI fc = new FeatureColour(singleColour.getBackground(),
+             minColour.getBackground(), maxColour.getBackground(), noColour,
+             minValue, maxValue);
+     /*
+      * easiest case - a single colour
+      */
+     if (simpleColour.isSelected())
+     {
+       ((FeatureColour) fc).setGraduatedColour(false);
+       return fc;
+     }
  
      /*
-      * make the graduated colour
+      * next easiest case - colour by Label, or attribute text
       */
-     FeatureColourI fc = new FeatureColour(minColour.getBackground(),
-             maxColour.getBackground(), noColour, minValue, maxValue);
+     if (byCategory.isSelected())
+     {
+       fc.setColourByLabel(true);
+       String byWhat = (String) colourByTextCombo.getSelectedItem();
+       if (!LABEL_18N.equals(byWhat))
+       {
+         fc.setAttributeName(
+                 FeatureMatcher.fromAttributeDisplayName(byWhat));
+       }
+       return fc;
+     }
  
      /*
+      * remaining case - graduated colour by score, or attribute value;
       * set attribute to colour by if selected
       */
      String byWhat = (String) colourByRangeCombo.getSelectedItem();
      andOrPanel.setBackground(Color.white);
      andFilters = new JRadioButton(MessageManager.getString("label.and"));
      orFilters = new JRadioButton(MessageManager.getString("label.or"));
 +    andFilters.setOpaque(false);
 +    orFilters.setOpaque(false);
      ActionListener actionListener = new ActionListener()
      {
        @Override
     * for adding a condition. This should be called after a filter has been
     * removed, added or amended.
     */
 -  private void updateFiltersTab()
 +  private void updateFiltersPanel()
    {
      /*
       * clear the panel and list of filter conditions
        {
          orFilters.setSelected(true);
        }
 -      featureFilters.getMatchers().forEach(matcher -> filters.add(matcher));
 +      // avoid use of lambda expression to keep SwingJS happy
 +      // featureFilters.getMatchers().forEach(item -> filters.add(item));
 +      for (FeatureMatcherI matcher : featureFilters.getMatchers())
 +      {
 +        filters.add(matcher);
 +      }
      }
  
      /*
       * drop-down choice of attribute, with description as a tooltip 
       * if we can obtain it
       */
 -    final JComboBox<String> attCombo = populateAttributesDropdown(attNames,
 -            true, true);
 +    JComboBox<String> attCombo = populateAttributesDropdown(attNames, true,
 +            true);
      String filterBy = setSelectedAttribute(attCombo, filter);
  
      JComboBox<Condition> condCombo = new JComboBox<>();
      if (!patternField.isEnabled()
              || (pattern != null && pattern.trim().length() > 0))
      {
 -      // todo: gif for button drawing '-' or 'x'
 -      JButton removeCondition = new BasicArrowButton(SwingConstants.WEST);
 +      JButton removeCondition = new JButton("\u2717"); // Dingbats cursive x
 +      removeCondition.setPreferredSize(new Dimension(23, 17));
        removeCondition
                .setToolTipText(MessageManager.getString("label.delete_row"));
        removeCondition.addActionListener(new ActionListener()
     * @param condCombo
     * @param patternField
     */
 -  private void populateConditions(String attName, Condition cond,
 +  void populateConditions(String attName, Condition cond,
            JComboBox<Condition> condCombo, JTextField patternField)
    {
      Datatype type = FeatureAttributes.getInstance().getDatatype(featureType,
      fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined);
      ap.paintAlignment(true, true);
  
 -    updateFiltersTab();
 +    updateFiltersPanel();
    }
  }
@@@ -20,6 -20,8 +20,8 @@@
   */
  package jalview.gui;
  
+ import jalview.api.AlignViewportI;
+ import jalview.api.FinderI;
  import jalview.datamodel.SearchResultMatchI;
  import jalview.datamodel.SearchResultsI;
  import jalview.datamodel.SequenceFeature;
@@@ -28,11 -30,13 +30,12 @@@ import jalview.jbgui.GFinder
  import jalview.util.MessageManager;
  import jalview.viewmodel.AlignmentViewport;
  
  import java.awt.event.ActionEvent;
  import java.awt.event.KeyEvent;
  import java.util.ArrayList;
+ import java.util.HashMap;
  import java.util.List;
- import java.util.Vector;
+ import java.util.Map;
  import java.util.regex.Pattern;
  import java.util.regex.PatternSyntaxException;
  
@@@ -41,6 -45,7 +44,7 @@@ import javax.swing.JComponent
  import javax.swing.JInternalFrame;
  import javax.swing.JLayeredPane;
  import javax.swing.KeyStroke;
+ import javax.swing.event.InternalFrameAdapter;
  import javax.swing.event.InternalFrameEvent;
  
  /**
   */
  public class Finder extends GFinder
  {
-   private static final int MY_HEIGHT = 120;
+   private static final int MIN_WIDTH = 350;
  
-   private static final int MY_WIDTH = 400;
+   private static final int MIN_HEIGHT = 120;
  
-   AlignmentViewport av;
+   private static final int MY_HEIGHT = 120;
  
-   AlignmentPanel ap;
+   private static final int MY_WIDTH = 400;
  
-   private static final int MIN_WIDTH = 350;
+   private AlignViewportI av;
  
-   private static final int MIN_HEIGHT = 120;
+   private AlignmentPanel ap;
  
-   JInternalFrame frame;
+   private JInternalFrame frame;
  
-   int seqIndex = 0;
+   /*
+    * Finder agent per viewport searched
+    */
+   private Map<AlignViewportI, FinderI> finders;
  
-   int resIndex = -1;
+   private SearchResultsI searchResults;
  
-   SearchResultsI searchResults;
+   /*
+    * true if we only search a given alignment view
+    */
+   private boolean focusfixed;
  
    /**
-    * Creates a new Finder object with no associated viewport or panel.
+    * Creates a new Finder object with no associated viewport or panel. Each Find
+    * or Find Next action will act on whichever viewport has focus at the time.
     */
    public Finder()
    {
      this(null, null);
-     focusfixed = false;
    }
  
    /**
    {
      av = viewport;
      ap = alignPanel;
-     focusfixed = true;
+     finders = new HashMap<>();
+     focusfixed = viewport != null;
      frame = new JInternalFrame();
      frame.setContentPane(this);
      frame.setLayer(JLayeredPane.PALETTE_LAYER);
      frame.addInternalFrameListener(
-             new javax.swing.event.InternalFrameAdapter()
+             new InternalFrameAdapter()
              {
                @Override
                public void internalFrameClosing(InternalFrameEvent e)
              });
      addEscapeHandler();
      Desktop.addInternalFrame(frame, MessageManager.getString("label.find"),
 -            MY_WIDTH, MY_HEIGHT);
 -    frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
 -    searchBox.requestFocus();
 +            true, MY_WIDTH, MY_HEIGHT, true, true);
 +    searchBox.getComponent().requestFocus();
    }
  
    /**
    }
  
    /**
-    * Performs the 'Find Next' action.
-    * 
-    * @param e
+    * Performs the 'Find Next' action on the alignment panel with focus
     */
    @Override
-   public void findNext_actionPerformed(ActionEvent e)
+   public void findNext_actionPerformed()
    {
      if (getFocusedViewport())
      {
    }
  
    /**
-    * Performs the 'Find All' action.
-    * 
-    * @param e
+    * Performs the 'Find All' action on the alignment panel with focus
     */
    @Override
-   public void findAll_actionPerformed(ActionEvent e)
+   public void findAll_actionPerformed()
    {
      if (getFocusedViewport())
      {
-       resIndex = -1;
-       seqIndex = 0;
        doSearch(true);
      }
    }
  
    /**
-    * do we only search a given alignment view ?
-    */
-   private boolean focusfixed;
-   /**
     * if !focusfixed and not in a desktop environment, checks that av and ap are
     * valid. Otherwise, gets the topmost alignment window and sets av and ap
     * accordingly
      for (int f = 0; f < frames.length; f++)
      {
        JInternalFrame alignFrame = frames[f];
-       if (alignFrame != null && alignFrame instanceof AlignFrame)
+       if (alignFrame != null && alignFrame instanceof AlignFrame
+               && !alignFrame.isIcon())
        {
          av = ((AlignFrame) alignFrame).viewport;
          ap = ((AlignFrame) alignFrame).alignPanel;
  
    /**
     * Opens a dialog that allows the user to create sequence features for the
 -   * find match results.
 +   * find match results
     */
    @Override
    public void createFeatures_actionPerformed()
    {
 +    if (searchResults.isEmpty())
 +    {
 +      return; // shouldn't happen
 +    }
-     List<SequenceI> seqs = new ArrayList<SequenceI>();
-     List<SequenceFeature> features = new ArrayList<SequenceFeature>();
+     List<SequenceI> seqs = new ArrayList<>();
+     List<SequenceFeature> features = new ArrayList<>();
  
 -    String searchString = searchBox.getEditor().getItem().toString().trim();
 +    String searchString = searchBox.getUserInput();
      String desc = "Search Results";
  
      /*
      for (SearchResultMatchI match : searchResults.getResults())
      {
        seqs.add(match.getSequence().getDatasetSequence());
 -      features.add(new SequenceFeature(searchString, desc,
 -              match
 -              .getStart(), match.getEnd(), desc));
 +      features.add(new SequenceFeature(searchString, desc, match.getStart(),
 +              match.getEnd(), desc));
      }
  
 -    if (ap.getSeqPanel().seqCanvas.getFeatureRenderer().amendFeatures(seqs,
 -            features, true, ap))
 -    {
 -      /*
 -       * ensure feature display is turned on to show the new features,
 -       * and remove them as highlighted regions
 -       */
 -      ap.alignFrame.showSeqFeatures.setSelected(true);
 -      av.setShowSequenceFeatures(true);
 -      ap.highlightSearchResults(null);
 -    }
 +    new FeatureEditor(ap, seqs, features, true).showDialog();
    }
  
    /**
    {
      createFeatures.setEnabled(false);
  
 -    String searchString = searchBox.getUserInput().trim();
 +    String searchString = searchBox.getUserInput();
  
      if (isInvalidSearchString(searchString))
      {
      // other stuff
      // TODO: add switches to control what is searched - sequences, IDS,
      // descriptions, features
-     jalview.analysis.Finder finder = new jalview.analysis.Finder(
-             av.getAlignment(), av.getSelectionGroup(), seqIndex, resIndex);
-     finder.setCaseSensitive(caseSensitive.isSelected());
-     finder.setIncludeDescription(searchDescription.isSelected());
-     finder.setFindAll(doFindAll);
-     finder.find(searchString); // returns true if anything was actually found
-     seqIndex = finder.getSeqIndex();
-     resIndex = finder.getResIndex();
+     FinderI finder = finders.get(av);
+     if (finder == null)
+     {
+       /*
+        * first time we've searched this viewport
+        */
+       finder = new jalview.analysis.Finder(av);
+       finders.put(av, finder);
+     }
  
-     searchResults = finder.getSearchResults(); // find(regex,
-     // caseSensitive.isSelected(), )
-     Vector<SequenceI> idMatch = finder.getIdMatch();
-     boolean haveResults = false;
-     // set or reset the GUI
-     if ((idMatch.size() > 0))
+     boolean isCaseSensitive = caseSensitive.isSelected();
+     boolean doSearchDescription = searchDescription.isSelected();
+     if (doFindAll)
      {
-       haveResults = true;
-       ap.getIdPanel().highlightSearchResults(idMatch);
+       finder.findAll(searchString, isCaseSensitive, doSearchDescription);
      }
      else
      {
-       ap.getIdPanel().highlightSearchResults(null);
+       finder.findNext(searchString, isCaseSensitive, doSearchDescription);
      }
  
-     if (searchResults.getSize() > 0)
+     searchResults = finder.getSearchResults();
+     List<SequenceI> idMatch = finder.getIdMatches();
+     ap.getIdPanel().highlightSearchResults(idMatch);
+     if (searchResults.isEmpty())
      {
-       haveResults = true;
-       createFeatures.setEnabled(true);
+       searchResults = null;
      }
      else
      {
-       searchResults = null;
+       createFeatures.setEnabled(true);
      }
  
 +    searchBox.updateCache();
 +
      ap.highlightSearchResults(searchResults);
      // TODO: add enablers for 'SelectSequences' or 'SelectColumns' or
      // 'SelectRegion' selection
-     if (!haveResults)
+     if (idMatch.isEmpty() && searchResults == null)
      {
-       resIndex = -1;
-       seqIndex = 0;
        JvOptionPane.showInternalMessageDialog(this,
                MessageManager.getString("label.finished_searching"), null,
 -              JvOptionPane.INFORMATION_MESSAGE);
 +              JvOptionPane.PLAIN_MESSAGE);
      }
      else
      {
            message += searchResults.getSize()
                    + " subsequence matches found.";
          }
-         resIndex = -1;
-         seqIndex = 0;
          JvOptionPane.showInternalMessageDialog(this, message, null,
 -                JvOptionPane.INFORMATION_MESSAGE);
 +                JvOptionPane.PLAIN_MESSAGE);
        }
      }
 -    searchBox.updateCache();
    }
  
    /**
@@@ -55,7 -55,7 +55,7 @@@ public class IdCanvas extends JPanel im
  
    BufferedImage image;
  
 -  Graphics2D gg;
 +//  Graphics2D gg;
  
    int imgHeight = 0;
  
      /*
       * for now, not attempting fast paint of wrapped ids...
       */
 -    if (gg == null || av.getWrapAlignment())
 +    if (image == null || av.getWrapAlignment())
      {
        repaint();
  
  
      ViewportRanges ranges = av.getRanges();
  
 +    Graphics2D gg = image.createGraphics();
      gg.copyArea(0, 0, getWidth(), imgHeight, 0,
              -vertical * av.getCharHeight());
  
  
      gg.translate(0, -transY);
  
 +    gg.dispose();
 +    
      fastPaint = true;
  
      // Call repaint on alignment panel so that repaints from other alignment
    @Override
    public void paintComponent(Graphics g)
    {
 -    super.paintComponent(g);
 +    //super.paintComponent(g);  // BH 2019
  
      g.setColor(Color.white);
      g.fillRect(0, 0, getWidth(), getHeight());
                  BufferedImage.TYPE_INT_RGB);
      }
      
 -    gg = (Graphics2D) image.getGraphics();
 +    Graphics2D gg = image.createGraphics();
      
      // Fill in the background
      gg.setColor(Color.white);
      gg.fillRect(0, 0, getWidth(), imgHeight);
      
      drawIds(gg, av, av.getRanges().getStartSeq(), av.getRanges().getEndSeq(), searchResults);
 +
 +    gg.dispose();
      
      g.drawImage(image, 0, 0, this);
    }
      int alignmentWidth = alignViewport.getAlignment().getWidth();
      final int alheight = alignViewport.getAlignment().getHeight();
  
-     if (alignViewport.hasHiddenColumns())
-     {
-       alignmentWidth = alignViewport.getAlignment().getHiddenColumns()
-               .absoluteToVisibleColumn(alignmentWidth) - 1;
-     }
      int annotationHeight = 0;
  
      AnnotationLabels labels = null;
   */
  package jalview.gui;
  
+ import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.Sequence;
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
+ import jalview.gui.SeqPanel.MousePos;
  import jalview.io.SequenceAnnotationReport;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
@@@ -31,8 -33,6 +33,8 @@@ import jalview.viewmodel.AlignmentViewp
  import jalview.viewmodel.ViewportRanges;
  
  import java.awt.BorderLayout;
 +import java.awt.event.ActionEvent;
 +import java.awt.event.ActionListener;
  import java.awt.event.MouseEvent;
  import java.awt.event.MouseListener;
  import java.awt.event.MouseMotionListener;
@@@ -41,8 -41,8 +43,9 @@@ import java.awt.event.MouseWheelListene
  import java.util.List;
  
  import javax.swing.JPanel;
+ import javax.swing.JPopupMenu;
  import javax.swing.SwingUtilities;
 +import javax.swing.Timer;
  import javax.swing.ToolTipManager;
  
  /**
@@@ -96,25 -96,46 +99,46 @@@ public class IdPanel extends JPane
    }
  
    /**
-    * Respond to mouse movement by constructing tooltip text for the sequence id
-    * under the mouse.
+    * Responds to mouse movement by setting tooltip text for the sequence id
+    * under the mouse (or possibly annotation label, when in wrapped mode)
     * 
     * @param e
-    *          DOCUMENT ME!
     */
    @Override
    public void mouseMoved(MouseEvent e)
    {
      SeqPanel sp = alignPanel.getSeqPanel();
-     int seq = Math.max(0, sp.findSeq(e));
-     if (seq > -1 && seq < av.getAlignment().getHeight())
+     MousePos pos = sp.findMousePosition(e);
+     if (pos.isOverAnnotation())
+     {
+       /*
+        * mouse is over an annotation label in wrapped mode
+        */
+       AlignmentAnnotation[] anns = av.getAlignment()
+               .getAlignmentAnnotation();
+       AlignmentAnnotation annotation = anns[pos.annotationIndex];
+       setToolTipText(AnnotationLabels.getTooltip(annotation));
+       alignPanel.alignFrame.setStatus(
+               AnnotationLabels.getStatusMessage(annotation, anns));
+     }
+     else
      {
-       SequenceI sequence = av.getAlignment().getSequenceAt(seq);
-       StringBuilder tip = new StringBuilder(64);
-       seqAnnotReport.createTooltipAnnotationReport(tip, sequence,
-               av.isShowDBRefs(), av.isShowNPFeats(), sp.seqCanvas.fr);
-       setToolTipText(JvSwingUtils.wrapTooltip(true,
-               sequence.getDisplayId(true) + " " + tip.toString()));
+       int seq = Math.max(0, pos.seqIndex);
+       if (seq < av.getAlignment().getHeight())
+       {
+         SequenceI sequence = av.getAlignment().getSequenceAt(seq);
+         StringBuilder tip = new StringBuilder(64);
+         tip.append(sequence.getDisplayId(true)).append(" ");
+         seqAnnotReport.createTooltipAnnotationReport(tip, sequence,
+                 av.isShowDBRefs(), av.isShowNPFeats(), sp.seqCanvas.fr);
+         setToolTipText(JvSwingUtils.wrapTooltip(true, tip.toString()));
+         StringBuilder text = new StringBuilder();
+         text.append("Sequence ").append(String.valueOf(seq + 1))
+                 .append(" ID: ")
+                 .append(sequence.getName());
+         alignPanel.alignFrame.setStatus(text.toString());
+       }
      }
    }
  
    {
      mouseDragging = true;
  
-     int seq = Math.max(0, alignPanel.getSeqPanel().findSeq(e));
+     MousePos pos = alignPanel.getSeqPanel().findMousePosition(e);
+     if (pos.isOverAnnotation())
+     {
+       // mouse is over annotation label in wrapped mode
+       return;
+     }
+     int seq = Math.max(0, pos.seqIndex);
  
      if (seq < lastid)
      {
        return;
      }
  
-     int seq = alignPanel.getSeqPanel().findSeq(e);
+     MousePos pos = alignPanel.getSeqPanel().findMousePosition(e);
+     int seq = pos.seqIndex;
+     if (pos.isOverAnnotation() || seq < 0)
+     {
+       return;
+     }
      String id = av.getAlignment().getSequenceAt(seq).getName();
      String url = Preferences.sequenceUrlLinks.getPrimaryUrl(id);
  
    }
  
    /**
 -   * DOCUMENT ME!
 +   * On (re-)entering the panel, stop any scrolling
     * 
     * @param e
 -   *          DOCUMENT ME!
     */
    @Override
    public void mouseEntered(MouseEvent e)
    {
 +    stopScrolling();
 +  }
 +
 +  /**
 +   * Interrupts the scroll thread if one is running
 +   */
 +  void stopScrolling()
 +  {
      if (scrollThread != null)
      {
        scrollThread.stopScrolling();
        return;
      }
  
 -    if (mouseDragging && (e.getY() < 0)
 -            && (av.getRanges().getStartSeq() > 0))
 +    if (mouseDragging)
      {
 -      scrollThread = new ScrollThread(true);
 +      /*
 +       * on mouse drag above or below the panel, start 
 +       * scrolling if there are more sequences to show
 +       */
 +      ViewportRanges ranges = av.getRanges();
 +      if (e.getY() < 0 && ranges.getStartSeq() > 0)
 +      {
 +        startScrolling(true);
 +      }
 +      else if (e.getY() >= getHeight()
 +              && ranges.getEndSeq() <= av.getAlignment().getHeight())
 +      {
 +        startScrolling(false);
 +      }
      }
 +  }
  
 -    if (mouseDragging && (e.getY() >= getHeight())
 -            && (av.getAlignment().getHeight() > av.getRanges().getEndSeq()))
 +  /**
 +   * Starts scrolling either up or down
 +   * 
 +   * @param up
 +   */
 +  void startScrolling(boolean up)
 +  {
 +    scrollThread = new ScrollThread(up);
 +    if (!Platform.isJS())
      {
 -      scrollThread = new ScrollThread(false);
 +      /*
 +       * Java - run in a new thread
 +       */
 +      scrollThread.start();
 +    }
 +    else
 +    {
 +      /*
 +       * for JalviewJS using Swing Timer
 +       */
 +      Timer t = new Timer(20, new ActionListener()
 +      {
 +        @Override
 +        public void actionPerformed(ActionEvent e)
 +        {
 +          if (scrollThread != null)
 +          {
 +            // if (!scrollOnce() {t.stop();}) gives compiler error :-(
 +            scrollThread.scrollOnce();
 +          }
 +        }
 +      });
 +      t.addActionListener(new ActionListener()
 +      {
 +        @Override
 +        public void actionPerformed(ActionEvent e)
 +        {
 +          if (scrollThread == null)
 +          {
 +            // finished and nulled itself
 +            t.stop();
 +          }
 +        }
 +      });
 +      t.start();
      }
    }
  
        return;
      }
  
+     MousePos pos = alignPanel.getSeqPanel().findMousePosition(e);
+     
      if (e.isPopupTrigger()) // Mac reports this in mousePressed
      {
-       showPopupMenu(e);
+       showPopupMenu(e, pos);
        return;
      }
  
       * (where isPopupTrigger() will answer true)
       * NB isRightMouseButton is also true for Cmd-click on Mac
       */
 -    if (SwingUtilities.isRightMouseButton(e) && !Platform.isAMac())
 +    if (Platform.isWinRightButton(e))
      {
        return;
      }
        av.getSelectionGroup().setEndRes(av.getAlignment().getWidth() - 1);
      }
  
-     int seq = alignPanel.getSeqPanel().findSeq(e);
      if (e.isShiftDown() && (lastid != -1))
      {
-       selectSeqs(lastid, seq);
+       selectSeqs(lastid, pos.seqIndex);
      }
      else
      {
-       selectSeq(seq);
+       selectSeq(pos.seqIndex);
      }
  
      av.isSelectionGroupChanged(true);
     * 
     * @param e
     */
-   void showPopupMenu(MouseEvent e)
+   void showPopupMenu(MouseEvent e, MousePos pos)
    {
-     int seq2 = alignPanel.getSeqPanel().findSeq(e);
-     Sequence sq = (Sequence) av.getAlignment().getSequenceAt(seq2);
+     if (pos.isOverAnnotation())
+     {
+       showAnnotationMenu(e, pos);
+       return;
+     }
+     Sequence sq = (Sequence) av.getAlignment().getSequenceAt(pos.seqIndex);
  
      /*
       *  build a new links menu based on the current links
       *  and any non-positional features
       */
+     List<SequenceFeature> features = null;
+     if (sq != null)
+     {
      List<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
-     List<SequenceFeature> features = sq.getFeatures().getNonPositionalFeatures();
+       features = sq.getFeatures().getNonPositionalFeatures();
      for (SequenceFeature sf : features)
      {
        if (sf.links != null)
        {
-         for (String link : sf.links)
-         {
-           nlinks.add(link);
-         }
+         nlinks.addAll(sf.links);
        }
      }
+     }
  
      PopupMenu pop = new PopupMenu(alignPanel, sq, features,
              Preferences.getGroupURLLinks());
    }
  
    /**
+    * On right mouse click on a Consensus annotation label, shows a limited popup
+    * menu, with options to configure the consensus calculation and rendering.
+    * 
+    * @param e
+    * @param pos
+    * @see AnnotationLabels#showPopupMenu(MouseEvent)
+    */
+   void showAnnotationMenu(MouseEvent e, MousePos pos)
+   {
+     if (pos.annotationIndex == -1)
+     {
+       return;
+     }
+     AlignmentAnnotation[] anns = this.av.getAlignment()
+             .getAlignmentAnnotation();
+     if (anns == null || pos.annotationIndex >= anns.length)
+     {
+       return;
+     }
+     AlignmentAnnotation ann = anns[pos.annotationIndex];
+     if (!ann.label.contains("Consensus"))
+     {
+       return;
+     }
+     JPopupMenu pop = new JPopupMenu(
+             MessageManager.getString("label.annotations"));
+     AnnotationLabels.addConsensusMenuOptions(this.alignPanel, ann, pop);
+     pop.show(this, e.getX(), e.getY());
+   }
+   /**
     * Toggle whether the sequence is part of the current selection group.
     * 
     * @param seq
      lastid = seq;
  
      SequenceI pickedSeq = av.getAlignment().getSequenceAt(seq);
-     av.getSelectionGroup().addOrRemove(pickedSeq, true);
+     av.getSelectionGroup().addOrRemove(pickedSeq, false);
    }
  
    /**
      for (int i = start; i <= end; i++)
      {
        av.getSelectionGroup().addSequence(av.getAlignment().getSequenceAt(i),
-               i == end);
+               false);
      }
    }
  
    @Override
    public void mouseReleased(MouseEvent e)
    {
-     stopScrolling();
+     if (scrollThread != null)
+     {
+       scrollThread.stopScrolling();
+     }
+     MousePos pos = alignPanel.getSeqPanel().findMousePosition(e);
  
      mouseDragging = false;
      PaintRefresher.Refresh(this, av.getSequenceSetId());
  
      if (e.isPopupTrigger()) // Windows reports this in mouseReleased
      {
-       showPopupMenu(e);
+       showPopupMenu(e, pos);
      }
    }
  
    {
      getIdCanvas().setHighlighted(list);
  
-     if (list == null)
+     if (list == null || list.isEmpty())
      {
        return;
      }
       * Scrolls the alignment either up or down, one row at a time, adding newly
       * visible sequences to the current selection. Speed is limited to a maximum
       * of ten rows per second. The thread exits when the end of the alignment is
 -     * reached or a flag is set to stop it.
 +     * reached or a flag is set to stop it by a call to stopScrolling.
       */
      @Override
      public void run()
  
        while (running)
        {
 -        ViewportRanges ranges = IdPanel.this.av.getRanges();
 -        if (ranges.scrollUp(up))
 -        {
 -          int toSeq = up ? ranges.getStartSeq() : ranges.getEndSeq();
 -          int fromSeq = toSeq < lastid ? lastid - 1 : lastid + 1;
 -          IdPanel.this.selectSeqs(fromSeq, toSeq);
 -
 -          lastid = toSeq;
 -        }
 -        else
 -        {
 -          /*
 -           * scroll did nothing - reached limit of visible alignment
 -           */
 -          running = false;
 -        }
 -
 -        alignPanel.paintAlignment(false, false);
 -
 +        running = scrollOnce();
          try
          {
            Thread.sleep(100);
          {
          }
        }
 +      IdPanel.this.scrollThread = null;
 +    }
 +
 +    /**
 +     * Scrolls one row up or down. Answers true if a scroll could be done, false
 +     * if not (top or bottom of alignment reached).
 +     */
 +    boolean scrollOnce()
 +    {
 +      ViewportRanges ranges = IdPanel.this.av.getRanges();
 +      if (ranges.scrollUp(up))
 +      {
 +        int toSeq = up ? ranges.getStartSeq() : ranges.getEndSeq();
 +        int fromSeq = toSeq < lastid ? lastid - 1 : lastid + 1;
 +        IdPanel.this.selectSeqs(fromSeq, toSeq);
 +        lastid = toSeq;
 +        alignPanel.paintAlignment(false, false);
 +        return true;
 +      }
 +
 +      return false;
      }
    }
  }
@@@ -55,25 -55,24 +55,25 @@@ import javax.swing.SwingUtilities
   * @author $author$
   * @version $Revision$
   */
 +@SuppressWarnings("serial")
  public class OverviewPanel extends JPanel
          implements Runnable, ViewportListenerI
  {
 -  private OverviewDimensions od;
 +  protected OverviewDimensions od;
  
    private OverviewCanvas oviewCanvas;
  
 -  private AlignViewport av;
 +  protected AlignViewport av;
  
    private AlignmentPanel ap;
  
 -  private JCheckBoxMenuItem displayToggle;
 +  protected JCheckBoxMenuItem displayToggle;
  
 -  private boolean showHidden = true;
 +  protected boolean showHidden = true;
  
 -  private boolean draggingBox = false;
 +  protected boolean draggingBox = false;
  
 -  private ProgressPanel progressPanel;
 +  protected ProgressPanel progressPanel;
  
    /**
     * Creates a new OverviewPanel object.
  
      // without this the overview window does not size to fit the overview canvas
      setPreferredSize(new Dimension(od.getWidth(), od.getHeight()));
+     
      addComponentListener(new ComponentAdapter()
      {
        @Override
        @Override
        public void mouseMoved(MouseEvent evt)
        {
-         if (!draggingBox)
-         // don't bother changing the cursor if we're dragging the box
-         // as we can't have moved inside or out of the box in that case
+         if (od.isPositionInBox(evt.getX(), evt.getY()))
          {
-           if (od.isPositionInBox(evt.getX(), evt.getY()))
-           {
-             // display drag cursor at mouse position
-             setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
-           }
-           else
-           {
-             // reset cursor
-             setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
-           }
+           /*
+            * using HAND_CURSOR rather than DRAG_CURSOR 
+            * as the latter is not supported on Mac
+            */
+           getParent().setCursor(
+                   Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+         }
+         else
+         {
+           // reset cursor
+           getParent().setCursor(
+                   Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
          }
        }
      });
  
      addMouseListener(new MouseAdapter()
        @Override
        public void mousePressed(MouseEvent evt)
        {
 -        if (SwingUtilities.isRightMouseButton(evt))
 -        {
 -          if (!Platform.isAMac())
 -          {
 -            showPopupMenu(evt);
 -          }
 +        
 +      if (Platform.isWinRightButton(evt)) {
 +              showPopupMenu(evt);
 +              return;
 +      }
 +        if (SwingUtilities.isRightMouseButton(evt)) {
 +              return;
          }
 -        else
 -        {
            // don't do anything if the mouse press is in the overview's box
            // (wait to see if it's a drag instead)
            // otherwise update the viewport
              od.updateViewportFromMouse(evt.getX(), evt.getY(),
                      av.getAlignment().getHiddenSequences(),
                      av.getAlignment().getHiddenColumns());
+             getParent().setCursor(
+                     Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
            }
            else
            {
                      av.getAlignment().getHiddenSequences(),
                      av.getAlignment().getHiddenColumns());
            }
 -        }
        }
  
        @Override
        }
  
      });
 +
 +    /*
 +     * Javascript does not call componentResized on initial display,
 +     * so do the update here
 +     */
 +    boolean doUpdate =  /** @j2sNative true || */ false;
 +    if (doUpdate)
 +      updateOverviewImage();
    }
  
    /*
     * Displays the popup menu and acts on user input
     */
 -  private void showPopupMenu(MouseEvent e)
 +  protected void showPopupMenu(MouseEvent e)
    {
      JPopupMenu popup = new JPopupMenu();
      ActionListener menuListener = new ActionListener()
    /*
     * Toggle overview display between showing hidden columns and hiding hidden columns
     */
 -  private void toggleHiddenColumns()
 +  protected void toggleHiddenColumns()
    {
      if (showHidden)
      {
  package jalview.gui;
  
  import jalview.analysis.scoremodels.ScoreModels;
- import jalview.analysis.scoremodels.SimilarityParams;
+ import jalview.api.AlignViewportI;
  import jalview.api.analysis.ScoreModelI;
  import jalview.api.analysis.SimilarityParamsI;
+ import jalview.bin.Cache;
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.AlignmentView;
  import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.SequenceI;
 +import jalview.gui.ImageExporter.ImageWriterI;
 +import jalview.gui.JalviewColourChooser.ColourChooserListener;
  import jalview.jbgui.GPCAPanel;
+ import jalview.math.RotatableMatrix.Axis;
  import jalview.util.ImageMaker;
  import jalview.util.MessageManager;
  import jalview.viewmodel.AlignmentViewport;
@@@ -49,56 -49,37 +51,36 @@@ import java.awt.print.PrinterException
  import java.awt.print.PrinterJob;
  
  import javax.swing.ButtonGroup;
- import javax.swing.JCheckBoxMenuItem;
 -import javax.swing.JColorChooser;
  import javax.swing.JMenuItem;
  import javax.swing.JRadioButtonMenuItem;
  import javax.swing.event.InternalFrameAdapter;
  import javax.swing.event.InternalFrameEvent;
  
  /**
-  * DOCUMENT ME!
-  * 
-  * @author $author$
-  * @version $Revision$
+  * The panel holding the Principal Component Analysis 3-D visualisation
   */
  public class PCAPanel extends GPCAPanel
          implements Runnable, IProgressIndicator
  {
+   private static final int MIN_WIDTH = 470;
  
-   private IProgressIndicator progressBar;
+   private static final int MIN_HEIGHT = 250;
  
-   RotatableCanvas rc;
+   private RotatableCanvas rc;
  
    AlignmentPanel ap;
  
    AlignmentViewport av;
  
-   PCAModel pcaModel;
+   private PCAModel pcaModel;
  
-   private static final int MIN_WIDTH = 470;
-   private static final int MIN_HEIGHT = 250;
+   private int top = 0;
  
-   int top = 0;
+   private IProgressIndicator progressBar;
  
    private boolean working;
  
    /**
-    * Creates a new PCAPanel object using default score model and parameters
-    * 
-    * @param alignPanel
-    */
-   public PCAPanel(AlignmentPanel alignPanel)
-   {
-     this(alignPanel,
-             ScoreModels.getInstance()
-                     .getDefaultModel(
-                             !alignPanel.av.getAlignment().isNucleotide())
-                     .getName(),
-             SimilarityParams.SeqSpace);
-   }
-   /**
     * Constructor given sequence data, a similarity (or distance) score model
     * name, and score calculation parameters
     * 
  
      ScoreModelI scoreModel = ScoreModels.getInstance()
              .getScoreModel(modelName, ap);
-     pcaModel = new PCAModel(seqstrings, seqs, nucleotide, scoreModel,
-             params);
+     setPcaModel(new PCAModel(seqstrings, seqs, nucleotide, scoreModel,
+             params));
      PaintRefresher.Register(this, av.getSequenceSetId());
  
-     rc = new RotatableCanvas(alignPanel);
-     this.getContentPane().add(rc, BorderLayout.CENTER);
-     Thread worker = new Thread(this);
-     worker.start();
+     setRotatableCanvas(new RotatableCanvas(alignPanel));
+     this.getContentPane().add(getRotatableCanvas(), BorderLayout.CENTER);
+     addKeyListener(getRotatableCanvas());
+     validate();
+     this.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
    }
  
    /**
     */
    protected void close_actionPerformed()
    {
-     pcaModel = null;
-   }
-   /**
-    * Repopulate the options and actions under the score model menu when it is
-    * selected. Options will depend on whether 'nucleotide' or 'peptide'
-    * modelling is selected (and also possibly on whether any additional score
-    * models have been added).
-    */
-   @Override
-   protected void scoreModel_menuSelected()
-   {
-     scoreModelMenu.removeAll();
-     for (final ScoreModelI sm : ScoreModels.getInstance().getModels())
-     {
-       final String name = sm.getName();
-       JCheckBoxMenuItem jm = new JCheckBoxMenuItem(name);
-       /*
-        * if the score model doesn't provide a description, try to look one
-        * up in the text bundle, falling back on its name
-        */
-       String tooltip = sm.getDescription();
-       if (tooltip == null)
-       {
-         tooltip = MessageManager.getStringOrReturn("label.score_model_",
-                 name);
-       }
-       jm.setToolTipText(tooltip);
-       jm.setSelected(pcaModel.getScoreModelName().equals(name));
-       if ((pcaModel.isNucleotide() && sm.isDNA())
-               || (!pcaModel.isNucleotide() && sm.isProtein()))
-       {
-         jm.addActionListener(new ActionListener()
-         {
-           @Override
-           public void actionPerformed(ActionEvent e)
-           {
-             if (!pcaModel.getScoreModelName().equals(name))
-             {
-               ScoreModelI sm2 = ScoreModels.getInstance()
-                       .getScoreModel(name, ap);
-               pcaModel.setScoreModel(sm2);
-               Thread worker = new Thread(PCAPanel.this);
-               worker.start();
-             }
-           }
-         });
-         scoreModelMenu.add(jm);
-       }
-     }
+     setPcaModel(null);
    }
  
    @Override
-   public void bgcolour_actionPerformed(ActionEvent e)
+   protected void bgcolour_actionPerformed()
    {
 -    Color col = JColorChooser.showDialog(this,
 -            MessageManager.getString("label.select_background_colour"),
 -            getRotatableCanvas().getBgColour());
 -
 -    if (col != null)
 +    String ttl = MessageManager.getString("label.select_background_colour");
 +    ColourChooserListener listener = new ColourChooserListener()
      {
 -      getRotatableCanvas().setBgColour(col);
 -    }
 -    getRotatableCanvas().repaint();
 +      @Override
 +      public void colourSelected(Color c)
 +      {
-         rc.bgColour = c;
++        rc.setBgColour(c);
 +        rc.repaint();
 +      }
 +    };
-     JalviewColourChooser.showColourChooser(this, ttl, rc.bgColour,
++    JalviewColourChooser.showColourChooser(this, ttl, rc.getBgColour(),
 +            listener);
    }
  
    /**
-    * DOCUMENT ME!
+    * Calculates the PCA and displays the results
     */
    @Override
    public void run()
    {
+     working = true;
      long progId = System.currentTimeMillis();
      IProgressIndicator progress = this;
      String message = MessageManager.getString("label.pca_recalculating");
        message = MessageManager.getString("label.pca_calculating");
      }
      progress.setProgressBar(message, progId);
-     working = true;
      try
      {
-       calcSettings.setEnabled(false);
-       pcaModel.run();
-       // ////////////////
+       getPcaModel().calculate();
        xCombobox.setSelectedIndex(0);
        yCombobox.setSelectedIndex(1);
        zCombobox.setSelectedIndex(2);
  
-       pcaModel.updateRc(rc);
+       getPcaModel().updateRc(getRotatableCanvas());
        // rc.invalidate();
-       nuclSetting.setSelected(pcaModel.isNucleotide());
-       protSetting.setSelected(!pcaModel.isNucleotide());
-       top = pcaModel.getTop();
+       setTop(getPcaModel().getTop());
  
      } catch (OutOfMemoryError er)
      {
      {
        progress.setProgressBar("", progId);
      }
-     calcSettings.setEnabled(true);
      repaint();
      if (getParent() == null)
      {
-       addKeyListener(rc);
-       Desktop.addInternalFrame(this, MessageManager
-               .getString("label.principal_component_analysis"), 475, 450);
-       this.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
+       Desktop.addInternalFrame(this,
+               MessageManager.formatMessage("label.calc_title", "PCA",
+                       getPcaModel().getScoreModelName()),
+               475, 450);
      }
      working = false;
    }
  
-   @Override
-   protected void nuclSetting_actionPerfomed(ActionEvent arg0)
-   {
-     if (!pcaModel.isNucleotide())
-     {
-       pcaModel.setNucleotide(true);
-       pcaModel.setScoreModel(
-               ScoreModels.getInstance().getDefaultModel(false));
-       Thread worker = new Thread(this);
-       worker.start();
-     }
-   }
-   @Override
-   protected void protSetting_actionPerfomed(ActionEvent arg0)
-   {
-     if (pcaModel.isNucleotide())
-     {
-       pcaModel.setNucleotide(false);
-       pcaModel.setScoreModel(
-               ScoreModels.getInstance().getDefaultModel(true));
-       Thread worker = new Thread(this);
-       worker.start();
-     }
-   }
    /**
-    * DOCUMENT ME!
+    * Updates the PCA display after a change of component to use for x, y or z
+    * axis
     */
-   void doDimensionChange()
+   @Override
+   protected void doDimensionChange()
    {
-     if (top == 0)
+     if (getTop() == 0)
      {
        return;
      }
  
-     int dim1 = top - xCombobox.getSelectedIndex();
-     int dim2 = top - yCombobox.getSelectedIndex();
-     int dim3 = top - zCombobox.getSelectedIndex();
-     pcaModel.updateRcView(dim1, dim2, dim3);
-     rc.img = null;
-     rc.rotmat.setIdentity();
-     rc.initAxes();
-     rc.paint(rc.getGraphics());
+     int dim1 = getTop() - xCombobox.getSelectedIndex();
+     int dim2 = getTop() - yCombobox.getSelectedIndex();
+     int dim3 = getTop() - zCombobox.getSelectedIndex();
+     getPcaModel().updateRcView(dim1, dim2, dim3);
+     getRotatableCanvas().resetView();
    }
  
    /**
-    * DOCUMENT ME!
+    * Sets the selected checkbox item index for PCA dimension (1, 2, 3...) for
+    * the given axis (X/Y/Z)
     * 
-    * @param e
-    *          DOCUMENT ME!
+    * @param index
+    * @param axis
     */
-   @Override
-   protected void xCombobox_actionPerformed(ActionEvent e)
+   public void setSelectedDimensionIndex(int index, Axis axis)
    {
-     doDimensionChange();
-   }
-   /**
-    * DOCUMENT ME!
-    * 
-    * @param e
-    *          DOCUMENT ME!
-    */
-   @Override
-   protected void yCombobox_actionPerformed(ActionEvent e)
-   {
-     doDimensionChange();
-   }
-   /**
-    * DOCUMENT ME!
-    * 
-    * @param e
-    *          DOCUMENT ME!
-    */
-   @Override
-   protected void zCombobox_actionPerformed(ActionEvent e)
-   {
-     doDimensionChange();
+     switch (axis)
+     {
+     case X:
+       xCombobox.setSelectedIndex(index);
+       break;
+     case Y:
+       yCombobox.setSelectedIndex(index);
+       break;
+     case Z:
+       zCombobox.setSelectedIndex(index);
+       break;
+     default:
+     }
    }
  
    @Override
-   public void outputValues_actionPerformed(ActionEvent e)
+   protected void outputValues_actionPerformed()
    {
      CutAndPasteTransfer cap = new CutAndPasteTransfer();
      try
      {
-       cap.setText(pcaModel.getDetails());
+       cap.setText(getPcaModel().getDetails());
        Desktop.addInternalFrame(cap,
                MessageManager.getString("label.pca_details"), 500, 500);
      } catch (OutOfMemoryError oom)
    }
  
    @Override
-   public void showLabels_actionPerformed(ActionEvent e)
+   protected void showLabels_actionPerformed()
    {
-     rc.showLabels(showLabels.getState());
+     getRotatableCanvas().showLabels(showLabels.getState());
    }
  
    @Override
-   public void print_actionPerformed(ActionEvent e)
+   protected void print_actionPerformed()
    {
      PCAPrinter printer = new PCAPrinter();
      printer.start();
    }
  
+   /**
+    * If available, shows the data which formed the inputs for the PCA as a new
+    * alignment
+    */
    @Override
-   public void originalSeqData_actionPerformed(ActionEvent e)
+   public void originalSeqData_actionPerformed()
    {
-     // this was cut'n'pasted from the equivalent TreePanel method - we should
-     // make this an abstract function of all jalview analysis windows
-     if (pcaModel.getSeqtrings() == null)
+     // JAL-2647 disabled after load from project (until save to project done)
+     if (getPcaModel().getInputData() == null)
      {
-       jalview.bin.Cache.log.info(
+       Cache.log.info(
                "Unexpected call to originalSeqData_actionPerformed - should have hidden this menu action.");
        return;
      }
      // decide if av alignment is sufficiently different to original data to
      // warrant a new window to be created
-     // create new alignmnt window with hidden regions (unhiding hidden regions
+     // create new alignment window with hidden regions (unhiding hidden regions
      // yields unaligned seqs)
      // or create a selection box around columns in alignment view
      // test Alignment(SeqCigar[])
      } catch (Exception ex)
      {
      }
-     ;
-     Object[] alAndColsel = pcaModel.getSeqtrings()
+     Object[] alAndColsel = getPcaModel().getInputData()
              .getAlignmentAndHiddenColumns(gc);
  
      if (alAndColsel != null && alAndColsel[0] != null)
      {
        pg.translate((int) pf.getImageableX(), (int) pf.getImageableY());
  
-       rc.drawBackground(pg, rc.bgColour);
-       rc.drawScene(pg);
-       if (rc.drawAxes == true)
+       getRotatableCanvas().drawBackground(pg);
+       getRotatableCanvas().drawScene(pg);
+       if (getRotatableCanvas().drawAxes)
        {
-         rc.drawAxes(pg);
+         getRotatableCanvas().drawAxes(pg);
        }
  
        if (pi == 0)
      }
    }
  
 -  /**
 -   * Handler for 'Save as EPS' option
 -   */
 -  @Override
 -  protected void eps_actionPerformed()
 -  {
 -    makePCAImage(ImageMaker.TYPE.EPS);
 -  }
 -
 -  /**
 -   * Handler for 'Save as PNG' option
 -   */
 -  @Override
 -  protected void png_actionPerformed()
 -  {
 -    makePCAImage(ImageMaker.TYPE.PNG);
 -  }
 -
 -  void makePCAImage(ImageMaker.TYPE type)
 +  public void makePCAImage(ImageMaker.TYPE type)
    {
-     int width = rc.getWidth();
-     int height = rc.getHeight();
+     int width = getRotatableCanvas().getWidth();
+     int height = getRotatableCanvas().getHeight();
 -
 -    ImageMaker im;
 -
 -    switch (type)
 +    ImageWriterI writer = new ImageWriterI()
      {
 -    case PNG:
 -      im = new ImageMaker(this, ImageMaker.TYPE.PNG,
 -              "Make PNG image from PCA", width, height, null, null, null, 0,
 -              false);
 -      break;
 -    case EPS:
 -      im = new ImageMaker(this, ImageMaker.TYPE.EPS,
 -              "Make EPS file from PCA", width, height, null,
 -              this.getTitle(), null, 0, false);
 -      break;
 -    default:
 -      im = new ImageMaker(this, ImageMaker.TYPE.SVG,
 -              "Make SVG file from PCA", width, height, null,
 -              this.getTitle(), null, 0, false);
 -    }
 -
 -    if (im.getGraphics() != null)
 -    {
 -      getRotatableCanvas().drawBackground(im.getGraphics());
 -      getRotatableCanvas().drawScene(im.getGraphics());
 -      if (getRotatableCanvas().drawAxes)
 +      @Override
 +      public void exportImage(Graphics g) throws Exception
        {
-         rc.drawBackground(g, Color.black);
-         rc.drawScene(g);
-         if (rc.drawAxes)
 -        getRotatableCanvas().drawAxes(im.getGraphics());
++      RotatableCanvas canvas = getRotatableCanvas();
++      canvas.drawBackground(g);
++      canvas.drawScene(g);
++        if (canvas.drawAxes)
 +        {
-           rc.drawAxes(g);
++          canvas.drawAxes(g);
 +        }
        }
 -      im.writeImage();
 -    }
 +    };
 +    String pca = MessageManager.getString("label.pca");
 +    ImageExporter exporter = new ImageExporter(writer, null, type, pca);
 +    exporter.doExport(null, this, width, height, pca);
    }
  
    @Override
-   public void viewMenu_menuSelected()
+   protected void viewMenu_menuSelected()
    {
      buildAssociatedViewMenu();
    }
  
+   /**
+    * Builds the menu showing the choice of possible views (for the associated
+    * sequence data) to which the PCA may be linked
+    */
    void buildAssociatedViewMenu()
    {
      AlignmentPanel[] aps = PaintRefresher
              .getAssociatedPanels(av.getSequenceSetId());
-     if (aps.length == 1 && rc.av == aps[0].av)
+     if (aps.length == 1 && getRotatableCanvas().av == aps[0].av)
      {
        associateViewsMenu.setVisible(false);
        return;
  
      JRadioButtonMenuItem item;
      ButtonGroup buttonGroup = new ButtonGroup();
-     int i, iSize = aps.length;
-     final PCAPanel thisPCAPanel = this;
-     for (i = 0; i < iSize; i++)
+     int iSize = aps.length;
+     for (int i = 0; i < iSize; i++)
      {
-       final AlignmentPanel ap = aps[i];
-       item = new JRadioButtonMenuItem(ap.av.getViewName(), ap.av == rc.av);
+       final AlignmentPanel panel = aps[i];
+       item = new JRadioButtonMenuItem(panel.av.getViewName(),
+               panel.av == getRotatableCanvas().av);
        buttonGroup.add(item);
        item.addActionListener(new ActionListener()
        {
          @Override
          public void actionPerformed(ActionEvent evt)
          {
-           rc.applyToAllViews = false;
-           rc.av = ap.av;
-           rc.ap = ap;
-           PaintRefresher.Register(thisPCAPanel, ap.av.getSequenceSetId());
+           selectAssociatedView(panel);
          }
        });
  
  
      buttonGroup.add(itemf);
  
-     itemf.setSelected(rc.applyToAllViews);
+     itemf.setSelected(getRotatableCanvas().isApplyToAllViews());
      itemf.addActionListener(new ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent evt)
        {
-         rc.applyToAllViews = itemf.isSelected();
+         getRotatableCanvas().setApplyToAllViews(itemf.isSelected());
        }
      });
      associateViewsMenu.add(itemf);
     * )
     */
    @Override
-   protected void outputPoints_actionPerformed(ActionEvent e)
+   protected void outputPoints_actionPerformed()
    {
      CutAndPasteTransfer cap = new CutAndPasteTransfer();
      try
      {
-       cap.setText(pcaModel.getPointsasCsv(false,
+       cap.setText(getPcaModel().getPointsasCsv(false,
                xCombobox.getSelectedIndex(), yCombobox.getSelectedIndex(),
                zCombobox.getSelectedIndex()));
        Desktop.addInternalFrame(cap, MessageManager
     * .ActionEvent)
     */
    @Override
-   protected void outputProjPoints_actionPerformed(ActionEvent e)
+   protected void outputProjPoints_actionPerformed()
    {
      CutAndPasteTransfer cap = new CutAndPasteTransfer();
      try
      {
-       cap.setText(pcaModel.getPointsasCsv(true,
+       cap.setText(getPcaModel().getPointsasCsv(true,
                xCombobox.getSelectedIndex(), yCombobox.getSelectedIndex(),
                zCombobox.getSelectedIndex()));
        Desktop.addInternalFrame(cap, MessageManager.formatMessage(
    }
  
    @Override
-   protected void resetButton_actionPerformed(ActionEvent e)
+   protected void resetButton_actionPerformed()
    {
-     int t = top;
-     top = 0; // ugly - prevents dimensionChanged events from being processed
+     int t = getTop();
+     setTop(0); // ugly - prevents dimensionChanged events from being processed
      xCombobox.setSelectedIndex(0);
      yCombobox.setSelectedIndex(1);
-     top = t;
+     setTop(t);
      zCombobox.setSelectedIndex(2);
    }
  
    {
      return working;
    }
+   /**
+    * Answers the selected checkbox item index for PCA dimension for the X, Y or
+    * Z axis of the display
+    * 
+    * @param axis
+    * @return
+    */
+   public int getSelectedDimensionIndex(Axis axis)
+   {
+     switch (axis)
+     {
+     case X:
+       return xCombobox.getSelectedIndex();
+     case Y:
+       return yCombobox.getSelectedIndex();
+     default:
+       return zCombobox.getSelectedIndex();
+     }
+   }
+   public void setShowLabels(boolean show)
+   {
+     showLabels.setSelected(show);
+   }
+   /**
+    * Sets the input data used to calculate the PCA. This is provided for
+    * 'restore from project', which does not currently support this (AL-2647), so
+    * sets the value to null, and hides the menu option for "Input Data...". J
+    * 
+    * @param data
+    */
+   public void setInputData(AlignmentView data)
+   {
+     getPcaModel().setInputData(data);
+     originalSeqData.setVisible(data != null);
+   }
+   public AlignViewportI getAlignViewport()
+   {
+     return av;
+   }
+   public PCAModel getPcaModel()
+   {
+     return pcaModel;
+   }
+   public void setPcaModel(PCAModel pcaModel)
+   {
+     this.pcaModel = pcaModel;
+   }
+   public RotatableCanvas getRotatableCanvas()
+   {
+     return rc;
+   }
+   public void setRotatableCanvas(RotatableCanvas rc)
+   {
+     this.rc = rc;
+   }
+   public int getTop()
+   {
+     return top;
+   }
+   public void setTop(int top)
+   {
+     this.top = top;
+   }
+   /**
+    * set the associated view for this PCA.
+    * 
+    * @param panel
+    */
+   public void selectAssociatedView(AlignmentPanel panel)
+   {
+     getRotatableCanvas().setApplyToAllViews(false);
+     ap = panel;
+     av = panel.av;
+     getRotatableCanvas().av = panel.av;
+     getRotatableCanvas().ap = panel;
+     PaintRefresher.Register(PCAPanel.this, panel.av.getSequenceSetId());
+   }
  }
@@@ -38,7 -38,6 +38,7 @@@ import jalview.datamodel.SequenceFeatur
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  import jalview.gui.ColourMenuHelper.ColourChangeListener;
 +import jalview.gui.JalviewColourChooser.ColourChooserListener;
  import jalview.io.FileFormatI;
  import jalview.io.FileFormats;
  import jalview.io.FormatAdapter;
@@@ -47,14 -46,13 +47,15 @@@ import jalview.schemes.Blosum62ColourSc
  import jalview.schemes.ColourSchemeI;
  import jalview.schemes.ColourSchemes;
  import jalview.schemes.PIDColourScheme;
+ import jalview.schemes.ResidueColourScheme;
  import jalview.util.GroupUrlLink;
  import jalview.util.GroupUrlLink.UrlStringTooLongException;
  import jalview.util.MessageManager;
 +import jalview.util.Platform;
  import jalview.util.StringUtils;
  import jalview.util.UrlLink;
  
 +import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
@@@ -71,14 -69,13 +72,16 @@@ import java.util.SortedMap
  import java.util.TreeMap;
  import java.util.Vector;
  
+ import javax.swing.ButtonGroup;
  import javax.swing.JCheckBoxMenuItem;
 -import javax.swing.JColorChooser;
 +import javax.swing.JInternalFrame;
 +import javax.swing.JLabel;
  import javax.swing.JMenu;
  import javax.swing.JMenuItem;
 +import javax.swing.JPanel;
  import javax.swing.JPopupMenu;
+ import javax.swing.JRadioButtonMenuItem;
 +import javax.swing.JScrollPane;
  
  /**
   * DOCUMENT ME!
@@@ -98,6 -95,8 +101,8 @@@ public class PopupMenu extends JPopupMe
  
    protected JCheckBoxMenuItem conservationMenuItem = new JCheckBoxMenuItem();
  
+   protected JRadioButtonMenuItem annotationColour;
    protected JMenuItem modifyConservation = new JMenuItem();
  
    AlignmentPanel ap;
          }
        }
      }
-     // for the case when no sequences are even visible
+     /*
+      * offer 'Reveal All'
+      * - in the IdPanel (seq not null) if any sequence is hidden
+      * - in the IdPanel or SeqPanel if all sequences are hidden (seq is null)
+      */
      if (alignPanel.av.hasHiddenRows())
      {
+       boolean addOption = seq != null;
+       if (!addOption && alignPanel.av.getAlignment().getHeight() == 0)
+       {
+         addOption = true;
+       }
+       if (addOption)
        {
          menuItem = new JMenuItem(
                  MessageManager.getString("action.reveal_all"));
              }
            }
          });
          add(menuItem);
        }
      }
     */
    protected void showFeatureDetails(SequenceFeature sf)
    {
 -    CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
 -    // it appears Java's CSS does not support border-collaps :-(
 -    cap.addStylesheetRule("table { border-collapse: collapse;}");
 -    cap.addStylesheetRule("table, td, th {border: 1px solid black;}");
 -    cap.setText(sf.getDetailsReport());
 -
 -    Desktop.addInternalFrame(cap,
 +    JInternalFrame details;
 +    if (/** @j2sNative true || */ false)
 +    {
 +      details = new JInternalFrame();
 +      JPanel panel = new JPanel(new BorderLayout());
 +      panel.setOpaque(true);
 +      panel.setBackground(Color.white);
 +      // TODO JAL-3026 set style of table correctly for feature details
 +      JLabel reprt = new JLabel(MessageManager.formatMessage("label.html_content",
 +            new Object[]
 +            { sf.getDetailsReport()}));
 +      reprt.setBackground(Color.WHITE);
 +      reprt.setOpaque(true);
 +      panel.add(reprt,BorderLayout.CENTER);
 +      details.setContentPane(panel);
 +      details.pack();
 +    }
 +    else
 +    {
 +      CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
 +      // it appears Java's CSS does not support border-collaps :-(
 +      cap.addStylesheetRule("table { border-collapse: collapse;}");
 +      cap.addStylesheetRule("table, td, th {border: 1px solid black;}");
 +      cap.setText(sf.getDetailsReport());
 +      details = cap;
 +    }
 +    Desktop.addInternalFrame(details,
              MessageManager.getString("label.feature_details"), 500, 500);
    }
  
        {
          sqi = sqi.getDatasetSequence();
        }
 -      DBRefEntry[] dbr = sqi.getDBRefs();
 -      if (dbr != null && dbr.length > 0)
 +      List<DBRefEntry> dbr = sqi.getDBRefs();
 +      int nd;
 +      if (dbr != null && (nd = dbr.size()) > 0)
        {
 -        for (int d = 0; d < dbr.length; d++)
 +        for (int d = 0; d < nd; d++)
          {
 -          String src = dbr[d].getSource(); // jalview.util.DBRefUtils.getCanonicalName(dbr[d].getSource()).toUpperCase();
 +          DBRefEntry e = dbr.get(d);
 +          String src = e.getSource(); // jalview.util.DBRefUtils.getCanonicalName(dbr[d].getSource()).toUpperCase();
            Object[] sarray = commonDbrefs.get(src);
            if (sarray == null)
            {
  
            if (((String[]) sarray[1])[sq] == null)
            {
 -            if (!dbr[d].hasMap() || (dbr[d].getMap()
 +            if (!e.hasMap() || (e.getMap()
                      .locateMappedRange(start, end) != null))
              {
 -              ((String[]) sarray[1])[sq] = dbr[d].getAccessionId();
 +              ((String[]) sarray[1])[sq] = e.getAccessionId();
                ((int[]) sarray[0])[0]++;
              }
            }
        @Override
        public void actionPerformed(ActionEvent actionEvent)
        {
 -        editSequence_actionPerformed(actionEvent);
 +        editSequence_actionPerformed();
        }
      });
      makeReferenceSeq.setText(
        }
      });
  
+     annotationColour = new JRadioButtonMenuItem(
+             MessageManager.getString("action.by_annotation"));
+     annotationColour.setName(ResidueColourScheme.ANNOTATION_COLOUR);
+     annotationColour.setEnabled(false);
+     annotationColour.setToolTipText(
+             MessageManager.getString("label.by_annotation_tooltip"));
      modifyConservation.setText(MessageManager
              .getString("label.modify_conservation_threshold"));
      modifyConservation.addActionListener(new ActionListener()
      colourMenu.add(textColour);
      colourMenu.addSeparator();
  
-     ColourMenuHelper.addMenuItems(colourMenu, this, sg, false);
+     ButtonGroup bg = ColourMenuHelper.addMenuItems(colourMenu, this, sg,
+             false);
+     bg.add(annotationColour);
+     colourMenu.add(annotationColour);
  
      colourMenu.addSeparator();
      colourMenu.add(conservationMenuItem);
  
    public void createSequenceDetailsReport(SequenceI[] sequences)
    {
 -    CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
      StringBuilder contents = new StringBuilder(128);
 +    contents.append("<html><body>");
      for (SequenceI seq : sequences)
      {
        contents.append("<p><h2>" + MessageManager.formatMessage(
                contents, seq, true, true, ap.getSeqPanel().seqCanvas.fr);
        contents.append("</p>");
      }
 -    cap.setText("<html>" + contents.toString() + "</html>");
 +    contents.append("</body></html>");
 +    String report = contents.toString();
 +    
 +    JInternalFrame frame;
 +    if (Platform.isJS())
 +    {
 +      JLabel textLabel = new JLabel();
 +      textLabel.setText(report);
 +      textLabel.setBackground(Color.WHITE);
 +      JPanel pane = new JPanel(new BorderLayout());
 +      ((JPanel) pane).setOpaque(true);
 +      pane.setBackground(Color.WHITE);
 +      ((JPanel) pane).add(textLabel, BorderLayout.NORTH);
 +      frame = new JInternalFrame();
 +      frame.getContentPane().add(new JScrollPane(pane));
 +    }
 +    else
 +    {
 +      CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
 +      cap.setText(report);
 +      frame = cap;
 +    }
  
 -    Desktop.addInternalFrame(cap,
 +    Desktop.addInternalFrame(frame,
              MessageManager.formatMessage("label.sequence_details_for",
                      (sequences.length == 1 ? new Object[]
                      { sequences[0].getDisplayId(true) }
                              { MessageManager
                                      .getString("label.selection") })),
              500, 400);
 -
    }
  
    protected void showNonconserved_actionPerformed()
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Shows a dialog where group name and description may be edited
     */
    protected void groupName_actionPerformed()
    {
      SequenceGroup sg = getGroup();
      EditNameDialog dialog = new EditNameDialog(sg.getName(),
              sg.getDescription(),
 -            "       " + MessageManager.getString("label.group_name") + " ",
 -            MessageManager.getString("label.group_description") + " ",
 +            MessageManager.getString("label.group_name"),
 +            MessageManager.getString("label.group_description"));
 +    dialog.showDialog(ap.alignFrame,
              MessageManager.getString("label.edit_group_name_description"),
 -            ap.alignFrame);
 -
 -    if (!dialog.accept)
 -    {
 -      return;
 -    }
 -
 -    sg.setName(dialog.getName());
 -    sg.setDescription(dialog.getDescription());
 -    refresh();
 +            new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                sg.setName(dialog.getName());
 +                sg.setDescription(dialog.getDescription());
 +                refresh();
 +              }
 +            });
    }
  
    /**
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Shows a dialog where sequence name and description may be edited
     */
    void sequenceName_actionPerformed()
    {
      EditNameDialog dialog = new EditNameDialog(sequence.getName(),
              sequence.getDescription(),
 -            "       " + MessageManager.getString("label.sequence_name")
 -                    + " ",
 -            MessageManager.getString("label.sequence_description") + " ",
 +            MessageManager.getString("label.sequence_name"),
 +            MessageManager.getString("label.sequence_description"));
 +    dialog.showDialog(ap.alignFrame,
              MessageManager.getString(
                      "label.edit_sequence_name_description"),
 -            ap.alignFrame);
 -
 -    if (!dialog.accept)
 -    {
 -      return;
 -    }
 -
 -    if (dialog.getName() != null)
 -    {
 -      if (dialog.getName().indexOf(" ") > -1)
 -      {
 -        JvOptionPane.showMessageDialog(ap,
 -                MessageManager
 -                        .getString("label.spaces_converted_to_backslashes"),
 -                MessageManager
 -                        .getString("label.no_spaces_allowed_sequence_name"),
 -                JvOptionPane.WARNING_MESSAGE);
 -      }
 -
 -      sequence.setName(dialog.getName().replace(' ', '_'));
 -      ap.paintAlignment(false, false);
 -    }
 -
 -    sequence.setDescription(dialog.getDescription());
 -
 -    ap.av.firePropertyChange("alignment", null,
 -            ap.av.getAlignment().getSequences());
 -
 +            new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                if (dialog.getName() != null)
 +                {
 +                  if (dialog.getName().indexOf(" ") > -1)
 +                  {
 +                    JvOptionPane.showMessageDialog(ap,
 +                            MessageManager.getString(
 +                                    "label.spaces_converted_to_underscores"),
 +                            MessageManager.getString(
 +                                    "label.no_spaces_allowed_sequence_name"),
 +                            JvOptionPane.WARNING_MESSAGE);
 +                  }
 +                  sequence.setName(dialog.getName().replace(' ', '_'));
 +                  ap.paintAlignment(false, false);
 +                }
 +                sequence.setDescription(dialog.getDescription());
 +                ap.av.firePropertyChange("alignment", null,
 +                        ap.av.getAlignment().getSequences());
 +              }
 +            });
    }
  
    /**
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Offers a colour chooser and sets the selected colour as the group outline
     */
    protected void outline_actionPerformed()
    {
 -    SequenceGroup sg = getGroup();
 -    Color col = JColorChooser.showDialog(this,
 -            MessageManager.getString("label.select_outline_colour"),
 -            Color.BLUE);
 -
 -    if (col != null)
 +    String title = MessageManager
 +            .getString("label.select_outline_colour");
 +    ColourChooserListener listener = new ColourChooserListener()
      {
 -      sg.setOutlineColour(col);
 -    }
 -
 -    refresh();
 +      @Override
 +      public void colourSelected(Color c)
 +      {
 +        getGroup().setOutlineColour(c);
 +        refresh();
 +      };
 +    };
 +    JalviewColourChooser.showColourChooser(Desktop.getDesktop(),
 +            title, Color.BLUE, listener);
    }
  
    /**
  
    public void copy_actionPerformed()
    {
 -    ap.alignFrame.copy_actionPerformed(null);
 +    ap.alignFrame.copy_actionPerformed();
    }
  
    public void cut_actionPerformed()
    {
 -    ap.alignFrame.cut_actionPerformed(null);
 +    ap.alignFrame.cut_actionPerformed();
    }
  
    void changeCase(ActionEvent e)
       */
      if (!seqs.isEmpty())
      {
 -      if (ap.getSeqPanel().seqCanvas.getFeatureRenderer()
 -              .amendFeatures(seqs, features, true, ap))
 -      {
 -        ap.alignFrame.setShowSeqFeatures(true);
 -        ap.av.setSearchResults(null); // clear highlighting
 -        ap.repaint(); // draw new/amended features
 -      }
 +      new FeatureEditor(ap, seqs, features, true).showDialog();
      }
    }
  
  
    }
  
 -  public void editSequence_actionPerformed(ActionEvent actionEvent)
 +  /**
 +   * Shows a dialog where sequence characters may be edited. Any changes are
 +   * applied, and added as an available 'Undo' item in the edit commands
 +   * history.
 +   */
 +  public void editSequence_actionPerformed()
    {
      SequenceGroup sg = ap.av.getSelectionGroup();
  
        EditNameDialog dialog = new EditNameDialog(
                sequence.getSequenceAsString(sg.getStartRes(),
                        sg.getEndRes() + 1),
 -              null, MessageManager.getString("label.edit_sequence"), null,
 +              null, MessageManager.getString("label.edit_sequence"), null);
 +      dialog.showDialog(ap.alignFrame,
                MessageManager.getString("label.edit_sequence"),
 -              ap.alignFrame);
 -
 -      if (dialog.accept)
 -      {
 -        EditCommand editCommand = new EditCommand(
 -                MessageManager.getString("label.edit_sequences"),
 -                Action.REPLACE,
 -                dialog.getName().replace(' ', ap.av.getGapCharacter()),
 -                sg.getSequencesAsArray(ap.av.getHiddenRepSequences()),
 -                sg.getStartRes(), sg.getEndRes() + 1, ap.av.getAlignment());
 -
 -        ap.alignFrame.addHistoryItem(editCommand);
 -
 -        ap.av.firePropertyChange("alignment", null,
 -                ap.av.getAlignment().getSequences());
 -      }
 +              new Runnable()
 +              {
 +                @Override
 +                public void run()
 +                {
 +                  EditCommand editCommand = new EditCommand(
 +                          MessageManager.getString("label.edit_sequences"),
 +                          Action.REPLACE,
 +                          dialog.getName().replace(' ',
 +                                  ap.av.getGapCharacter()),
 +                          sg.getSequencesAsArray(
 +                                  ap.av.getHiddenRepSequences()),
 +                          sg.getStartRes(), sg.getEndRes() + 1,
 +                          ap.av.getAlignment());
 +                  ap.alignFrame.addHistoryItem(editCommand);
 +                  ap.av.firePropertyChange("alignment", null,
 +                          ap.av.getAlignment().getSequences());
 +                }
 +              });
      }
    }
  
       * switch to the chosen colour scheme (or null for None)
       */
      ColourSchemeI colourScheme = ColourSchemes.getInstance()
-             .getColourScheme(colourSchemeName, sg,
+             .getColourScheme(colourSchemeName, ap.av, sg,
                      ap.av.getHiddenRepSequences());
      sg.setColourScheme(colourScheme);
      if (colourScheme instanceof Blosum62ColourScheme
@@@ -24,6 -24,7 +24,7 @@@ import jalview.analysis.AnnotationSorte
  import jalview.bin.Cache;
  import jalview.gui.Help.HelpId;
  import jalview.gui.StructureViewer.ViewerType;
+ import jalview.io.BackupFiles;
  import jalview.io.FileFormatI;
  import jalview.io.JalviewFileChooser;
  import jalview.io.JalviewFileView;
@@@ -54,7 -55,7 +55,7 @@@ import java.util.ArrayList
  import java.util.List;
  
  import javax.help.HelpSetException;
 -import javax.swing.JColorChooser;
 +import javax.swing.JComboBox;
  import javax.swing.JFileChooser;
  import javax.swing.JInternalFrame;
  import javax.swing.JPanel;
@@@ -188,13 -189,11 +189,13 @@@ public class Preferences extends GPrefe
      super();
      frame = new JInternalFrame();
      frame.setContentPane(this);
 -    wsPrefs = new WsPreferences();
 -    wsTab.add(wsPrefs, BorderLayout.CENTER);
 +    if (!Platform.isJS())
 +    {
 +      wsPrefs = new WsPreferences();
 +      wsTab.add(wsPrefs, BorderLayout.CENTER);
 +    }
      int width = 500, height = 450;
 -    new jalview.util.Platform();
 -    if (Platform.isAMac())
 +    if (Platform.isAMacAndNotJS())
      {
        width = 570;
        height = 480;
              new RowSorter.SortKey(m.getNameColumn(), SortOrder.ASCENDING));
  
      sorter.setSortKeys(sortKeys);
 -    sorter.sort();
 +    // BH 2018 setSortKeys will do the sort
 +    // sorter.sort();
  
      // set up filtering
      ActionListener onReset;
      /*
       * Set Output tab defaults
       */
 -    epsRendering.addItem(promptEachTimeOpt);
 -    epsRendering.addItem(lineArtOpt);
 -    epsRendering.addItem(textOpt);
 -    String defaultEPS = Cache.getDefault("EPS_RENDERING",
 -            "Prompt each time");
 -    if (defaultEPS.equalsIgnoreCase("Text"))
 -    {
 -      epsRendering.setSelectedItem(textOpt);
 -    }
 -    else if (defaultEPS.equalsIgnoreCase("Lineart"))
 -    {
 -      epsRendering.setSelectedItem(lineArtOpt);
 -    }
 -    else
 -    {
 -      epsRendering.setSelectedItem(promptEachTimeOpt);
 -    }
 +    setupOutputCombo(epsRendering, "EPS_RENDERING");
 +    setupOutputCombo(htmlRendering, "HTML_RENDERING");
 +    setupOutputCombo(svgRendering, "SVG_RENDERING");
      autoIdWidth.setSelected(Cache.getDefault("FIGURE_AUTOIDWIDTH", false));
      userIdWidth.setEnabled(!autoIdWidth.isSelected());
      userIdWidthlabel.setEnabled(!autoIdWidth.isSelected());
  
      annotations_actionPerformed(null); // update the display of the annotation
                                         // settings
+     
+     
+     /*
+      * Set Backups tab defaults
+      */
+     loadLastSavedBackupsOptions();
    }
  
    /**
 +   * A helper method that sets the items and initial selection in a character
 +   * rendering option list (Prompt each time/Lineart/Text)
 +   * 
 +   * @param comboBox
 +   * @param propertyKey
 +   */
 +  protected void setupOutputCombo(JComboBox<Object> comboBox,
 +          String propertyKey)
 +  {
 +    comboBox.addItem(promptEachTimeOpt);
 +    comboBox.addItem(lineArtOpt);
 +    comboBox.addItem(textOpt);
 +    
 +    /*
 +     * JalviewJS doesn't support Lineart so force it to Text
 +     */
 +    String defaultOption = Platform.isJS() ? "Text"
 +            : Cache.getDefault(propertyKey, "Prompt each time");
 +    if (defaultOption.equalsIgnoreCase("Text"))
 +    {
 +      comboBox.setSelectedItem(textOpt);
 +    }
 +    else if (defaultOption.equalsIgnoreCase("Lineart"))
 +    {
 +      comboBox.setSelectedItem(lineArtOpt);
 +    }
 +    else
 +    {
 +      comboBox.setSelectedItem(promptEachTimeOpt);
 +    }
 +  }
 +
 +  /**
     * Save user selections on the Preferences tabs to the Cache and write out to
     * file.
     * 
       */
      Cache.applicationProperties.setProperty("EPS_RENDERING",
              ((OptionsParam) epsRendering.getSelectedItem()).getCode());
 +    Cache.applicationProperties.setProperty("HTML_RENDERING",
 +            ((OptionsParam) htmlRendering.getSelectedItem()).getCode());
 +    Cache.applicationProperties.setProperty("SVG_RENDERING",
 +            ((OptionsParam) svgRendering.getSelectedItem()).getCode());
  
      /*
       * Save Connections settings
      Cache.applicationProperties.setProperty("PAD_GAPS",
              Boolean.toString(padGaps.isSelected()));
  
 -    wsPrefs.updateAndRefreshWsMenuConfig(false);
 +    if (!Platform.isJS())
 +    {
 +      wsPrefs.updateAndRefreshWsMenuConfig(false);
 +    }
+     /*
+      * Save Backups settings
+      */
+     Cache.applicationProperties.setProperty(BackupFiles.CONFIRM_DELETE_OLD,
+             Boolean.toString(backupfilesConfirmDelete.isSelected()));
+     Cache.applicationProperties.setProperty(BackupFiles.ENABLED,
+             Boolean.toString(enableBackupFiles.isSelected()));
+     Cache.applicationProperties.setProperty(BackupFiles.NO_MAX,
+             Boolean.toString(backupfilesKeepAll.isSelected()));
+     Cache.applicationProperties.setProperty(BackupFiles.REVERSE_ORDER,
+             Boolean.toString(suffixReverse.isSelected()));
+     Cache.applicationProperties.setProperty(BackupFiles.SUFFIX,
+             suffixTemplate.getText());
+     Cache.applicationProperties.setProperty(BackupFiles.ROLL_MAX,
+             Integer.toString(getSpinnerInt(backupfilesRollMaxSpinner, 4)));
+     Cache.applicationProperties.setProperty(BackupFiles.SUFFIX_DIGITS,
+             Integer.toString(getSpinnerInt(suffixDigitsSpinner, 3)));
+     Cache.applicationProperties.setProperty(BackupFiles.NS+"_PRESET",
+             Integer.toString(getComboIntStringKey(backupfilesPresetsCombo)));
      Cache.saveProperties();
      Desktop.instance.doConfigureStructurePrefs();
      try
    @Override
    public void startupFileTextfield_mouseClicked()
    {
 +    // TODO: JAL-3048 not needed for Jalview-JS
      String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
      JalviewFileChooser chooser = JalviewFileChooser
              .forRead(Cache.getProperty("LAST_DIRECTORY"), fileFormat);
    {
      try
      {
 -      wsPrefs.updateWsMenuConfig(true);
 -      wsPrefs.refreshWs_actionPerformed(e);
 +      if (!Platform.isJS())
 +      {
 +        wsPrefs.updateWsMenuConfig(true);
 +        wsPrefs.refreshWs_actionPerformed(e);
 +      }
        frame.setClosed(true);
      } catch (Exception ex)
      {
    @Override
    public void defaultBrowser_mouseClicked(MouseEvent e)
    {
 -    JFileChooser chooser = new JFileChooser(".");
 -    chooser.setDialogTitle(
 -            MessageManager.getString("label.select_default_browser"));
 +    // TODO: JAL-3048 not needed for j2s
 +    /*
 +     * @j2sNative
 +     */
 +    {
 +      JFileChooser chooser = new JFileChooser(".");
 +      chooser.setDialogTitle(
 +              MessageManager.getString("label.select_default_browser"));
  
 -    int value = chooser.showOpenDialog(this);
 +      int value = chooser.showOpenDialog(this);
  
 -    if (value == JFileChooser.APPROVE_OPTION)
 -    {
 -      defaultBrowser.setText(chooser.getSelectedFile().getAbsolutePath());
 +      if (value == JFileChooser.APPROVE_OPTION)
 +      {
 +        defaultBrowser.setText(chooser.getSelectedFile().getAbsolutePath());
 +      }
      }
 -
    }
  
    /*
    @Override
    public void minColour_actionPerformed(JPanel panel)
    {
 -    Color col = JColorChooser.showDialog(this,
 +    JalviewColourChooser.showColourChooser(this,
              MessageManager.getString("label.select_colour_minimum_value"),
 -            minColour.getBackground());
 -    if (col != null)
 -    {
 -      panel.setBackground(col);
 -    }
 -    panel.repaint();
 +            panel);
    }
  
    @Override
    public void maxColour_actionPerformed(JPanel panel)
    {
 -    Color col = JColorChooser.showDialog(this,
 +    JalviewColourChooser.showColourChooser(this,
              MessageManager.getString("label.select_colour_maximum_value"),
 -            maxColour.getBackground());
 -    if (col != null)
 -    {
 -      panel.setBackground(col);
 -    }
 -    panel.repaint();
 +            panel);
    }
  
    @Override
    {
      if (!useLegacyGap.isSelected())
      {
 -      Color col = JColorChooser.showDialog(this,
 +      JalviewColourChooser.showColourChooser(this,
                MessageManager.getString("label.select_gap_colour"),
 -              gapColour.getBackground());
 -      if (col != null)
 -      {
 -        gap.setBackground(col);
 -      }
 -      gap.repaint();
 +              gap);
      }
    }
  
    @Override
    public void hiddenColour_actionPerformed(JPanel hidden)
    {
 -    Color col = JColorChooser.showDialog(this,
 +    JalviewColourChooser.showColourChooser(this,
              MessageManager.getString("label.select_hidden_colour"),
 -            hiddenColour.getBackground());
 -    if (col != null)
 -    {
 -      hidden.setBackground(col);
 -    }
 -    hidden.repaint();
 +            hidden);
    }
  
    @Override
        }
      } catch (NumberFormatException x)
      {
 +      userIdWidth.setText("");
        JvOptionPane.showInternalMessageDialog(Desktop.desktop,
                MessageManager
                        .getString("warn.user_defined_width_requirements"),
                MessageManager.getString("label.invalid_id_column_width"),
                JvOptionPane.WARNING_MESSAGE);
 -      userIdWidth.setText("");
      }
    }
  
@@@ -23,7 -23,6 +23,6 @@@ package jalview.gui
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.SequenceGroup;
- import jalview.datamodel.SequenceI;
  import jalview.renderer.ScaleRenderer;
  import jalview.renderer.ScaleRenderer.ScaleMark;
  import jalview.util.MessageManager;
@@@ -132,9 -131,8 +131,9 @@@ public class ScalePanel extends JPane
      if (evt.isPopupTrigger()) // Mac: mousePressed
      {
        rightMouseButtonPressed(evt, res);
 +      return;
      }
 -    else if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
 +    if (Platform.isWinRightButton(evt))
      {
        /*
         * defer right-mouse click handling to mouse up on Windows
         */
        return;
      }
 -    else
 -    {
 -      leftMouseButtonPressed(evt, res);
 -    }
 +    leftMouseButtonPressed(evt, res);
    }
  
    /**
    protected void rightMouseButtonPressed(MouseEvent evt, final int res)
    {
      JPopupMenu pop = new JPopupMenu();
-     if (reveal != null)
+     /*
+      * grab the hidden range in case mouseMoved nulls it
+      */
+     final int[] hiddenRange = reveal;
+     if (hiddenRange != null)
      {
        JMenuItem item = new JMenuItem(
                MessageManager.getString("label.reveal"));
          @Override
          public void actionPerformed(ActionEvent e)
          {
-           av.showColumn(reveal[0]);
+           av.showColumn(hiddenRange[0]);
            reveal = null;
+           ap.updateLayout();
            ap.paintAlignment(true, true);
            av.sendSelection();
          }
            {
              av.showAllHiddenColumns();
              reveal = null;
+             ap.updateLayout();
              ap.paintAlignment(true, true);
              av.sendSelection();
            }
              av.setSelectionGroup(null);
            }
  
+           ap.updateLayout();
            ap.paintAlignment(true, true);
            av.sendSelection();
          }
      }
  
      av.getColumnSelection().addElement(res);
-     SequenceGroup sg = new SequenceGroup();
-     // try to be as quick as possible
-     SequenceI[] iVec = av.getAlignment().getSequencesArray();
-     for (int i = 0; i < iVec.length; i++)
-     {
-       sg.addSequence(iVec[i], false);
-       iVec[i] = null;
-     }
-     iVec = null;
+     SequenceGroup sg = new SequenceGroup(av.getAlignment().getSequences());
      sg.setStartRes(res);
      sg.setEndRes(res);
  
    @Override
    public void mouseReleased(MouseEvent evt)
    {
+     boolean wasDragging = mouseDragging;
      mouseDragging = false;
      ap.getSeqPanel().stopScrolling();
  
 +    // todo res calculation should be a method on AlignViewport
      int xCords = Math.max(0, evt.getX()); // prevent negative X coordinates
      int res = (xCords / av.getCharWidth())
              + av.getRanges().getStartRes();
 +
      if (av.hasHiddenColumns())
      {
        res = av.getAlignment().getHiddenColumns()
                .visibleToAbsoluteColumn(res);
      }
-     res = Math.min(res, av.getAlignment().getWidth() - 1);
+     res = Math.min(res, av.getRanges().getEndRes());
+     res = Math.max(0, res);
  
      if (!stretchingGroup)
      {
        {
          sg.setStartRes(res);
        }
+       if (wasDragging)
+       {
+         min = Math.min(res, min);
+         max = Math.max(res, max);
+         av.getColumnSelection().stretchGroup(res, sg, min, max);
+       }
      }
      stretchingGroup = false;
      ap.paintAlignment(false, false);
+     av.isSelectionGroupChanged(true);
+     av.isColSelChanged(true);
      av.sendSelection();
    }
  
    /**
     * Action on dragging the mouse in the scale panel is to expand or shrink the
-    * selection group range (including any hidden columns that it spans)
+    * selection group range (including any hidden columns that it spans). Note
+    * that the selection is only broadcast at the start of the drag (on
+    * mousePressed) and at the end (on mouseReleased), to avoid overload
+    * redrawing of other views.
     * 
     * @param evt
     */
    {
      if (mouseDragging)
      {
 +      mouseDragging = false;
        ap.getSeqPanel().stopScrolling();
      }
    }
    @Override
    public void paintComponent(Graphics g)
    {
 -    super.paintComponent(g);
 +    //super.paintComponent(g);  // BH 2019
  
      /*
       * shouldn't get called in wrapped mode as the scale above is
@@@ -39,12 -39,13 +39,12 @@@ import java.awt.FontMetrics
  import java.awt.Graphics;
  import java.awt.Graphics2D;
  import java.awt.RenderingHints;
 -import java.awt.Shape;
  import java.awt.image.BufferedImage;
  import java.beans.PropertyChangeEvent;
  import java.util.Iterator;
  import java.util.List;
  
 -import javax.swing.JComponent;
 +import javax.swing.JPanel;
  
  /**
   * The Swing component on which the alignment sequences, and annotations (if
   * Wrapped mode, but not the scale above in Unwrapped mode.
   * 
   */
 -public class SeqCanvas extends JComponent implements ViewportListenerI
 +public class SeqCanvas extends JPanel implements ViewportListenerI
  {
+   /*
+    * pixels gap between sequences and annotations when in wrapped mode
+    */
+   static final int SEQS_ANNOTATION_GAP = 3;
    private static final String ZEROS = "0000000000";
  
    final FeatureRenderer fr;
  
    private int labelWidthWest; // label left width in pixels if shown
  
-   private int wrappedSpaceAboveAlignment; // gap between widths
+   int wrappedSpaceAboveAlignment; // gap between widths
  
-   private int wrappedRepeatHeightPx; // height in pixels of wrapped width
+   int wrappedRepeatHeightPx; // height in pixels of wrapped width
  
    private int wrappedVisibleWidths; // number of wrapped widths displayed
  
 -  private Graphics2D gg;
 +  // Don't do this! Graphics handles are supposed to be transient
 +  //private Graphics2D gg;
  
    /**
     * Creates a new SeqCanvas object.
  
    public SequenceRenderer getSequenceRenderer()
    {
 -    return seqRdr;
 +    return seqRdr; 
    }
  
    public FeatureRenderer getFeatureRenderer()
      int yPos = ypos + charHeight;
      int startX = startx;
      int endX = endx;
 -
 +    
      if (av.hasHiddenColumns())
      {
        HiddenColumns hiddenColumns = av.getAlignment().getHiddenColumns();
          }
        }
  
 +      
        /*
         * white fill the space for the scale
         */
          g.drawString(valueAsString, xOffset, y);
        }
      }
 +
    }
  
    /**
     */
    public void fastPaint(int horizontal, int vertical)
    {
 -    if (fastpainting || gg == null || img == null)
 +    if (fastpainting  || img == null)
      {
        return;
      }
        int transX = 0;
        int transY = 0;
        
 +      Graphics gg = img.getGraphics();
        gg.copyArea(horizontal * charWidth, vertical * charHeight,
                img.getWidth(), img.getHeight(), -horizontal * charWidth,
                -vertical * charHeight);
        gg.translate(transX, transY);
        drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
        gg.translate(-transX, -transY);
 -
 +      gg.dispose();
 +      
        // Call repaint on alignment panel so that repaints from other alignment
        // panel components can be aggregated. Otherwise performance of the
        // overview window and others may be adversely affected.
    @Override
    public void paintComponent(Graphics g)
    {
 -    super.paintComponent(g);    
 -    
 +    //super.paintComponent(g); // BH 2019
 +
      int charHeight = av.getCharHeight();
      int charWidth = av.getCharWidth();
 -    
 +
      ViewportRanges ranges = av.getRanges();
 -    
 +
      int width = getWidth();
      int height = getHeight();
 -    
 +
      width -= (width % charWidth);
      height -= (height % charHeight);
 +
      
      if ((img != null) && (fastPaint
              || (getVisibleRect().width != g.getClipBounds().width)
                || height != img.getHeight())
        {
          img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
 -        gg = (Graphics2D) img.getGraphics();
 -        gg.setFont(av.getFont());
        }
 -    
 +      
 +      Graphics2D gg = (Graphics2D) img.getGraphics();
 +      gg.setFont(av.getFont());
 +
        if (av.antiAlias)
        {
          gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                  RenderingHints.VALUE_ANTIALIAS_ON);
        }
 -    
 +
        gg.setColor(Color.white);
        gg.fillRect(0, 0, img.getWidth(), img.getHeight());
 -    
 +
        if (av.getWrapAlignment())
        {
          drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
                ranges.getEndRes(), ranges.getStartSeq(), ranges.getEndSeq());
  
        g.drawImage(img, 0, 0, this);
 +      gg.dispose();
      }
  
      if (av.cursorMode)
      calculateWrappedGeometry(canvasWidth, canvasHeight);
  
      /*
-      * draw one width at a time (excluding any scales or annotation shown),
+      * draw one width at a time (excluding any scales shown),
       * until we have run out of either alignment or vertical space available
       */
      int ypos = wrappedSpaceAboveAlignment;
              * (av.getScaleAboveWrapped() ? 2 : 1);
  
      /*
-      * height in pixels of the wrapped widths
+      * compute height in pixels of the wrapped widths
+      * - start with space above plus sequences
       */
      wrappedRepeatHeightPx = wrappedSpaceAboveAlignment;
-     // add sequences
      wrappedRepeatHeightPx += av.getAlignment().getHeight()
              * charHeight;
-     // add annotations panel height if shown
-     wrappedRepeatHeightPx += getAnnotationHeight();
+     /*
+      * add annotations panel height if shown
+      * also gap between sequences and annotations
+      */
+     if (av.isShowAnnotation())
+     {
+       wrappedRepeatHeightPx += getAnnotationHeight();
+       wrappedRepeatHeightPx += SEQS_ANNOTATION_GAP; // 3px
+     }
  
      /*
       * number of visible widths (the last one may be part height),
     * @param endColumn
     * @param canvasHeight
     */
-   protected void drawWrappedWidth(Graphics g, int ypos, int startColumn,
-           int endColumn, int canvasHeight)
+   protected void drawWrappedWidth(Graphics g, final int ypos,
+           final int startColumn, final int endColumn,
+           final int canvasHeight)
    {
      ViewportRanges ranges = av.getRanges();
      int viewportWidth = ranges.getViewportWidth();
      int xOffset = labelWidthWest
              + ((startColumn - ranges.getStartRes()) % viewportWidth)
              * charWidth;
 -    g.translate(xOffset, 0);
 -
 -    // When printing we have an extra clipped region,
 -    // the Printable page which we need to account for here
 -    Shape clip = g.getClip();
  
 -    if (clip == null)
 -    {
 -      g.setClip(0, 0, viewportWidth * charWidth, canvasHeight);
 -    }
 -    else
 -    {
 -      g.setClip(0, (int) clip.getBounds().getY(),
 -              viewportWidth * charWidth, (int) clip.getBounds().getHeight());
 -    }
 +    g.translate(xOffset, 0);
  
      /*
       * white fill the region to be drawn (so incremental fast paint doesn't
  
      if (av.isShowAnnotation())
      {
-       g.translate(0, cHeight + ypos + 3);
+       final int yShift = cHeight + ypos + SEQS_ANNOTATION_GAP;
+       g.translate(0, yShift);
        if (annotations == null)
        {
          annotations = new AnnotationPanel(av);
  
        annotations.renderer.drawComponent(annotations, av, g, -1,
                startColumn, endx + 1);
-       g.translate(0, -cHeight - ypos - 3);
+       g.translate(0, -yShift);
      }
 -    g.setClip(clip);
      g.translate(-xOffset, 0);
    }
  
      int charWidth = av.getCharWidth();
  
      g.setFont(av.getFont());
 +
      g.setColor(Color.black);
  
      int ypos = wrappedSpaceAboveAlignment;
        if (av.getScaleRightWrapped())
        {
          int x = labelWidthWest + viewportWidth * charWidth;
 +        
          g.translate(x, 0);
          drawVerticalScale(g, startCol, endColumn, ypos, false);
          g.translate(-x, 0);
      int startx = startRes;
      int endx;
      int ypos = hgap; // vertical offset
-     int maxwidth = av.getAlignment().getWidth();
-     if (av.hasHiddenColumns())
-     {
-       maxwidth = av.getAlignment().getHiddenColumns()
-               .absoluteToVisibleColumn(maxwidth);
-     }
+     int maxwidth = av.getAlignment().getVisibleWidth();
  
      // chop the wrapped alignment extent up into panel-sized blocks and treat
      // each block as if it were a block from an unwrapped alignment
        do
        {
          g.setColor(group.getOutlineColour());
 -
          drawPartialGroupOutline(g, group, startRes, endRes, startSeq,
                  endSeq, offset);
  
          groupIndex++;
 -
          if (groupIndex >= av.getAlignment().getGroups().size())
          {
            break;
          }
 -
          group = av.getAlignment().getGroups().get(groupIndex);
 -
        } while (groupIndex < av.getAlignment().getGroups().size());
 -
      }
 -
    }
  
    /**
     * Highlights search results in the visible region by rendering as white text
     * on a black background. Any previous highlighting is removed. Answers true
     * if any highlight was left on the visible alignment (so status bar should be
 +   * set to match), else false. This method does _not_ set the 'fastPaint' flag,
 +   * so allows the next repaint to update the whole display.
 +   * 
 +   * @param results
 +   * @return
 +   */
 +  public boolean highlightSearchResults(SearchResultsI results)
 +  {
 +    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
 +   * if any highlight was left on the visible alignment (so status bar should be
     * set to match), else false.
     * <p>
 -   * Currently fastPaint is not implemented for wrapped alignments. If a wrapped
 -   * alignment had to be scrolled to show the highlighted region, then it should
 -   * be fully redrawn, otherwise a fast paint can be performed. This argument
 -   * could be removed if fast paint of scrolled wrapped alignment is coded in
 -   * future (JAL-2609).
 +   * Optionally, set the 'fastPaint' flag for a faster redraw if only the
 +   * highlighted regions are modified. This speeds up highlighting across linked
 +   * alignments.
 +   * <p>
 +   * Currently fastPaint is not implemented for scrolled wrapped alignments. If
 +   * a wrapped alignment had to be scrolled to show the highlighted region, then
 +   * it should be fully redrawn, otherwise a fast paint can be performed. This
 +   * argument could be removed if fast paint of scrolled wrapped alignment is
 +   * coded in future (JAL-2609).
     * 
     * @param results
 -   * @param noFastPaint
 +   * @param doFastPaint
 +   *          if true, sets a flag so the next repaint only redraws the modified
 +   *          image
     * @return
     */
    public boolean highlightSearchResults(SearchResultsI results,
 -          boolean noFastPaint)
 +          boolean doFastPaint)
    {
      if (fastpainting)
      {
      boolean wrapped = av.getWrapAlignment();
      try
      {
 -      fastPaint = !noFastPaint;
 +      fastPaint = doFastPaint;
        fastpainting = fastPaint;
  
        /*
     */
    protected boolean drawMappedPositions(SearchResultsI results)
    {
 -    if ((results == null) || (gg == null)) // JAL-2784 check gg is not null
 +    if ((results == null) || (img == null)) // JAL-2784 check gg is not null
      {
        return false;
      }
        }
        int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
        int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
 +      Graphics gg = img.getGraphics();
        gg.translate(transX, transY);
        drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
        gg.translate(-transX, -transY);
 +      gg.dispose();
      }
  
      return matchFound;
        }
        ViewportRanges vpRanges = av.getRanges();
  
-       int range = vpRanges.getEndRes() - vpRanges.getStartRes();
+       int range = vpRanges.getEndRes() - vpRanges.getStartRes() + 1;
        if (scrollX > range)
        {
          scrollX = range;
    {
      ViewportRanges ranges = av.getRanges();
  
-     if (Math.abs(scrollX) > ranges.getViewportWidth())
+     if (Math.abs(scrollX) >= ranges.getViewportWidth())
      {
        /*
-        * shift of more than one view width is 
+        * shift of one view width or more is 
         * overcomplicated to handle in this method
         */
        fastPaint = false;
        return;
      }
  
 -    if (fastpainting || gg == null)
 +    if (fastpainting || img == null)
      {
        return;
      }
  
      try
      {
 +      
 +      Graphics gg = img.getGraphics();
 +      
        calculateWrappedGeometry(getWidth(), getHeight());
  
        /*
         */
        drawWrappedDecorators(gg, ranges.getStartRes());
  
 +      gg.dispose();
 +      
        repaint();
      } finally
      {
        return;
      }
  
 +    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,
        gg.translate(-xOffset, 0);
  
        drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
 +      
      }
  
      /*
        gg.setColor(Color.white);
        gg.fillRect(0, canvasHeight - heightBelow, getWidth(), heightBelow);
      }
 -  }
 +    gg.dispose();
 + }
  
    /**
     * Shifts the visible alignment by the specified number of columns - left if
      {
        return;
      }
 +
 +    Graphics gg = img.getGraphics();
 +
      int charWidth = av.getCharWidth();
  
      int canvasHeight = getHeight();
  
        while (y >= 0)
        {
+         /*
+          * shift 'widthToCopy' residues by 'positions' places to the right
+          */
          gg.copyArea(copyFromLeftStart, y, widthToCopy, heightToCopy,
                  positions * charWidth, 0);
          if (y > 0)
          {
+           /*
+            * copy 'positions' residue from the row above (right hand end)
+            * to this row's left hand end
+            */
            gg.copyArea(copyFromRightStart, y - wrappedRepeatHeightPx,
                    positions * charWidth, heightToCopy, -widthToCopy,
                    wrappedRepeatHeightPx);
                    * charWidth, heightToCopy, widthToCopy,
                    -wrappedRepeatHeightPx);
          }
 -
          y += wrappedRepeatHeightPx;
          xpos += viewportWidth;
        }
      }
 +    gg.dispose();
    }
  
    
     */
    protected boolean drawMappedPositionsWrapped(SearchResultsI results)
    {
 -    if ((results == null) || (gg == null)) // JAL-2784 check gg is not null
 +    if ((results == null) || (img == null)) // JAL-2784 check gg is not null
      {
        return false;
      }
  
      int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
  
 +    
 +    Graphics gg = img.getGraphics();
 +
      for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
              .getEndSeq(); seqNo++)
      {
        }
      }
    
 +    gg.dispose();
 +
      return matchFound;
    }
  
@@@ -25,6 -25,7 +25,7 @@@ import jalview.bin.Cache
  import jalview.commands.EditCommand;
  import jalview.commands.EditCommand.Action;
  import jalview.commands.EditCommand.Edit;
+ import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.HiddenColumns;
@@@ -55,8 -56,6 +56,8 @@@ import java.awt.Color
  import java.awt.Font;
  import java.awt.FontMetrics;
  import java.awt.Point;
 +import java.awt.event.ActionEvent;
 +import java.awt.event.ActionListener;
  import java.awt.event.MouseEvent;
  import java.awt.event.MouseListener;
  import java.awt.event.MouseMotionListener;
@@@ -65,11 -64,8 +66,11 @@@ import java.awt.event.MouseWheelListene
  import java.util.Collections;
  import java.util.List;
  
 +import javax.swing.JLabel;
  import javax.swing.JPanel;
 +import javax.swing.JToolTip;
  import javax.swing.SwingUtilities;
 +import javax.swing.Timer;
  import javax.swing.ToolTipManager;
  
  /**
@@@ -82,6 -78,82 +83,82 @@@ public class SeqPanel extends JPane
          implements MouseListener, MouseMotionListener, MouseWheelListener,
          SequenceListener, SelectionListener
  {
+   /*
+    * a class that holds computed mouse position
+    * - column of the alignment (0...)
+    * - sequence offset (0...)
+    * - annotation row offset (0...)
+    * where annotation offset is -1 unless the alignment is shown
+    * in wrapped mode, annotations are shown, and the mouse is
+    * over an annnotation row
+    */
+   static class MousePos
+   {
+     /*
+      * alignment column position of cursor (0...)
+      */
+     final int column;
+     /*
+      * index in alignment of sequence under cursor,
+      * or nearest above if cursor is not over a sequence
+      */
+     final int seqIndex;
+     /*
+      * index in annotations array of annotation under the cursor
+      * (only possible in wrapped mode with annotations shown),
+      * or -1 if cursor is not over an annotation row
+      */
+     final int annotationIndex;
+     MousePos(int col, int seq, int ann)
+     {
+       column = col;
+       seqIndex = seq;
+       annotationIndex = ann;
+     }
+     boolean isOverAnnotation()
+     {
+       return annotationIndex != -1;
+     }
+     @Override
+     public boolean equals(Object obj)
+     {
+       if (obj == null || !(obj instanceof MousePos))
+       {
+         return false;
+       }
+       MousePos o = (MousePos) obj;
+       boolean b = (column == o.column && seqIndex == o.seqIndex
+               && annotationIndex == o.annotationIndex);
+       // System.out.println(obj + (b ? "= " : "!= ") + this);
+       return b;
+     }
+     /**
+      * A simple hashCode that ensures that instances that satisfy equals() have
+      * the same hashCode
+      */
+     @Override
+     public int hashCode()
+     {
+       return column + seqIndex + annotationIndex;
+     }
+     /**
+      * toString method for debug output purposes only
+      */
+     @Override
+     public String toString()
+     {
+       return String.format("c%d:s%d:a%d", column, seqIndex,
+               annotationIndex);
+     }
+   }
    private static final int MAX_TOOLTIP_LENGTH = 300;
  
    public SeqCanvas seqCanvas;
    public AlignmentPanel ap;
  
    /*
-    * last column position for mouseMoved event
+    * last position for mouseMoved event
     */
-   private int lastMouseColumn;
+   private MousePos lastMousePosition;
  
-   /*
-    * last sequence offset for mouseMoved event
-    */
-   private int lastMouseSeq;
+   protected int editLastRes;
  
-   protected int lastres;
-   protected int startseq;
+   protected int editStartSeq;
  
    protected AlignViewport av;
  
      ToolTipManager.sharedInstance().registerComponent(this);
      ToolTipManager.sharedInstance().setInitialDelay(0);
      ToolTipManager.sharedInstance().setDismissDelay(10000);
 +    
 +    
      this.av = viewport;
      setBackground(Color.white);
  
        ssm.addStructureViewerListener(this);
        ssm.addSelectionListener(this);
      }
-     lastMouseColumn = -1;
-     lastMouseSeq = -1;
    }
  
    int startWrapBlock = -1;
    int wrappedBlock = -1;
  
    /**
+    * Computes the column and sequence row (and possibly annotation row when in
+    * wrapped mode) for the given mouse position
+    * 
+    * @param evt
+    * @return
+    */
+   MousePos findMousePosition(MouseEvent evt)
+   {
+     int col = findColumn(evt);
+     int seqIndex = -1;
+     int annIndex = -1;
+     int y = evt.getY();
+     int charHeight = av.getCharHeight();
+     int alignmentHeight = av.getAlignment().getHeight();
+     if (av.getWrapAlignment())
+     {
+       seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
+               seqCanvas.getHeight());
+       /*
+        * yPos modulo height of repeating width
+        */
+       int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
+       /*
+        * height of sequences plus space / scale above,
+        * plus gap between sequences and annotations
+        */
+       int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
+               + alignmentHeight * charHeight
+               + SeqCanvas.SEQS_ANNOTATION_GAP;
+       if (yOffsetPx >= alignmentHeightPixels)
+       {
+         /*
+          * mouse is over annotations; find annotation index, also set
+          * last sequence above (for backwards compatible behaviour)
+          */
+         AlignmentAnnotation[] anns = av.getAlignment()
+                 .getAlignmentAnnotation();
+         int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
+         annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
+         seqIndex = alignmentHeight - 1;
+       }
+       else
+       {
+         /*
+          * mouse is over sequence (or the space above sequences)
+          */
+         yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
+         if (yOffsetPx >= 0)
+         {
+           seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
+         }
+       }
+     }
+     else
+     {
+       seqIndex = Math.min((y / charHeight) + av.getRanges().getStartSeq(),
+               alignmentHeight - 1);
+     }
+     return new MousePos(col, seqIndex, annIndex);
+   }
+   /**
     * Returns the aligned sequence position (base 0) at the mouse position, or
     * the closest visible one
     * 
      int res = 0;
      int x = evt.getX();
  
-     int startRes = av.getRanges().getStartRes();
+     final int startRes = av.getRanges().getStartRes();
+     final int charWidth = av.getCharWidth();
      if (av.getWrapAlignment())
      {
        int hgap = av.getCharHeight();
        if (av.getScaleAboveWrapped())
        {
  
        int y = evt.getY();
        y = Math.max(0, y - hgap);
-       x = Math.max(0, x - seqCanvas.getLabelWidthWest());
+       x -= seqCanvas.getLabelWidthWest();
+       if (x < 0)
+       {
+         // mouse is over left scale
+         return -1;
+       }
  
        int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
        if (cwidth < 1)
        {
          return 0;
        }
+       if (x >= cwidth * charWidth)
+       {
+         // mouse is over right scale
+         return -1;
+       }
  
        wrappedBlock = y / cHeight;
        wrappedBlock += startRes / cwidth;
        // allow for wrapped view scrolled right (possible from Overview)
        int startOffset = startRes % cwidth;
        res = wrappedBlock * cwidth + startOffset
-               + +Math.min(cwidth - 1, x / av.getCharWidth());
+               + Math.min(cwidth - 1, x / charWidth);
      }
      else
      {
-       if (x > seqCanvas.getX() + seqCanvas.getWidth())
-       {
-         // make sure we calculate relative to visible alignment, rather than
-         // right-hand gutter
-         x = seqCanvas.getX() + seqCanvas.getWidth();
-       }
-       res = (x / av.getCharWidth()) + startRes;
-       if (res > av.getRanges().getEndRes())
-       {
-         // moused off right
-         res = av.getRanges().getEndRes();
-       }
+       /*
+        * make sure we calculate relative to visible alignment, 
+        * rather than right-hand gutter
+        */
+       x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
+       res = (x / charWidth) + startRes;
+       res = Math.min(res, av.getRanges().getEndRes());
      }
  
      if (av.hasHiddenColumns())
      }
  
      return res;
-   }
-   int findSeq(MouseEvent evt)
-   {
-     int seq = 0;
-     int y = evt.getY();
-     if (av.getWrapAlignment())
-     {
-       int hgap = av.getCharHeight();
-       if (av.getScaleAboveWrapped())
-       {
-         hgap += av.getCharHeight();
-       }
-       int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
-               + hgap + seqCanvas.getAnnotationHeight();
-       y -= hgap;
-       seq = Math.min((y % cHeight) / av.getCharHeight(),
-               av.getAlignment().getHeight() - 1);
-     }
-     else
-     {
-       seq = Math.min(
-               (y / av.getCharHeight()) + av.getRanges().getStartSeq(),
-               av.getAlignment().getHeight() - 1);
-     }
-     return seq;
    }
  
    /**
        /*
         * Tidy up come what may...
         */
-       startseq = -1;
-       lastres = -1;
+       editStartSeq = -1;
+       editLastRes = -1;
        editingSeqs = false;
        groupEditing = false;
        keyboardNo1 = null;
    void insertGapAtCursor(boolean group)
    {
      groupEditing = group;
-     startseq = seqCanvas.cursorY;
-     lastres = seqCanvas.cursorX;
+     editStartSeq = seqCanvas.cursorY;
+     editLastRes = seqCanvas.cursorX;
      editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
      endEditing();
    }
    void deleteGapAtCursor(boolean group)
    {
      groupEditing = group;
-     startseq = seqCanvas.cursorY;
-     lastres = seqCanvas.cursorX + getKeyboardNo1();
+     editStartSeq = seqCanvas.cursorY;
+     editLastRes = seqCanvas.cursorX + getKeyboardNo1();
      editSequence(false, false, seqCanvas.cursorX);
      endEditing();
    }
    {
      // TODO not called - delete?
      groupEditing = group;
-     startseq = seqCanvas.cursorY;
-     lastres = seqCanvas.cursorX;
+     editStartSeq = seqCanvas.cursorY;
+     editLastRes = seqCanvas.cursorX;
      editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
      endEditing();
    }
    @Override
    public void mouseReleased(MouseEvent evt)
    {
+     MousePos pos = findMousePosition(evt);
+     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
+     {
+       return;
+     }
      boolean didDrag = mouseDragging; // did we come here after a drag
      mouseDragging = false;
      mouseWheelPressed = false;
  
      if (evt.isPopupTrigger()) // Windows: mouseReleased
      {
-       showPopupMenu(evt);
+       showPopupMenu(evt, pos);
        evt.consume();
        return;
      }
    public void mousePressed(MouseEvent evt)
    {
      lastMousePress = evt.getPoint();
+     MousePos pos = findMousePosition(evt);
+     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
+     {
+       return;
+     }
  
      if (SwingUtilities.isMiddleMouseButton(evt))
      {
      }
      else
      {
-       doMousePressedDefineMode(evt);
+       doMousePressedDefineMode(evt, pos);
        return;
      }
  
-     int seq = findSeq(evt);
-     int res = findColumn(evt);
-     if (seq < 0 || res < 0)
-     {
-       return;
-     }
+     int seq = pos.seqIndex;
+     int res = pos.column;
  
      if ((seq < av.getAlignment().getHeight())
              && (res < av.getAlignment().getSequenceAt(seq).getLength()))
      {
-       startseq = seq;
-       lastres = res;
+       editStartSeq = seq;
+       editLastRes = res;
      }
      else
      {
-       startseq = -1;
-       lastres = -1;
+       editStartSeq = -1;
+       editLastRes = -1;
      }
  
      return;
  
    String lastMessage;
  
 +  private String formattedTooltipText;
 +
    @Override
    public void mouseOverSequence(SequenceI sequence, int index, int pos)
    {
        // over residue to change abruptly, causing highlighted residue in panel 2
        // to change, causing a scroll in panel 1 etc)
        ap.setToScrollComplementPanel(false);
-       wasScrolled = ap.scrollToPosition(results, false);
+       wasScrolled = ap.scrollToPosition(results);
        if (wasScrolled)
        {
          seqCanvas.revalidate();
        ap.setToScrollComplementPanel(true);
      }
  
 -    boolean noFastPaint = wasScrolled && av.getWrapAlignment();
 -    if (seqCanvas.highlightSearchResults(results, noFastPaint))
 +    boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
 +    if (seqCanvas.highlightSearchResults(results, fastPaint))
      {
        setStatusMessage(results);
      }
        mouseDragged(evt);
      }
  
-     final int column = findColumn(evt);
-     final int seq = findSeq(evt);
+     final MousePos mousePos = findMousePosition(evt);
+     if (mousePos.equals(lastMousePosition))
+     {
+       /*
+        * just a pixel move without change of 'cell'
+        */
+       return;
+     }
+     lastMousePosition = mousePos;
  
-     if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
+     if (mousePos.isOverAnnotation())
      {
-       lastMouseSeq = -1;
+       mouseMovedOverAnnotation(mousePos);
        return;
      }
-     if (column == lastMouseColumn && seq == lastMouseSeq)
+     final int seq = mousePos.seqIndex;
+     final int column = mousePos.column;
+     if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
      {
-       /*
-        * just a pixel move without change of residue
-        */
+       lastMousePosition = null;
+       setToolTipText(null);
+       lastTooltip = null;
+       ap.alignFrame.setStatus("");
        return;
      }
-     lastMouseColumn = column;
-     lastMouseSeq = seq;
  
      SequenceI sequence = av.getAlignment().getSequenceAt(seq);
  
        mouseOverSequence(sequence, column, pos);
      }
  
 -    tooltipText.setLength(6); // Cuts the buffer back to <html>
 +    tooltipText.setLength(6); // "<html>"
  
      SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
      if (groups != null)
        String textString = tooltipText.toString();
        if (lastTooltip == null || !lastTooltip.equals(textString))
        {
 -        String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
 +        formattedTooltipText = JvSwingUtils.wrapTooltip(true,
                  textString);
 -        setToolTipText(formattedTooltipText);
 +        setToolTipText(formattedTooltipText);        
          lastTooltip = textString;
        }
      }
    }
  
-   
+   /**
+    * When the view is in wrapped mode, and the mouse is over an annotation row,
+    * shows the corresponding tooltip and status message (if any)
+    * 
+    * @param pos
+    * @param column
+    */
+   protected void mouseMovedOverAnnotation(MousePos pos)
+   {
+     final int column = pos.column;
+     final int rowIndex = pos.annotationIndex;
+     if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
+             || rowIndex < 0)
+     {
+       return;
+     }
+     AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
+     String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
+             anns);
+     setToolTipText(tooltip);
+     lastTooltip = tooltip;
+     String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
+             anns[rowIndex]);
+     ap.alignFrame.setStatus(msg);
+   }
    private Point lastp = null;
  
 +  private JToolTip tempTip = new JLabel().createToolTip();
 +
    /*
     * (non-Javadoc)
     * 
    @Override
    public Point getToolTipLocation(MouseEvent event)
    {
 +    // BH 2018
 +
-     if (tooltipText == null || tooltipText.length() == 6)
+     if (tooltipText == null || tooltipText.length() <= 6)
+     {
 -      lastp = null;
        return null;
+     }
  
 -    int x = event.getX();
 -    int w = getWidth();
 -    // switch sides when tooltip is too close to edge
 -    int wdth = (w - x < 200) ? -(w / 2) : 5;
 -    Point p = lastp;
 -    if (!event.isShiftDown() || p == null)
 +    if (lastp != null && event.isShiftDown())
+     {
 -      p = new Point(event.getX() + wdth, event.getY() - 20);
 -      lastp = p;
 +      return lastp;
+     }
 -    /*
 -     * TODO: try to set position so region is not obscured by tooltip
 -     */
 -    return p;
 +
 +    Point p = lastp;
 +    int x = event.getX();
 +    int y = event.getY();
 +    int w = getWidth();
 +
 +    tempTip.setTipText(formattedTooltipText);
 +    int tipWidth = (int) tempTip.getPreferredSize().getWidth();
 +    
 +    // was      x += (w - x < 200) ? -(w / 2) : 5;
 +    x = (x + tipWidth < w ? x + 10 : w - tipWidth);
 +    p = new Point(x, y + 20); // BH 2018 was - 20?
-     /*
-      * TODO: try to modify position region is not obcured by tooltip
-      * 
-      * Done? 
-      */
 +
 +    return lastp = p;
    }
  
    String lastTooltip;
    @Override
    public void mouseDragged(MouseEvent evt)
    {
+     MousePos pos = findMousePosition(evt);
+     if (pos.isOverAnnotation() || pos.column == -1)
+     {
+       return;
+     }
      if (mouseWheelPressed)
      {
        boolean inSplitFrame = ap.av.getCodingComplement() != null;
  
      if (!editingSeqs)
      {
-       doMouseDraggedDefineMode(evt);
+       dragStretchGroup(evt);
        return;
      }
  
-     int res = findColumn(evt);
+     int res = pos.column;
  
      if (res < 0)
      {
        res = 0;
      }
  
-     if ((lastres == -1) || (lastres == res))
+     if ((editLastRes == -1) || (editLastRes == res))
      {
        return;
      }
  
-     if ((res < av.getAlignment().getWidth()) && (res < lastres))
+     if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
      {
        // dragLeft, delete gap
        editSequence(false, false, res);
      }
    }
  
-   // TODO: Make it more clever than many booleans
+   /**
+    * Edits the sequence to insert or delete one or more gaps, in response to a
+    * mouse drag or cursor mode command. The number of inserts/deletes may be
+    * specified with the cursor command, or else depends on the mouse event
+    * (normally one column, but potentially more for a fast mouse drag).
+    * <p>
+    * Delete gaps is limited to the number of gaps left of the cursor position
+    * (mouse drag), or at or right of the cursor position (cursor mode).
+    * <p>
+    * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
+    * the current selection group.
+    * <p>
+    * In locked editing mode (with a selection group present), inserts/deletions
+    * within the selection group are limited to its boundaries (and edits outside
+    * the group stop at its border).
+    * 
+    * @param insertGap
+    *          true to insert gaps, false to delete gaps
+    * @param editSeq
+    *          (unused parameter)
+    * @param startres
+    *          the column at which to perform the action; the number of columns
+    *          affected depends on <code>this.editLastRes</code> (cursor column
+    *          position)
+    */
    synchronized void editSequence(boolean insertGap, boolean editSeq,
-           int startres)
+           final int startres)
    {
      int fixedLeft = -1;
      int fixedRight = -1;
      boolean fixedColumns = false;
      SequenceGroup sg = av.getSelectionGroup();
  
-     SequenceI seq = av.getAlignment().getSequenceAt(startseq);
+     final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
  
      // No group, but the sequence may represent a group
      if (!groupEditing && av.hasHiddenRows())
        }
      }
  
-     StringBuilder message = new StringBuilder(64);
+     StringBuilder message = new StringBuilder(64); // for status bar
+     /*
+      * make a name for the edit action, for
+      * status bar message and Undo/Redo menu
+      */
+     String label = null;
      if (groupEditing)
      {
-       message.append("Edit group:");
-       if (editCommand == null)
-       {
-         editCommand = new EditCommand(
-                 MessageManager.getString("action.edit_group"));
-       }
+         message.append("Edit group:");
+       label = MessageManager.getString("action.edit_group");
      }
      else
      {
-       message.append("Edit sequence: " + seq.getName());
-       String label = seq.getName();
+         message.append("Edit sequence: " + seq.getName());
+       label = seq.getName();
        if (label.length() > 10)
        {
          label = label.substring(0, 10);
        }
-       if (editCommand == null)
-       {
-         editCommand = new EditCommand(MessageManager
-                 .formatMessage("label.edit_params", new String[]
-                 { label }));
-       }
+       label = MessageManager.formatMessage("label.edit_params",
+               new String[]
+               { label });
+     }
+     /*
+      * initialise the edit command if there is not
+      * already one being extended
+      */
+     if (editCommand == null)
+     {
+       editCommand = new EditCommand(label);
      }
  
      if (insertGap)
        message.append(" delete ");
      }
  
-     message.append(Math.abs(startres - lastres) + " gaps.");
+     message.append(Math.abs(startres - editLastRes) + " gaps.");
      ap.alignFrame.setStatus(message.toString());
  
-     // Are we editing within a selection group?
-     if (groupEditing || (sg != null
-             && sg.getSequences(av.getHiddenRepSequences()).contains(seq)))
+     /*
+      * is there a selection group containing the sequence being edited?
+      * if so the boundary of the group is the limit of the edit
+      * (but the edit may be inside or outside the selection group)
+      */
+     boolean inSelectionGroup = sg != null
+             && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
+     if (groupEditing || inSelectionGroup)
      {
        fixedColumns = true;
  
        fixedLeft = sg.getStartRes();
        fixedRight = sg.getEndRes();
  
-       if ((startres < fixedLeft && lastres >= fixedLeft)
-               || (startres >= fixedLeft && lastres < fixedLeft)
-               || (startres > fixedRight && lastres <= fixedRight)
-               || (startres <= fixedRight && lastres > fixedRight))
+       if ((startres < fixedLeft && editLastRes >= fixedLeft)
+               || (startres >= fixedLeft && editLastRes < fixedLeft)
+               || (startres > fixedRight && editLastRes <= fixedRight)
+               || (startres <= fixedRight && editLastRes > fixedRight))
        {
          endEditing();
          return;
        int y2 = av.getAlignment().getHiddenColumns()
                .getNextHiddenBoundary(false, startres);
  
-       if ((insertGap && startres > y1 && lastres < y1)
-               || (!insertGap && startres < y2 && lastres > y2))
+       if ((insertGap && startres > y1 && editLastRes < y1)
+               || (!insertGap && startres < y2 && editLastRes > y2))
        {
          endEditing();
          return;
        }
      }
  
+     boolean success = doEditSequence(insertGap, editSeq, startres,
+             fixedRight, fixedColumns, sg);
+     /*
+      * report what actually happened (might be less than
+      * what was requested), by inspecting the edit commands added
+      */
+     String msg = getEditStatusMessage(editCommand);
+     ap.alignFrame.setStatus(msg == null ? " " : msg);
+     if (!success)
+     {
+       endEditing();
+     }
+     editLastRes = startres;
+     seqCanvas.repaint();
+   }
+   /**
+    * A helper method that performs the requested editing to insert or delete
+    * gaps (if possible). Answers true if the edit was successful, false if could
+    * only be performed in part or not at all. Failure may occur in 'locked edit'
+    * mode, when an insertion requires a matching gapped position (or column) to
+    * delete, and deletion requires an adjacent gapped position (or column) to
+    * remove.
+    * 
+    * @param insertGap
+    *          true if inserting gap(s), false if deleting
+    * @param editSeq
+    *          (unused parameter, currently always false)
+    * @param startres
+    *          the column at which to perform the edit
+    * @param fixedRight
+    *          fixed right boundary column of a locked edit (within or to the
+    *          left of a selection group)
+    * @param fixedColumns
+    *          true if this is a locked edit
+    * @param sg
+    *          the sequence group (if group edit is being performed)
+    * @return
+    */
+   protected boolean doEditSequence(final boolean insertGap,
+           final boolean editSeq, final int startres, int fixedRight,
+           final boolean fixedColumns, final SequenceGroup sg)
+   {
+     final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
+     SequenceI[] seqs = new SequenceI[] { seq };
      if (groupEditing)
      {
        List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
          if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
                  && sg.getEndRes() == av.getAlignment().getWidth() - 1)
          {
-           sg.setEndRes(av.getAlignment().getWidth() + startres - lastres);
+           sg.setEndRes(
+                   av.getAlignment().getWidth() + startres - editLastRes);
            fixedRight = sg.getEndRes();
          }
  
          // Find the next gap before the end
          // of the visible region boundary
          boolean blank = false;
-         for (; fixedRight > lastres; fixedRight--)
+         for (; fixedRight > editLastRes; fixedRight--)
          {
            blank = true;
  
            for (g = 0; g < groupSize; g++)
            {
-             for (int j = 0; j < startres - lastres; j++)
+             for (int j = 0; j < startres - editLastRes; j++)
              {
-               if (!Comparison.isGap(groupSeqs[g].getCharAt(fixedRight - j)))
+               if (!Comparison
+                       .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
                {
                  blank = false;
                  break;
          {
            if (sg.getSize() == av.getAlignment().getHeight())
            {
-             if ((av.hasHiddenColumns() && startres < av.getAlignment()
-                     .getHiddenColumns()
-                     .getNextHiddenBoundary(false, startres)))
+             if ((av.hasHiddenColumns()
+                     && startres < av.getAlignment().getHiddenColumns()
+                             .getNextHiddenBoundary(false, startres)))
              {
-               endEditing();
-               return;
+               return false;
              }
  
              int alWidth = av.getAlignment().getWidth();
              }
              // We can still insert gaps if the selectionGroup
              // contains all the sequences
-             sg.setEndRes(sg.getEndRes() + startres - lastres);
-             fixedRight = alWidth + startres - lastres;
+             sg.setEndRes(sg.getEndRes() + startres - editLastRes);
+             fixedRight = alWidth + startres - editLastRes;
            }
            else
            {
-             endEditing();
-             return;
+             return false;
            }
          }
        }
  
          for (g = 0; g < groupSize; g++)
          {
-           for (int j = startres; j < lastres; j++)
+           for (int j = startres; j < editLastRes; j++)
            {
              if (groupSeqs[g].getLength() <= j)
              {
              if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
              {
                // Not a gap, block edit not valid
-               endEditing();
-               return;
+               return false;
              }
            }
          }
          // dragging to the right
          if (fixedColumns && fixedRight != -1)
          {
-           for (int j = lastres; j < startres; j++)
+           for (int j = editLastRes; j < startres; j++)
            {
-             insertChar(j, groupSeqs, fixedRight);
+             insertGap(j, groupSeqs, fixedRight);
            }
          }
          else
          {
            appendEdit(Action.INSERT_GAP, groupSeqs, startres,
-                   startres - lastres);
+                   startres - editLastRes, false);
          }
        }
        else
          // dragging to the left
          if (fixedColumns && fixedRight != -1)
          {
-           for (int j = lastres; j > startres; j--)
+           for (int j = editLastRes; j > startres; j--)
            {
              deleteChar(startres, groupSeqs, fixedRight);
            }
          else
          {
            appendEdit(Action.DELETE_GAP, groupSeqs, startres,
-                   lastres - startres);
+                   editLastRes - startres, false);
          }
        }
      }
      else
-     // ///Editing a single sequence///////////
      {
+       /*
+        * editing a single sequence
+        */
        if (insertGap)
        {
          // dragging to the right
          if (fixedColumns && fixedRight != -1)
          {
-           for (int j = lastres; j < startres; j++)
+           for (int j = editLastRes; j < startres; j++)
            {
-             insertChar(j, new SequenceI[] { seq }, fixedRight);
+             if (!insertGap(j, seqs, fixedRight))
+             {
+               /*
+                * e.g. cursor mode command specified 
+                * more inserts than are possible
+                */
+               return false;
+             }
            }
          }
          else
          {
-           appendEdit(Action.INSERT_GAP, new SequenceI[] { seq }, lastres,
-                   startres - lastres);
+           appendEdit(Action.INSERT_GAP, seqs, editLastRes,
+                   startres - editLastRes, false);
          }
        }
        else
            // dragging to the left
            if (fixedColumns && fixedRight != -1)
            {
-             for (int j = lastres; j > startres; j--)
+             for (int j = editLastRes; j > startres; j--)
              {
                if (!Comparison.isGap(seq.getCharAt(startres)))
                {
-                 endEditing();
-                 break;
+                 return false;
                }
-               deleteChar(startres, new SequenceI[] { seq }, fixedRight);
+               deleteChar(startres, seqs, fixedRight);
              }
            }
            else
            {
              // could be a keyboard edit trying to delete none gaps
              int max = 0;
-             for (int m = startres; m < lastres; m++)
+             for (int m = startres; m < editLastRes; m++)
              {
                if (!Comparison.isGap(seq.getCharAt(m)))
                {
                }
                max++;
              }
              if (max > 0)
              {
-               appendEdit(Action.DELETE_GAP, new SequenceI[] { seq },
-                       startres, max);
+               appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
              }
            }
          }
          {// insertGap==false AND editSeq==TRUE;
            if (fixedColumns && fixedRight != -1)
            {
-             for (int j = lastres; j < startres; j++)
+             for (int j = editLastRes; j < startres; j++)
              {
-               insertChar(j, new SequenceI[] { seq }, fixedRight);
+               insertGap(j, seqs, fixedRight);
              }
            }
            else
            {
-             appendEdit(Action.INSERT_NUC, new SequenceI[] { seq }, lastres,
-                     startres - lastres);
+             appendEdit(Action.INSERT_NUC, seqs, editLastRes,
+                     startres - editLastRes, false);
            }
          }
        }
      }
  
-     lastres = startres;
-     seqCanvas.repaint();
+     return true;
+   }
+   /**
+    * Constructs an informative status bar message while dragging to insert or
+    * delete gaps. Answers null if inserts and deletes cancel out.
+    * 
+    * @param editCommand
+    *          a command containing the list of individual edits
+    * @return
+    */
+   protected static String getEditStatusMessage(EditCommand editCommand)
+   {
+     if (editCommand == null)
+     {
+       return null;
+     }
+     /*
+      * add any inserts, and subtract any deletes,  
+      * not counting those auto-inserted when doing a 'locked edit'
+      * (so only counting edits 'under the cursor')
+      */
+     int count = 0;
+     for (Edit cmd : editCommand.getEdits())
+     {
+       if (!cmd.isSystemGenerated())
+       {
+         count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
+                 : -cmd.getNumber();
+       }
+     }
+     if (count == 0)
+     {
+       /*
+        * inserts and deletes cancel out
+        */
+       return null;
+     }
+     String msgKey = count > 1 ? "label.insert_gaps"
+             : (count == 1 ? "label.insert_gap"
+                     : (count == -1 ? "label.delete_gap"
+                             : "label.delete_gaps"));
+     count = Math.abs(count);
+     return MessageManager.formatMessage(msgKey, String.valueOf(count));
    }
  
-   void insertChar(int j, SequenceI[] seq, int fixedColumn)
+   /**
+    * Inserts one gap at column j, deleting the right-most gapped column up to
+    * (and including) fixedColumn. Returns true if the edit is successful, false
+    * if no blank column is available to allow the insertion to be balanced by a
+    * deletion.
+    * 
+    * @param j
+    * @param seq
+    * @param fixedColumn
+    * @return
+    */
+   boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
    {
      int blankColumn = fixedColumn;
      for (int s = 0; s < seq.length; s++)
        {
          blankColumn = fixedColumn;
          endEditing();
-         return;
+         return false;
        }
      }
  
-     appendEdit(Action.DELETE_GAP, seq, blankColumn, 1);
+     appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
  
-     appendEdit(Action.INSERT_GAP, seq, j, 1);
+     appendEdit(Action.INSERT_GAP, seq, j, 1, false);
  
+     return true;
    }
  
    /**
-    * Helper method to add and perform one edit action.
+    * Helper method to add and perform one edit action
     * 
     * @param action
     * @param seq
     * @param pos
     * @param count
+    * @param systemGenerated
+    *          true if the edit is a 'balancing' delete (or insert) to match a
+    *          user's insert (or delete) in a locked editing region
     */
    protected void appendEdit(Action action, SequenceI[] seq, int pos,
-           int count)
+           int count, boolean systemGenerated)
    {
  
      final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
              av.getAlignment().getGapCharacter());
+     edit.setSystemGenerated(systemGenerated);
  
      editCommand.appendEdit(edit, av.getAlignment(), true, null);
    }
  
-   void deleteChar(int j, SequenceI[] seq, int fixedColumn)
+   /**
+    * Deletes the character at column j, and inserts a gap at fixedColumn, in
+    * each of the given sequences. The caller should ensure that all sequences
+    * are gapped in column j.
+    * 
+    * @param j
+    * @param seqs
+    * @param fixedColumn
+    */
+   void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
    {
+     appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
  
-     appendEdit(Action.DELETE_GAP, seq, j, 1);
-     appendEdit(Action.INSERT_GAP, seq, fixedColumn, 1);
+     appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
    }
  
    /**
    @Override
    public void mouseExited(MouseEvent e)
    {
-     if (mouseDragging)
+     ap.alignFrame.setStatus(" ");
+     if (av.getWrapAlignment())
+     {
+       return;
+     }
+     if (mouseDragging && scrollThread == null)
      {
 -      scrollThread = new ScrollThread();
 +      startScrolling(e.getPoint());
      }
    }
  
    public void mouseClicked(MouseEvent evt)
    {
      SequenceGroup sg = null;
-     SequenceI sequence = av.getAlignment().getSequenceAt(findSeq(evt));
+     MousePos pos = findMousePosition(evt);
+     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
+     {
+       return;
+     }
      if (evt.getClickCount() > 1)
      {
        sg = av.getSelectionGroup();
          av.setSelectionGroup(null);
        }
  
-       int column = findColumn(evt);
+       int column = pos.column;
  
        /*
         * find features at the position (if not gapped), or straddling
         * the position (if at a gap)
         */
+       SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
        List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
                .findFeaturesAtColumn(sequence, column + 1);
  
          SearchResultsI highlight = new SearchResults();
          highlight.addResult(sequence, features.get(0).getBegin(), features
                  .get(0).getEnd());
 -        seqCanvas.highlightSearchResults(highlight, false);
 +        seqCanvas.highlightSearchResults(highlight, true);
  
          /*
 -         * open the Amend Features dialog; clear highlighting afterwards,
 -         * whether changes were made or not
 +         * open the Amend Features dialog
           */
 -        List<SequenceI> seqs = Collections.singletonList(sequence);
 -        seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
 -                ap);
 -        av.setSearchResults(null); // clear highlighting
 -        seqCanvas.repaint(); // draw new/amended features
 +        new FeatureEditor(ap, Collections.singletonList(sequence), features,
 +                false).showDialog();
        }
      }
    }
    /**
     * DOCUMENT ME!
     * 
-    * @param evt
+    * @param pos
     *          DOCUMENT ME!
     */
-   public void doMousePressedDefineMode(MouseEvent evt)
+   protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
    {
-     final int res = findColumn(evt);
-     final int seq = findSeq(evt);
-     oldSeq = seq;
-     updateOverviewAndStructs = false;
-     startWrapBlock = wrappedBlock;
-     if (av.getWrapAlignment() && seq > av.getAlignment().getHeight())
+     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
      {
        return;
      }
  
-     if (seq < 0 || res < 0)
-     {
-       return;
-     }
+     final int res = pos.column;
+     final int seq = pos.seqIndex;
+     oldSeq = seq;
+     updateOverviewAndStructs = false;
+     startWrapBlock = wrappedBlock;
  
      SequenceI sequence = av.getAlignment().getSequenceAt(seq);
  
        }
      }
  
 -    if (evt.isPopupTrigger()) // Mac: mousePressed
 -    {
 -      showPopupMenu(evt, pos);
 -      return;
 -    }
 -
      /*
       * defer right-mouse click handling to mouseReleased on Windows
       * (where isPopupTrigger() will answer true)
       * NB isRightMouseButton is also true for Cmd-click on Mac
       */
 -    if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
 +    if (Platform.isWinRightButton(evt))
 +    {
 +      return;
 +    }
 +
 +    if (evt.isPopupTrigger()) // Mac: mousePressed
      {
-       showPopupMenu(evt);
++      showPopupMenu(evt, pos);
        return;
      }
  
      if (av.cursorMode)
      {
-       seqCanvas.cursorX = findColumn(evt);
-       seqCanvas.cursorY = findSeq(evt);
+       seqCanvas.cursorX = res;
+       seqCanvas.cursorY = seq;
        seqCanvas.repaint();
        return;
      }
  
    /**
     * Build and show a pop-up menu at the right-click mouse position
-    * 
+    *
     * @param evt
-    * @param res
-    * @param sequences
+    * @param pos
     */
-   void showPopupMenu(MouseEvent evt)
+   void showPopupMenu(MouseEvent evt, MousePos pos)
    {
-     final int column = findColumn(evt);
-     final int seq = findSeq(evt);
+     final int column = pos.column;
+     final int seq = pos.seqIndex;
      SequenceI sequence = av.getAlignment().getSequenceAt(seq);
      List<SequenceFeature> features = ap.getFeatureRenderer()
              .findFeaturesAtColumn(sequence, column + 1);
     *          true if this event is happening after a mouse drag (rather than a
     *          mouse down)
     */
-   public void doMouseReleasedDefineMode(MouseEvent evt, boolean afterDrag)
+   protected void doMouseReleasedDefineMode(MouseEvent evt,
+           boolean afterDrag)
    {
      if (stretchGroup == null)
      {
              && afterDrag;
      if (stretchGroup.cs != null)
      {
-       stretchGroup.cs.alignmentChanged(stretchGroup,
-               av.getHiddenRepSequences());
+       if (afterDrag)
+       {
+         stretchGroup.cs.alignmentChanged(stretchGroup,
+                 av.getHiddenRepSequences());
+       }
  
        ResidueShaderI groupColourScheme = stretchGroup
                .getGroupColourScheme();
    }
  
    /**
-    * DOCUMENT ME!
+    * Resizes the borders of a selection group depending on the direction of
+    * mouse drag
     * 
     * @param evt
-    *          DOCUMENT ME!
     */
-   public void doMouseDraggedDefineMode(MouseEvent evt)
+   protected void dragStretchGroup(MouseEvent evt)
    {
-     int res = findColumn(evt);
-     int y = findSeq(evt);
+     if (stretchGroup == null)
+     {
+       return;
+     }
  
-     if (wrappedBlock != startWrapBlock)
+     MousePos pos = findMousePosition(evt);
+     if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
      {
        return;
      }
  
-     if (stretchGroup == null)
+     int res = pos.column;
+     int y = pos.seqIndex;
+     if (wrappedBlock != startWrapBlock)
      {
        return;
      }
      {
        scrollThread.setMousePosition(evt.getPoint());
      }
+     /*
+      * construct a status message showing the range of the selection
+      */
+     StringBuilder status = new StringBuilder(64);
+     List<SequenceI> seqs = stretchGroup.getSequences();
+     String name = seqs.get(0).getName();
+     if (name.length() > 20)
+     {
+       name = name.substring(0, 20);
+     }
+     status.append(name).append(" - ");
+     name = seqs.get(seqs.size() - 1).getName();
+     if (name.length() > 20)
+     {
+       name = name.substring(0, 20);
+     }
+     status.append(name).append(" ");
+     int startRes = stretchGroup.getStartRes();
+     status.append(" cols ").append(String.valueOf(startRes + 1))
+             .append("-");
+     int endRes = stretchGroup.getEndRes();
+     status.append(String.valueOf(endRes + 1));
+     status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
+             .append(String.valueOf(endRes - startRes + 1)).append(")");
+     ap.alignFrame.setStatus(status.toString());
    }
  
    /**
  
    /**
     * Starts a thread to scroll the alignment, towards a given mouse position
 -   * outside the panel bounds
 +   * outside the panel bounds, unless the alignment is in wrapped mode
     * 
     * @param mousePos
     */
    void startScrolling(Point mousePos)
    {
 -    if (scrollThread == null)
 +    /*
 +     * set this.mouseDragging in case this was called from 
 +     * a drag in ScalePanel or AnnotationPanel
 +     */
 +    mouseDragging = true;
 +    if (!av.getWrapAlignment() && scrollThread == null)
      {
        scrollThread = new ScrollThread();
 +      scrollThread.setMousePosition(mousePos);
 +      if (!Platform.isJS())
 +      {
 +        /*
 +         * Java - run in a new thread
 +         */
 +        scrollThread.start();
 +      }
 +      else
 +      {
 +        /*
 +         * Javascript - run every 20ms until scrolling stopped
 +         * or reaches the limit of scrollable alignment
 +         */
 +        // java.util.Timer version:
 +        // Timer t = new Timer("ScrollThreadTimer", true);
 +        // TimerTask task = new TimerTask()
 +        // {
 +        // @Override
 +        // public void run()
 +        // {
 +        // if (!scrollThread.scrollOnce())
 +        // {
 +        // cancel();
 +        // }
 +        // }
 +        // };
 +        // t.schedule(task, 20, 20);
 +        Timer t = new Timer(20, new ActionListener()
 +        {
 +          @Override
 +          public void actionPerformed(ActionEvent e)
 +          {
 +            if (scrollThread != null)
 +            {
 +              // if (!scrollOnce() {t.stop();}) gives compiler error :-(
 +              scrollThread.scrollOnce();
 +            }
 +          }
 +        });
 +        t.addActionListener(new ActionListener()
 +        {
 +          @Override
 +          public void actionPerformed(ActionEvent e)
 +          {
 +            if (scrollThread == null)
 +            {
 +              // finished and nulled itself
 +              t.stop();
 +            }
 +          }
 +        });
 +        t.start();
 +      }
      }
    }
  
    /**
 -   * Performs scrolling of the visible alignment left, right, up or down
 +   * Performs scrolling of the visible alignment left, right, up or down, until
 +   * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
 +   * limit of the alignment is reached
     */
    class ScrollThread extends Thread
    {
      private Point mousePos;
  
 -    private volatile boolean threadRunning = true;
 +    private volatile boolean keepRunning = true;
  
      /**
       * Constructor
      public ScrollThread()
      {
        setName("SeqPanel$ScrollThread");
 -      start();
      }
  
      /**
       * Sets the position of the mouse that determines the direction of the
 -     * scroll to perform
 +     * scroll to perform. If this is called as the mouse moves, scrolling should
 +     * respond accordingly. For example, if the mouse is dragged right, scroll
 +     * right should start; if the drag continues down, scroll down should also
 +     * happen.
       * 
       * @param p
       */
       */
      public void stopScrolling()
      {
 -      threadRunning = false;
 +      keepRunning = false;
      }
  
      /**
      @Override
      public void run()
      {
 -      while (threadRunning && mouseDragging)
 +      while (keepRunning)
        {
          if (mousePos != null)
          {
 -          boolean scrolled = false;
 -          ViewportRanges ranges = SeqPanel.this.av.getRanges();
 -
 -          /*
 -           * scroll up or down
 -           */
 -          if (mousePos.y < 0)
 -          {
 -            // mouse is above this panel - try scroll up
 -            scrolled = ranges.scrollUp(true);
 -          }
 -          else if (mousePos.y >= getHeight())
 -          {
 -            // mouse is below this panel - try scroll down
 -            scrolled = ranges.scrollUp(false);
 -          }
 -
 -          /*
 -           * scroll left or right
 -           */
 -          if (mousePos.x < 0)
 -          {
 -            scrolled |= ranges.scrollRight(false);
 -          }
 -          else if (mousePos.x >= getWidth())
 -          {
 -            scrolled |= ranges.scrollRight(true);
 -          }
 -          if (!scrolled)
 -          {
 -            /*
 -             * we have reached the limit of the visible alignment - quit
 -             */
 -            threadRunning = false;
 -            SeqPanel.this.ap.repaint();
 -          }
 +          keepRunning = scrollOnce();
          }
 -
          try
          {
            Thread.sleep(20);
          {
          }
        }
 +      SeqPanel.this.scrollThread = null;
 +    }
 +
 +    /**
 +     * Scrolls
 +     * <ul>
 +     * <li>one row up, if the mouse is above the panel</li>
 +     * <li>one row down, if the mouse is below the panel</li>
 +     * <li>one column left, if the mouse is left of the panel</li>
 +     * <li>one column right, if the mouse is right of the panel</li>
 +     * </ul>
 +     * Answers true if a scroll was performed, false if not - meaning either
 +     * that the mouse position is within the panel, or the edge of the alignment
 +     * has been reached.
 +     */
 +    boolean scrollOnce()
 +    {
 +      /*
 +       * quit after mouseUp ensures interrupt in JalviewJS
 +       */
 +      if (!mouseDragging)
 +      {
 +        return false;
 +      }
 +
 +      boolean scrolled = false;
 +      ViewportRanges ranges = SeqPanel.this.av.getRanges();
 +
 +      /*
 +       * scroll up or down
 +       */
 +      if (mousePos.y < 0)
 +      {
 +        // mouse is above this panel - try scroll up
 +        scrolled = ranges.scrollUp(true);
 +      }
 +      else if (mousePos.y >= getHeight())
 +      {
 +        // mouse is below this panel - try scroll down
 +        scrolled = ranges.scrollUp(false);
 +      }
 +
 +      /*
 +       * scroll left or right
 +       */
 +      if (mousePos.x < 0)
 +      {
 +        scrolled |= ranges.scrollRight(false);
 +      }
 +      else if (mousePos.x >= getWidth())
 +      {
 +        scrolled |= ranges.scrollRight(true);
 +      }
 +      return scrolled;
      }
    }
  
      HiddenColumns hs = new HiddenColumns();
      MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
      av.setColumnSelection(cs);
-     av.getAlignment().setHiddenColumns(hs);
+     boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
  
      // lastly, update any dependent dialogs
      if (ap.getCalculationDialog() != null)
        ap.getCalculationDialog().validateCalcTypes();
      }
  
-     PaintRefresher.Refresh(this, av.getSequenceSetId());
+     /*
+      * repaint alignment, and also Overview or Structure
+      * if hidden column selection has changed
+      */
+     ap.paintAlignment(hiddenChanged, hiddenChanged);
  
      return true;
    }
@@@ -26,6 -26,7 +26,7 @@@ import jalview.jbgui.GSliderPanel
  import jalview.renderer.ResidueShaderI;
  import jalview.util.MessageManager;
  
+ import java.awt.event.ActionEvent;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
  import java.beans.PropertyVetoException;
@@@ -117,7 -118,7 +118,7 @@@ public class SliderPanel extends GSlide
        @Override
        public void stateChanged(ChangeEvent evt)
        {
 -        valueField.setText(slider.getValue() + "");
 +        valueField.setText(String.valueOf(slider.getValue()));
          valueChanged(slider.getValue());
        }
      });
      });
  
      slider.setValue(value);
 -    valueField.setText(value + "");
 +    valueField.setText(String.valueOf(value));
    }
  
    /**
      if (!conservationSlider.isVisible())
      {
        Desktop.addInternalFrame(conservationSlider,
 -              conservationSlider.getTitle(), 420, 90, false);
 +              conservationSlider.getTitle(), true, FRAME_WIDTH,
 +              FRAME_HEIGHT, false, true);
        conservationSlider.addInternalFrameListener(new InternalFrameAdapter()
        {
          @Override
  
      if (!PIDSlider.isVisible())
      {
 -      Desktop.addInternalFrame(PIDSlider, PIDSlider.getTitle(), 420, 90,
 -              false);
 +      Desktop.addInternalFrame(PIDSlider, PIDSlider.getTitle(), true,
 +              FRAME_WIDTH, FRAME_HEIGHT, false, true);
        PIDSlider.setLayer(JLayeredPane.PALETTE_LAYER);
        PIDSlider.addInternalFrameListener(new InternalFrameAdapter()
        {
    public void setAllGroupsCheckEnabled(boolean b)
    {
      allGroupsCheck.setEnabled(b);
-     allGroupsCheck.setSelected(ap.av.getColourAppliesToAllGroups());
    }
  
    /**
      }
      return title;
    }
 -
 -  @Override
 -  protected void allGroupsCheck_actionPerformed(ActionEvent e)
 -  {
 -    if (allGroupsCheck.isSelected())
 -    {
 -      valueChanged(slider.getValue());
 -    }
 -  }
  }
@@@ -27,7 -27,6 +27,7 @@@ import jalview.datamodel.AlignmentI
  import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.SequenceI;
 +import jalview.gui.JalviewColourChooser.ColourChooserListener;
  import jalview.gui.StructureViewer.ViewerType;
  import jalview.gui.ViewSelectionMenu.ViewSetProvider;
  import jalview.io.DataSourceType;
@@@ -58,6 -57,7 +58,6 @@@ import java.util.Vector
  
  import javax.swing.ButtonGroup;
  import javax.swing.JCheckBoxMenuItem;
 -import javax.swing.JColorChooser;
  import javax.swing.JMenu;
  import javax.swing.JMenuItem;
  import javax.swing.JRadioButtonMenuItem;
@@@ -564,7 -564,8 +564,8 @@@ public abstract class StructureViewerBa
    {
      AlignmentI al = getAlignmentPanel().av.getAlignment();
      ColourSchemeI cs = ColourSchemes.getInstance()
-             .getColourScheme(colourSchemeName, al, null);
+             .getColourScheme(colourSchemeName, getAlignmentPanel().av, al,
+                     null);
      getBinding().setJalviewColourScheme(cs);
    }
  
      return reply;
    }
  
 +  /**
 +   * Opens a colour chooser dialog, and applies the chosen colour to the
 +   * background of the structure viewer
 +   */
    @Override
    public void background_actionPerformed(ActionEvent actionEvent)
    {
 -    Color col = JColorChooser.showDialog(this,
 -            MessageManager.getString("label.select_background_colour"),
 -            null);
 -    if (col != null)
 +    String ttl = MessageManager.getString("label.select_background_colour");
 +    ColourChooserListener listener = new ColourChooserListener()
      {
 -      getBinding().setBackgroundColour(col);
 -    }
 +      @Override
 +      public void colourSelected(Color c)
 +      {
 +        getBinding().setBackgroundColour(c);
 +      }
 +    };
 +    JalviewColourChooser.showColourChooser(this, ttl, null, listener);
    }
  
    @Override
    @Override
    public void pdbFile_actionPerformed(ActionEvent actionEvent)
    {
 +    // TODO: JAL-3048 not needed for Jalview-JS - save PDB file
      JalviewFileChooser chooser = new JalviewFileChooser(
              Cache.getProperty("LAST_DIRECTORY"));
  
@@@ -27,13 -27,9 +27,10 @@@ import jalview.datamodel.Sequence
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  import jalview.datamodel.SequenceNode;
 +import jalview.gui.JalviewColourChooser.ColourChooserListener;
  import jalview.schemes.ColourSchemeI;
- import jalview.schemes.ColourSchemeProperty;
- import jalview.schemes.UserColourScheme;
  import jalview.structure.SelectionSource;
  import jalview.util.Format;
- import jalview.util.MappingUtils;
  import jalview.util.MessageManager;
  
  import java.awt.Color;
@@@ -57,6 -53,7 +54,6 @@@ import java.util.Hashtable
  import java.util.List;
  import java.util.Vector;
  
 -import javax.swing.JColorChooser;
  import javax.swing.JPanel;
  import javax.swing.JScrollPane;
  import javax.swing.SwingUtilities;
@@@ -512,29 -509,21 +509,21 @@@ public class TreeCanvas extends JPanel 
        return;
      }
  
-     if ((node.left() == null) && (node.right() == null)) // TODO: internal node
+     node.color = c;
+     if (node.element() instanceof SequenceI)
      {
-       node.color = c;
-       if (node.element() instanceof SequenceI)
+       final SequenceI seq = (SequenceI) node.element();
+       AlignmentPanel[] aps = getAssociatedPanels();
+       if (aps != null)
        {
-         AlignmentPanel[] aps = getAssociatedPanels();
-         if (aps != null)
+         for (int a = 0; a < aps.length; a++)
          {
-           for (int a = 0; a < aps.length; a++)
-           {
-             final SequenceI seq = (SequenceI) node.element();
-             aps[a].av.setSequenceColour(seq, c);
-           }
+           aps[a].av.setSequenceColour(seq, c);
          }
        }
      }
-     else
-     {
-       node.color = c;
-       setColor((SequenceNode) node.left(), c);
-       setColor((SequenceNode) node.right(), c);
-     }
+     setColor((SequenceNode) node.left(), c);
+     setColor((SequenceNode) node.right(), c);
    }
  
    /**
     */
    void chooseSubtreeColour()
    {
 -    Color col = JColorChooser.showDialog(this,
 -            MessageManager.getString("label.select_subtree_colour"),
 -            highlightNode.color);
 -    if (col != null)
 -    {
 -      setColor(highlightNode, col);
 -      PaintRefresher.Refresh(tp, getAssociatedPanel().av.getSequenceSetId());
 -      repaint();
 -    }
 +    String ttl = MessageManager.getString("label.select_subtree_colour");
 +    ColourChooserListener listener = new ColourChooserListener() {
 +      @Override
 +      public void colourSelected(Color c)
 +      {
 +        setColor(highlightNode, c);
 +        PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
 +        repaint();
 +      }
 +    };
 +    JalviewColourChooser.showColourChooser(this, ttl,  highlightNode.color, listener);
    }
  
    @Override
                      .deleteAllGroups();
              aps[a].av.getCodingComplement().clearSequenceColours();
            }
+           aps[a].av.setUpdateStructures(true);
          }
          colourGroups(groups);
  
        }
  
        ColourSchemeI cs = null;
-       SequenceGroup sg = new SequenceGroup(sequences, null, cs, true, true,
+       SequenceGroup _sg = new SequenceGroup(sequences, null, cs, true, true,
                false, 0, av.getAlignment().getWidth() - 1);
  
-       if (av.getGlobalColourScheme() != null)
-       {
-         if (av.getGlobalColourScheme() instanceof UserColourScheme)
-         {
-           cs = new UserColourScheme(
-                   ((UserColourScheme) av.getGlobalColourScheme())
-                           .getColours());
-         }
-         else
-         {
-           cs = ColourSchemeProperty.getColourScheme(sg, ColourSchemeProperty
-                   .getColourName(av.getGlobalColourScheme()));
-         }
-         // cs is null if shading is an annotationColourGradient
-         // if (cs != null)
-         // {
-         // cs.setThreshold(av.getViewportColourScheme().getThreshold(),
-         // av.isIgnoreGapsConsensus());
-         // }
-       }
-       sg.setColourScheme(cs);
-       sg.getGroupColourScheme().setThreshold(
-               av.getResidueShading().getThreshold(),
-               av.isIgnoreGapsConsensus());
-       // sg.recalcConservation();
-       sg.setName("JTreeGroup:" + sg.hashCode());
-       sg.setIdColour(col);
+       _sg.setName("JTreeGroup:" + _sg.hashCode());
+       _sg.setIdColour(col);
  
        for (int a = 0; a < aps.length; a++)
        {
-         if (aps[a].av.getGlobalColourScheme() != null
-                 && aps[a].av.getResidueShading().conservationApplied())
-         {
-           Conservation c = new Conservation("Group", sg.getSequences(null),
-                   sg.getStartRes(), sg.getEndRes());
-           c.calculate();
-           c.verdict(false, aps[a].av.getConsPercGaps());
-           sg.cs.setConservation(c);
-         }
+         SequenceGroup sg = new SequenceGroup(_sg);
+         AlignViewport viewport = aps[a].av;
  
-         aps[a].av.getAlignment().addGroup(new SequenceGroup(sg));
-         // TODO can we push all of the below into AlignViewportI?
-         final AlignViewportI codingComplement = aps[a].av
-                 .getCodingComplement();
-         if (codingComplement != null)
+         // Propagate group colours in each view
+         if (viewport.getGlobalColourScheme() != null)
          {
-           SequenceGroup mappedGroup = MappingUtils.mapSequenceGroup(sg, av,
-                   codingComplement);
-           if (mappedGroup.getSequences().size() > 0)
+           cs = viewport.getGlobalColourScheme().getInstance(viewport, sg);
+           sg.setColourScheme(cs);
+           sg.getGroupColourScheme().setThreshold(
+                   viewport.getResidueShading().getThreshold(),
+                   viewport.isIgnoreGapsConsensus());
+           if (viewport.getResidueShading().conservationApplied())
            {
-             codingComplement.getAlignment().addGroup(mappedGroup);
-             for (SequenceI seq : mappedGroup.getSequences())
-             {
-               codingComplement.setSequenceColour(seq, col.brighter());
-             }
+             Conservation c = new Conservation("Group",
+                     sg.getSequences(null), sg.getStartRes(),
+                     sg.getEndRes());
+             c.calculate();
+             c.verdict(false, viewport.getConsPercGaps());
+             sg.cs.setConservation(c);
            }
          }
+         // indicate that associated structure views will need an update
+         viewport.setUpdateStructures(true);
+         // propagate structure view update and sequence group to complement view
+         viewport.addSequenceGroup(sg);
        }
      }
  
-     // notify the panel(s) to redo any group specific stuff.
+     // notify the panel(s) to redo any group specific stuff
+     // also updates structure views if necessary
      for (int a = 0; a < aps.length; a++)
      {
        aps[a].updateAnnotation();
-       // TODO: JAL-868 - need to ensure view colour change message is broadcast
-       // to any Jmols listening in
        final AlignViewportI codingComplement = aps[a].av
                .getCodingComplement();
        if (codingComplement != null)
    {
      this.threshold = threshold;
    }
+   public boolean isApplyToAllViews()
+   {
+     return this.applyToAllViews;
+   }
+   public void setApplyToAllViews(boolean applyToAllViews)
+   {
+     this.applyToAllViews = applyToAllViews;
+   }
  }
@@@ -28,6 -28,7 +28,6 @@@ import jalview.analysis.TreeModel
  import jalview.analysis.scoremodels.ScoreModels;
  import jalview.api.analysis.ScoreModelI;
  import jalview.api.analysis.SimilarityParamsI;
 -import jalview.bin.Cache;
  import jalview.commands.CommandI;
  import jalview.commands.OrderCommand;
  import jalview.datamodel.Alignment;
@@@ -40,12 -41,11 +40,12 @@@ import jalview.datamodel.NodeTransformI
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceI;
  import jalview.datamodel.SequenceNode;
 +import jalview.gui.ImageExporter.ImageWriterI;
  import jalview.io.JalviewFileChooser;
  import jalview.io.JalviewFileView;
  import jalview.io.NewickFile;
  import jalview.jbgui.GTreePanel;
 -import jalview.util.ImageMaker;
 +import jalview.util.ImageMaker.TYPE;
  import jalview.util.MessageManager;
  import jalview.viewmodel.AlignmentViewport;
  
@@@ -53,13 -53,14 +53,13 @@@ import java.awt.Font
  import java.awt.Graphics;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
 -import java.awt.image.BufferedImage;
  import java.beans.PropertyChangeEvent;
  import java.beans.PropertyChangeListener;
 +import java.io.File;
  import java.io.FileOutputStream;
  import java.util.ArrayList;
  import java.util.List;
  
 -import javax.imageio.ImageIO;
  import javax.swing.ButtonGroup;
  import javax.swing.JMenuItem;
  import javax.swing.JRadioButtonMenuItem;
@@@ -125,7 -126,6 +125,7 @@@ public class TreePanel extends GTreePan
  
    public AlignmentViewport getViewPort()
    {
 +    // @Mungo - Why don't we return our own viewport ???
      return getTreeCanvas().getViewport();
    }
  
            NewickFile newTree, AlignmentView inputData)
    {
  
 -    setViewport(ap.av);
 +    av = ap.av;
      this.treeType = type;
      this.scoreModelName = modelName;
  
      treeCanvas = new TreeCanvas(this, ap, scrollPane);
 -    scrollPane.setViewportView(getTreeCanvas());
 +    scrollPane.setViewportView(treeCanvas);
  
      PaintRefresher.Register(this, ap.av.getSequenceSetId());
  
        @Override
        public void internalFrameClosed(InternalFrameEvent evt)
        {
 -        if (getViewport() != null)
 +        if (av != null)
          {
 -          getViewport().removePropertyChangeListener(listener);
 +          av.removePropertyChangeListener(listener);
          }
        }
      });
            }
  
            tree.updatePlaceHolders((List<SequenceI>) evt.getNewValue());
 -          getTreeCanvas().nameHash.clear(); // reset the mapping between canvas
 +          treeCanvas.nameHash.clear(); // reset the mapping between canvas
            // rectangles and leafnodes
            repaint();
          }
        }
      };
 -    getViewport().addPropertyChangeListener(listener);
 +    av.addPropertyChangeListener(listener);
      return listener;
    }
  
    void buildAssociatedViewMenu()
    {
      AlignmentPanel[] aps = PaintRefresher
 -            .getAssociatedPanels(getViewport().getSequenceSetId());
 +            .getAssociatedPanels(av.getSequenceSetId());
      if (aps.length == 1 && getTreeCanvas().getAssociatedPanel() == aps[0])
      {
        associateLeavesMenu.setVisible(false);
      for (i = 0; i < iSize; i++)
      {
        final AlignmentPanel ap = aps[i];
 -      item = new JRadioButtonMenuItem(ap.av.getViewName(), ap == getTreeCanvas().getAssociatedPanel());
 +      item = new JRadioButtonMenuItem(ap.av.getViewName(),
 +              ap == treeCanvas.getAssociatedPanel());
        buttonGroup.add(item);
        item.addActionListener(new ActionListener()
        {
          @Override
          public void actionPerformed(ActionEvent evt)
          {
 -          getTreeCanvas().applyToAllViews = false;
 -          getTreeCanvas().setAssociatedPanel(ap);
 -          getTreeCanvas().setViewport(ap.av);
 +          treeCanvas.applyToAllViews = false;
 +          treeCanvas.setAssociatedPanel(ap);
 +          treeCanvas.setViewport(ap.av);
            PaintRefresher.Register(thisTreePanel, ap.av.getSequenceSetId());
          }
        });
      final JRadioButtonMenuItem itemf = new JRadioButtonMenuItem(
              MessageManager.getString("label.all_views"));
      buttonGroup.add(itemf);
 -    itemf.setSelected(getTreeCanvas().applyToAllViews);
 +    itemf.setSelected(treeCanvas.applyToAllViews);
      itemf.addActionListener(new ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent evt)
        {
 -        getTreeCanvas().applyToAllViews = itemf.isSelected();
 +        treeCanvas.applyToAllViews = itemf.isSelected();
        }
      });
      associateLeavesMenu.add(itemf);
  
        if (newtree != null)
        {
 -        tree = new TreeModel(getViewport().getAlignment().getSequencesArray(), odata,
 +        tree = new TreeModel(av.getAlignment().getSequencesArray(), odata,
                  newtree);
          if (tree.getOriginalData() == null)
          {
        else
        {
          ScoreModelI sm = ScoreModels.getInstance()
 -                .getScoreModel(scoreModelName, getTreeCanvas().getAssociatedPanel());
 +                .getScoreModel(scoreModelName,
 +                        treeCanvas.getAssociatedPanel());
          TreeBuilder njtree = treeType.equals(TreeBuilder.NEIGHBOUR_JOINING)
 -                ? new NJTree(getViewport(), sm, similarityParams)
 -                : new AverageDistanceTree(getViewport(), sm, similarityParams);
 +                ? new NJTree(av, sm, similarityParams)
 +                : new AverageDistanceTree(av, sm, similarityParams);
          tree = new TreeModel(njtree);
          showDistances(true);
        }
  
        tree.reCount(tree.getTopNode());
        tree.findHeight(tree.getTopNode());
 -      getTreeCanvas().setTree(tree);
 -      getTreeCanvas().repaint();
 -      getViewport().setCurrentTree(tree);
 -      if (getViewport().getSortByTree())
 +      treeCanvas.setTree(tree);
 +      treeCanvas.repaint();
 +      av.setCurrentTree(tree);
 +      if (av.getSortByTree())
        {
          sortByTree_actionPerformed();
        }
  
    public void showDistances(boolean b)
    {
 -    getTreeCanvas().setShowDistances(b);
 +    treeCanvas.setShowDistances(b);
      distanceMenu.setSelected(b);
    }
  
    public void showBootstrap(boolean b)
    {
 -    getTreeCanvas().setShowBootstrap(b);
 +    treeCanvas.setShowBootstrap(b);
      bootstrapMenu.setSelected(b);
    }
  
    public void showPlaceholders(boolean b)
    {
      placeholdersMenu.setState(b);
 -    getTreeCanvas().setMarkPlaceholders(b);
 +    treeCanvas.setMarkPlaceholders(b);
    }
  
    /**
    @Override
    public void saveAsNewick_actionPerformed(ActionEvent e)
    {
 +    // TODO: JAL-3048 save newick file for Jalview-JS
      JalviewFileChooser chooser = new JalviewFileChooser(
              jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
      chooser.setFileView(new JalviewFileView());
    public void printMenu_actionPerformed(ActionEvent e)
    {
      // Putting in a thread avoids Swing painting problems
 -    getTreeCanvas().startPrinting();
 +    treeCanvas.startPrinting();
    }
  
    @Override
      {
        // we try to get the associated view's gap character
        // but this may fail if the view was closed...
 -      gc = getViewport().getGapCharacter();
 +      gc = av.getGapCharacter();
  
      } catch (Exception ex)
      {
        // AlignmentOrder origorder = new AlignmentOrder(alAndColsel[0]);
  
        AlignmentI al = new Alignment((SequenceI[]) alAndColsel[0]);
 -      AlignmentI dataset = (getViewport() != null && getViewport().getAlignment() != null)
 -              ? getViewport().getAlignment().getDataset()
 +      AlignmentI dataset = (av != null && av.getAlignment() != null)
 +              ? av.getAlignment().getDataset()
                : null;
        if (dataset != null)
        {
    @Override
    public void fitToWindow_actionPerformed(ActionEvent e)
    {
 -    getTreeCanvas().fitToWindow = fitToWindow.isSelected();
 +    treeCanvas.fitToWindow = fitToWindow.isSelected();
      repaint();
    }
  
    public void sortByTree_actionPerformed()
    {
  
 -    if (getTreeCanvas().applyToAllViews)
 +    if (treeCanvas.applyToAllViews)
      {
        final ArrayList<CommandI> commands = new ArrayList<>();
        for (AlignmentPanel ap : PaintRefresher
 -              .getAssociatedPanels(getViewport().getSequenceSetId()))
 +              .getAssociatedPanels(av.getSequenceSetId()))
        {
          commands.add(sortAlignmentIn(ap.av.getAlignPanel()));
        }
 -      getViewport().getAlignPanel().alignFrame.addHistoryItem(new CommandI()
 +      av.getAlignPanel().alignFrame.addHistoryItem(new CommandI()
        {
  
          @Override
          }
        });
        for (AlignmentPanel ap : PaintRefresher
 -              .getAssociatedPanels(getViewport().getSequenceSetId()))
 +              .getAssociatedPanels(av.getSequenceSetId()))
        {
          // ensure all the alignFrames refresh their GI after adding an undo item
          ap.alignFrame.updateEditMenuBar();
      }
      else
      {
 -      getTreeCanvas().getAssociatedPanel().alignFrame
 -              .addHistoryItem(sortAlignmentIn(getTreeCanvas().getAssociatedPanel()));
 +      treeCanvas.getAssociatedPanel().alignFrame
 +              .addHistoryItem(
 +                      sortAlignmentIn(treeCanvas.getAssociatedPanel()));
      }
  
    }
    @Override
    public void font_actionPerformed(ActionEvent e)
    {
 -    if (getTreeCanvas() == null)
 +    if (treeCanvas == null)
      {
        return;
      }
  
    public Font getTreeFont()
    {
 -    return getTreeCanvas().font;
 +    return treeCanvas.font;
    }
  
    public void setTreeFont(Font f)
    {
 -    if (getTreeCanvas() != null)
 +    if (treeCanvas != null)
      {
 -      getTreeCanvas().setFont(f);
 +      treeCanvas.setFont(f);
      }
    }
  
    @Override
    public void distanceMenu_actionPerformed(ActionEvent e)
    {
 -    getTreeCanvas().setShowDistances(distanceMenu.isSelected());
 +    treeCanvas.setShowDistances(distanceMenu.isSelected());
    }
  
    /**
    @Override
    public void bootstrapMenu_actionPerformed(ActionEvent e)
    {
 -    getTreeCanvas().setShowBootstrap(bootstrapMenu.isSelected());
 +    treeCanvas.setShowBootstrap(bootstrapMenu.isSelected());
    }
  
    /**
    @Override
    public void placeholdersMenu_actionPerformed(ActionEvent e)
    {
 -    getTreeCanvas().setMarkPlaceholders(placeholdersMenu.isSelected());
 +    treeCanvas.setMarkPlaceholders(placeholdersMenu.isSelected());
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Outputs the Tree in image format (currently EPS or PNG). The user is
 +   * prompted for the file to save to, and for EPS (unless a preference is
 +   * already set) for the choice of Text or Lineart for character rendering.
     */
    @Override
 -  public void epsTree_actionPerformed(ActionEvent e)
 +  public void writeTreeImage(TYPE imageFormat)
    {
 -    boolean accurateText = true;
 -
 -    String renderStyle = jalview.bin.Cache.getDefault("EPS_RENDERING",
 -            "Prompt each time");
 -
 -    // If we need to prompt, and if the GUI is visible then
 -    // Prompt for EPS rendering style
 -    if (renderStyle.equalsIgnoreCase("Prompt each time")
 -            && !(System.getProperty("java.awt.headless") != null && System
 -                    .getProperty("java.awt.headless").equals("true")))
 -    {
 -      EPSOptions eps = new EPSOptions();
 -      renderStyle = eps.getValue();
 -
 -      if (renderStyle == null || eps.cancelled)
 -      {
 -        return;
 -      }
 -
 -    }
 -
 -    if (renderStyle.equalsIgnoreCase("text"))
 +    int width = treeCanvas.getWidth();
 +    int height = treeCanvas.getHeight();
 +    ImageWriterI writer = new ImageWriterI()
      {
 -      accurateText = false;
 -    }
 -
 -    int width = getTreeCanvas().getWidth();
 -    int height = getTreeCanvas().getHeight();
 -
 -    try
 -    {
 -      JalviewFileChooser chooser = new JalviewFileChooser(
 -              ImageMaker.EPS_EXTENSION, ImageMaker.EPS_EXTENSION);
 -      chooser.setFileView(new JalviewFileView());
 -      chooser.setDialogTitle(
 -              MessageManager.getString("label.create_eps_from_tree"));
 -      chooser.setToolTipText(MessageManager.getString("action.save"));
 -
 -      int value = chooser.showSaveDialog(this);
 -
 -      if (value != JalviewFileChooser.APPROVE_OPTION)
 -      {
 -        return;
 -      }
 -
 -      Cache.setProperty("LAST_DIRECTORY",
 -              chooser.getSelectedFile().getParent());
 -
 -      FileOutputStream out = new FileOutputStream(
 -              chooser.getSelectedFile());
 -      EpsGraphics2D pg = new EpsGraphics2D("Tree", out, 0, 0, width,
 -              height);
 -
 -      pg.setAccurateTextMode(accurateText);
 -
 -      getTreeCanvas().draw(pg, width, height);
 -
 -      pg.flush();
 -      pg.close();
 -    } catch (Exception ex)
 -    {
 -      ex.printStackTrace();
 -    }
 -  }
 -
 -  /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 -   */
 -  @Override
 -  public void pngTree_actionPerformed(ActionEvent e)
 -  {
 -    int width = getTreeCanvas().getWidth();
 -    int height = getTreeCanvas().getHeight();
 -
 -    try
 -    {
 -      JalviewFileChooser chooser = new JalviewFileChooser(
 -              ImageMaker.PNG_EXTENSION, ImageMaker.PNG_DESCRIPTION);
 -
 -      chooser.setFileView(new jalview.io.JalviewFileView());
 -      chooser.setDialogTitle(
 -              MessageManager.getString("label.create_png_from_tree"));
 -      chooser.setToolTipText(MessageManager.getString("action.save"));
 -
 -      int value = chooser.showSaveDialog(this);
 -
 -      if (value != jalview.io.JalviewFileChooser.APPROVE_OPTION)
 +      @Override
 +      public void exportImage(Graphics g) throws Exception
        {
 -        return;
 +        treeCanvas.draw(g, width, height);
        }
 -
 -      jalview.bin.Cache.setProperty("LAST_DIRECTORY",
 -              chooser.getSelectedFile().getParent());
 -
 -      FileOutputStream out = new FileOutputStream(
 -              chooser.getSelectedFile());
 -
 -      BufferedImage bi = new BufferedImage(width, height,
 -              BufferedImage.TYPE_INT_RGB);
 -      Graphics png = bi.getGraphics();
 -
 -      getTreeCanvas().draw(png, width, height);
 -
 -      ImageIO.write(bi, "png", out);
 -      out.close();
 -    } catch (Exception ex)
 -    {
 -      ex.printStackTrace();
 -    }
 +    };
 +    String tree = MessageManager.getString("label.tree");
 +    ImageExporter exporter = new ImageExporter(writer, null, imageFormat,
 +            tree);
 +    exporter.doExport(null, this, width, height, tree.toLowerCase());
    }
  
    /**
            if (sq != null)
            {
              // search dbrefs, features and annotation
 -            DBRefEntry[] refs = jalview.util.DBRefUtils
 +            List<DBRefEntry> refs = jalview.util.DBRefUtils
                      .selectRefs(sq.getDBRefs(), new String[]
                      { labelClass.toUpperCase() });
              if (refs != null)
              {
 -              for (int i = 0; i < refs.length; i++)
 +              for (int i = 0, ni = refs.size(); i < ni; i++)
                {
                  if (newname == null)
                  {
 -                  newname = new String(refs[i].getAccessionId());
 +                  newname = new String(refs.get(i).getAccessionId());
                  }
                  else
                  {
 -                  newname = newname + "; " + refs[i].getAccessionId();
 +                  newname += "; " + refs.get(i).getAccessionId();
                  }
                }
              }
      /*
       * put them together as <method> Using <model>
       */
-     final String ttl = MessageManager.formatMessage("label.treecalc_title",
+     final String ttl = MessageManager.formatMessage("label.calc_title",
              treecalcnm, smn);
      return ttl;
    }
  
 +  /**
 +   * Builds an EPS image and writes it to the specified file.
 +   * 
 +   * @param outFile
 +   * @param textOption
 +   *          true for Text character rendering, false for Lineart
 +   */
 +  protected void writeEpsFile(File outFile, boolean textOption)
 +  {
 +    try
 +    {
 +      int width = treeCanvas.getWidth();
 +      int height = treeCanvas.getHeight();
 +
 +      FileOutputStream out = new FileOutputStream(
 +              outFile);
 +      EpsGraphics2D pg = new EpsGraphics2D("Tree", out, 0, 0, width,
 +              height);
 +      pg.setAccurateTextMode(!textOption);
 +      treeCanvas.draw(pg, width, height);
 +
 +      pg.flush();
 +      pg.close();
 +    } catch (Exception ex)
 +    {
 +      System.err.println("Error writing tree as EPS");
 +      ex.printStackTrace();
 +    }
 +  }
 +  
    public AlignViewport getViewport()
    {
      return av;
@@@ -646,7 -646,7 +646,7 @@@ public class AnnotationFil
  
    String refSeqId = null;
  
 -  public boolean annotateAlignmentView(AlignViewportI viewport, String file,
 +  public boolean annotateAlignmentView(AlignViewportI viewport, Object file,
            DataSourceType protocol)
    {
      ColumnSelection colSel = viewport.getColumnSelection();
    }
  
    public boolean readAnnotationFile(AlignmentI al, HiddenColumns hidden,
 -          String file, DataSourceType sourceType)
 +          Object file, DataSourceType sourceType)
    {
      BufferedReader in = null;
      try
      {
        if (sourceType == DataSourceType.FILE)
        {
 -        in = new BufferedReader(new FileReader(file));
 +        in = FileLoader.getBuffereReader(file);
        }
        else if (sourceType == DataSourceType.URL)
        {
 -        URL url = new URL(file);
 +        URL url = new URL(file.toString());
          in = new BufferedReader(new InputStreamReader(url.openStream()));
        }
        else if (sourceType == DataSourceType.PASTE)
        {
 -        in = new BufferedReader(new StringReader(file));
 +        in = new BufferedReader(new StringReader(file.toString()));
        }
        else if (sourceType == DataSourceType.CLASSLOADER)
        {
          }
          else if (key.equalsIgnoreCase("colour"))
          {
+           // TODO need to notify colourscheme of view reference once it is
+           // available
            sg.cs.setColourScheme(
-                   ColourSchemeProperty.getColourScheme(al, value));
+                   ColourSchemeProperty.getColourScheme(null, al, value));
          }
          else if (key.equalsIgnoreCase("pidThreshold"))
          {
@@@ -24,6 -24,7 +24,7 @@@ import jalview.analysis.AlignmentUtils
  import jalview.analysis.SequenceIdMatcher;
  import jalview.api.AlignViewportI;
  import jalview.api.FeatureColourI;
+ import jalview.api.FeatureRenderer;
  import jalview.api.FeaturesSourceI;
  import jalview.datamodel.AlignedCodonFrame;
  import jalview.datamodel.Alignment;
@@@ -104,11 -105,11 +105,11 @@@ public class FeaturesFile extends Align
    /**
     * Constructor which does not parse the file immediately
     * 
 -   * @param file
 +   * @param file File or String filename
     * @param paste
     * @throws IOException
     */
 -  public FeaturesFile(String file, DataSourceType paste)
 +  public FeaturesFile(Object file, DataSourceType paste)
            throws IOException
    {
      super(false, file, paste);
     * @param type
     * @throws IOException
     */
 -  public FeaturesFile(boolean parseImmediately, String file,
 +  public FeaturesFile(boolean parseImmediately, Object file,
            DataSourceType type) throws IOException
    {
      super(parseImmediately, file, type);
    }
  
    /**
-    * Returns contents of a Jalview format features file, for visible features, as
-    * filtered by type and group. Features with a null group are displayed if their
-    * feature type is visible. Non-positional features may optionally be included
-    * (with no check on type or group).
+    * Returns contents of a Jalview format features file, for visible features,
+    * as filtered by type and group. Features with a null group are displayed if
+    * their feature type is visible. Non-positional features may optionally be
+    * included (with no check on type or group).
     * 
     * @param sequences
-    *          source of features
-    * @param visible
-    *          map of colour for each visible feature type
-    * @param featureFilters
-    * @param visibleFeatureGroups
+    * @param fr
     * @param includeNonPositional
     *          if true, include non-positional features (regardless of group or
     *          type)
     * @return
     */
    public String printJalviewFormat(SequenceI[] sequences,
-           Map<String, FeatureColourI> visible,
-           Map<String, FeatureMatcherSetI> featureFilters,
-           List<String> visibleFeatureGroups, boolean includeNonPositional)
+           FeatureRenderer fr, boolean includeNonPositional)
    {
-     if (!includeNonPositional && (visible == null || visible.isEmpty()))
+     Map<String, FeatureColourI> visibleColours = fr
+             .getDisplayedFeatureCols();
+     Map<String, FeatureMatcherSetI> featureFilters = fr.getFeatureFilters();
+     if (!includeNonPositional
+             && (visibleColours == null || visibleColours.isEmpty()))
      {
        // no point continuing.
        return "No Features Visible";
       */
      // TODO: decide if feature links should also be written here ?
      StringBuilder out = new StringBuilder(256);
-     if (visible != null)
+     if (visibleColours != null)
      {
-       for (Entry<String, FeatureColourI> featureColour : visible.entrySet())
+       for (Entry<String, FeatureColourI> featureColour : visibleColours
+               .entrySet())
        {
          FeatureColourI colour = featureColour.getValue();
          out.append(colour.toJalviewFormat(featureColour.getKey())).append(
        }
      }
  
-     String[] types = visible == null ? new String[0] : visible.keySet()
-             .toArray(new String[visible.keySet().size()]);
+     String[] types = visibleColours == null ? new String[0]
+             : visibleColours.keySet()
+                     .toArray(new String[visibleColours.keySet().size()]);
  
      /*
       * feature filters if any
       */
-     outputFeatureFilters(out, visible, featureFilters);
+     outputFeatureFilters(out, visibleColours, featureFilters);
  
      /*
-      * sort groups alphabetically, and ensure that features with a
-      * null or empty group are output after those in named groups
+      * output features within groups
       */
-     List<String> sortedGroups = new ArrayList<>(visibleFeatureGroups);
-     sortedGroups.remove(null);
-     sortedGroups.remove("");
-     Collections.sort(sortedGroups);
-     sortedGroups.add(null);
-     sortedGroups.add("");
-     boolean foundSome = false;
+     int count = outputFeaturesByGroup(out, fr, types, sequences,
+             includeNonPositional);
  
-     /*
-      * first output any non-positional features
-      */
-     if (includeNonPositional)
-     {
-       for (int i = 0; i < sequences.length; i++)
-       {
-         String sequenceName = sequences[i].getName();
-         for (SequenceFeature feature : sequences[i].getFeatures()
-                 .getNonPositionalFeatures())
-         {
-           foundSome = true;
-           out.append(formatJalviewFeature(sequenceName, feature));
-         }
-       }
-     }
-     /*
-      * positional features within groups
-      */
-     foundSome |= outputFeaturesByGroup(out, sortedGroups, types, sequences);
-     return foundSome ? out.toString() : "No Features Visible";
+     return count > 0 ? out.toString() : "No Features Visible";
    }
  
    /**
      }
      if (!first)
      {
-       out.append(ENDFILTERS).append(newline).append(newline);
+       out.append(ENDFILTERS).append(newline);
      }
  
    }
  
    /**
-    * Appends output of sequence features within feature groups to the output
-    * buffer. Groups other than the null or empty group are sandwiched by
-    * STARTGROUP and ENDGROUP lines.
+    * Appends output of visible sequence features within feature groups to the
+    * output buffer. Groups other than the null or empty group are sandwiched by
+    * STARTGROUP and ENDGROUP lines. Answers the number of features written.
     * 
     * @param out
-    * @param groups
+    * @param fr
     * @param featureTypes
     * @param sequences
+    * @param includeNonPositional
     * @return
     */
-   private boolean outputFeaturesByGroup(StringBuilder out,
-           List<String> groups, String[] featureTypes, SequenceI[] sequences)
+   private int outputFeaturesByGroup(StringBuilder out,
+           FeatureRenderer fr, String[] featureTypes,
+           SequenceI[] sequences, boolean includeNonPositional)
    {
-     boolean foundSome = false;
-     for (String group : groups)
+     List<String> featureGroups = fr.getFeatureGroups();
+     /*
+      * sort groups alphabetically, and ensure that features with a
+      * null or empty group are output after those in named groups
+      */
+     List<String> sortedGroups = new ArrayList<>(featureGroups);
+     sortedGroups.remove(null);
+     sortedGroups.remove("");
+     Collections.sort(sortedGroups);
+     sortedGroups.add(null);
+     sortedGroups.add("");
+     int count = 0;
+     List<String> visibleGroups = fr.getDisplayedFeatureGroups();
+     /*
+      * loop over all groups (may be visible or not);
+      * non-positional features are output even if group is not visible
+      */
+     for (String group : sortedGroups)
      {
-       boolean isNamedGroup = (group != null && !"".equals(group));
-       if (isNamedGroup)
-       {
-         out.append(newline);
-         out.append(STARTGROUP).append(TAB);
-         out.append(group);
-         out.append(newline);
-       }
+       boolean firstInGroup = true;
+       boolean isNullGroup = group == null || "".equals(group);
  
-       /*
-        * output positional features within groups
-        */
        for (int i = 0; i < sequences.length; i++)
        {
          String sequenceName = sequences[i].getName();
          List<SequenceFeature> features = new ArrayList<>();
-         if (featureTypes.length > 0)
+         /*
+          * get any non-positional features in this group, if wanted
+          * (for any feature type, whether visible or not)
+          */
+         if (includeNonPositional)
+         {
+           features.addAll(sequences[i].getFeatures()
+                   .getFeaturesForGroup(false, group));
+         }
+         /*
+          * add positional features for visible feature types, but
+          * (for named groups) only if feature group is visible
+          */
+         if (featureTypes.length > 0
+                 && (isNullGroup || visibleGroups.contains(group)))
          {
            features.addAll(sequences[i].getFeatures().getFeaturesForGroup(
                    true, group, featureTypes));
          }
  
-         for (SequenceFeature sequenceFeature : features)
+         for (SequenceFeature sf : features)
          {
-           foundSome = true;
-           out.append(formatJalviewFeature(sequenceName, sequenceFeature));
+           if (sf.isNonPositional() || fr.isVisible(sf))
+           {
+             count++;
+             if (firstInGroup)
+             {
+               out.append(newline);
+               if (!isNullGroup)
+               {
+                 out.append(STARTGROUP).append(TAB).append(group)
+                         .append(newline);
+               }
+             }
+             firstInGroup = false;
+             out.append(formatJalviewFeature(sequenceName, sf));
+           }
          }
        }
  
-       if (isNamedGroup)
+       if (!isNullGroup && !firstInGroup)
        {
-         out.append(ENDGROUP).append(TAB);
-         out.append(group);
-         out.append(newline);
+         out.append(ENDGROUP).append(TAB).append(group).append(newline);
        }
      }
-     return foundSome;
+     return count;
    }
  
    /**
     * @return
     */
    public String printGffFormat(SequenceI[] sequences,
-           Map<String, FeatureColourI> visible,
-           List<String> visibleFeatureGroups,
-           boolean includeNonPositionalFeatures)
+           FeatureRenderer fr, boolean includeNonPositionalFeatures)
    {
+     Map<String, FeatureColourI> visibleColours = fr.getDisplayedFeatureCols();
      StringBuilder out = new StringBuilder(256);
  
      out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion));
  
      if (!includeNonPositionalFeatures
-             && (visible == null || visible.isEmpty()))
+             && (visibleColours == null || visibleColours.isEmpty()))
      {
        return out.toString();
      }
  
-     String[] types = visible == null ? new String[0] : visible.keySet()
-             .toArray(
-             new String[visible.keySet().size()]);
+     String[] types = visibleColours == null ? new String[0]
+             : visibleColours.keySet()
+                     .toArray(new String[visibleColours.keySet().size()]);
  
      for (SequenceI seq : sequences)
      {
        {
          features.addAll(seq.getFeatures().getNonPositionalFeatures());
        }
-       if (visible != null && !visible.isEmpty())
+       if (visibleColours != null && !visibleColours.isEmpty())
        {
          features.addAll(seq.getFeatures().getPositionalFeatures(types));
        }
  
        for (SequenceFeature sf : features)
        {
-         String source = sf.featureGroup;
-         if (!sf.isNonPositional() && source != null
-                 && !visibleFeatureGroups.contains(source))
+         if (!sf.isNonPositional() && !fr.isVisible(sf))
          {
-           // group is not visible
+           /*
+            * feature hidden by group visibility, colour threshold,
+            * or feature filter condition
+            */
            continue;
          }
  
+         String source = sf.featureGroup;
          if (source == null)
          {
            source = sf.getDescription();
@@@ -25,30 -25,32 +25,35 @@@ import jalview.bin.Cache
  import jalview.gui.JvOptionPane;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
 +import jalview.util.dialogrunner.DialogRunnerI;
  
  import java.awt.Component;
  import java.awt.Dimension;
  import java.awt.EventQueue;
  import java.awt.HeadlessException;
+ import java.awt.event.ActionEvent;
+ import java.awt.event.ActionListener;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
 +import java.beans.PropertyChangeEvent;
 +import java.beans.PropertyChangeListener;
  import java.io.File;
  import java.util.ArrayList;
 +import java.util.HashMap;
  import java.util.List;
 +import java.util.Map;
  import java.util.StringTokenizer;
  import java.util.Vector;
  
+ import javax.swing.BoxLayout;
  import javax.swing.DefaultListCellRenderer;
+ import javax.swing.JCheckBox;
  import javax.swing.JFileChooser;
  import javax.swing.JList;
  import javax.swing.JPanel;
  import javax.swing.JScrollPane;
  import javax.swing.SpringLayout;
 -import javax.swing.SwingUtilities;
 -import javax.swing.border.TitledBorder;
+ import javax.swing.filechooser.FileFilter;
  import javax.swing.plaf.basic.BasicFileChooserUI;
  
  /**
   * @author AMW
   *
   */
 -public class JalviewFileChooser extends JFileChooser
 +public class JalviewFileChooser extends JFileChooser implements DialogRunnerI,
 +    PropertyChangeListener
  {
 +  private static final long serialVersionUID = 1L;
 +
 +  private Map<Object, Runnable> callbacks = new HashMap<>();
 +  
 +  File selectedFile = null;
 +
    /**
+    * backupfilesCheckBox = "Include backup files" checkbox includeBackupfiles =
+    * flag set by checkbox
+    */
+   private JCheckBox backupfilesCheckBox = null;
+   protected boolean includeBackupFiles = false;
+   /**
     * Factory method to return a file chooser that offers readable alignment file
     * formats
     * 
    public static JalviewFileChooser forRead(String directory,
            String selected)
    {
+     return JalviewFileChooser.forRead(directory, selected, false);
+   }
+   public static JalviewFileChooser forRead(String directory,
+           String selected, boolean allowBackupFiles)
+   {
      List<String> extensions = new ArrayList<>();
      List<String> descs = new ArrayList<>();
      for (FileFormatI format : FileFormats.getInstance().getFormats())
          descs.add(format.getName());
        }
      }
      return new JalviewFileChooser(directory,
              extensions.toArray(new String[extensions.size()]),
-             descs.toArray(new String[descs.size()]), selected, true);
+             descs.toArray(new String[descs.size()]), selected, true,
+             allowBackupFiles);
    }
  
    /**
    }
  
    JalviewFileChooser(String dir, String[] extensions, String[] descs,
 -          String selected, boolean allFiles)
 +          String selected, boolean acceptAny)
    {
 -    this(dir, extensions, descs, selected, allFiles, false);
++    this(dir, extensions, descs, selected, acceptAny, false);
+   }
+   public JalviewFileChooser(String dir, String[] extensions, String[] descs,
 -          String selected, boolean allFiles, boolean allowBackupFiles)
++          String selected, boolean acceptAny, boolean allowBackupFiles)
+   {
      super(safePath(dir));
      if (extensions.length == descs.length)
      {
        {
          formats.add(new String[] { extensions[i], descs[i] });
        }
-       init(formats, selected, acceptAny);
 -      init(formats, selected, allFiles, allowBackupFiles);
++      init(formats, selected, acceptAny, allowBackupFiles);
      }
      else
      {
    }
  
    /**
 +   * Overridden for JalviewJS compatibility: only one thread in Javascript, 
 +   * so we can't wait for user choice in another thread and then perform the 
 +   * desired action
 +   */
 +  @Override
 +  public int showOpenDialog(Component parent)
 +  {
 +    int value = super.showOpenDialog(this);
 +    
 +    if (!Platform.isJS())
 +    {
 +      /*
 +       * code here is not run in JalviewJS, instead
 +       * propertyChange() is called for dialog action
 +       */
 +      handleResponse(value);
 +    }
 +    return value;
 +  }
 +
 +  /**
     * 
     * @param formats
     *          a list of {extensions, description} for each file format
     * @param selected
 -   * @param allFiles
 +   * @param acceptAny
     *          if true, 'any format' option is included
     */
 -  void init(List<String[]> formats, String selected, boolean allFiles)
 +  void init(List<String[]> formats, String selected, boolean acceptAny)
    {
 -    init(formats, selected, allFiles, false);
++    init(formats, selected, acceptAny, false);
+   }
 -  void init(List<String[]> formats, String selected, boolean allFiles,
++  void init(List<String[]> formats, String selected, boolean acceptAny,
+           boolean allowBackupFiles)
+   {
  
      JalviewFileFilter chosen = null;
  
      // SelectAllFilter needs to be set first before adding further
      // file filters to fix bug on Mac OSX
 -    setAcceptAllFileFilterUsed(allFiles);
 +    setAcceptAllFileFilterUsed(acceptAny);
  
      for (String[] format : formats)
      {
        JalviewFileFilter jvf = new JalviewFileFilter(format[0], format[1]);
+       if (allowBackupFiles)
+       {
+         jvf.setParentJFC(this);
+       }
        addChoosableFileFilter(jvf);
        if ((selected != null) && selected.equalsIgnoreCase(format[1]))
        {
        setFileFilter(chosen);
      }
  
-     setAccessory(new RecentlyOpened());
+     if (allowBackupFiles)
+     {
+       JPanel multi = new JPanel();
+       multi.setLayout(new BoxLayout(multi, BoxLayout.PAGE_AXIS));
+       if (backupfilesCheckBox == null)
+       {
+         try {
+           includeBackupFiles = Boolean.parseBoolean(
+                   Cache.getProperty(BackupFiles.NS + "_FC_INCLUDE"));
+         } catch (Exception e)
+         {
+           includeBackupFiles = false;
+         }
+         backupfilesCheckBox = new JCheckBox(
+                 MessageManager.getString("label.include_backup_files"),
+                 includeBackupFiles);
+         backupfilesCheckBox.setAlignmentX(Component.CENTER_ALIGNMENT);
+         JalviewFileChooser jfc = this;
+         backupfilesCheckBox.addActionListener(new ActionListener()
+         {
+           @Override
+           public void actionPerformed(ActionEvent e)
+           {
+             includeBackupFiles = backupfilesCheckBox.isSelected();
+             Cache.setProperty(BackupFiles.NS + "_FC_INCLUDE",
+                     String.valueOf(includeBackupFiles));
+             FileFilter f = jfc.getFileFilter();
+             // deselect the selected file if it's no longer choosable
+             File selectedFile = jfc.getSelectedFile();
+             if (selectedFile != null && !f.accept(selectedFile))
+             {
+               jfc.setSelectedFile(null);
+             }
+             // fake the OK button changing (to force it to upate)
+             String s = jfc.getApproveButtonText();
+             jfc.firePropertyChange(
+                     APPROVE_BUTTON_TEXT_CHANGED_PROPERTY, null, s);
+             // fake the file filter changing (its behaviour actually has)
+             jfc.firePropertyChange(FILE_FILTER_CHANGED_PROPERTY, null, f);
+             jfc.rescanCurrentDirectory();
+             jfc.revalidate();
+             jfc.repaint();
+           }
+         });
+       }
+       multi.add(new RecentlyOpened());
+       multi.add(backupfilesCheckBox);
+       setAccessory(multi);
+     }
+     else
+     {
+       // set includeBackupFiles=false to avoid other file choosers from picking
+       // up backup files (Just In Case)
+       includeBackupFiles = false;
+       setAccessory(new RecentlyOpened());
+     }
    }
  
    @Override
      return null;
    }
  
    @Override
    public File getSelectedFile()
    {
 -    File selfile = super.getSelectedFile();
 -    if (selfile == null && ourselectedFile != null)
 -    {
 -      return ourselectedFile;
 -    }
 -    return selfile;
 +    File f = super.getSelectedFile();
 +    return f == null ? selectedFile : f;
    }
  
    @Override
    public int showSaveDialog(Component parent) throws HeadlessException
    {
      this.setAccessory(null);
 +    // Java 9,10,11 on OSX - clear selected file so name isn't auto populated
      this.setSelectedFile(null);
 +
      return super.showSaveDialog(parent);
    }
  
        return;
      }
  
 -    ourselectedFile = getSelectedFile();
 +    selectedFile = getSelectedFile();
  
 -    if (ourselectedFile == null)
 +    if (selectedFile == null)
      {
        // Workaround for Java 9,10 on OSX - no selected file, but there is a
        // filename typed in
          String filename = ((BasicFileChooserUI) getUI()).getFileName();
          if (filename != null && filename.length() > 0)
          {
 -          ourselectedFile = new File(getCurrentDirectory(), filename);
 +          selectedFile = new File(getCurrentDirectory(), filename);
          }
        } catch (Throwable x)
        {
        // THE
        // USER PROMPTED FOR A NEW FILENAME
      }
 -    if (ourselectedFile == null)
 +
 +    if (selectedFile == null)
      {
        return;
      }
      {
        JalviewFileFilter jvf = (JalviewFileFilter) getFileFilter();
  
 -      if (!jvf.accept(ourselectedFile))
 +      if (!jvf.accept(selectedFile))
        {
          String withExtension = getSelectedFile().getName() + "."
                  + jvf.getAcceptableExtension();
 -        ourselectedFile = (new File(getCurrentDirectory(), withExtension));
 -        setSelectedFile(ourselectedFile);
 +        selectedFile = (new File(getCurrentDirectory(), withExtension));
 +        setSelectedFile(selectedFile);
        }
      }
  
 -    if (ourselectedFile.exists())
 +    if (selectedFile.exists())
      {
        int confirm = JvOptionPane.showConfirmDialog(this,
                MessageManager.getString("label.overwrite_existing_file"),
                MessageManager.getString("label.file_already_exists"),
                JvOptionPane.YES_NO_OPTION);
        if (confirm != JvOptionPane.YES_OPTION)
        {
          return;
      }
    }
  
    class RecentlyOpened extends JPanel
    {
      private static final long serialVersionUID = 1L;
 -
 -    private JList<String> list;
 +    JList<String> list;
  
      RecentlyOpened()
      {
 +      setPreferredSize(new Dimension(300,100));
        String historyItems = Cache.getProperty("RECENT_FILE");
        StringTokenizer st;
        Vector<String> recent = new Vector<>();
        if (historyItems != null)
        {
          st = new StringTokenizer(historyItems, "\t");
 +
          while (st.hasMoreTokens())
          {
            recent.addElement(st.nextToken());
        }
  
        list = new JList<>(recent);
 -
 +  
        DefaultListCellRenderer dlcr = new DefaultListCellRenderer();
 -      dlcr.setHorizontalAlignment(DefaultListCellRenderer.RIGHT);
 +//      dlcr.setHorizontalAlignment(DefaultListCellRenderer.RIGHT);
        list.setCellRenderer(dlcr);
  
        list.addMouseListener(new MouseAdapter()
          }
        });
  
 -      this.setBorder(new TitledBorder(
 +      this.setBorder(new javax.swing.border.TitledBorder(
                MessageManager.getString("label.recently_opened")));
  
        final JScrollPane scroller = new JScrollPane(list);
        layout.putConstraint(SpringLayout.NORTH, scroller, 5,
                SpringLayout.NORTH, this);
  
 -      if (Platform.isAMac())
 +      if (Platform.isAMacAndNotJS())
        {
          scroller.setPreferredSize(new Dimension(500, 100));
        }
        else
        {
 -        scroller.setPreferredSize(new Dimension(130, 200));
 +        scroller.setPreferredSize(new Dimension(530, 200));
        }
  
        this.add(scroller);
  
 -      SwingUtilities.invokeLater(new Runnable()
 +      javax.swing.SwingUtilities.invokeLater(new Runnable()
        {
          @Override
          public void run()
                    .setValue(scroller.getHorizontalScrollBar().getMaximum());
          }
        });
 +
 +    }
 +
 +  }
 +
 +  @Override
 +  public DialogRunnerI setResponseHandler(Object response, Runnable action)
 +  {
 +      callbacks.put(response,  action);
 +      return this;
 +  }
 +
 +  @Override
 +  public void handleResponse(Object response)
 +  {
 +    /*
 +       * this test is for NaN in Chrome
 +       */
 +    if (response != null && !response.equals(response))
 +    {
 +      return;
 +    }
 +    Runnable action = callbacks.get(response);
 +    if (action != null)
 +    {
 +      action.run();
 +    }
 +  }
 +
 +  /**
 +   * JalviewJS signals file selection by a property change event
 +   * for property "SelectedFile".  This methods responds to that by
 +   * running the response action for 'OK' in the dialog.
 +   * 
 +   * @param evt
 +   */
 +  @Override
 +  public void propertyChange(PropertyChangeEvent evt)
 +  {
 +    // TODO other properties need runners...
 +    switch (evt.getPropertyName())
 +    {
 +    /*
 +     * property name here matches that used in JFileChooser.js
 +     */
 +    case "SelectedFile": 
 +      handleResponse(APPROVE_OPTION);
 +      break;
      }
    }
  }
@@@ -543,7 -543,7 +543,7 @@@ public class VCFLoade
     */
    protected void transferAddedFeatures(SequenceI seq)
    {
 -    DBRefEntry[] dbrefs = seq.getDBRefs();
 +    List<DBRefEntry> dbrefs = seq.getDBRefs();
      if (dbrefs == null)
      {
        return;
    }
  
    /**
-    * A convenience method to get the AF value for the given alternate allele
-    * index
-    * 
-    * @param variant
-    * @param alleleIndex
-    * @return
-    */
-   protected float getAlleleFrequency(VariantContext variant, int alleleIndex)
-   {
-     float score = 0f;
-     String attributeValue = getAttributeValue(variant,
-             ALLELE_FREQUENCY_KEY, alleleIndex);
-     if (attributeValue != null)
-     {
-       try
-       {
-         score = Float.parseFloat(attributeValue);
-       } catch (NumberFormatException e)
-       {
-         // leave as 0
-       }
-     }
-     return score;
-   }
-   /**
     * A convenience method to get an attribute value for an alternate allele
     * 
     * @param variant
        type = getOntologyTerm(consequence);
      }
  
-     float score = getAlleleFrequency(variant, altAlleleIndex);
      SequenceFeature sf = new SequenceFeature(type, alleles, featureStart,
-             featureEnd, score, FEATURE_GROUP_VCF);
+             featureEnd, FEATURE_GROUP_VCF);
      sf.setSource(sourceId);
  
      sf.setValue(Gff3Helper.ALLELES, alleles);
  package jalview.jbgui;
  
  import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
+ import jalview.analysis.GeneticCodeI;
+ import jalview.analysis.GeneticCodes;
  import jalview.api.SplitContainerI;
  import jalview.bin.Cache;
  import jalview.gui.JvSwingUtils;
  import jalview.gui.Preferences;
  import jalview.io.FileFormats;
+ import jalview.schemes.ResidueColourScheme;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
  
@@@ -75,7 -78,7 +78,7 @@@ public class GAlignFrame extends JInter
  
    protected JMenu sortByAnnotScore = new JMenu();
  
-   public JLabel statusBar = new JLabel();
+   protected JLabel statusBar = new JLabel();
  
    protected JMenu outputTextboxMenu = new JMenu();
  
  
    protected JMenuItem modifyPID;
  
-   protected JMenuItem annotationColour;
+   protected JRadioButtonMenuItem annotationColour;
  
    protected JMenu sortByTreeMenu = new JMenu();
  
  
    protected JCheckBoxMenuItem showDbRefsMenuitem = new JCheckBoxMenuItem();
  
-   protected JMenuItem showTranslation = new JMenuItem();
+   protected JMenu showTranslation = new JMenu();
  
    protected JMenuItem showReverse = new JMenuItem();
  
  
    protected JCheckBoxMenuItem hiddenMarkers = new JCheckBoxMenuItem();
  
 -  protected JTabbedPane tabbedPane = new JTabbedPane();
 +  protected JTabbedPane tabbedPane = jalview.jbgui.GDesktop.createTabbedPane();
  
    protected JMenuItem reload = new JMenuItem();
  
    protected JCheckBoxMenuItem normaliseSequenceLogo = new JCheckBoxMenuItem();
  
    protected JCheckBoxMenuItem applyAutoAnnotationSettings = new JCheckBoxMenuItem();
 +  
 +  protected JMenuItem openFeatureSettings;
  
    private SequenceAnnotationOrder annotationSortOrder;
  
    private boolean showAutoCalculatedAbove = false;
  
-   private Map<KeyStroke, JMenuItem> accelerators = new HashMap<KeyStroke, JMenuItem>();
+   private Map<KeyStroke, JMenuItem> accelerators = new HashMap<>();
  
    private SplitContainerI splitFrame;
  
            @Override
            public void actionPerformed(ActionEvent e)
            {
 -            outputText_actionPerformed(e);
 +            outputText_actionPerformed(e.getActionCommand());
            }
          });
  
        System.err.println(e.toString());
      }
  
 -    if (!Platform.isAMac())
 +    if (Platform.allowMnemonics()) // was "not mac and not JS"
      {
        closeMenuItem.setMnemonic('C');
        outputTextboxMenu.setMnemonic('T');
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        saveAs_actionPerformed(e);
 +        saveAs_actionPerformed();
        }
      };
  
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        copy_actionPerformed(e);
 +        copy_actionPerformed();
        }
      };
      addMenuActionAndAccelerator(keyStroke, copy, al);
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        cut_actionPerformed(e);
 +        cut_actionPerformed();
        }
      };
      addMenuActionAndAccelerator(keyStroke, cut, al);
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        delete_actionPerformed(e);
 +        delete_actionPerformed();
        }
      });
  
          vamsasStore_actionPerformed(e);
        }
      });
-     showTranslation
-             .setText(MessageManager.getString("label.translate_cDNA"));
-     showTranslation.addActionListener(new ActionListener()
-     {
-       @Override
-       public void actionPerformed(ActionEvent e)
+     /*
+      * Translate as cDNA with sub-menu of translation tables
+      */
+     showTranslation.setText(MessageManager
+             .getString("label.translate_cDNA"));
+     boolean first = true;
+     for (final GeneticCodeI table : GeneticCodes.getInstance()
+             .getCodeTables())
+     {
+       JMenuItem item = new JMenuItem(table.getId() + " " + table.getName());
+       showTranslation.add(item);
+       item.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           showTranslation_actionPerformed(table);
+         }
+       });
+       if (first)
        {
-         showTranslation_actionPerformed(e);
+         showTranslation.addSeparator();
        }
-     });
+       first = false;
+     }
      showReverse.setText(MessageManager.getString("label.reverse"));
      showReverse.addActionListener(new ActionListener()
      {
        }
      });
  
 -    JMenuItem openFeatureSettings = new JMenuItem(
 +    openFeatureSettings = new JMenuItem(
              MessageManager.getString("action.feature_settings"));
      openFeatureSettings.addActionListener(new ActionListener()
      {
          featureSettings_actionPerformed(e);
        }
      });
 +
 +    /*
 +     * add sub-menu of database we can fetch from
 +     */
      JMenuItem fetchSequence = new JMenuItem(
              MessageManager.getString("label.fetch_sequences"));
      fetchSequence.addActionListener(new ActionListener()
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        fetchSequence_actionPerformed(e);
 +        fetchSequence_actionPerformed();
        }
      });
  
      alignFrameMenuBar.add(formatMenu);
      alignFrameMenuBar.add(colourMenu);
      alignFrameMenuBar.add(calculateMenu);
 -    alignFrameMenuBar.add(webService);
 +    if (!Platform.isJS())
 +    {
 +      alignFrameMenuBar.add(webService);
 +    }
  
      fileMenu.add(fetchSequence);
      fileMenu.add(addSequenceMenu);
      fileMenu.add(exportAnnotations);
      fileMenu.add(loadTreeMenuItem);
      fileMenu.add(associatedData);
 -    fileMenu.add(loadVcf);
 +    if (!Platform.isJS())
 +    {
 +      fileMenu.add(loadVcf);
 +    }
      fileMenu.addSeparator();
      fileMenu.add(closeMenuItem);
  
      calculateMenu.addSeparator();
      calculateMenu.add(expandAlignment);
      calculateMenu.add(extractScores);
 -    calculateMenu.addSeparator();
 -    calculateMenu.add(runGroovy);
 +    if (!Platform.isJS())
 +    {
 +      calculateMenu.addSeparator();
 +      calculateMenu.add(runGroovy);
 +    }
  
      webServiceNoServices = new JMenuItem(
              MessageManager.getString("label.no_services"));
      webService.add(webServiceNoServices);
 -    exportImageMenu.add(htmlMenuItem);
 +    if (!Platform.isJS())
 +    {
 +      exportImageMenu.add(htmlMenuItem);
 +    }
      exportImageMenu.add(epsFile);
      exportImageMenu.add(createPNG);
 -    exportImageMenu.add(createBioJS);
 -    exportImageMenu.add(createSVG);
 +    if (!Platform.isJS())
 +    {
 +      exportImageMenu.add(createBioJS);
 +      exportImageMenu.add(createSVG);
 +    }
      addSequenceMenu.add(addFromFile);
      addSequenceMenu.add(addFromText);
      addSequenceMenu.add(addFromURL);
        }
      });
  
-     annotationColour = new JMenuItem(
+     annotationColour = new JRadioButtonMenuItem(
              MessageManager.getString("action.by_annotation"));
+     annotationColour.setName(ResidueColourScheme.ANNOTATION_COLOUR);
      annotationColour.addActionListener(new ActionListener()
      {
        @Override
    {
    }
  
 -  protected void outputText_actionPerformed(ActionEvent e)
 +  protected void outputText_actionPerformed(String formatName)
    {
    }
  
    {
    }
  
 -  protected void copy_actionPerformed(ActionEvent e)
 +  protected void copy_actionPerformed()
    {
    }
  
 -  protected void cut_actionPerformed(ActionEvent e)
 +  protected void cut_actionPerformed()
    {
    }
  
 -  protected void delete_actionPerformed(ActionEvent e)
 +  protected void delete_actionPerformed()
    {
    }
  
    {
    }
  
 -  protected void saveAs_actionPerformed(ActionEvent e)
 +  protected void saveAs_actionPerformed()
    {
    }
  
  
    }
  
-   public void showTranslation_actionPerformed(ActionEvent e)
+   public void showTranslation_actionPerformed(GeneticCodeI codeTable)
    {
  
    }
  
    }
  
 -  public void fetchSequence_actionPerformed(ActionEvent e)
 +  public void fetchSequence_actionPerformed()
    {
  
    }
@@@ -44,23 -44,6 +44,23 @@@ import javax.swing.JMenuItem
   */
  public class GDesktop extends JFrame
  {
 +
 +  public static javax.swing.JTabbedPane createTabbedPane()
 +  {
 +    // now just always return JTabbedPane
 +   return new javax.swing.JTabbedPane();
 +//    // BH 2018 coercing jalview.jbgui.swing.JTabbedPane() for now
 +//    if (/** @j2sNative false && */
 +//    true)
 +//    {
 +//      // Java
 +//      return new javax.swing.JTabbedPane();
 +//    }
 +//    // JavaScript
 +//    return (javax.swing.JTabbedPane) (Object) new jalview.jbgui.swing.JTabbedPane();
 +  }
 +
 +  
    protected static JMenu windowMenu = new JMenu();
  
    JMenuBar desktopMenubar = new JMenuBar();
@@@ -93,6 -76,8 +93,8 @@@
  
    JMenuItem saveState = new JMenuItem();
  
+   JMenuItem saveAsState = new JMenuItem();
    JMenuItem loadState = new JMenuItem();
  
    JMenu inputMenu = new JMenu();
     */
    public GDesktop()
    {
 +    super();
      try
      {
        jbInit();
        e.printStackTrace();
      }
  
 -    if (!Platform.isAMac())
 +    if (Platform.allowMnemonics()) 
      {
 +      //BH was !Platform.isAMacAndNotJS()) i.e. "JS or not Mac"
 +      // but here we want just not a Mac, period, right?
        FileMenu.setMnemonic('F');
        inputLocalFileMenuItem.setMnemonic('L');
 -      VamsasMenu.setMnemonic('V');
 +      VamsasMenu.setMnemonic('V'); 
        inputURLMenuItem.setMnemonic('U');
        inputTextboxMenuItem.setMnemonic('C');
        quit.setMnemonic('Q');
                @Override
                public void actionPerformed(ActionEvent e)
                {
 -                documentationMenuItem_actionPerformed(e);
 +                documentationMenuItem_actionPerformed();
                }
              });
      this.getContentPane().setLayout(flowLayout1);
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         saveState_actionPerformed(true);
+         saveState_actionPerformed();
        }
      });
-     JMenuItem saveAsJaxb = new JMenuItem("Save Project as JAXB");
-     saveAsJaxb.addActionListener(new ActionListener()
+     saveAsState.setText(MessageManager.getString("action.save_project_as"));
+     saveAsState.addActionListener(new ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         saveState_actionPerformed(false);
+         saveAsState_actionPerformed(e);
        }
      });
      loadState.setText(MessageManager.getString("action.load_project"));
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         loadState_actionPerformed(true);
-       }
-     });
-     JMenuItem loadAsJaxb = new JMenuItem("Load Project as JAXB");
-     loadAsJaxb.addActionListener(new ActionListener()
-     {
-       @Override
-       public void actionPerformed(ActionEvent e)
-       {
-         loadState_actionPerformed(false);
+         loadState_actionPerformed();
        }
      });
      inputMenu.setText(MessageManager.getString("label.input_alignment"));
      FileMenu.add(inputMenu);
      FileMenu.add(inputSequence);
      FileMenu.addSeparator();
-     FileMenu.add(saveAsJaxb);
-     FileMenu.add(loadAsJaxb);
-     if (!Platform.isJS()) 
-     {
-       FileMenu.add(saveState);
-       FileMenu.add(loadState);
-     }
+     FileMenu.add(saveState);
+     FileMenu.add(saveAsState);
+     FileMenu.add(loadState);
      FileMenu.addSeparator();
      FileMenu.add(quit);
      HelpMenu.add(aboutMenuItem);
      VamsasMenu.add(vamsasSave);
      VamsasMenu.add(vamsasStop);
      toolsMenu.add(preferences);
 -    toolsMenu.add(showMemusage);
 -    toolsMenu.add(showConsole);
 -    toolsMenu.add(showNews);
 -    toolsMenu.add(garbageCollect);
 -    toolsMenu.add(groovyShell);
 +    if (!Platform.isJS())
 +    {
 +      toolsMenu.add(showMemusage);
 +      toolsMenu.add(showConsole);
 +    }
 +    if (!Platform.isJS())
 +    {
 +      toolsMenu.add(showNews);
 +      toolsMenu.add(garbageCollect);
 +      toolsMenu.add(groovyShell);
 +    }
      toolsMenu.add(experimentalFeatures);
      // toolsMenu.add(snapShotWindow);
      inputMenu.add(inputLocalFileMenuItem);
    {
    }
  
 -  /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 -   */
 -  protected void documentationMenuItem_actionPerformed(ActionEvent e)
 +  protected void documentationMenuItem_actionPerformed()
    {
    }
  
     * @param e
     *          DOCUMENT ME!
     */
-   public void SaveState_actionPerformed(ActionEvent e)
+   protected void preferences_actionPerformed(ActionEvent e)
    {
    }
  
     * @param e
     *          DOCUMENT ME!
     */
-   protected void preferences_actionPerformed(ActionEvent e)
+   public void saveState_actionPerformed()
    {
    }
  
-   /**
-    * DOCUMENT ME!
-    * 
-    * @param e
-    *          DOCUMENT ME!
-    */
-   public void saveState_actionPerformed(boolean asCastor)
+   public void saveAsState_actionPerformed(ActionEvent e)
    {
    }
  
     * @param e
     *          DOCUMENT ME!
     */
-   public void loadState_actionPerformed(boolean asCastor)
+   public void loadState_actionPerformed()
    {
    }
  
@@@ -43,44 -43,24 +43,23 @@@ import javax.swing.SwingConstants
  import javax.swing.SwingUtilities;
  import javax.swing.event.CaretEvent;
  import javax.swing.event.CaretListener;
  
  public class GFinder extends JPanel
  {
-   JLabel jLabelFind = new JLabel();
+   private static final java.awt.Font VERDANA_12 = new java.awt.Font("Verdana", 0,
+           12);
  
-   protected JButton findAll = new JButton();
-   protected JButton findNext = new JButton();
-   JPanel actionsPanel = new JPanel();
-   GridLayout gridLayout1 = new GridLayout();
+   private static final String FINDER_CACHE_KEY = "CACHE.FINDER";
  
    protected JButton createFeatures = new JButton();
  
-   protected JvCacheableInputBox<String> searchBox = new JvCacheableInputBox<String>(
+   protected JvCacheableInputBox<String> searchBox = new JvCacheableInputBox<>(
            getCacheKey());
  
-   BorderLayout mainBorderLayout = new BorderLayout();
-   JPanel jPanel2 = new JPanel();
-   JPanel jPanel3 = new JPanel();
-   JPanel jPanel4 = new JPanel();
-   BorderLayout borderLayout2 = new BorderLayout();
-   JPanel jPanel6 = new JPanel();
    protected JCheckBox caseSensitive = new JCheckBox();
  
    protected JCheckBox searchDescription = new JCheckBox();
  
-   GridLayout optionsGridLayout = new GridLayout();
-   private static final String FINDER_CACHE_KEY = "CACHE.FINDER";
    public GFinder()
    {
      try
  
    private void jbInit() throws Exception
    {
-     jLabelFind.setFont(new java.awt.Font("Verdana", 0, 12));
-     jLabelFind.setText(MessageManager.getString("label.find"));
+     BorderLayout mainBorderLayout = new BorderLayout();
      this.setLayout(mainBorderLayout);
-     findAll.setFont(new java.awt.Font("Verdana", 0, 12));
-     findAll.setText(MessageManager.getString("action.find_all"));
+     mainBorderLayout.setHgap(5);
+     mainBorderLayout.setVgap(5);
+     JLabel jLabelFind = new JLabel(MessageManager.getString("label.find"));
+     jLabelFind.setFont(VERDANA_12);
+     JButton findAll = new JButton(
+             MessageManager.getString("action.find_all"));
+     findAll.setFont(VERDANA_12);
      findAll.addActionListener(new java.awt.event.ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         findAll_actionPerformed(e);
+         findAll_actionPerformed();
        }
      });
-     findNext.setFont(new java.awt.Font("Verdana", 0, 12));
-     findNext.setText(MessageManager.getString("action.find_next"));
+     JButton findNext = new JButton(
+             MessageManager.getString("action.find_next"));
+     findNext.setFont(VERDANA_12);
      findNext.addActionListener(new java.awt.event.ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         findNext_actionPerformed(e);
+         findNext_actionPerformed();
        }
      });
+     JPanel actionsPanel = new JPanel();
+     GridLayout gridLayout1 = new GridLayout();
      actionsPanel.setLayout(gridLayout1);
      gridLayout1.setHgap(0);
      gridLayout1.setRows(3);
      gridLayout1.setVgap(2);
      createFeatures.setEnabled(false);
-     createFeatures.setFont(new java.awt.Font("Verdana", 0, 12));
+     createFeatures.setFont(VERDANA_12);
      createFeatures.setMargin(new Insets(0, 0, 0, 0));
      createFeatures.setText(MessageManager.getString("label.new_feature"));
      createFeatures.addActionListener(new java.awt.event.ActionListener()
          createFeatures_actionPerformed();
        }
      });
 -    searchBox.setFont(new java.awt.Font("Verdana", Font.PLAIN, 12));
 -    ((JTextComponent) searchBox.getEditor().getEditorComponent())
 -            .addCaretListener(new CaretListener()
 +    searchBox.getComponent()
 +            .setFont(new java.awt.Font("Verdana", Font.PLAIN, 12));
 +    searchBox.addCaretListener(new CaretListener()
              {
                @Override
                public void caretUpdate(CaretEvent e)
                {
-                 textfield_caretUpdate(e);
+                 textfield_caretUpdate();
                }
              });
 -    searchBox.getEditor().getEditorComponent()
 -            .addKeyListener(new java.awt.event.KeyAdapter()
 +    searchBox.addKeyListener(new java.awt.event.KeyAdapter()
              {
                @Override
                public void keyPressed(KeyEvent e)
                  textfield_keyPressed(e);
                }
              });
-     mainBorderLayout.setHgap(5);
-     mainBorderLayout.setVgap(5);
-     jPanel4.setLayout(borderLayout2);
-     jPanel2.setPreferredSize(new Dimension(10, 1));
-     jPanel3.setPreferredSize(new Dimension(10, 1));
      caseSensitive.setHorizontalAlignment(SwingConstants.LEFT);
      caseSensitive.setText(MessageManager.getString("label.match_case"));
  
      actionsPanel.add(createFeatures, null);
      this.add(jLabelFind, java.awt.BorderLayout.WEST);
      this.add(actionsPanel, java.awt.BorderLayout.EAST);
-     // this.add(jPanel2, java.awt.BorderLayout.SOUTH);
+     JPanel jPanel2 = new JPanel();
+     jPanel2.setPreferredSize(new Dimension(10, 1));
+     JPanel jPanel3 = new JPanel();
+     jPanel3.setPreferredSize(new Dimension(10, 1));
+     JPanel jPanel4 = new JPanel();
+     jPanel4.setLayout(new BorderLayout());
+     this.add(jPanel2, java.awt.BorderLayout.SOUTH);
      this.add(jPanel3, java.awt.BorderLayout.NORTH);
      this.add(jPanel4, java.awt.BorderLayout.CENTER);
 -    jPanel4.add(searchBox, java.awt.BorderLayout.NORTH);
 +    jPanel4.add(searchBox.getComponent(), java.awt.BorderLayout.NORTH);
  
      JPanel optionsPanel = new JPanel();
  
+     GridLayout optionsGridLayout = new GridLayout();
      optionsGridLayout.setHgap(0);
      optionsGridLayout.setRows(2);
      optionsGridLayout.setVgap(2);
        if (!searchBox.isPopupVisible())
        {
          e.consume();
-         findNext_actionPerformed(null);
+         findNext_actionPerformed();
        }
      }
    }
  
-   protected void findNext_actionPerformed(ActionEvent e)
+   protected void findNext_actionPerformed()
    {
    }
  
-   protected void findAll_actionPerformed(ActionEvent e)
+   protected void findAll_actionPerformed()
    {
    }
  
    {
    }
  
-   public void textfield_caretUpdate(CaretEvent e)
+   public void textfield_caretUpdate()
    {
-     if (searchBox.getUserInput().indexOf(">") > -1)
+     // disabled as appears to be running a non-functional
+     if (false && searchBox.getUserInput().indexOf(">") > -1)
      {
        SwingUtilities.invokeLater(new Runnable()
        {
              str = jalview.analysis.AlignSeq.extractGaps(
                      jalview.util.Comparison.GapChars,
                      al.getSequenceAt(0).getSequenceAsString());
+             // todo and what? set str as searchBox text?
            }
          }
        });
@@@ -20,7 -20,6 +20,7 @@@
   */
  package jalview.jbgui;
  
 +import jalview.util.ImageMaker.TYPE;
  import jalview.util.MessageManager;
  
  import java.awt.BorderLayout;
@@@ -47,13 -46,11 +47,11 @@@ public class GPCAPanel extends JInterna
  {
    private static final Font VERDANA_12 = new Font("Verdana", 0, 12);
  
-   protected JComboBox<String> xCombobox = new JComboBox<String>();
+   protected JComboBox<String> xCombobox = new JComboBox<>();
  
-   protected JComboBox<String> yCombobox = new JComboBox<String>();
+   protected JComboBox<String> yCombobox = new JComboBox<>();
  
-   protected JComboBox<String> zCombobox = new JComboBox<String>();
-   protected JMenu scoreModelMenu = new JMenu();
+   protected JComboBox<String> zCombobox = new JComboBox<>();
  
    protected JMenu viewMenu = new JMenu();
  
  
    protected JMenu associateViewsMenu = new JMenu();
  
-   protected JMenu calcSettings = new JMenu();
-   protected JCheckBoxMenuItem nuclSetting = new JCheckBoxMenuItem();
-   protected JCheckBoxMenuItem protSetting = new JCheckBoxMenuItem();
    protected JLabel statusBar = new JLabel();
  
    protected JPanel statusPanel = new JPanel();
  
+   protected JMenuItem originalSeqData;
+   /**
+    * Constructor
+    */
    public GPCAPanel()
    {
      try
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         zCombobox_actionPerformed(e);
+         doDimensionChange();
        }
      });
      yCombobox.setFont(VERDANA_12);
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         yCombobox_actionPerformed(e);
+         doDimensionChange();
        }
      });
      xCombobox.setFont(VERDANA_12);
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         xCombobox_actionPerformed(e);
+         doDimensionChange();
        }
      });
      JButton resetButton = new JButton();
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         resetButton_actionPerformed(e);
+         resetButton_actionPerformed();
        }
      });
      JMenu fileMenu = new JMenu();
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        eps_actionPerformed();
 +        makePCAImage(TYPE.EPS);
        }
      });
      JMenuItem png = new JMenuItem("PNG");
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        png_actionPerformed();
 +        makePCAImage(TYPE.PNG);
        }
      });
      JMenuItem outputValues = new JMenuItem();
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         outputValues_actionPerformed(e);
+         outputValues_actionPerformed();
        }
      });
      JMenuItem outputPoints = new JMenuItem();
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         outputPoints_actionPerformed(e);
+         outputPoints_actionPerformed();
        }
      });
      JMenuItem outputProjPoints = new JMenuItem();
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         outputProjPoints_actionPerformed(e);
+         outputProjPoints_actionPerformed();
        }
      });
      JMenuItem print = new JMenuItem();
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         print_actionPerformed(e);
+         print_actionPerformed();
        }
      });
      viewMenu.setText(MessageManager.getString("action.view"));
        {
        }
      });
-     scoreModelMenu
-             .setText(MessageManager.getString("label.select_score_model"));
-     scoreModelMenu.addMenuListener(new MenuListener()
-     {
-       @Override
-       public void menuSelected(MenuEvent e)
-       {
-         scoreModel_menuSelected();
-       }
-       @Override
-       public void menuDeselected(MenuEvent e)
-       {
-       }
-       @Override
-       public void menuCanceled(MenuEvent e)
-       {
-       }
-     });
      showLabels.setText(MessageManager.getString("label.show_labels"));
      showLabels.addActionListener(new ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         showLabels_actionPerformed(e);
+         showLabels_actionPerformed();
        }
      });
      JMenuItem bgcolour = new JMenuItem();
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         bgcolour_actionPerformed(e);
+         bgcolour_actionPerformed();
        }
      });
-     JMenuItem originalSeqData = new JMenuItem();
+     originalSeqData = new JMenuItem();
      originalSeqData.setText(MessageManager.getString("label.input_data"));
      originalSeqData.addActionListener(new ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         originalSeqData_actionPerformed(e);
+         originalSeqData_actionPerformed();
        }
      });
      associateViewsMenu.setText(
              MessageManager.getString("label.associate_nodes_with"));
-     calcSettings.setText(MessageManager.getString("action.change_params"));
-     nuclSetting
-             .setText(MessageManager.getString("label.nucleotide_matrix"));
-     protSetting.setText(MessageManager.getString("label.protein_matrix"));
-     nuclSetting.addActionListener(new ActionListener()
-     {
-       @Override
-       public void actionPerformed(ActionEvent arg0)
-       {
-         nuclSetting_actionPerfomed(arg0);
-       }
-     });
-     protSetting.addActionListener(new ActionListener()
-     {
-       @Override
-       public void actionPerformed(ActionEvent arg0)
-       {
-         protSetting_actionPerfomed(arg0);
-       }
-     });
  
-     calcSettings.add(nuclSetting);
-     calcSettings.add(protSetting);
-     calcSettings.add(scoreModelMenu);
      statusPanel.setLayout(new GridLayout());
      statusBar.setFont(VERDANA_12);
      // statusPanel.setBackground(Color.lightGray);
      JMenuBar jMenuBar1 = new JMenuBar();
      jMenuBar1.add(fileMenu);
      jMenuBar1.add(viewMenu);
-     jMenuBar1.add(calcSettings);
      setJMenuBar(jMenuBar1);
      fileMenu.add(saveMenu);
      fileMenu.add(outputValues);
      viewMenu.add(associateViewsMenu);
    }
  
-   protected void scoreModel_menuSelected()
-   {
-     // TODO Auto-generated method stub
-   }
-   protected void resetButton_actionPerformed(ActionEvent e)
-   {
-     // TODO Auto-generated method stub
-   }
-   protected void protSetting_actionPerfomed(ActionEvent arg0)
+   protected void resetButton_actionPerformed()
    {
-     // TODO Auto-generated method stub
    }
  
-   protected void nuclSetting_actionPerfomed(ActionEvent arg0)
+   protected void outputPoints_actionPerformed()
    {
-     // TODO Auto-generated method stub
    }
  
-   protected void outputPoints_actionPerformed(ActionEvent e)
+   protected void outputProjPoints_actionPerformed()
    {
-     // TODO Auto-generated method stub
    }
  
-   protected void outputProjPoints_actionPerformed(ActionEvent e)
-   {
-     // TODO Auto-generated method stub
-   }
-   protected void xCombobox_actionPerformed(ActionEvent e)
 -  protected void eps_actionPerformed()
 -  {
 -  }
 -
 -  protected void png_actionPerformed()
++  public void makePCAImage(TYPE imageType)
    {
    }
  
-   protected void yCombobox_actionPerformed(ActionEvent e)
+   protected void outputValues_actionPerformed()
    {
    }
  
-   protected void zCombobox_actionPerformed(ActionEvent e)
+   protected void print_actionPerformed()
    {
    }
  
-   public void makePCAImage(TYPE imageType)
+   protected void showLabels_actionPerformed()
    {
    }
  
-   public void outputValues_actionPerformed(ActionEvent e)
+   protected void bgcolour_actionPerformed()
    {
    }
  
-   public void print_actionPerformed(ActionEvent e)
+   protected void originalSeqData_actionPerformed()
    {
    }
  
-   public void showLabels_actionPerformed(ActionEvent e)
+   protected void viewMenu_menuSelected()
    {
    }
  
-   public void bgcolour_actionPerformed(ActionEvent e)
+   protected void doDimensionChange()
    {
-   }
-   public void originalSeqData_actionPerformed(ActionEvent e)
-   {
-   }
-   public void viewMenu_menuSelected()
-   {
    }
  }
   */
  package jalview.jbgui;
  
+ 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.util.MessageManager;
 +import jalview.util.Platform;
  
  import java.awt.BorderLayout;
  import java.awt.Color;
@@@ -43,8 -48,11 +49,11 @@@ import java.awt.event.ActionEvent
  import java.awt.event.ActionListener;
  import java.awt.event.FocusEvent;
  import java.awt.event.KeyEvent;
+ import java.awt.event.KeyListener;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
+ import java.util.HashMap;
+ import java.util.Map;
  
  import javax.swing.AbstractCellEditor;
  import javax.swing.BorderFactory;
@@@ -55,13 -63,18 +64,18 @@@ import javax.swing.JCheckBox
  import javax.swing.JComboBox;
  import javax.swing.JFileChooser;
  import javax.swing.JLabel;
+ import javax.swing.JList;
  import javax.swing.JPanel;
  import javax.swing.JRadioButton;
  import javax.swing.JScrollPane;
+ import javax.swing.JSpinner;
  import javax.swing.JTabbedPane;
  import javax.swing.JTable;
+ import javax.swing.JTextArea;
  import javax.swing.JTextField;
  import javax.swing.ListSelectionModel;
+ import javax.swing.SpinnerModel;
+ import javax.swing.SpinnerNumberModel;
  import javax.swing.SwingConstants;
  import javax.swing.border.Border;
  import javax.swing.border.EmptyBorder;
@@@ -85,6 -98,9 +99,9 @@@ public class GPreferences extends JPane
    private static final Font LABEL_FONT_ITALIC = JvSwingUtils
            .getLabelFont(false, true);
  
+   private static final Font LABEL_FONT_BOLD = JvSwingUtils
+           .getLabelFont(true, false);
    /*
     * Visual tab components
     */
     */
    protected JComboBox<Object> epsRendering = new JComboBox<>();
  
 +  protected JComboBox<Object> htmlRendering = new JComboBox<>();
 +
 +  protected JComboBox<Object> svgRendering = new JComboBox<>();
 +
    protected JLabel userIdWidthlabel = new JLabel();
  
    protected JCheckBox autoIdWidth = new JCheckBox();
  
    protected JCheckBox sortByTree = new JCheckBox();
  
    /*
     * Web Services tab
     */
    protected JPanel wsTab = new JPanel();
  
+   /*
+    * Backups tab components
+    * a lot of these are member variables instead of local variables only so that they
+    * can be enabled/disabled easily in one go
+    */
+   protected JCheckBox enableBackupFiles = new JCheckBox();
+   protected JPanel presetsPanel = new JPanel();
+   protected JButton revertButton = new JButton();
+   protected JComboBox<IntKeyStringValueEntry> backupfilesPresetsCombo = new JComboBox<>();
+   protected JPanel suffixPanel = new JPanel();
+   protected JPanel keepfilesPanel = new JPanel();
+   protected JPanel exampleFilesPanel = new JPanel();
+   protected JTextField suffixTemplate = new JTextField(null, 8);
+   protected JLabel suffixTemplateLabel = new JLabel();
+   protected JLabel suffixDigitsLabel = new JLabel();
+   protected JSpinner suffixDigitsSpinner = new JSpinner();
+   protected JalviewBooleanRadioButtons suffixReverse = new JalviewBooleanRadioButtons();
+   protected JalviewBooleanRadioButtons backupfilesKeepAll = new JalviewBooleanRadioButtons();
+   public JSpinner backupfilesRollMaxSpinner = new JSpinner();
+   protected JLabel oldBackupFilesLabel = new JLabel();
+   protected JalviewBooleanRadioButtons backupfilesConfirmDelete = new JalviewBooleanRadioButtons();
+   protected JTextArea backupfilesExampleLabel = new JTextArea();
    /**
     * Creates a new GPreferences object.
     */
     */
    private void jbInit() throws Exception
    {
 -    final JTabbedPane tabbedPane = new JTabbedPane();
 +    final JTabbedPane tabbedPane = jalview.jbgui.GDesktop.createTabbedPane();
      this.setLayout(new BorderLayout());
      JPanel okCancelPanel = initOkCancelPanel();
      this.add(tabbedPane, BorderLayout.CENTER);
      tabbedPane.add(initConnectionsTab(),
              MessageManager.getString("label.connections"));
  
+     tabbedPane.add(initBackupsTab(),
+             MessageManager.getString("label.backups"));
      tabbedPane.add(initLinksTab(),
              MessageManager.getString("label.urllinks"));
  
      /*
       * See WsPreferences for the real work of configuring this tab.
       */
 -    wsTab.setLayout(new BorderLayout());
 -    tabbedPane.add(wsTab, MessageManager.getString("label.web_services"));
 +    if (!Platform.isJS())
 +    {
 +      wsTab.setLayout(new BorderLayout());
 +      tabbedPane.add(wsTab, MessageManager.getString("label.web_services"));
 +    }
  
      /*
       * Handler to validate a tab before leaving it - currently only for
    }
  
    /**
 -   * Initialises the Output tabbed panel.
 +   * Initialises the Output tab
     * 
     * @return
     */
    {
      JPanel outputTab = new JPanel();
      outputTab.setLayout(null);
 -    JLabel epsLabel = new JLabel();
 +
 +    JLabel epsLabel = new JLabel(
 +            MessageManager.formatMessage("label.rendering_style", "EPS"));
      epsLabel.setFont(LABEL_FONT);
      epsLabel.setHorizontalAlignment(SwingConstants.RIGHT);
 -    epsLabel.setText(MessageManager.getString("label.eps_rendering_style"));
 -    epsLabel.setBounds(new Rectangle(9, 31, 140, 24));
 +    epsLabel.setBounds(new Rectangle(9, 31, 160, 24));
      epsRendering.setFont(LABEL_FONT);
 -    epsRendering.setBounds(new Rectangle(154, 34, 187, 21));
 +    epsRendering.setBounds(new Rectangle(174, 34, 187, 21));
 +    JLabel htmlLabel = new JLabel(
 +            MessageManager.formatMessage("label.rendering_style", "HTML"));
 +    htmlLabel.setFont(LABEL_FONT);
 +    htmlLabel.setHorizontalAlignment(SwingConstants.RIGHT);
 +    htmlLabel.setBounds(new Rectangle(9, 55, 160, 24));
 +    htmlRendering.setFont(LABEL_FONT);
 +    htmlRendering.setBounds(new Rectangle(174, 58, 187, 21));
 +    JLabel svgLabel = new JLabel(
 +            MessageManager.formatMessage("label.rendering_style", "SVG"));
 +    svgLabel.setFont(LABEL_FONT);
 +    svgLabel.setHorizontalAlignment(SwingConstants.RIGHT);
 +    svgLabel.setBounds(new Rectangle(9, 79, 160, 24));
 +    svgRendering.setFont(LABEL_FONT);
 +    svgRendering.setBounds(new Rectangle(174, 82, 187, 21));
 +
      JLabel jLabel1 = new JLabel();
      jLabel1.setFont(LABEL_FONT);
      jLabel1.setHorizontalAlignment(SwingConstants.CENTER);
      jLabel1.setText(MessageManager.getString("label.append_start_end"));
      jLabel1.setFont(LABEL_FONT);
 +
      fastajv.setFont(LABEL_FONT);
      fastajv.setHorizontalAlignment(SwingConstants.LEFT);
      clustaljv.setText(MessageManager.getString("label.clustal") + "     ");
      TitledBorder titledBorder2 = new TitledBorder(
              MessageManager.getString("label.file_output"));
      jPanel11.setBorder(titledBorder2);
 -    jPanel11.setBounds(new Rectangle(30, 72, 196, 182));
 +    jPanel11.setBounds(new Rectangle(30, 120, 196, 182));
      GridLayout gridLayout3 = new GridLayout();
      jPanel11.setLayout(gridLayout3);
      gridLayout3.setRows(8);
              MessageManager.getString("label.automatically_set_id_width"));
      autoIdWidth.setToolTipText(JvSwingUtils.wrapTooltip(true, MessageManager
              .getString("label.adjusts_width_generated_eps_png")));
 -    autoIdWidth.setBounds(new Rectangle(228, 96, 188, 23));
 +    autoIdWidth.setBounds(new Rectangle(228, 144, 320, 23));
      autoIdWidth.addActionListener(new ActionListener()
      {
  
      userIdWidthlabel.setToolTipText(
              JvSwingUtils.wrapTooltip(true, MessageManager.getString(
                      "label.manually_specify_width_left_column")));
 -    userIdWidthlabel.setBounds(new Rectangle(236, 120, 168, 23));
 +    userIdWidthlabel.setBounds(new Rectangle(236, 168, 320, 23));
      userIdWidth.setFont(JvSwingUtils.getTextAreaFont());
      userIdWidth.setText("");
 -    userIdWidth.setBounds(new Rectangle(232, 144, 84, 23));
 +    userIdWidth.setBounds(new Rectangle(232, 192, 84, 23));
      userIdWidth.addActionListener(new ActionListener()
      {
  
      modellerOutput.setFont(LABEL_FONT);
      modellerOutput
              .setText(MessageManager.getString("label.use_modeller_output"));
 -    modellerOutput.setBounds(new Rectangle(228, 226, 168, 23));
 +    modellerOutput.setBounds(new Rectangle(228, 274, 320, 23));
      embbedBioJSON.setFont(LABEL_FONT);
      embbedBioJSON.setText(MessageManager.getString("label.embbed_biojson"));
 -    embbedBioJSON.setBounds(new Rectangle(228, 200, 250, 23));
 +    embbedBioJSON.setBounds(new Rectangle(228, 248, 250, 23));
  
      jPanel11.add(jLabel1);
      jPanel11.add(blcjv);
      outputTab.add(userIdWidth);
      outputTab.add(userIdWidthlabel);
      outputTab.add(modellerOutput);
 -    outputTab.add(embbedBioJSON);
 -    outputTab.add(epsLabel);
 -    outputTab.add(epsRendering);
 +    if (!Platform.isJS())
 +    {
 +      /*
 +       * JalviewJS doesn't support Lineart option or SVG output
 +       */
 +      outputTab.add(embbedBioJSON);
 +      outputTab.add(epsLabel);
 +      outputTab.add(epsRendering);
 +      outputTab.add(htmlLabel);
 +      outputTab.add(htmlRendering);
 +      outputTab.add(svgLabel);
 +      outputTab.add(svgRendering);
 +    }
      outputTab.add(jPanel11);
      return outputTab;
    }
      linkTab.setLayout(new GridBagLayout());
  
      // Set up table for Url links
+     linkUrlTable.getTableHeader().setReorderingAllowed(false);
      linkUrlTable.setFillsViewportHeight(true);
      linkUrlTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
      linkUrlTable.setAutoCreateRowSorter(true);
          hiddenColour_actionPerformed(hiddenColour);
        }
      });
-     
      useLegacyGap = new JCheckBox(
              MessageManager.getString("label.ov_legacy_gap"));
      useLegacyGap.setFont(LABEL_FONT);
      useLegacyGap.setHorizontalAlignment(SwingConstants.LEFT);
      useLegacyGap.setVerticalTextPosition(SwingConstants.TOP);
-     gapLabel = new JLabel(
-             MessageManager.getString("label.gap_colour"));
+     gapLabel = new JLabel(MessageManager.getString("label.gap_colour"));
      gapLabel.setFont(LABEL_FONT);
      gapLabel.setHorizontalAlignment(SwingConstants.LEFT);
      gapLabel.setVerticalTextPosition(SwingConstants.TOP);
      docFieldPref.setBounds(new Rectangle(10, ypos, 450, 120));
      structureTab.add(docFieldPref);
  
 +    /*
 +     * hide Chimera options in JalviewJS
 +     */
 +    if (Platform.isJS()) 
 +    {
 +      pathLabel.setVisible(false);
 +      chimeraPath.setVisible(false);
 +      viewerLabel.setVisible(false);
 +      structViewer.setVisible(false);
 +    }
 +    
      return structureTab;
    }
  
      visualTab.add(fontNameCB);
      visualTab.add(fontSizeCB);
      visualTab.add(fontStyleCB);
 +    
 +    if (Platform.isJS())
 +    {
 +      startupCheckbox.setVisible(false);
 +      startupFileTextfield.setVisible(false);
 +    }
 +    
      return visualTab;
    }
  
+   /**
+    * Load the saved Backups options EXCEPT "Enabled" and "Scheme"
+    */
+   protected void loadLastSavedBackupsOptions()
+   {
+     enableBackupFiles
+             .setSelected(Cache.getDefault(BackupFiles.ENABLED, true));
+     setComboIntStringKey(backupfilesPresetsCombo,
+             Cache.getDefault(BackupFiles.NS + "_PRESET", 1));
+     suffixTemplate.setText(Cache.getDefault(BackupFiles.SUFFIX,
+             ".bak" + BackupFiles.NUM_PLACEHOLDER));
+     suffixDigitsSpinner
+             .setValue(Cache.getDefault(BackupFiles.SUFFIX_DIGITS, 3));
+     suffixReverse.setSelected(
+             Cache.getDefault(BackupFiles.REVERSE_ORDER, false));
+     backupfilesKeepAll
+             .setSelected(Cache.getDefault(BackupFiles.NO_MAX, false));
+     backupfilesRollMaxSpinner
+             .setValue(Cache.getDefault(BackupFiles.ROLL_MAX, 3));
+     backupfilesConfirmDelete.setSelected(
+             Cache.getDefault(BackupFiles.CONFIRM_DELETE_OLD, true));
+     backupsOptionsSetEnabled();
+     updateBackupFilesExampleLabel();
+   }
+   private boolean warnAboutSuffixReverseChange()
+   {
+     boolean savedSuffixReverse = Cache.getDefault(BackupFiles.REVERSE_ORDER,
+             false);
+     int savedSuffixDigits = Cache.getDefault(BackupFiles.SUFFIX_DIGITS, 3);
+     String savedSuffixTemplate = Cache.getDefault(BackupFiles.SUFFIX,
+             ".bak" + BackupFiles.NUM_PLACEHOLDER);
+     boolean nowSuffixReverse = suffixReverse.isSelected();
+     int nowSuffixDigits = getSpinnerInt(suffixDigitsSpinner, 3);
+     String nowSuffixTemplate = suffixTemplate.getText();
+     return nowSuffixReverse != savedSuffixReverse
+             && nowSuffixDigits == savedSuffixDigits
+             && nowSuffixTemplate != null
+             && nowSuffixTemplate.equals(savedSuffixTemplate);
+   }
+   /**
+    * Initialises the Backups tabbed panel.
+    * 
+    * @return
+    */
+   private JPanel initBackupsTab()
+   {
+     JPanel backupsTab = new JPanel();
+     backupsTab.setBorder(new TitledBorder(
+             MessageManager.getString("label.backup_files")));
+     backupsTab.setLayout(new GridBagLayout());
+     GridBagConstraints gbc = new GridBagConstraints();
+     gbc.weightx = 0.0;
+     gbc.weighty = 0.0;
+     gbc.anchor = GridBagConstraints.FIRST_LINE_START;
+     gbc.fill = GridBagConstraints.NONE;
+     initBackupsTabPresetsPanel();
+     initBackupsTabSuffixPanel();
+     initBackupsTabKeepFilesPanel();
+     initBackupsTabFilenameExamplesPanel();
+     enableBackupFiles.setFont(LABEL_FONT_BOLD);
+     enableBackupFiles
+             .setText(MessageManager.getString("label.enable_backupfiles"));
+     enableBackupFiles.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         // enable other options only when the first is checked
+         backupsOptionsSetEnabled();
+       }
+     });
+     // enable checkbox 1 col
+     gbc.gridwidth = 1;
+     gbc.gridheight = 1;
+     gbc.gridx = 0;
+     gbc.gridy = 0; // row 0
+     backupsTab.add(enableBackupFiles, gbc);
+     // summary of scheme box (over two rows)
+     gbc.gridx = 1;
+     gbc.weightx = 0.0;
+     gbc.gridheight = 2;
+     gbc.anchor = GridBagConstraints.FIRST_LINE_END;
+     gbc.fill = GridBagConstraints.BOTH;
+     backupsTab.add(exampleFilesPanel, gbc);
+     gbc.gridheight = 1;
+     gbc.anchor = GridBagConstraints.FIRST_LINE_START;
+     gbc.fill = GridBagConstraints.NONE;
+     // fill empty space on right
+     gbc.gridx++;
+     gbc.weightx = 1.0;
+     backupsTab.add(new JPanel(), gbc);
+     // schemes box
+     gbc.weightx = 0.0;
+     gbc.gridx = 0;
+     gbc.gridy++; // row 1
+     backupsTab.add(presetsPanel, gbc);
+     // gbc.anchor = GridBagConstraints.NORTHWEST;
+     // now using whole row
+     gbc.gridwidth = 2;
+     gbc.gridheight = 1;
+     // keep files box
+     gbc.gridx = 0;
+     gbc.gridy++; // row 2
+     backupsTab.add(keepfilesPanel, gbc);
+     // filename strategy box
+     gbc.gridy++; // row 3
+     backupsTab.add(suffixPanel, gbc);
+     // fill empty space
+     gbc.gridy++; // row 4
+     gbc.weighty = 1.0;
+     backupsTab.add(new JPanel(), gbc);
+     backupsOptionsSetEnabled();
+     return backupsTab;
+   }
+   protected static final int BACKUPFILESSCHEMECUSTOMISE = 0;
+   private static final IntKeyStringValueEntry[] backupfilesPresetEntries = {
+       new IntKeyStringValueEntry(1,
+               MessageManager.getString("label.default")),
+       new IntKeyStringValueEntry(2,
+               MessageManager.getString("label.single_file")),
+       new IntKeyStringValueEntry(3,
+               MessageManager.getString("label.keep_all_versions")),
+       new IntKeyStringValueEntry(4,
+               MessageManager.getString("label.rolled_backups")),
+       // ...
+       // IMPORTANT, keep "Custom" entry with key 0 (even though it appears last)
+       new IntKeyStringValueEntry(BACKUPFILESSCHEMECUSTOMISE,
+               MessageManager.getString("label.customise")) };
+   private static final Map<Integer, BackupFilesPresetEntry> backupfilesPresetEntriesValues = new HashMap<Integer, BackupFilesPresetEntry>()
+   {
+     /**
+      * 
+      */
+     private static final long serialVersionUID = 125L;
+     {
+       put(1, new BackupFilesPresetEntry(
+               ".bak" + BackupFiles.NUM_PLACEHOLDER, 3, false, false, 3,
+               false));
+       put(2, new BackupFilesPresetEntry("~", 1, false, false, 1, false));
+       put(3, new BackupFilesPresetEntry(".v" + BackupFiles.NUM_PLACEHOLDER,
+               3, false, true, 10, true));
+       put(4, new BackupFilesPresetEntry(
+               "_bak." + BackupFiles.NUM_PLACEHOLDER, 1, true, false, 9,
+               false));
+     }
+   };
+   private JPanel initBackupsTabPresetsPanel()
+   {
+     String title = MessageManager.getString("label.schemes");
+     // TitledBorder tb = new TitledBorder(new EmptyBorder(0, 0, 0, 0), title);
+     // TitledBorder tb = new TitledBorder(title);
+     // tb.setTitleFont(LABEL_FONT);
+     // presetsPanel.setBorder(tb);
+     presetsPanel.setLayout(new GridBagLayout());
+     GridBagConstraints gbc = new GridBagConstraints();
+     gbc.weightx = 0.0;
+     gbc.weighty = 0.0;
+     gbc.anchor = GridBagConstraints.BASELINE_LEADING;
+     gbc.fill = GridBagConstraints.NONE;
+     gbc.gridwidth = 1;
+     gbc.gridheight = 1;
+     // "Scheme: "
+     gbc.gridx = 0;
+     gbc.gridy = 0;
+     presetsPanel.add(new JLabel(title + ":"), gbc);
+     for (int i = 0; i < backupfilesPresetEntries.length; i++)
+     {
+       backupfilesPresetsCombo.addItem(backupfilesPresetEntries[i]);
+     }
+     // put "Previously saved scheme" item in italics (it's not really
+     // selectable, as such -- it deselects itself when selected) and
+     // "Customise" in bold
+     backupfilesPresetsCombo
+             .setRenderer(new BackupFilesPresetsComboBoxRenderer());
+     backupfilesPresetsCombo.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         backupsTabUpdatePresets();
+       }
+     });
+     // dropdown list of preset schemes
+     gbc.gridx = 1;
+     presetsPanel.add(backupfilesPresetsCombo, gbc);
+     revertButton.setText(MessageManager.getString("label.cancel_changes"));
+     revertButton.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         loadLastSavedBackupsOptions();
+       }
+     });
+     revertButton.setFont(LABEL_FONT);
+     // "Cancel changes" button (aligned with combo box above)
+     gbc.gridx = 1;
+     gbc.gridy++;
+     presetsPanel.add(revertButton, gbc);
+     return presetsPanel;
+   }
+   private JPanel initBackupsTabFilenameExamplesPanel()
+   {
+     String title = MessageManager
+             .getString("label.summary_of_backups_scheme");
+     TitledBorder tb = new TitledBorder(title);
+     exampleFilesPanel.setBorder(tb);
+     exampleFilesPanel.setLayout(new GridBagLayout());
+     backupfilesExampleLabel.setEditable(false);
+     backupfilesExampleLabel
+             .setBackground(exampleFilesPanel.getBackground());
+     updateBackupFilesExampleLabel();
+     GridBagConstraints gbc = new GridBagConstraints();
+     gbc.weightx = 1.0;
+     gbc.weighty = 1.0;
+     gbc.fill = GridBagConstraints.NONE;
+     gbc.anchor = GridBagConstraints.FIRST_LINE_START;
+     exampleFilesPanel.add(backupfilesExampleLabel, gbc);
+     return exampleFilesPanel;
+   }
+   private void backupsTabUpdatePresets()
+   {
+     IntKeyStringValueEntry entry = (IntKeyStringValueEntry) backupfilesPresetsCombo
+             .getSelectedItem();
+     int key = entry.getKey();
+     String value = entry.getValue();
+     // BACKUPFILESSCHEMECUSTOMISE (==0) reserved for "Custom"
+     if (key != BACKUPFILESSCHEMECUSTOMISE)
+     {
+       if (backupfilesPresetEntriesValues.containsKey(key))
+       {
+         backupsSetOptions(backupfilesPresetEntriesValues.get(key));
+       }
+       else
+       {
+         System.out.println("Preset '" + value + "' not implemented");
+       }
+     }
+     backupfilesCustomOptionsSetEnabled();
+     updateBackupFilesExampleLabel();
+   }
+   protected int getComboIntStringKey(JComboBox<IntKeyStringValueEntry> c)
+   {
+     IntKeyStringValueEntry e = (IntKeyStringValueEntry) c.getSelectedItem();
+     return e != null ? e.getKey() : 0;
+   }
+   protected void setComboIntStringKey(JComboBox<IntKeyStringValueEntry> c,
+           int key)
+   {
+     for (int i = 0; i < c.getItemCount(); i++)
+     {
+       IntKeyStringValueEntry e = c.getItemAt(i);
+       if (e.getKey() == key)
+       {
+         c.setSelectedIndex(i);
+         break;
+       }
+     }
+     backupsTabUpdatePresets();
+   }
+   private JPanel initBackupsTabSuffixPanel()
+   {
+     suffixPanel.setBorder(new TitledBorder(
+             MessageManager.getString("label.backup_filename_strategy")));
+     suffixPanel.setLayout(new GridBagLayout());
+     suffixTemplateLabel
+             .setText(MessageManager.getString("label.append_to_filename"));
+     suffixTemplateLabel.setHorizontalAlignment(SwingConstants.LEFT);
+     suffixTemplateLabel.setFont(LABEL_FONT);
+     final String tooltip = JvSwingUtils.wrapTooltip(true,
+             MessageManager.getString("label.append_to_filename_tooltip"));
+     suffixTemplate.setToolTipText(tooltip);
+     suffixTemplate.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         updateBackupFilesExampleLabel();
+         backupfilesCustomOptionsSetEnabled();
+       }
+     });
+     KeyListener kl = new KeyListener()
+     {
+       @Override
+       public void keyReleased(KeyEvent e)
+       {
+         updateBackupFilesExampleLabel();
+         backupfilesCustomOptionsSetEnabled();
+       }
+       @Override
+       public void keyPressed(KeyEvent e)
+       {
+       }
+       // disable use of ':' or '/' or '\'
+       @Override
+       public void keyTyped(KeyEvent e)
+       {
+         char c = e.getKeyChar();
+         if (c == ':' || c == '/' || c == '\\')
+         {
+           // don't process ':' or '/' or '\'
+           e.consume();
+         }
+       }
+     };
+     suffixTemplate.addKeyListener(kl);
+     // digits spinner
+     suffixDigitsLabel
+             .setText(MessageManager.getString("label.index_digits"));
+     suffixDigitsLabel.setHorizontalAlignment(SwingConstants.LEFT);
+     suffixDigitsLabel.setFont(LABEL_FONT);
+     int defaultmin = 1;
+     int defaultmax = 6;
+     ChangeListener c = new ChangeListener()
+     {
+       @Override
+       public void stateChanged(ChangeEvent e)
+       {
+         updateBackupFilesExampleLabel();
+       }
+     };
+     setIntegerSpinner(suffixDigitsSpinner, defaultmin, defaultmax, 3, c);
+     suffixReverse.setLabels(MessageManager.getString("label.reverse_roll"),
+             MessageManager.getString("label.increment_index"));
+     suffixReverse.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         boolean okay = true;
+         if (warnAboutSuffixReverseChange())
+         {
+           // Warning popup
+           okay = confirmSuffixReverseChange();
+         }
+         if (okay)
+         {
+           updateBackupFilesExampleLabel();
+         }
+         else
+         {
+           boolean savedSuffixReverse = Cache
+                   .getDefault(BackupFiles.REVERSE_ORDER, false);
+           suffixReverse.setSelected(savedSuffixReverse);
+         }
+       }
+     });
+     GridBagConstraints sgbc = new GridBagConstraints();
+     // first row (template text box)
+     sgbc.anchor = GridBagConstraints.WEST;
+     sgbc.gridx = 0;
+     sgbc.gridy = 0;
+     sgbc.gridwidth = 1;
+     sgbc.gridheight = 1;
+     sgbc.weightx = 1.0;
+     sgbc.weighty = 0.0;
+     sgbc.fill = GridBagConstraints.NONE;
+     suffixPanel.add(suffixTemplateLabel, sgbc);
+     sgbc.gridx = 1;
+     sgbc.fill = GridBagConstraints.HORIZONTAL;
+     suffixPanel.add(suffixTemplate, sgbc);
+     // second row (number of digits spinner)
+     sgbc.gridy = 1;
+     sgbc.gridx = 0;
+     sgbc.fill = GridBagConstraints.NONE;
+     suffixPanel.add(suffixDigitsLabel, sgbc);
+     sgbc.gridx = 1;
+     sgbc.fill = GridBagConstraints.HORIZONTAL;
+     suffixPanel.add(suffixDigitsSpinner, sgbc);
+     // third row (forward order radio selection)
+     sgbc.gridx = 0;
+     sgbc.gridy = 2;
+     sgbc.gridwidth = GridBagConstraints.REMAINDER;
+     sgbc.fill = GridBagConstraints.HORIZONTAL;
+     suffixPanel.add(suffixReverse.getFalseButton(), sgbc);
+     // fourth row (reverse order radio selection)
+     sgbc.gridy = 3;
+     suffixPanel.add(suffixReverse.getTrueButton(), sgbc);
+     return suffixPanel;
+   }
+   private boolean confirmSuffixReverseChange()
+   {
+     boolean ret = false;
+     String warningMessage = MessageManager
+             .getString("label.warning_confirm_change_reverse");
+     int confirm = JvOptionPane.showConfirmDialog(Desktop.desktop,
+             warningMessage,
+             MessageManager.getString("label.change_increment_decrement"),
+             JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
+     ret = (confirm == JvOptionPane.YES_OPTION);
+     return ret;
+   }
+   private JPanel initBackupsTabKeepFilesPanel()
+   {
+     keepfilesPanel.setBorder(
+             new TitledBorder(MessageManager.getString("label.keep_files")));
+     keepfilesPanel.setLayout(new GridBagLayout());
+     backupfilesKeepAll.setLabels(
+             MessageManager.getString("label.keep_all_backup_files"),
+             MessageManager.getString(
+                     "label.keep_only_this_number_of_backup_files"));
+     backupfilesKeepAll.addTrueActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         updateBackupFilesExampleLabel();
+       }
+     });
+     backupfilesKeepAll.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         keepRollMaxOptionsEnabled();
+         updateBackupFilesExampleLabel();
+       }
+     });
+     ChangeListener c = new ChangeListener()
+     {
+       @Override
+       public void stateChanged(ChangeEvent e)
+       {
+         updateBackupFilesExampleLabel();
+       }
+     };
+     setIntegerSpinner(backupfilesRollMaxSpinner, 1, 999, 4, true, c);
+     backupfilesConfirmDelete.setLabels(
+             MessageManager.getString("label.always_ask"),
+             MessageManager.getString("label.auto_delete"));
+     // update the enabled section
+     keepRollMaxOptionsEnabled();
+     GridBagConstraints kgbc = new GridBagConstraints();
+     // first row (template text box)
+     kgbc.anchor = GridBagConstraints.WEST;
+     kgbc.gridx = 0;
+     kgbc.gridy = 0;
+     kgbc.gridwidth = GridBagConstraints.REMAINDER;
+     kgbc.gridheight = 1;
+     kgbc.weightx = 1.0;
+     kgbc.weighty = 0.0;
+     kgbc.fill = GridBagConstraints.HORIZONTAL;
+     keepfilesPanel.add(backupfilesKeepAll.getTrueButton(), kgbc);
+     // second row
+     kgbc.gridy = 1;
+     kgbc.gridx = 0;
+     kgbc.gridwidth = GridBagConstraints.RELATIVE;
+     keepfilesPanel.add(backupfilesKeepAll.getFalseButton(), kgbc);
+     kgbc.gridx = 1;
+     kgbc.gridwidth = GridBagConstraints.REMAINDER;
+     keepfilesPanel.add(backupfilesRollMaxSpinner, kgbc);
+     // third row (indented)
+     kgbc.gridy = 2;
+     kgbc.insets = new Insets(0, 20, 0, 0);
+     kgbc.gridx = 0;
+     kgbc.gridwidth = GridBagConstraints.REMAINDER;
+     kgbc.fill = GridBagConstraints.HORIZONTAL;
+     kgbc.weightx = 1.0;
+     /*
+     keepfilesPanel.add(backupfilesConfirmDelete.getTrueButton(), kgbc);
+     
+     // fourth row (indented)
+     kgbc.gridy = 3;
+     keepfilesPanel.add(backupfilesConfirmDelete.getFalseButton(), kgbc);
+     */
+     JPanel jp = new JPanel();
+     jp.setLayout(new FlowLayout());
+     oldBackupFilesLabel
+             .setText(MessageManager
+                     .getString("label.autodelete_old_backup_files"));
+     oldBackupFilesLabel.setFont(LABEL_FONT);
+     oldBackupFilesLabel.setHorizontalAlignment(SwingConstants.LEFT);
+     jp.add(oldBackupFilesLabel);
+     jp.add(backupfilesConfirmDelete.getTrueButton());
+     jp.add(backupfilesConfirmDelete.getFalseButton());
+     keepfilesPanel.add(jp, kgbc);
+     return keepfilesPanel;
+   }
+   protected void updateBackupFilesExampleLabel()
+   {
+     int exampleindex = 12;
+     String base = MessageManager.getString("label.filename") + ".fa";
+     if (base == null || base.length() == 0)
+     {
+       base = "file_name.fa";
+     }
+     boolean reverse = suffixReverse.isSelected();
+     boolean keepAll = backupfilesKeepAll.isSelected();
+     int rollMax = 4;
+     String suffix = suffixTemplate.getText();
+     int digits = 3;
+     backupfilesExampleLabel.setFont(LABEL_FONT_ITALIC);
+     if (suffix == null || suffix.length() == 0)
+     {
+       backupfilesExampleLabel
+               .setText(MessageManager.getString("label.no_backup_files"));
+       backupfilesExampleLabel.setFont(LABEL_FONT_BOLD);
+       return;
+     }
+     rollMax = getSpinnerInt(backupfilesRollMaxSpinner, 4);
+     rollMax = rollMax < 1 ? 1 : rollMax;
+     if (suffix.indexOf(BackupFiles.NUM_PLACEHOLDER) == -1)
+     {
+       rollMax = 1;
+     }
+     digits = getSpinnerInt(suffixDigitsSpinner, 3);
+     digits = digits < 1 ? 1 : digits;
+     int lowersurround = 2;
+     int uppersurround = 0;
+     StringBuilder exampleSB = new StringBuilder();
+     boolean firstLine = true;
+     if (reverse)
+     {
+       int min = 1;
+       int max = keepAll ? exampleindex : rollMax;
+       for (int index = min; index <= max; index++)
+       {
+         if (index == min + lowersurround && index < max - uppersurround - 1)
+         {
+           exampleSB.append("\n...");
+         }
+         else if (index > min + lowersurround && index < max - uppersurround)
+         {
+           // nothing
+         }
+         else
+         {
+           if (firstLine)
+           {
+             firstLine = false;
+           }
+           else
+           {
+             exampleSB.append("\n");
+           }
+           exampleSB.append(BackupFilenameParts.getBackupFilename(index,
+                   base, suffix, digits));
+           if (min == max)
+           {
+             // no extra text needed
+           }
+           else if (index == min)
+           {
+             String newest = MessageManager.getString("label.braced_newest");
+             if (newest != null && newest.length() > 0)
+             {
+               exampleSB.append(" " + newest);
+             }
+           }
+           else if (index == max)
+           {
+             String oldest = MessageManager.getString("label.braced_oldest");
+             if (oldest != null && oldest.length() > 0)
+             {
+               exampleSB.append(" " + oldest);
+             }
+           }
+         }
+       }
+     }
+     else
+     {
+       int min = (keepAll || exampleindex - rollMax < 0) ? 1
+               : exampleindex - rollMax + 1;
+       int max = exampleindex;
+       for (int index = min; index <= max; index++)
+       {
+         if (index == min + lowersurround && index < max - uppersurround - 1)
+         {
+           exampleSB.append("\n...");
+         }
+         else if (index > min + lowersurround && index < max - uppersurround)
+         {
+           // nothing
+         }
+         else
+         {
+           if (firstLine)
+           {
+             firstLine = false;
+           }
+           else
+           {
+             exampleSB.append("\n");
+           }
+           exampleSB.append(BackupFilenameParts.getBackupFilename(index,
+                   base, suffix, digits));
+           if (min == max)
+           {
+             // no extra text needed
+           }
+           else if (index == min)
+           {
+             String oldest = MessageManager.getString("label.braced_oldest");
+             if (oldest != null && oldest.length() > 0)
+             {
+               exampleSB.append(" " + oldest);
+             }
+           }
+           else if (index == max)
+           {
+             String newest = MessageManager.getString("label.braced_newest");
+             if (newest != null && newest.length() > 0)
+             {
+               exampleSB.append(" " + newest);
+             }
+           }
+         }
+       }
+     }
+     backupfilesExampleLabel.setText(exampleSB.toString());
+   }
+   protected void setIntegerSpinner(JSpinner s, int min, int max, int def,
+           boolean useExistingVal, ChangeListener c)
+   {
+     int i = def;
+     if (useExistingVal)
+     {
+       try
+       {
+         i = Integer.parseInt((String) s.getValue());
+       } catch (Exception e)
+       {
+         System.out.println(
+                 "Exception casting the initial value of s.getValue()");
+       }
+     }
+     setIntegerSpinner(s, min, max, i, c);
+   }
+   protected void setIntegerSpinner(JSpinner s, int min, int max, int def,
+           ChangeListener c)
+   {
+     // integer spinner for number of digits
+     if (def > max)
+     {
+       max = def;
+     }
+     SpinnerModel sModel = new SpinnerNumberModel(def, min, max, 1);
+     s.setModel(sModel);
+     s.addChangeListener(c);
+   }
+   protected static int getSpinnerInt(JSpinner s, int def)
+   {
+     int i = def;
+     try
+     {
+       s.commitEdit();
+       i = (Integer) s.getValue();
+     } catch (Exception e)
+     {
+       System.out.println("Failed casting (Integer) JSpinner s.getValue()");
+     }
+     return i;
+   }
+   private void keepRollMaxOptionsEnabled()
+   {
+     boolean enabled = backupfilesKeepAll.isEnabled()
+             && !backupfilesKeepAll.isSelected();
+     oldBackupFilesLabel.setEnabled(enabled);
+     backupfilesRollMaxSpinner.setEnabled(enabled);
+     backupfilesConfirmDelete.setEnabled(enabled);
+   }
+   private void backupfilesKeepAllSetEnabled(boolean tryEnabled)
+   {
+     boolean enabled = tryEnabled && enableBackupFiles.isSelected()
+             && getComboIntStringKey(backupfilesPresetsCombo) == 0
+             && suffixTemplate.getText()
+                     .indexOf(BackupFiles.NUM_PLACEHOLDER) > -1;
+     keepfilesPanel.setEnabled(enabled);
+     backupfilesKeepAll.setEnabled(enabled);
+     oldBackupFilesLabel.setEnabled(enabled);
+     keepRollMaxOptionsEnabled();
+   }
+   private void backupfilesSuffixTemplateDigitsSetEnabled()
+   {
+     boolean enabled = suffixTemplate.isEnabled() && suffixTemplate.getText()
+             .indexOf(BackupFiles.NUM_PLACEHOLDER) > -1;
+     suffixDigitsLabel.setEnabled(enabled);
+     suffixDigitsSpinner.setEnabled(enabled);
+     suffixReverse.setEnabled(enabled);
+   }
+   private void backupfilesSuffixTemplateSetEnabled(boolean tryEnabled)
+   {
+     boolean enabled = tryEnabled && enableBackupFiles.isSelected()
+             && getComboIntStringKey(backupfilesPresetsCombo) == 0;
+     suffixPanel.setEnabled(enabled);
+     suffixTemplateLabel.setEnabled(enabled);
+     suffixTemplate.setEnabled(enabled);
+     backupfilesSuffixTemplateDigitsSetEnabled();
+   }
+   protected void backupfilesCustomOptionsSetEnabled()
+   {
+     int scheme = getComboIntStringKey(backupfilesPresetsCombo);
+     boolean enabled = scheme == 0 && enableBackupFiles.isSelected();
+     backupfilesSuffixTemplateSetEnabled(enabled);
+     backupfilesKeepAllSetEnabled(enabled);
+   }
+   private void backupfilesSummarySetEnabled()
+   {
+     boolean enabled = enableBackupFiles.isSelected();
+     backupfilesExampleLabel.setEnabled(enabled);
+     exampleFilesPanel.setEnabled(enabled);
+   }
+   private void backupfilesPresetsSetEnabled()
+   {
+     boolean enabled = enableBackupFiles.isSelected();
+     presetsPanel.setEnabled(enabled);
+     backupfilesPresetsCombo.setEnabled(enabled);
+   }
+   protected void backupsOptionsSetEnabled()
+   {
+     backupfilesPresetsSetEnabled();
+     backupfilesSummarySetEnabled();
+     backupfilesCustomOptionsSetEnabled();
+   }
+   protected void backupsSetOptions(String suffix, int digits,
+           boolean reverse, boolean keepAll, int rollMax,
+           boolean confirmDelete)
+   {
+     suffixTemplate.setText(suffix);
+     suffixDigitsSpinner.setValue(digits);
+     suffixReverse.setSelected(reverse);
+     backupfilesKeepAll.setSelected(keepAll);
+     backupfilesRollMaxSpinner.setValue(rollMax);
+     backupfilesConfirmDelete.setSelected(confirmDelete);
+   }
+   protected void backupsSetOptions(BackupFilesPresetEntry p)
+   {
+     backupsSetOptions(p.suffix, p.digits, p.reverse, p.keepAll, p.rollMax,
+             p.confirmDelete);
+   }
    protected void autoIdWidth_actionPerformed()
    {
      // TODO Auto-generated method stub
  
    }
  }
+ class IntKeyStringValueEntry
+ {
+   int k;
+   String v;
+   public IntKeyStringValueEntry(int k, String v)
+   {
+     this.k = k;
+     this.v = v;
+   }
+   @Override
+   public String toString()
+   {
+     return this.getValue();
+   }
+   public int getKey()
+   {
+     return k;
+   }
+   public String getValue()
+   {
+     return v;
+   }
+ }
+ class BackupFilesPresetEntry
+ {
+   String suffix;
+   int digits;
+   boolean reverse;
+   boolean keepAll;
+   int rollMax;
+   boolean confirmDelete;
+   public BackupFilesPresetEntry(String suffix, int digits, boolean reverse,
+           boolean keepAll, int rollMax, boolean confirmDelete)
+   {
+     this.suffix = suffix;
+     this.digits = digits;
+     this.reverse = reverse;
+     this.keepAll = keepAll;
+     this.rollMax = rollMax;
+     this.confirmDelete = confirmDelete;
+   }
+ }
+ class BackupFilesPresetsComboBoxRenderer extends DefaultListCellRenderer
+ {
+   /**
+    * 
+    */
+   private static final long serialVersionUID = 88L;
+   @Override
+   public Component getListCellRendererComponent(JList list, Object value,
+           int index, boolean isSelected, boolean cellHasFocus)
+   {
+     super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+     
+     try {
+       IntKeyStringValueEntry e = (IntKeyStringValueEntry) value;
+       if (e.getKey() == GPreferences.BACKUPFILESSCHEMECUSTOMISE)
+       {
+         // "Customise" item
+         this.setFont(this.getFont().deriveFont(Font.BOLD));
+       }
+     } catch (Exception e) {
+       return this;
+     }
+     return this;
+   }
+ }
   */
  package jalview.project;
  
+ import static jalview.math.RotatableMatrix.Axis.X;
+ import static jalview.math.RotatableMatrix.Axis.Y;
+ import static jalview.math.RotatableMatrix.Axis.Z;
  import jalview.analysis.Conservation;
+ import jalview.analysis.PCA;
+ import jalview.analysis.scoremodels.ScoreModels;
+ import jalview.analysis.scoremodels.SimilarityParams;
  import jalview.api.FeatureColourI;
  import jalview.api.ViewStyleI;
+ import jalview.api.analysis.ScoreModelI;
+ import jalview.api.analysis.SimilarityParamsI;
  import jalview.api.structures.JalviewStructureDisplayI;
  import jalview.bin.Cache;
  import jalview.datamodel.AlignedCodonFrame;
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.DBRefEntry;
  import jalview.datamodel.GraphLine;
  import jalview.datamodel.PDBEntry;
+ import jalview.datamodel.Point;
  import jalview.datamodel.RnaViewerModel;
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceGroup;
@@@ -50,18 -59,21 +60,21 @@@ import jalview.gui.AppVarna
  import jalview.gui.ChimeraViewFrame;
  import jalview.gui.Desktop;
  import jalview.gui.FeatureRenderer;
- import jalview.gui.Jalview2XML_V1;
  import jalview.gui.JvOptionPane;
  import jalview.gui.OOMWarning;
+ import jalview.gui.PCAPanel;
  import jalview.gui.PaintRefresher;
  import jalview.gui.SplitFrame;
  import jalview.gui.StructureViewer;
  import jalview.gui.StructureViewer.ViewerType;
  import jalview.gui.StructureViewerBase;
  import jalview.gui.TreePanel;
+ import jalview.io.BackupFiles;
  import jalview.io.DataSourceType;
  import jalview.io.FileFormat;
  import jalview.io.NewickFile;
+ import jalview.math.Matrix;
+ import jalview.math.MatrixI;
  import jalview.renderer.ResidueShaderI;
  import jalview.schemes.AnnotationColourGradient;
  import jalview.schemes.ColourSchemeI;
@@@ -78,6 -90,7 +91,7 @@@ import jalview.util.StringUtils
  import jalview.util.jarInputStreamProvider;
  import jalview.util.matcher.Condition;
  import jalview.viewmodel.AlignmentViewport;
+ import jalview.viewmodel.PCAModel;
  import jalview.viewmodel.ViewportRanges;
  import jalview.viewmodel.seqfeatures.FeatureRendererSettings;
  import jalview.viewmodel.seqfeatures.FeaturesDisplayed;
@@@ -93,6 -106,8 +107,8 @@@ import jalview.xml.binding.jalview.Anno
  import jalview.xml.binding.jalview.Annotation.ThresholdLine;
  import jalview.xml.binding.jalview.AnnotationColourScheme;
  import jalview.xml.binding.jalview.AnnotationElement;
+ import jalview.xml.binding.jalview.DoubleMatrix;
+ import jalview.xml.binding.jalview.DoubleVector;
  import jalview.xml.binding.jalview.Feature;
  import jalview.xml.binding.jalview.Feature.OtherData;
  import jalview.xml.binding.jalview.FeatureMatcherSet.CompoundMatcher;
@@@ -107,6 -122,11 +123,11 @@@ import jalview.xml.binding.jalview.Jalv
  import jalview.xml.binding.jalview.JalviewModel.JSeq.Pdbids.StructureState;
  import jalview.xml.binding.jalview.JalviewModel.JSeq.RnaViewer;
  import jalview.xml.binding.jalview.JalviewModel.JSeq.RnaViewer.SecondaryStructure;
+ import jalview.xml.binding.jalview.JalviewModel.PcaViewer;
+ import jalview.xml.binding.jalview.JalviewModel.PcaViewer.Axis;
+ import jalview.xml.binding.jalview.JalviewModel.PcaViewer.SeqPointMax;
+ import jalview.xml.binding.jalview.JalviewModel.PcaViewer.SeqPointMin;
+ import jalview.xml.binding.jalview.JalviewModel.PcaViewer.SequencePoint;
  import jalview.xml.binding.jalview.JalviewModel.Tree;
  import jalview.xml.binding.jalview.JalviewModel.UserColours;
  import jalview.xml.binding.jalview.JalviewModel.Viewport;
@@@ -119,6 -139,7 +140,7 @@@ import jalview.xml.binding.jalview.MapL
  import jalview.xml.binding.jalview.Mapping;
  import jalview.xml.binding.jalview.NoValueColour;
  import jalview.xml.binding.jalview.ObjectFactory;
+ import jalview.xml.binding.jalview.PcaDataType;
  import jalview.xml.binding.jalview.Pdbentry.Property;
  import jalview.xml.binding.jalview.Sequence;
  import jalview.xml.binding.jalview.Sequence.DBRef;
@@@ -131,7 -152,6 +153,7 @@@ import java.awt.Color
  import java.awt.Font;
  import java.awt.Rectangle;
  import java.io.BufferedReader;
 +import java.io.ByteArrayInputStream;
  import java.io.DataInputStream;
  import java.io.DataOutputStream;
  import java.io.File;
@@@ -188,33 -208,18 +210,39 @@@ import javax.xml.stream.XMLStreamReader
   */
  public class Jalview2XML
  {
 +
 +  // BH 2018 we add the .jvp binary extension to J2S so that
 +  // it will declare that binary when we do the file save from the browser
 +
 +  private static void addJ2SBinaryType(String ext)
 +  {
 +    ext = "." + ext + "?";
 +
 +    /**
 +     * @j2sNative
 +     * 
 +     *            J2S._binaryTypes.push(ext);
 +     * 
 +     */
 +  }
 +
 +  static
 +  {
 +    addJ2SBinaryType(".jvp?");
 +  }
 +
    private static final String VIEWER_PREFIX = "viewer_";
  
    private static final String RNA_PREFIX = "rna_";
  
    private static final String UTF_8 = "UTF-8";
  
+   /**
+    * prefix for recovering datasets for alignments with multiple views where
+    * non-existent dataset IDs were written for some views
+    */
+   private static final String UNIQSEQSETID = "uniqueSeqSetId.";
    // use this with nextCounter() to make unique names for entities
    private int counter = 0;
  
    public void saveState(File statefile)
    {
      FileOutputStream fos = null;
      try
      {
        fos = new FileOutputStream(statefile);
        JarOutputStream jout = new JarOutputStream(fos);
        saveState(jout);
+       fos.close();
  
      } catch (Exception e)
      {
+       Cache.log.error("Couln't write Jalview state to " + statefile, e);
        // TODO: inform user of the problem - they need to know if their data was
        // not saved !
        if (errorMessage == null)
        {
-         errorMessage = "Couldn't write Jalview Archive to output file '"
+         errorMessage = "Did't write Jalview Archive to output file '"
                  + statefile + "' - See console error log for details";
        }
        else
        {
-         errorMessage += "(output file was '" + statefile + "')";
+         errorMessage += "(Didn't write Jalview Archive to output file '"
+                 + statefile + ")";
        }
        e.printStackTrace();
      } finally
    {
      try
      {
-       FileOutputStream fos = new FileOutputStream(jarFile);
+       // create backupfiles object and get new temp filename destination
+       BackupFiles backupfiles = new BackupFiles(jarFile);
+       FileOutputStream fos = new FileOutputStream(
+               backupfiles.getTempFilePath());
        JarOutputStream jout = new JarOutputStream(fos);
        List<AlignFrame> frames = new ArrayList<>();
  
        }
        ;
        jout.close();
-       return true;
+       boolean success = true;
+       backupfiles.setWriteSuccess(success);
+       success = backupfiles.rollBackupsAndRenameTempFile();
+       return success;
      } catch (Exception ex)
      {
        errorMessage = "Couldn't Write alignment view to Jalview Archive - see error output for details";
                tree.setXpos(tp.getX());
                tree.setYpos(tp.getY());
                tree.setId(makeHashCode(tp, null));
+               tree.setLinkToAllViews(
+                       tp.getTreeCanvas().isApplyToAllViews());
                // jms.addTree(tree);
                object.getTree().add(tree);
              }
        }
      }
  
+     /*
+      * save PCA viewers
+      */
+     if (!storeDS && Desktop.desktop != null)
+     {
+       for (JInternalFrame frame : Desktop.desktop.getAllFrames())
+       {
+         if (frame instanceof PCAPanel)
+         {
+           PCAPanel panel = (PCAPanel) frame;
+           if (panel.getAlignViewport().getAlignment() == jal)
+           {
+             savePCA(panel, object);
+           }
+         }
+       }
+     }
      // SAVE ANNOTATIONS
      /**
       * store forward refs from an annotationRow to any groups
        // using save and then load
        try
        {
 +      fileName = fileName.replace('\\', '/');
          System.out.println("Writing jar entry " + fileName);
          JarEntry entry = new JarEntry(fileName);
          jout.putNextEntry(entry);
    }
  
    /**
+    * Writes PCA viewer attributes and computed values to an XML model object and
+    * adds it to the JalviewModel. Any exceptions are reported by logging.
+    */
+   protected void savePCA(PCAPanel panel, JalviewModel object)
+   {
+     try
+     {
+       PcaViewer viewer = new PcaViewer();
+       viewer.setHeight(panel.getHeight());
+       viewer.setWidth(panel.getWidth());
+       viewer.setXpos(panel.getX());
+       viewer.setYpos(panel.getY());
+       viewer.setTitle(panel.getTitle());
+       PCAModel pcaModel = panel.getPcaModel();
+       viewer.setScoreModelName(pcaModel.getScoreModelName());
+       viewer.setXDim(panel.getSelectedDimensionIndex(X));
+       viewer.setYDim(panel.getSelectedDimensionIndex(Y));
+       viewer.setZDim(panel.getSelectedDimensionIndex(Z));
+       viewer.setBgColour(
+               panel.getRotatableCanvas().getBackgroundColour().getRGB());
+       viewer.setScaleFactor(panel.getRotatableCanvas().getScaleFactor());
+       float[] spMin = panel.getRotatableCanvas().getSeqMin();
+       SeqPointMin spmin = new SeqPointMin();
+       spmin.setXPos(spMin[0]);
+       spmin.setYPos(spMin[1]);
+       spmin.setZPos(spMin[2]);
+       viewer.setSeqPointMin(spmin);
+       float[] spMax = panel.getRotatableCanvas().getSeqMax();
+       SeqPointMax spmax = new SeqPointMax();
+       spmax.setXPos(spMax[0]);
+       spmax.setYPos(spMax[1]);
+       spmax.setZPos(spMax[2]);
+       viewer.setSeqPointMax(spmax);
+       viewer.setShowLabels(panel.getRotatableCanvas().isShowLabels());
+       viewer.setLinkToAllViews(
+               panel.getRotatableCanvas().isApplyToAllViews());
+       SimilarityParamsI sp = pcaModel.getSimilarityParameters();
+       viewer.setIncludeGaps(sp.includeGaps());
+       viewer.setMatchGaps(sp.matchGaps());
+       viewer.setIncludeGappedColumns(sp.includeGappedColumns());
+       viewer.setDenominateByShortestLength(sp.denominateByShortestLength());
+       /*
+        * sequence points on display
+        */
+       for (jalview.datamodel.SequencePoint spt : pcaModel
+               .getSequencePoints())
+       {
+         SequencePoint point = new SequencePoint();
+         point.setSequenceRef(seqHash(spt.getSequence()));
+         point.setXPos(spt.coord.x);
+         point.setYPos(spt.coord.y);
+         point.setZPos(spt.coord.z);
+         viewer.getSequencePoint().add(point);
+       }
+       /*
+        * (end points of) axes on display
+        */
+       for (Point p : panel.getRotatableCanvas().getAxisEndPoints())
+       {
+         Axis axis = new Axis();
+         axis.setXPos(p.x);
+         axis.setYPos(p.y);
+         axis.setZPos(p.z);
+         viewer.getAxis().add(axis);
+       }
+       /*
+        * raw PCA data (note we are not restoring PCA inputs here -
+        * alignment view, score model, similarity parameters)
+        */
+       PcaDataType data = new PcaDataType();
+       viewer.setPcaData(data);
+       PCA pca = pcaModel.getPcaData();
+       DoubleMatrix pm = new DoubleMatrix();
+       saveDoubleMatrix(pca.getPairwiseScores(), pm);
+       data.setPairwiseMatrix(pm);
+       DoubleMatrix tm = new DoubleMatrix();
+       saveDoubleMatrix(pca.getTridiagonal(), tm);
+       data.setTridiagonalMatrix(tm);
+       DoubleMatrix eigenMatrix = new DoubleMatrix();
+       data.setEigenMatrix(eigenMatrix);
+       saveDoubleMatrix(pca.getEigenmatrix(), eigenMatrix);
+       object.getPcaViewer().add(viewer);
+     } catch (Throwable t)
+     {
+       Cache.log.error("Error saving PCA: " + t.getMessage());
+     }
+   }
+   /**
+    * Stores values from a matrix into an XML element, including (if present) the
+    * D or E vectors
+    * 
+    * @param m
+    * @param xmlMatrix
+    * @see #loadDoubleMatrix(DoubleMatrix)
+    */
+   protected void saveDoubleMatrix(MatrixI m, DoubleMatrix xmlMatrix)
+   {
+     xmlMatrix.setRows(m.height());
+     xmlMatrix.setColumns(m.width());
+     for (int i = 0; i < m.height(); i++)
+     {
+       DoubleVector row = new DoubleVector();
+       for (int j = 0; j < m.width(); j++)
+       {
+         row.getV().add(m.getValue(i, j));
+       }
+       xmlMatrix.getRow().add(row);
+     }
+     if (m.getD() != null)
+     {
+       DoubleVector dVector = new DoubleVector();
+       for (double d : m.getD())
+       {
+         dVector.getV().add(d);
+       }
+       xmlMatrix.setD(dVector);
+     }
+     if (m.getE() != null)
+     {
+       DoubleVector eVector = new DoubleVector();
+       for (double e : m.getE())
+       {
+         eVector.getV().add(e);
+       }
+       xmlMatrix.setE(eVector);
+     }
+   }
+   /**
+    * Loads XML matrix data into a new Matrix object, including the D and/or E
+    * vectors (if present)
+    * 
+    * @param mData
+    * @return
+    * @see Jalview2XML#saveDoubleMatrix(MatrixI, DoubleMatrix)
+    */
+   protected MatrixI loadDoubleMatrix(DoubleMatrix mData)
+   {
+     int rows = mData.getRows();
+     double[][] vals = new double[rows][];
+     for (int i = 0; i < rows; i++)
+     {
+       List<Double> dVector = mData.getRow().get(i).getV();
+       vals[i] = new double[dVector.size()];
+       int dvi = 0;
+       for (Double d : dVector)
+       {
+         vals[i][dvi++] = d;
+       }
+     }
+     MatrixI m = new Matrix(vals);
+     if (mData.getD() != null)
+     {
+       List<Double> dVector = mData.getD().getV();
+       double[] vec = new double[dVector.size()];
+       int dvi = 0;
+       for (Double d : dVector)
+       {
+         vec[dvi++] = d;
+       }
+       m.setD(vec);
+     }
+     if (mData.getE() != null)
+     {
+       List<Double> dVector = mData.getE().getV();
+       double[] vec = new double[dVector.size()];
+       int dvi = 0;
+       for (Double d : dVector)
+       {
+         vec[dvi++] = d;
+       }
+       m.setE(vec);
+     }
+     return m;
+   }
+   /**
     * Save any Varna viewers linked to this sequence. Writes an rnaViewer element
     * for each viewer, with
     * <ul>
    {
      if (jout != null)
      {
 +      jarEntryName = jarEntryName.replace('\\','/');
        System.out.println("Writing jar entry " + jarEntryName);
        jout.putNextEntry(new JarEntry(jarEntryName));
        DataOutputStream dout = new DataOutputStream(jout);
      vamsasSeq.setName(jds.getName());
      vamsasSeq.setSequence(jds.getSequenceAsString());
      vamsasSeq.setDescription(jds.getDescription());
 -    jalview.datamodel.DBRefEntry[] dbrefs = null;
 +    List<DBRefEntry> dbrefs = null;
      if (jds.getDatasetSequence() != null)
      {
        vamsasSeq.setDsseqid(seqHash(jds.getDatasetSequence()));
      }
      if (dbrefs != null)
      {
 -      for (int d = 0; d < dbrefs.length; d++)
 +      for (int d = 0, nd = dbrefs.size(); d < nd; d++)
        {
          DBRef dbref = new DBRef();
 -        dbref.setSource(dbrefs[d].getSource());
 -        dbref.setVersion(dbrefs[d].getVersion());
 -        dbref.setAccessionId(dbrefs[d].getAccessionId());
 -        if (dbrefs[d].hasMap())
 +        DBRefEntry ref = dbrefs.get(d);
 +        dbref.setSource(ref.getSource());
 +        dbref.setVersion(ref.getVersion());
 +        dbref.setAccessionId(ref.getAccessionId());
 +        if (ref.hasMap())
          {
 -          Mapping mp = createVamsasMapping(dbrefs[d].getMap(), parentseq,
 +          Mapping mp = createVamsasMapping(ref.getMap(), parentseq,
                    jds, recurse);
            dbref.setMapping(mp);
          }
     * @param file
     *          - HTTP URL or filename
     */
 -  public AlignFrame loadJalviewAlign(final String file)
 +  public AlignFrame loadJalviewAlign(final Object file)
    {
  
      jalview.gui.AlignFrame af = null;
      return af;
    }
  
 -  private jarInputStreamProvider createjarInputStreamProvider(
 -          final String file) throws MalformedURLException
 -  {
 -    URL url = null;
 -    errorMessage = null;
 -    uniqueSetSuffix = null;
 -    seqRefIds = null;
 -    viewportsAdded.clear();
 -    frefedSequence = null;
 -
 -    if (file.startsWith("http://"))
 -    {
 -      url = new URL(file);
 -    }
 -    final URL _url = url;
 -    return new jarInputStreamProvider()
 -    {
 -
 -      @Override
 -      public JarInputStream getJarInputStream() throws IOException
 -      {
 -        if (_url != null)
 -        {
 -          return new JarInputStream(_url.openStream());
 -        }
 -        else
 -        {
 -          return new JarInputStream(new FileInputStream(file));
 -        }
 -      }
 -
 -      @Override
 -      public String getFilename()
 -      {
 -        return file;
 -      }
 -    };
 -  }
 +      @SuppressWarnings("unused")
 +      private jarInputStreamProvider createjarInputStreamProvider(final Object ofile) throws MalformedURLException {
 +
 +              // BH 2018 allow for bytes already attached to File object
 +              try {
 +                      String file = (ofile instanceof File ? ((File) ofile).getCanonicalPath() : ofile.toString());
 +                      byte[] bytes = /** @j2sNative ofile._bytes || */
 +                                      null;
 +                      URL url = null;
 +                      errorMessage = null;
 +                      uniqueSetSuffix = null;
 +                      seqRefIds = null;
 +                      viewportsAdded.clear();
 +                      frefedSequence = null;
 +
 +                      if (file.startsWith("http://")) {
 +                              url = new URL(file);
 +                      }
 +                      final URL _url = url;
 +                      return new jarInputStreamProvider() {
 +
 +                              @Override
 +                              public JarInputStream getJarInputStream() throws IOException {
 +                                      if (bytes != null) {
 +//                                            System.out.println("Jalview2XML: opening byte jarInputStream for bytes.length=" + bytes.length);
 +                                              return new JarInputStream(new ByteArrayInputStream(bytes));
 +                                      }
 +                                      if (_url != null) {
 +//                                            System.out.println("Jalview2XML: opening url jarInputStream for " + _url);
 +                                              return new JarInputStream(_url.openStream());
 +                                      } else {
 +//                                            System.out.println("Jalview2XML: opening file jarInputStream for " + file);
 +                                              return new JarInputStream(new FileInputStream(file));
 +                                      }
 +                              }
 +
 +                              @Override
 +                              public String getFilename() {
 +                                      return file;
 +                              }
 +                      };
 +              } catch (IOException e) {
 +                      e.printStackTrace();
 +                      return null;
 +              }
 +      }
  
    /**
     * Recover jalview session from a jalview project archive. Caller may
  
          if (jarentry != null && jarentry.getName().endsWith(".xml"))
          {
 -          InputStreamReader in = new InputStreamReader(jin, UTF_8);
 -          // JalviewModel object = new JalviewModel();
 -
            JAXBContext jc = JAXBContext
                    .newInstance("jalview.xml.binding.jalview");
            XMLStreamReader streamReader = XMLInputFactory.newInstance()
                    .unmarshal(streamReader, JalviewModel.class);
            JalviewModel object = jbe.getValue();
  
 -          /*
 -          Unmarshaller unmar = new Unmarshaller(object);
 -          unmar.setValidation(false);
 -          object = (JalviewModel) unmar.unmarshal(in);
 -          */
            if (true) // !skipViewport(object))
            {
              _af = loadFromObject(object, file, true, jprovider);
        ex.printStackTrace(System.err);
        if (attemptversion1parse)
        {
-         // Is Version 1 Jar file?
-         try
-         {
-           af = new Jalview2XML_V1(raiseGUI).LoadJalviewAlign(jprovider);
-         } catch (Exception ex2)
-         {
-           System.err.println("Exception whilst loading as jalviewXMLV1:");
-           ex2.printStackTrace();
-           af = null;
-         }
+         // used to attempt to parse as V1 castor-generated xml
        }
        if (Desktop.instance != null)
        {
              : null;
  
      // ////////////////////////////////
+     // INITIALISE ALIGNMENT SEQUENCESETID AND VIEWID
+     //
+     //
+     // If we just load in the same jar file again, the sequenceSetId
+     // will be the same, and we end up with multiple references
+     // to the same sequenceSet. We must modify this id on load
+     // so that each load of the file gives a unique id
+     /**
+      * used to resolve correct alignment dataset for alignments with multiple
+      * views
+      */
+     String uniqueSeqSetId = null;
+     String viewId = null;
+     if (view != null)
+     {
+       uniqueSeqSetId = view.getSequenceSetId() + uniqueSetSuffix;
+       viewId = (view.getId() == null ? null
+               : view.getId() + uniqueSetSuffix);
+     }
+     // ////////////////////////////////
      // LOAD SEQUENCES
  
      List<SequenceI> hiddenSeqs = null;
  
        // finally, verify all data in vamsasSet is actually present in al
        // passing on flag indicating if it is actually a stored dataset
-       recoverDatasetFor(vamsasSet, al, isdsal);
+       recoverDatasetFor(vamsasSet, al, isdsal, uniqueSeqSetId);
      }
  
      if (referenceseqForView != null)
            }
            else
            {
-             cs = ColourSchemeProperty.getColourScheme(al,
+             cs = ColourSchemeProperty.getColourScheme(null, al,
                      jGroup.getColour());
            }
          }
      // ///////////////////////////////
      // LOAD VIEWPORT
  
-     // If we just load in the same jar file again, the sequenceSetId
-     // will be the same, and we end up with multiple references
-     // to the same sequenceSet. We must modify this id on load
-     // so that each load of the file gives a unique id
-     String uniqueSeqSetId = view.getSequenceSetId() + uniqueSetSuffix;
-     String viewId = (view.getId() == null ? null
-             : view.getId() + uniqueSetSuffix);
      AlignFrame af = null;
      AlignViewport av = null;
      // now check to see if we really need to create a new viewport.
      if (loadTreesAndStructures)
      {
        loadTrees(jalviewModel, view, af, av, ap);
+       loadPCAViewers(jalviewModel, ap);
        loadPDBStructures(jprovider, jseqs, af, ap);
        loadRnaViewers(jprovider, jseqs, ap);
      }
            // TODO: verify 'associate with all views' works still
            tp.getTreeCanvas().setViewport(av); // af.viewport;
            tp.getTreeCanvas().setAssociatedPanel(ap); // af.alignPanel;
+           // FIXME: should we use safeBoolean here ?
+           tp.getTreeCanvas().setApplyToAllViews(tree.isLinkToAllViews());
  
          }
          if (tp == null)
    {
      AlignFrame af = null;
      af = new AlignFrame(al, safeInt(view.getWidth()),
 -            safeInt(view.getHeight()), uniqueSeqSetId, viewId);
 +            safeInt(view.getHeight()), uniqueSeqSetId, viewId) 
 +//    {
 +//            
 +//            @Override
 +//            protected void processKeyEvent(java.awt.event.KeyEvent e) {
 +//                    System.out.println("Jalview2XML   AF " + e);
 +//                    super.processKeyEvent(e);
 +//                    
 +//            }
 +//            
 +//    }
 +    ;
  
      af.setFileName(file, FileFormat.Jalview);
  
        }
        else
        {
-         cs = ColourSchemeProperty.getColourScheme(al, view.getBgColour());
+         cs = ColourSchemeProperty.getColourScheme(af.getViewport(), al,
+                 view.getBgColour());
        }
      }
  
+     /*
+      * turn off 'alignment colour applies to all groups'
+      * while restoring global colour scheme
+      */
+     viewport.setColourAppliesToAllGroups(false);
      viewport.setGlobalColourScheme(cs);
      viewport.getResidueShading().setThreshold(pidThreshold,
              view.isIgnoreGapsinConsensus());
      viewport.getResidueShading()
              .setConsensus(viewport.getSequenceConsensusHash());
-     viewport.setColourAppliesToAllGroups(false);
      if (safeBoolean(view.isConservationSelected()) && cs != null)
      {
        viewport.getResidueShading()
                .setConservationInc(safeInt(view.getConsThreshold()));
      }
      af.changeColour(cs);
      viewport.setColourAppliesToAllGroups(true);
  
      viewport
            float min = safeFloat(safeFloat(setting.getMin()));
            float max = setting.getMax() == null ? 1f
                    : setting.getMax().floatValue();
-           FeatureColourI gc = new FeatureColour(minColour, maxColour,
+           FeatureColourI gc = new FeatureColour(maxColour, minColour,
+                   maxColour,
                    noValueColour, min, max);
            if (setting.getAttributeName().size() > 0)
            {
      else
      {
        cs = new AnnotationColourGradient(matchedAnnotation,
-               ColourSchemeProperty.getColourScheme(al,
+               ColourSchemeProperty.getColourScheme(af.getViewport(), al,
                        viewAnnColour.getColourScheme()),
                safeInt(viewAnnColour.getAboveThreshold()));
      }
    }
  
    private void recoverDatasetFor(SequenceSet vamsasSet, AlignmentI al,
-           boolean ignoreUnrefed)
+           boolean ignoreUnrefed, String uniqueSeqSetId)
    {
      jalview.datamodel.AlignmentI ds = getDatasetFor(
              vamsasSet.getDatasetId());
+     AlignmentI xtant_ds = ds;
+     if (xtant_ds == null)
+     {
+       // good chance we are about to create a new dataset, but check if we've
+       // seen some of the dataset sequence IDs before.
+       // TODO: skip this check if we are working with project generated by
+       // version 2.11 or later
+       xtant_ds = checkIfHasDataset(vamsasSet.getSequence());
+       if (xtant_ds != null)
+       {
+         ds = xtant_ds;
+         addDatasetRef(vamsasSet.getDatasetId(), ds);
+       }
+     }
      Vector dseqs = null;
+     if (!ignoreUnrefed)
+     {
+       // recovering an alignment View
+       AlignmentI seqSetDS = getDatasetFor(UNIQSEQSETID + uniqueSeqSetId);
+       if (seqSetDS != null)
+       {
+         if (ds != null && ds != seqSetDS)
+         {
+           warn("JAL-3171 regression: Overwriting a dataset reference for an alignment"
+                   + " - CDS/Protein crossreference data may be lost");
+           if (xtant_ds != null)
+           {
+             // This can only happen if the unique sequence set ID was bound to a
+             // dataset that did not contain any of the sequences in the view
+             // currently being restored.
+             warn("JAL-3171 SERIOUS!  TOTAL CONFUSION - please consider contacting the Jalview Development team so they can investigate why your project caused this message to be displayed.");
+           }
+         }
+         ds = seqSetDS;
+         addDatasetRef(vamsasSet.getDatasetId(), ds);
+       }
+     }
      if (ds == null)
      {
+       // try even harder to restore dataset
+       AlignmentI xtantDS = checkIfHasDataset(vamsasSet.getSequence());
        // create a list of new dataset sequences
        dseqs = new Vector();
      }
      if (al.getDataset() == null && !ignoreUnrefed)
      {
        al.setDataset(ds);
+       // register dataset for the alignment's uniqueSeqSetId for legacy projects
+       addDatasetRef(UNIQSEQSETID + uniqueSeqSetId, ds);
      }
+     updateSeqDatasetBinding(vamsasSet.getSequence(), ds);
    }
  
    /**
+    * XML dataset sequence ID to materialised dataset reference
+    */
+   HashMap<String, AlignmentI> seqToDataset = new HashMap<>();
+   /**
+    * @return the first materialised dataset reference containing a dataset
+    *         sequence referenced in the given view
+    * @param list
+    *          - sequences from the view
+    */
+   AlignmentI checkIfHasDataset(List<Sequence> list)
+   {
+     for (Sequence restoredSeq : list)
+     {
+       AlignmentI datasetFor = seqToDataset.get(restoredSeq.getDsseqid());
+       if (datasetFor != null)
+       {
+         return datasetFor;
+       }
+     }
+     return null;
+   }
+   /**
+    * Register ds as the containing dataset for the dataset sequences referenced
+    * by sequences in list
+    * 
+    * @param list
+    *          - sequences in a view
+    * @param ds
+    */
+   void updateSeqDatasetBinding(List<Sequence> list, AlignmentI ds)
+   {
+     for (Sequence restoredSeq : list)
+     {
+       AlignmentI prevDS = seqToDataset.put(restoredSeq.getDsseqid(), ds);
+       if (prevDS != null && prevDS != ds)
+       {
+         warn("Dataset sequence appears in many datasets: "
+                 + restoredSeq.getDsseqid());
+         // TODO: try to merge!
+       }
+     }
+   }
+   /**
     * 
     * @param vamsasSeq
     *          sequence definition to create/merge dataset sequence for
      jalview.datamodel.Mapping jmap = new jalview.datamodel.Mapping(dsto, fr,
              fto, m.getMapFromUnit().intValue(),
              m.getMapToUnit().intValue());
-     // if (m.getMappingChoice() != null)
-     // {
-     // MappingChoice mc = m.getMappingChoice();
+     /*
+      * (optional) choice of dseqFor or Sequence
+      */
      if (m.getDseqFor() != null)
      {
        String dsfor = m.getDseqFor();
        if (seqRefIds.containsKey(dsfor))
        {
-         /**
+         /*
           * recover from hash
           */
          jmap.setTo(seqRefIds.get(dsfor));
          frefedSequence.add(newMappingRef(dsfor, jmap));
        }
      }
-     else
+     else if (m.getSequence() != null)
      {
-       /**
+       /*
         * local sequence definition
         */
        Sequence ms = m.getSequence();
      initSeqRefs();
      JalviewModel jm = saveState(ap, null, null, null);
  
+     addDatasetRef(
+             jm.getVamsasModel().getSequenceSet().get(0).getDatasetId(),
+             ap.getAlignment().getDataset());
      uniqueSetSuffix = "";
      // jm.getJalviewModelSequence().getViewport(0).setId(null);
      jm.getViewport().get(0).setId(null);
    }
  
    /**
+    * Loads any saved PCA viewers
+    * 
+    * @param jms
+    * @param ap
+    */
+   protected void loadPCAViewers(JalviewModel model, AlignmentPanel ap)
+   {
+     try
+     {
+       List<PcaViewer> pcaviewers = model.getPcaViewer();
+       for (PcaViewer viewer : pcaviewers)
+       {
+         String modelName = viewer.getScoreModelName();
+         SimilarityParamsI params = new SimilarityParams(
+                 viewer.isIncludeGappedColumns(), viewer.isMatchGaps(),
+                 viewer.isIncludeGaps(),
+                 viewer.isDenominateByShortestLength());
+         /*
+          * create the panel (without computing the PCA)
+          */
+         PCAPanel panel = new PCAPanel(ap, modelName, params);
+         panel.setTitle(viewer.getTitle());
+         panel.setBounds(new Rectangle(viewer.getXpos(), viewer.getYpos(),
+                 viewer.getWidth(), viewer.getHeight()));
+         boolean showLabels = viewer.isShowLabels();
+         panel.setShowLabels(showLabels);
+         panel.getRotatableCanvas().setShowLabels(showLabels);
+         panel.getRotatableCanvas()
+                 .setBgColour(new Color(viewer.getBgColour()));
+         panel.getRotatableCanvas()
+                 .setApplyToAllViews(viewer.isLinkToAllViews());
+         /*
+          * load PCA output data
+          */
+         ScoreModelI scoreModel = ScoreModels.getInstance()
+                 .getScoreModel(modelName, ap);
+         PCA pca = new PCA(null, scoreModel, params);
+         PcaDataType pcaData = viewer.getPcaData();
+         MatrixI pairwise = loadDoubleMatrix(pcaData.getPairwiseMatrix());
+         pca.setPairwiseScores(pairwise);
+         MatrixI triDiag = loadDoubleMatrix(pcaData.getTridiagonalMatrix());
+         pca.setTridiagonal(triDiag);
+         MatrixI result = loadDoubleMatrix(pcaData.getEigenMatrix());
+         pca.setEigenmatrix(result);
+         panel.getPcaModel().setPCA(pca);
+         /*
+          * we haven't saved the input data! (JAL-2647 to do)
+          */
+         panel.setInputData(null);
+         /*
+          * add the sequence points for the PCA display
+          */
+         List<jalview.datamodel.SequencePoint> seqPoints = new ArrayList<>();
+         for (SequencePoint sp : viewer.getSequencePoint())
+         {
+           String seqId = sp.getSequenceRef();
+           SequenceI seq = seqRefIds.get(seqId);
+           if (seq == null)
+           {
+             throw new IllegalStateException(
+                     "Unmatched seqref for PCA: " + seqId);
+           }
+           Point pt = new Point(sp.getXPos(), sp.getYPos(), sp.getZPos());
+           jalview.datamodel.SequencePoint seqPoint = new jalview.datamodel.SequencePoint(
+                   seq, pt);
+           seqPoints.add(seqPoint);
+         }
+         panel.getRotatableCanvas().setPoints(seqPoints, seqPoints.size());
+         /*
+          * set min-max ranges and scale after setPoints (which recomputes them)
+          */
+         panel.getRotatableCanvas().setScaleFactor(viewer.getScaleFactor());
+         SeqPointMin spMin = viewer.getSeqPointMin();
+         float[] min = new float[] { spMin.getXPos(), spMin.getYPos(),
+             spMin.getZPos() };
+         SeqPointMax spMax = viewer.getSeqPointMax();
+         float[] max = new float[] { spMax.getXPos(), spMax.getYPos(),
+             spMax.getZPos() };
+         panel.getRotatableCanvas().setSeqMinMax(min, max);
+         // todo: hold points list in PCAModel only
+         panel.getPcaModel().setSequencePoints(seqPoints);
+         panel.setSelectedDimensionIndex(viewer.getXDim(), X);
+         panel.setSelectedDimensionIndex(viewer.getYDim(), Y);
+         panel.setSelectedDimensionIndex(viewer.getZDim(), Z);
+         // is this duplication needed?
+         panel.setTop(seqPoints.size() - 1);
+         panel.getPcaModel().setTop(seqPoints.size() - 1);
+         /*
+          * add the axes' end points for the display
+          */
+         for (int i = 0; i < 3; i++)
+         {
+           Axis axis = viewer.getAxis().get(i);
+           panel.getRotatableCanvas().getAxisEndPoints()[i] = new Point(
+                   axis.getXPos(), axis.getYPos(), axis.getZPos());
+         }
+         Desktop.addInternalFrame(panel, MessageManager.formatMessage(
+                 "label.calc_title", "PCA", modelName), 475, 450);
+       }
+     } catch (Exception ex)
+     {
+       Cache.log.error("Error loading PCA: " + ex.toString());
+     }
+   }
+   /**
     * Populates an XML model of the feature colour scheme for one feature type
     * 
     * @param featureType
          noValueColour = maxcol;
        }
    
-       colour = new FeatureColour(mincol, maxcol, noValueColour,
+       colour = new FeatureColour(maxcol, mincol, maxcol, noValueColour,
                safeFloat(colourModel.getMin()),
                safeFloat(colourModel.getMax()));
        final List<String> attributeName = colourModel.getAttributeName();
@@@ -22,641 -22,684 +22,673 @@@ package jalview.util
  
  import jalview.datamodel.DBRefEntry;
  import jalview.datamodel.DBRefSource;
 +import jalview.datamodel.Mapping;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.SequenceI;
  
  import java.util.ArrayList;
 -import java.util.Arrays;
 +import java.util.BitSet;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
+ import java.util.Set;
  
  import com.stevesoft.pat.Regex;
  
  /**
   * Utilities for handling DBRef objects and their collections.
   */
- public class DBRefUtils {
+ public class DBRefUtils
+ {
+   /*
+    * lookup from lower-case form of a name to its canonical (standardised) form
+    */
+   private static Map<String, String> canonicalSourceNameLookup = new HashMap<>();
  
 -
 -  static
 -  {
 -    // TODO load these from a resource file?
 -    canonicalSourceNameLookup.put("uniprotkb/swiss-prot",
 -            DBRefSource.UNIPROT);
 -    canonicalSourceNameLookup.put("uniprotkb/trembl", DBRefSource.UNIPROT);
 -
 -    // Ensembl values for dbname in xref REST service:
 -    canonicalSourceNameLookup.put("uniprot/sptrembl", DBRefSource.UNIPROT);
 -    canonicalSourceNameLookup.put("uniprot/swissprot", DBRefSource.UNIPROT);
 -
 -    canonicalSourceNameLookup.put("pdb", DBRefSource.PDB);
 -    canonicalSourceNameLookup.put("ensembl", DBRefSource.ENSEMBL);
 -    // Ensembl Gn and Tr are for Ensembl genomic and transcript IDs as served
 -    // from ENA.
 -    canonicalSourceNameLookup.put("ensembl-tr", DBRefSource.ENSEMBL);
 -    canonicalSourceNameLookup.put("ensembl-gn", DBRefSource.ENSEMBL);
 -
 -    // Make sure we have lowercase entries for all canonical string lookups
 -    Set<String> keys = canonicalSourceNameLookup.keySet();
 -    for (String k : keys)
 -    {
 -      canonicalSourceNameLookup.put(k.toLowerCase(),
 -              canonicalSourceNameLookup.get(k));
 -    }
 -
 -  }
 +      public final static int DB_SOURCE = 1;
 +      public final static int DB_VERSION = 2;
 +      public final static int DB_ID = 4;
 +      public final static int DB_MAP = 8;
 +
 +      public final static int SEARCH_MODE_NO_MAP_NO_VERSION = DB_SOURCE | DB_ID;
 +      public final static int SEARCH_MODE_FULL = DB_SOURCE | DB_VERSION | DB_ID | DB_MAP;
 +
-       /*
-        * lookup from lower-case form of a name to its canonical (standardised) form
-        */
-       private static Map<String, String> canonicalSourceNameLookup = new HashMap<String, String>();
-       private static Map<String, String> dasCoordinateSystemsLookup = new HashMap<String, String>();
-       static {
++      static 
++      {
 +              // TODO load these from a resource file?
 +              canonicalSourceNameLookup.put("uniprotkb/swiss-prot", DBRefSource.UNIPROT);
 +              canonicalSourceNameLookup.put("uniprotkb/trembl", DBRefSource.UNIPROT);
 +
 +              // Ensembl values for dbname in xref REST service:
 +              canonicalSourceNameLookup.put("uniprot/sptrembl", DBRefSource.UNIPROT);
 +              canonicalSourceNameLookup.put("uniprot/swissprot", DBRefSource.UNIPROT);
 +
 +              canonicalSourceNameLookup.put("pdb", DBRefSource.PDB);
 +              canonicalSourceNameLookup.put("ensembl", DBRefSource.ENSEMBL);
 +              // Ensembl Gn and Tr are for Ensembl genomic and transcript IDs as served
 +              // from ENA.
 +              canonicalSourceNameLookup.put("ensembl-tr", DBRefSource.ENSEMBL);
 +              canonicalSourceNameLookup.put("ensembl-gn", DBRefSource.ENSEMBL);
 +
-               // Make sure we have lowercase entries for all canonical string lookups
- // BH 2019.01.25 unnecessary -- they are all lower case already
-               // Set<String> keys = canonicalSourceNameLookup.keySet();
- //    for (String k : keys)
- //    {
- //      canonicalSourceNameLookup.put(k.toLowerCase(),
- //              canonicalSourceNameLookup.get(k));
- //    }
-               dasCoordinateSystemsLookup.put("pdbresnum", DBRefSource.PDB);
-               dasCoordinateSystemsLookup.put("uniprot", DBRefSource.UNIPROT);
-               dasCoordinateSystemsLookup.put("embl", DBRefSource.EMBL);
-               // dasCoordinateSystemsLookup.put("embl", DBRefSource.EMBLCDS);
++          // guarantee we always have lowercase entries for canonical string lookups
++          for (String k : canonicalSourceNameLookup.keySet())
++          {
++            canonicalSourceNameLookup.put(k.toLowerCase(),
++                    canonicalSourceNameLookup.get(k));
++          }
++   }
+   /**
+    * Returns those DBRefEntry objects whose source identifier (once converted to
+    * Jalview's canonical form) is in the list of sources to search for. Returns
+    * null if no matches found.
+    * 
 -   * @param dbrefs
 -   *          DBRefEntry objects to search
 -   * @param sources
 -   *          array of sources to select
++   * @param dbrefs  DBRefEntry objects to search
++   * @param sources array of sources to select
+    * @return
+    */
 -  public static DBRefEntry[] selectRefs(DBRefEntry[] dbrefs,
 -          String[] sources)
++  public static List<DBRefEntry> selectRefs(List<DBRefEntry> dbrefs, String[] sources) 
+   {
 -    if (dbrefs == null || sources == null)
 -    {
 -      return dbrefs;
 -    }
 -    HashSet<String> srcs = new HashSet<>();
 -    for (String src : sources)
 -    {
 -      srcs.add(src.toUpperCase());
 -    }
 -
 -    List<DBRefEntry> res = new ArrayList<>();
 -    for (DBRefEntry dbr : dbrefs)
 -    {
 -      String source = getCanonicalName(dbr.getSource());
 -      if (srcs.contains(source.toUpperCase()))
 -      {
 -        res.add(dbr);
 -      }
 -    }
 -
 -    if (res.size() > 0)
 -    {
 -      DBRefEntry[] reply = new DBRefEntry[res.size()];
 -      return res.toArray(reply);
 -    }
 -    return null;
++    if (dbrefs == null || sources == null) 
++      {
++        return dbrefs;
 +      }
 +
-       /**
-        * Returns those DBRefEntry objects whose source identifier (once converted to
-        * Jalview's canonical form) is in the list of sources to search for. Returns
-        * null if no matches found.
-        * 
-        * @param dbrefs  DBRefEntry objects to search
-        * @param sources array of sources to select
-        * @return
-        */
-       public static List<DBRefEntry> selectRefs(List<DBRefEntry> dbrefs, String[] sources) {
-               if (dbrefs == null || sources == null) {
-                       return dbrefs;
-               }
-               // BH TODO
-               HashSet<String> srcs = new HashSet<String>();
-               for (String src : sources) {
-                       srcs.add(src.toUpperCase());
-               }
-               int nrefs = dbrefs.size();
-               List<DBRefEntry> res = new ArrayList<DBRefEntry>();
-               for (int ib = 0; ib < nrefs; ib++) {
-                       DBRefEntry dbr = dbrefs.get(ib);
-                       String source = getCanonicalName(dbr.getSource());
-                       if (srcs.contains(source.toUpperCase())) {
-                               res.add(dbr);
-                       }
-               }
-               if (res.size() > 0) {
-                       // List<DBRefEntry> reply = new DBRefEntry[res.size()];
-                       return res;// .toArray(reply);
-               }
-               return null;
++      // BH TODO (what?)
++      HashSet<String> srcs = new HashSet<String>();
++      for (String src : sources) 
++      {
++        srcs.add(src.toUpperCase());
 +      }
 +
-       private static boolean selectRefsBS(List<DBRefEntry> dbrefs, int sourceKeys, BitSet bsSelect) {
-               if (dbrefs == null || sourceKeys == 0) {
-                       return false;
-               }
-               for (int i = 0, n = dbrefs.size(); i < n; i++) {
-                       DBRefEntry dbr = dbrefs.get(i);
-                       if ((dbr.getSourceKey() & sourceKeys) != 0) {
-                               bsSelect.clear(i);
-                       }
-               }
-               return !bsSelect.isEmpty();
++      int nrefs = dbrefs.size();
++      List<DBRefEntry> res = new ArrayList<DBRefEntry>();
++      for (int ib = 0; ib < nrefs; ib++) 
++      {
++        DBRefEntry dbr = dbrefs.get(ib);
++        String source = getCanonicalName(dbr.getSource());
++        if (srcs.contains(source.toUpperCase())) 
++        {
++          res.add(dbr);
++        }
 +      }
-       /**
-        * isDasCoordinateSystem
-        * 
-        * @param string     String
-        * @param dBRefEntry DBRefEntry
-        * @return boolean true if Source DBRefEntry is compatible with DAS
-        *         CoordinateSystem name
-        */
-       public static boolean isDasCoordinateSystem(String string, DBRefEntry dBRefEntry) {
-               if (string == null || dBRefEntry == null) {
-                       return false;
-               }
-               String coordsys = dasCoordinateSystemsLookup.get(string.toLowerCase());
-               return coordsys == null ? false : coordsys.equals(dBRefEntry.getSource());
++      if (res.size() > 0) 
++      {
++              // List<DBRefEntry> reply = new DBRefEntry[res.size()];
++              return res;// .toArray(reply);
 +      }
++      return null;
+   }
 -  /**
 -   * look up source in an internal list of database reference sources and return
 -   * the canonical jalview name for the source, or the original string if it has
 -   * no canonical form.
 -   * 
 -   * @param source
 -   * @return canonical jalview source (one of jalview.datamodel.DBRefSource.*)
 -   *         or original source
 -   */
 -  public static String getCanonicalName(String source)
++  private static boolean selectRefsBS(List<DBRefEntry> dbrefs, int sourceKeys, BitSet bsSelect) 
+   {
 -    if (source == null)
 -    {
 -      return null;
 -    }
 -    String canonical = canonicalSourceNameLookup.get(source.toLowerCase());
 -    return canonical == null ? source : canonical;
 -  }
 -
 -  /**
 -   * Returns a (possibly empty) list of those references that match the given
 -   * entry. Currently uses a comparator which matches if
 -   * <ul>
 -   * <li>database sources are the same</li>
 -   * <li>accession ids are the same</li>
 -   * <li>both have no mapping, or the mappings are the same</li>
 -   * </ul>
 -   * 
 -   * @param ref
 -   *          Set of references to search
 -   * @param entry
 -   *          pattern to match
 -   * @return
 -   */
 -  public static List<DBRefEntry> searchRefs(DBRefEntry[] ref,
 -          DBRefEntry entry)
 -  {
 -    return searchRefs(ref, entry,
 -            matchDbAndIdAndEitherMapOrEquivalentMapList);
 -  }
 -
 -  /**
 -   * Returns a list of those references that match the given accession id
 -   * <ul>
 -   * <li>database sources are the same</li>
 -   * <li>accession ids are the same</li>
 -   * <li>both have no mapping, or the mappings are the same</li>
 -   * </ul>
 -   * 
 -   * @param refs
 -   *          Set of references to search
 -   * @param accId
 -   *          accession id to match
 -   * @return
 -   */
 -  public static List<DBRefEntry> searchRefs(DBRefEntry[] refs, String accId)
 -  {
 -    return searchRefs(refs, new DBRefEntry("", "", accId), matchId);
++      if (dbrefs == null || sourceKeys == 0) 
++      {
++        return false;
++      }
++      for (int i = 0, n = dbrefs.size(); i < n; i++) 
++      {
++        DBRefEntry dbr = dbrefs.get(i);
++        if ((dbr.getSourceKey() & sourceKeys) != 0) 
++        {
++          bsSelect.clear(i);
++        }
++      }
++      return !bsSelect.isEmpty();
+   }
+   /**
+    * Returns a (possibly empty) list of those references that match the given
+    * entry, according to the given comparator.
+    * 
+    * @param refs
+    *          an array of database references to search
+    * @param entry
+    *          an entry to compare against
+    * @param comparator
+    * @return
+    */
+   static List<DBRefEntry> searchRefs(DBRefEntry[] refs, DBRefEntry entry,
+           DbRefComp comparator)
+   {
+     List<DBRefEntry> rfs = new ArrayList<>();
+     if (refs == null || entry == null)
+     {
+       return rfs;
+     }
+     for (int i = 0; i < refs.length; i++)
+     {
+       if (comparator.matches(entry, refs[i]))
+       {
+         rfs.add(refs[i]);
+       }
+     }
+     return rfs;
+   }
  
 -  interface DbRefComp
 -  {
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb);
 -  }
 -
 -  /**
 -   * match on all non-null fields in refa
 -   */
 -  // TODO unused - remove?
 -  public static DbRefComp matchNonNullonA = new DbRefComp()
 -  {
 -    @Override
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb)
 -    {
 -      if (refa.getSource() == null
 -              || DBRefUtils.getCanonicalName(refb.getSource()).equals(
 -                      DBRefUtils.getCanonicalName(refa.getSource())))
 -      {
 -        if (refa.getVersion() == null
 -                || refb.getVersion().equals(refa.getVersion()))
 -        {
 -          if (refa.getAccessionId() == null
 -                  || refb.getAccessionId().equals(refa.getAccessionId()))
 -          {
 -            if (refa.getMap() == null || (refb.getMap() != null
 -                    && refb.getMap().equals(refa.getMap())))
 -            {
 -              return true;
 -            }
 -          }
 -        }
 -      }
 -      return false;
 -    }
 -  };
 -
 -  /**
 -   * either field is null or field matches for all of source, version, accession
 -   * id and map.
 -   */
 -  // TODO unused - remove?
 -  public static DbRefComp matchEitherNonNull = new DbRefComp()
 -  {
 -    @Override
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb)
 -    {
 -      if (nullOrEqualSource(refa.getSource(), refb.getSource())
 -              && nullOrEqual(refa.getVersion(), refb.getVersion())
 -              && nullOrEqual(refa.getAccessionId(), refb.getAccessionId())
 -              && nullOrEqual(refa.getMap(), refb.getMap()))
 -      {
 -        return true;
 -      }
 -      return false;
 -    }
 -  };
 -
 -  /**
 -   * accession ID and DB must be identical. Version is ignored. Map is either
 -   * not defined or is a match (or is compatible?)
 -   */
 -  // TODO unused - remove?
 -  public static DbRefComp matchDbAndIdAndEitherMap = new DbRefComp()
 -  {
 -    @Override
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb)
 -    {
 -      if (refa.getSource() != null && refb.getSource() != null
 -              && DBRefUtils.getCanonicalName(refb.getSource()).equals(
 -                      DBRefUtils.getCanonicalName(refa.getSource())))
 -      {
 -        // We dont care about version
 -        if (refa.getAccessionId() != null && refb.getAccessionId() != null
 -                // FIXME should be && not || here?
 -                || refb.getAccessionId().equals(refa.getAccessionId()))
 -        {
 -          if ((refa.getMap() == null || refb.getMap() == null)
 -                  || (refa.getMap() != null && refb.getMap() != null
 -                          && refb.getMap().equals(refa.getMap())))
 -          {
 -            return true;
 -          }
 -        }
 -      }
 -      return false;
 -    }
 -  };
 -
 -  /**
 -   * accession ID and DB must be identical. Version is ignored. No map on either
 -   * or map but no maplist on either or maplist of map on a is the complement of
 -   * maplist of map on b.
 -   */
 -  // TODO unused - remove?
 -  public static DbRefComp matchDbAndIdAndComplementaryMapList = new DbRefComp()
 -  {
 -    @Override
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb)
 -    {
 -      if (refa.getSource() != null && refb.getSource() != null
 -              && DBRefUtils.getCanonicalName(refb.getSource()).equals(
 -                      DBRefUtils.getCanonicalName(refa.getSource())))
 -      {
 -        // We dont care about version
 -        if (refa.getAccessionId() != null && refb.getAccessionId() != null
 -                || refb.getAccessionId().equals(refa.getAccessionId()))
 -        {
 -          if ((refa.getMap() == null && refb.getMap() == null)
 -                  || (refa.getMap() != null && refb.getMap() != null))
 -          {
 -            if ((refb.getMap().getMap() == null
 -                    && refa.getMap().getMap() == null)
 -                    || (refb.getMap().getMap() != null
 -                            && refa.getMap().getMap() != null
 -                            && refb.getMap().getMap().getInverse()
 -                                    .equals(refa.getMap().getMap())))
 -            {
 -              return true;
 -            }
 -          }
 -        }
 -      }
 -      return false;
 -    }
 -  };
 -
 -  /**
 -   * accession ID and DB must be identical. Version is ignored. No map on both
 -   * or or map but no maplist on either or maplist of map on a is equivalent to
 -   * the maplist of map on b.
 -   */
 -  // TODO unused - remove?
 -  public static DbRefComp matchDbAndIdAndEquivalentMapList = new DbRefComp()
 -  {
 -    @Override
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb)
 -    {
 -      if (refa.getSource() != null && refb.getSource() != null
 -              && DBRefUtils.getCanonicalName(refb.getSource()).equals(
 -                      DBRefUtils.getCanonicalName(refa.getSource())))
 -      {
 -        // We dont care about version
 -        // if ((refa.getVersion()==null || refb.getVersion()==null)
 -        // || refb.getVersion().equals(refa.getVersion()))
 -        // {
 -        if (refa.getAccessionId() != null && refb.getAccessionId() != null
 -                || refb.getAccessionId().equals(refa.getAccessionId()))
 -        {
 -          if (refa.getMap() == null && refb.getMap() == null)
 -          {
 -            return true;
 -          }
 -          if (refa.getMap() != null && refb.getMap() != null
 -                  && ((refb.getMap().getMap() == null
 -                          && refa.getMap().getMap() == null)
 -                          || (refb.getMap().getMap() != null
 -                                  && refa.getMap().getMap() != null
 -                                  && refb.getMap().getMap()
 -                                          .equals(refa.getMap().getMap()))))
 -          {
 -            return true;
 -          }
 -        }
 -      }
 -      return false;
 -    }
 -  };
 -
 -  /**
 -   * accession ID and DB must be identical, or null on a. Version is ignored. No
 -   * map on either or map but no maplist on either or maplist of map on a is
 -   * equivalent to the maplist of map on b.
 -   */
 -  public static DbRefComp matchDbAndIdAndEitherMapOrEquivalentMapList = new DbRefComp()
 -  {
 -    @Override
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb)
 -    {
 -      if (refa.getSource() != null && refb.getSource() != null
 -              && DBRefUtils.getCanonicalName(refb.getSource()).equals(
 -                      DBRefUtils.getCanonicalName(refa.getSource())))
 -      {
 -        // We dont care about version
 -
 -        if (refa.getAccessionId() == null
 -                || refa.getAccessionId().equals(refb.getAccessionId()))
 -        {
 -          if (refa.getMap() == null || refb.getMap() == null)
 -          {
 -            return true;
 -          }
 -          if ((refa.getMap() != null && refb.getMap() != null)
 -                  && (refb.getMap().getMap() == null
 -                          && refa.getMap().getMap() == null)
 -                  || (refb.getMap().getMap() != null
 -                          && refa.getMap().getMap() != null
 -                          && (refb.getMap().getMap()
 -                                  .equals(refa.getMap().getMap()))))
 -          {
 -            return true;
 -          }
 -        }
 -      }
 -      return false;
 -    }
 -  };
 -
 -  /**
 -   * accession ID only must be identical.
 -   */
 -  public static DbRefComp matchId = new DbRefComp()
 -  {
 -    @Override
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb)
 -    {
 -      if (refa.getAccessionId() != null && refb.getAccessionId() != null
 -              && refb.getAccessionId().equals(refa.getAccessionId()))
 -      {
 -        return true;
 -      }
 -      return false;
 -    }
 -  };
 -
 -  /**
 -   * Parses a DBRefEntry and adds it to the sequence, also a PDBEntry if the
 -   * database is PDB.
 -   * <p>
 -   * Used by file parsers to generate DBRefs from annotation within file (eg
 -   * Stockholm)
 -   * 
 -   * @param dbname
 -   * @param version
 -   * @param acn
 -   * @param seq
 -   *          where to annotate with reference
 -   * @return parsed version of entry that was added to seq (if any)
 -   */
 -  public static DBRefEntry parseToDbRef(SequenceI seq, String dbname,
 -          String version, String acn)
 -  {
 -    DBRefEntry ref = null;
 -    if (dbname != null)
 -    {
 -      String locsrc = DBRefUtils.getCanonicalName(dbname);
 -      if (locsrc.equals(DBRefSource.PDB))
 -      {
 -        /*
 -         * Check for PFAM style stockhom PDB accession id citation e.g.
 -         * "1WRI A; 7-80;"
 -         */
 -        Regex r = new com.stevesoft.pat.Regex(
 -                "([0-9][0-9A-Za-z]{3})\\s*(.?)\\s*;\\s*([0-9]+)-([0-9]+)");
 -        if (r.search(acn.trim()))
 -        {
 -          String pdbid = r.stringMatched(1);
 -          String chaincode = r.stringMatched(2);
 -          if (chaincode == null)
 -          {
 -            chaincode = " ";
 -          }
 -          // String mapstart = r.stringMatched(3);
 -          // String mapend = r.stringMatched(4);
 -          if (chaincode.equals(" "))
 -          {
 -            chaincode = "_";
 -          }
 -          // construct pdb ref.
 -          ref = new DBRefEntry(locsrc, version, pdbid + chaincode);
 -          PDBEntry pdbr = new PDBEntry();
 -          pdbr.setId(pdbid);
 -          pdbr.setType(PDBEntry.Type.PDB);
 -          pdbr.setChainCode(chaincode);
 -          seq.addPDBId(pdbr);
 -        }
 -        else
 -        {
 -          System.err.println("Malformed PDB DR line:" + acn);
 -        }
 -      }
 -      else
 -      {
 -        // default:
 -        ref = new DBRefEntry(locsrc, version, acn);
 -      }
 -    }
 -    if (ref != null)
 -    {
 -      seq.addDBRef(ref);
 -    }
 -    return ref;
 -  }
 -
 -  /**
 -   * Returns true if either object is null, or they are equal
 -   * 
 -   * @param o1
 -   * @param o2
 -   * @return
 -   */
 -  public static boolean nullOrEqual(Object o1, Object o2)
 -  {
 -    if (o1 == null || o2 == null)
 -    {
 -      return true;
 -    }
 -    return o1.equals(o2);
 -  }
 -
 -  /**
 -   * canonicalise source string before comparing. null is always wildcard
 -   * 
 -   * @param o1
 -   *          - null or source string to compare
 -   * @param o2
 -   *          - null or source string to compare
 -   * @return true if either o1 or o2 are null, or o1 equals o2 under
 -   *         DBRefUtils.getCanonicalName
 -   *         (o1).equals(DBRefUtils.getCanonicalName(o2))
 -   */
 -  public static boolean nullOrEqualSource(String o1, String o2)
 -  {
 -    if (o1 == null || o2 == null)
 -    {
 -      return true;
 -    }
 -    return DBRefUtils.getCanonicalName(o1)
 -            .equals(DBRefUtils.getCanonicalName(o2));
 -  }
 -
 -  /**
 -   * Selects just the DNA or protein references from a set of references
 -   * 
 -   * @param selectDna
 -   *          if true, select references to 'standard' DNA databases, else to
 -   *          'standard' peptide databases
 -   * @param refs
 -   *          a set of references to select from
 -   * @return
 -   */
 -  public static DBRefEntry[] selectDbRefs(boolean selectDna,
 -          DBRefEntry[] refs)
 -  {
 -    return selectRefs(refs,
 -            selectDna ? DBRefSource.DNACODINGDBS : DBRefSource.PROTEINDBS);
 -    // could attempt to find other cross
 -    // refs here - ie PDB xrefs
 -    // (not dna, not protein seq)
 -  }
 -
 -  /**
 +      /**
 +       * look up source in an internal list of database reference sources and return
 +       * the canonical jalview name for the source, or the original string if it has
 +       * no canonical form.
 +       * 
 +       * @param source
 +       * @return canonical jalview source (one of jalview.datamodel.DBRefSource.*) or
 +       *         original source
 +       */
-       public static String getCanonicalName(String source) {
-               if (source == null) {
-                       return null;
-               }
-               String canonical = canonicalSourceNameLookup.get(source.toLowerCase());
-               return canonical == null ? source : canonical;
++      public static String getCanonicalName(String source) 
++      {
++        if (source == null) 
++        {
++              return null;
++        }
++        String canonical = canonicalSourceNameLookup.get(source.toLowerCase());
++        return canonical == null ? source : canonical;
 +      }
 +
 +      /**
 +       * Returns a (possibly empty) list of those references that match the given
 +       * entry. Currently uses a comparator which matches if
 +       * <ul>
 +       * <li>database sources are the same</li>
 +       * <li>accession ids are the same</li>
 +       * <li>both have no mapping, or the mappings are the same</li>
 +       * </ul>
 +       * 
 +       * @param ref   Set of references to search
 +       * @param entry pattern to match
 +       * @param mode  SEARCH_MODE_FULL for all; SEARCH_MODE_NO_MAP_NO_VERSION optional
 +       * @return
 +       */
 +      public static List<DBRefEntry> searchRefs(List<DBRefEntry> ref, DBRefEntry entry, int mode) {
 +              return searchRefs(ref, entry, matchDbAndIdAndEitherMapOrEquivalentMapList, mode);
 +      }
 +
 +      /**
 +       * Returns a list of those references that match the given accession id
 +       * <ul>
 +       * <li>database sources are the same</li>
 +       * <li>accession ids are the same</li>
 +       * <li>both have no mapping, or the mappings are the same</li>
 +       * </ul>
 +       * 
 +       * @param refs  Set of references to search
 +       * @param accId accession id to match
 +       * @return
 +       */
 +      public static List<DBRefEntry> searchRefs(List<DBRefEntry> refs, String accId) {
 +              List<DBRefEntry> rfs = new ArrayList<DBRefEntry>();
 +              if (refs == null || accId == null) {
 +                      return rfs;
 +              }
 +              for (int i = 0, n = refs.size(); i < n; i++) {
 +                      DBRefEntry e = refs.get(i);
 +                      if (accId.equals(e.getAccessionId())) {
 +                              rfs.add(e);
 +                      }
 +              }
 +              return rfs;
 +//    return searchRefs(refs, new DBRefEntry("", "", accId), matchId, SEARCH_MODE_FULL);
 +      }
 +
 +      /**
 +       * Returns a (possibly empty) list of those references that match the given
 +       * entry, according to the given comparator.
 +       * 
 +       * @param refs       an array of database references to search
 +       * @param entry      an entry to compare against
 +       * @param comparator
 +       * @param mode       SEARCH_MODE_FULL for all; SEARCH_MODE_NO_MAP_NO_VERSION
 +       *                   optional
 +       * @return
 +       */
 +      static List<DBRefEntry> searchRefs(List<DBRefEntry> refs, DBRefEntry entry, DbRefComp comparator, int mode) {
 +              List<DBRefEntry> rfs = new ArrayList<DBRefEntry>();
 +              if (refs == null || entry == null) {
 +                      return rfs;
 +              }
 +              for (int i = 0, n = refs.size(); i < n; i++) {
 +                      DBRefEntry e = refs.get(i);
 +                      if (comparator.matches(entry, e, SEARCH_MODE_FULL)) {
 +                              rfs.add(e);
 +                      }
 +              }
 +              return rfs;
 +      }
 +
 +      interface DbRefComp {
 +              default public boolean matches(DBRefEntry refa, DBRefEntry refb) {
 +                      return matches(refa, refb, SEARCH_MODE_FULL);
 +              };
 +
 +              public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode);
 +      }
 +
 +      /**
 +       * match on all non-null fields in refa
 +       */
 +      // TODO unused - remove? would be broken by equating "" with null
 +      public static DbRefComp matchNonNullonA = new DbRefComp() {
 +              @Override
 +              public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode) {
 +                      if ((mode & DB_SOURCE) != 0 && 
 +                                      (refa.getSource() == null || DBRefUtils.getCanonicalName(refb.getSource())
 +                                      .equals(DBRefUtils.getCanonicalName(refa.getSource())))) {
 +                              if ((mode & DB_VERSION) != 0 && 
 +                                              (refa.getVersion() == null || refb.getVersion().equals(refa.getVersion()))) {
 +                                      if ((mode & DB_ID) != 0 && 
 +                                                      (refa.getAccessionId() == null || refb.getAccessionId().equals(refa.getAccessionId()))) {
 +                                              if ((mode & DB_MAP) != 0 && 
 +                                                              (refa.getMap() == null || (refb.getMap() != null && refb.getMap().equals(refa.getMap())))) {
 +                                                      return true;
 +                                              }
 +                                      }
 +                              }
 +                      }
 +                      return false;
 +              }
 +      };
 +
 +      /**
 +       * either field is null or field matches for all of source, version, accession
 +       * id and map.
 +       */
 +      // TODO unused - remove?
 +      public static DbRefComp matchEitherNonNull = new DbRefComp() {
 +              @Override
 +              public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode) {
 +                      if (nullOrEqualSource(refa.getSource(), refb.getSource())
 +                                      && nullOrEqual(refa.getVersion(), refb.getVersion())
 +                                      && nullOrEqual(refa.getAccessionId(), refb.getAccessionId())
 +                                      && nullOrEqual(refa.getMap(), refb.getMap())) {
 +                              return true;
 +                      }
 +                      return false;
 +              }
 +
 +      };
 +
 +      /**
 +       * accession ID and DB must be identical. Version is ignored. Map is either not
 +       * defined or is a match (or is compatible?)
 +       */
 +      // TODO unused - remove?
 +      public static DbRefComp matchDbAndIdAndEitherMap = new DbRefComp() {
 +              @Override
 +              public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode) {
 +                      if (refa.getSource() != null && refb.getSource() != null && DBRefUtils.getCanonicalName(refb.getSource())
 +                                      .equals(DBRefUtils.getCanonicalName(refa.getSource()))) {
 +                              // We dont care about version
 +                              if (refa.getAccessionId() != null && refb.getAccessionId() != null
 +                                              // FIXME should be && not || here?
 +                                              || refb.getAccessionId().equals(refa.getAccessionId())) {
 +                                      if ((refa.getMap() == null || refb.getMap() == null) || (refa.getMap() != null
 +                                                      && refb.getMap() != null && refb.getMap().equals(refa.getMap()))) {
 +                                              return true;
 +                                      }
 +                              }
 +                      }
 +                      return false;
 +              }
 +      };
 +
 +      /**
 +       * accession ID and DB must be identical. Version is ignored. No map on either
 +       * or map but no maplist on either or maplist of map on a is the complement of
 +       * maplist of map on b.
 +       */
 +      // TODO unused - remove?
 +      public static DbRefComp matchDbAndIdAndComplementaryMapList = new DbRefComp() {
 +              @Override
 +              public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode) {
 +                      if (refa.getSource() != null && refb.getSource() != null && DBRefUtils.getCanonicalName(refb.getSource())
 +                                      .equals(DBRefUtils.getCanonicalName(refa.getSource()))) {
 +                              // We dont care about version
 +                              if (refa.getAccessionId() != null && refb.getAccessionId() != null
 +                                              || refb.getAccessionId().equals(refa.getAccessionId())) {
 +                                      if ((refa.getMap() == null && refb.getMap() == null)
 +                                                      || (refa.getMap() != null && refb.getMap() != null)) {
 +                                              if ((refb.getMap().getMap() == null && refa.getMap().getMap() == null)
 +                                                              || (refb.getMap().getMap() != null && refa.getMap().getMap() != null
 +                                                                              && refb.getMap().getMap().getInverse().equals(refa.getMap().getMap()))) {
 +                                                      return true;
 +                                              }
 +                                      }
 +                              }
 +                      }
 +                      return false;
 +              }
 +      };
 +
 +      /**
 +       * accession ID and DB must be identical. Version is ignored. No map on both or
 +       * or map but no maplist on either or maplist of map on a is equivalent to the
 +       * maplist of map on b.
 +       */
 +      // TODO unused - remove?
 +      public static DbRefComp matchDbAndIdAndEquivalentMapList = new DbRefComp() {
 +              @Override
 +              public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode) {
 +                      if (refa.getSource() != null && refb.getSource() != null && DBRefUtils.getCanonicalName(refb.getSource())
 +                                      .equals(DBRefUtils.getCanonicalName(refa.getSource()))) {
 +                              // We dont care about version
 +                              // if ((refa.getVersion()==null || refb.getVersion()==null)
 +                              // || refb.getVersion().equals(refa.getVersion()))
 +                              // {
 +                              if (refa.getAccessionId() != null && refb.getAccessionId() != null
 +                                              || refb.getAccessionId().equals(refa.getAccessionId())) {
 +                                      if (refa.getMap() == null && refb.getMap() == null) {
 +                                              return true;
 +                                      }
 +                                      if (refa.getMap() != null && refb.getMap() != null
 +                                                      && ((refb.getMap().getMap() == null && refa.getMap().getMap() == null)
 +                                                                      || (refb.getMap().getMap() != null && refa.getMap().getMap() != null
 +                                                                                      && refb.getMap().getMap().equals(refa.getMap().getMap())))) {
 +                                              return true;
 +                                      }
 +                              }
 +                      }
 +                      return false;
 +              }
 +      };
 +
 +      /**
 +       * accession ID and DB must be identical, or null on a. Version is ignored. No
 +       * map on either or map but no maplist on either or maplist of map on a is
 +       * equivalent to the maplist of map on b.
 +       */
-       public static DbRefComp matchDbAndIdAndEitherMapOrEquivalentMapList = new DbRefComp() {
++      public static DbRefComp matchDbAndIdAndEitherMapOrEquivalentMapList = new DbRefComp() 
++      {
 +              @Override
-               public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode) {
++              public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode) 
++              {
 +                      if (refa.getSource() != null && refb.getSource() != null && DBRefUtils.getCanonicalName(refb.getSource())
-                                       .equals(DBRefUtils.getCanonicalName(refa.getSource()))) {
++                                      .equals(DBRefUtils.getCanonicalName(refa.getSource()))) 
++                      {
 +                              // We dont care about version
-                               if (refa.getAccessionId() == null || refa.getAccessionId().equals(refb.getAccessionId())) {
-                                       if (refa.getMap() == null || refb.getMap() == null) {
++                              if (refa.getAccessionId() == null || refa.getAccessionId().equals(refb.getAccessionId())) 
++                              {
++                                      if (refa.getMap() == null || refb.getMap() == null) 
++                                      {
 +                                              return true;
 +                                      }
 +                                      if ((refa.getMap() != null && refb.getMap() != null)
 +                                                      && (refb.getMap().getMap() == null && refa.getMap().getMap() == null)
 +                                                      || (refb.getMap().getMap() != null && refa.getMap().getMap() != null
-                                                                       && (refb.getMap().getMap().equals(refa.getMap().getMap())))) {
++                                                                      && (refb.getMap().getMap().equals(refa.getMap().getMap())))) 
++                                      {
 +                                              return true;
 +                                      }
 +                              }
 +                      }
 +                      return false;
 +              }
 +      };
 +
 +      /**
-        * accession ID only must be identical.
-        */
-       public static DbRefComp matchId = new DbRefComp() {
-               @Override
-               public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode) {
-                       if (refa.getAccessionId() != null && refb.getAccessionId() != null
-                                       && refb.getAccessionId().equals(refa.getAccessionId())) {
-                               return true;
-                       }
-                       return false;
-               }
-       };
+    * Returns the (possibly empty) list of those supplied dbrefs which have the
+    * specified source database, with a case-insensitive match of source name
+    * 
+    * @param dbRefs
+    * @param source
+    * @return
+    */
+   public static List<DBRefEntry> searchRefsForSource(DBRefEntry[] dbRefs,
+           String source)
+   {
+     List<DBRefEntry> matches = new ArrayList<>();
+     if (dbRefs != null && source != null)
+     {
+       for (DBRefEntry dbref : dbRefs)
+       {
+         if (source.equalsIgnoreCase(dbref.getSource()))
+         {
+           matches.add(dbref);
+         }
+       }
+     }
+     return matches;
+   }
  
 -  /**
 -   * promote direct database references to primary for nucleotide or protein
 -   * sequences if they have an appropriate primary ref
 -   * <table>
 -   * <tr>
 -   * <th>Seq Type</th>
 -   * <th>Primary DB</th>
 -   * <th>Direct which will be promoted</th>
 -   * </tr>
 -   * <tr align=center>
 -   * <td>peptides</td>
 -   * <td>Ensembl</td>
 -   * <td>Uniprot</td>
 -   * </tr>
 -   * <tr align=center>
 -   * <td>peptides</td>
 -   * <td>Ensembl</td>
 -   * <td>Uniprot</td>
 -   * </tr>
 -   * <tr align=center>
 -   * <td>dna</td>
 -   * <td>Ensembl</td>
 -   * <td>ENA</td>
 -   * </tr>
 -   * </table>
 -   * 
 -   * @param sequence
 -   */
 -  public static void ensurePrimaries(SequenceI sequence)
 -  {
 -    List<DBRefEntry> pr = sequence.getPrimaryDBRefs();
 -    if (pr.size() == 0)
 -    {
 -      // nothing to do
 -      return;
 -    }
 -    List<DBRefEntry> selfs = new ArrayList<>();
 -    {
 -      DBRefEntry[] selfArray = selectDbRefs(!sequence.isProtein(),
 -              sequence.getDBRefs());
 -      if (selfArray == null || selfArray.length == 0)
 -      {
 -        // nothing to do
 -        return;
 -      }
 -      selfs.addAll(Arrays.asList(selfArray));
 -    }
 -
 -    // filter non-primary refs
 -    for (DBRefEntry p : pr)
 -    {
 -      while (selfs.contains(p))
 -      {
 -        selfs.remove(p);
 -      }
 -    }
 -    List<DBRefEntry> toPromote = new ArrayList<>();
 -
 -    for (DBRefEntry p : pr)
 -    {
 -      List<String> promType = new ArrayList<>();
 -      if (sequence.isProtein())
 -      {
 -        switch (getCanonicalName(p.getSource()))
 -        {
 -        case DBRefSource.UNIPROT:
 -          // case DBRefSource.UNIPROTKB:
 -          // case DBRefSource.UP_NAME:
 -          // search for and promote ensembl
 -          promType.add(DBRefSource.ENSEMBL);
 -          break;
 -        case DBRefSource.ENSEMBL:
 -          // search for and promote Uniprot
 -          promType.add(DBRefSource.UNIPROT);
 -          break;
 -        }
 -      }
 -      else
 -      {
 -        // TODO: promote transcript refs
 -      }
 -
 -      // collate candidates and promote them
 -      DBRefEntry[] candidates = selectRefs(selfs.toArray(new DBRefEntry[0]),
 -              promType.toArray(new String[0]));
 -      if (candidates != null)
 -      {
 -        for (DBRefEntry cand : candidates)
 -        {
 -          if (cand.hasMap())
 -          {
 -            if (cand.getMap().getTo() != null
 -                    && cand.getMap().getTo() != sequence)
 -            {
 -              // can't promote refs with mappings to other sequences
 -              continue;
 -            }
 -            if (cand.getMap().getMap().getFromLowest() != sequence
 -                    .getStart()
 -                    && cand.getMap().getMap().getFromHighest() != sequence
 -                            .getEnd())
 -            {
 -              // can't promote refs with mappings from a region of this sequence
 -              // - eg CDS
 -              continue;
 -            }
 -          }
 -          // and promote
 -          cand.setVersion(p.getVersion() + " (promoted)");
 -          selfs.remove(cand);
 -          toPromote.add(cand);
 -          if (!cand.isPrimaryCandidate())
 -          {
 -            System.out.println(
 -                    "Warning: Couldn't promote dbref " + cand.toString()
 -                            + " for sequence " + sequence.toString());
 -          }
 -        }
 -      }
 -    }
 -  }
 +      /**
 +       * Parses a DBRefEntry and adds it to the sequence, also a PDBEntry if the
 +       * database is PDB.
 +       * <p>
 +       * Used by file parsers to generate DBRefs from annotation within file (eg
 +       * Stockholm)
 +       * 
 +       * @param dbname
 +       * @param version
 +       * @param acn
 +       * @param seq     where to annotate with reference
 +       * @return parsed version of entry that was added to seq (if any)
 +       */
 +      public static DBRefEntry parseToDbRef(SequenceI seq, String dbname, String version, String acn) {
 +              DBRefEntry ref = null;
 +              if (dbname != null) {
 +                      String locsrc = DBRefUtils.getCanonicalName(dbname);
 +                      if (locsrc.equals(DBRefSource.PDB)) {
 +                              /*
 +                               * Check for PFAM style stockhom PDB accession id citation e.g. "1WRI A; 7-80;"
 +                               */
 +                              Regex r = new com.stevesoft.pat.Regex("([0-9][0-9A-Za-z]{3})\\s*(.?)\\s*;\\s*([0-9]+)-([0-9]+)");
 +                              if (r.search(acn.trim())) {
 +                                      String pdbid = r.stringMatched(1);
 +                                      String chaincode = r.stringMatched(2);
 +                                      if (chaincode == null) {
 +                                              chaincode = " ";
 +                                      }
 +                                      // String mapstart = r.stringMatched(3);
 +                                      // String mapend = r.stringMatched(4);
 +                                      if (chaincode.equals(" ")) {
 +                                              chaincode = "_";
 +                                      }
 +                                      // construct pdb ref.
 +                                      ref = new DBRefEntry(locsrc, version, pdbid + chaincode);
 +                                      PDBEntry pdbr = new PDBEntry();
 +                                      pdbr.setId(pdbid);
 +                                      pdbr.setType(PDBEntry.Type.PDB);
 +                                      pdbr.setChainCode(chaincode);
 +                                      seq.addPDBId(pdbr);
 +                              } else {
 +                                      System.err.println("Malformed PDB DR line:" + acn);
 +                              }
 +                      } else {
 +                              // default:
 +                              ref = new DBRefEntry(locsrc, version, acn);
 +                      }
 +              }
 +              if (ref != null) {
 +                      seq.addDBRef(ref);
 +              }
 +              return ref;
 +      }
 +
 +      /**
 +       * Returns true if either object is null, or they are equal
 +       * 
 +       * @param o1
 +       * @param o2
 +       * @return
 +       */
 +      public static boolean nullOrEqual(Object o1, Object o2) {
 +              if (o1 == null || o2 == null) {
 +                      return true;
 +              }
 +              return o1.equals(o2);
 +      }
 +
 +      /**
 +       * canonicalise source string before comparing. null is always wildcard
 +       * 
 +       * @param o1 - null or source string to compare
 +       * @param o2 - null or source string to compare
 +       * @return true if either o1 or o2 are null, or o1 equals o2 under
 +       *         DBRefUtils.getCanonicalName
 +       *         (o1).equals(DBRefUtils.getCanonicalName(o2))
 +       */
 +      public static boolean nullOrEqualSource(String o1, String o2) {
 +              if (o1 == null || o2 == null) {
 +                      return true;
 +              }
 +              return DBRefUtils.getCanonicalName(o1).equals(DBRefUtils.getCanonicalName(o2));
 +      }
 +
 +      /**
 +       * Selects just the DNA or protein references from a set of references
 +       * 
 +       * @param selectDna if true, select references to 'standard' DNA databases, else
 +       *                  to 'standard' peptide databases
 +       * @param refs      a set of references to select from
 +       * @return
 +       */
 +      public static List<DBRefEntry> selectDbRefs(boolean selectDna, List<DBRefEntry> refs) {
 +              return selectRefs(refs, selectDna ? DBRefSource.DNACODINGDBS : DBRefSource.PROTEINDBS);
 +              // could attempt to find other cross
 +              // refs here - ie PDB xrefs
 +              // (not dna, not protein seq)
 +      }
 +
 +      /**
 +       * Returns the (possibly empty) list of those supplied dbrefs which have the
 +       * specified source database, with a case-insensitive match of source name
 +       * 
 +       * @param dbRefs
 +       * @param source
 +       * @return
 +       */
 +      public static List<DBRefEntry> searchRefsForSource(List<DBRefEntry> dbRefs, String source) {
 +              List<DBRefEntry> matches = new ArrayList<DBRefEntry>();
 +              if (dbRefs != null && source != null) {
 +                      for (DBRefEntry dbref : dbRefs) {
 +                              if (source.equalsIgnoreCase(dbref.getSource())) {
 +                                      matches.add(dbref);
 +                              }
 +                      }
 +              }
 +              return matches;
 +      }
 +
 +      /**
 +       * promote direct database references to primary for nucleotide or protein
 +       * sequences if they have an appropriate primary ref
 +       * <table>
 +       * <tr>
 +       * <th>Seq Type</th>
 +       * <th>Primary DB</th>
 +       * <th>Direct which will be promoted</th>
 +       * </tr>
 +       * <tr align=center>
 +       * <td>peptides</td>
 +       * <td>Ensembl</td>
 +       * <td>Uniprot</td>
 +       * </tr>
 +       * <tr align=center>
 +       * <td>peptides</td>
 +       * <td>Ensembl</td>
 +       * <td>Uniprot</td>
 +       * </tr>
 +       * <tr align=center>
 +       * <td>dna</td>
 +       * <td>Ensembl</td>
 +       * <td>ENA</td>
 +       * </tr>
 +       * </table>
 +       * 
 +       * @param sequence
 +       */
 +      public static void ensurePrimaries(SequenceI sequence, List<DBRefEntry> pr) {
 +              if (pr.size() == 0) {
 +                      // nothing to do
 +                      return;
 +              }
 +              int sstart = sequence.getStart();
 +              int send = sequence.getEnd();
 +              boolean isProtein = sequence.isProtein();
 +              BitSet bsSelect = new BitSet();
 +
 +//    List<DBRefEntry> selfs = new ArrayList<DBRefEntry>();
 +//    {
 +
 +//      List<DBRefEntry> selddfs = selectDbRefs(!isprot, sequence.getDBRefs());
 +//      if (selfs == null || selfs.size() == 0)
 +//      {
 +//        // nothing to do
 +//        return;
 +//      }
 +
 +              List<DBRefEntry> dbrefs = sequence.getDBRefs();
 +              bsSelect.set(0, dbrefs.size());
 +
 +              if (!selectRefsBS(dbrefs, isProtein ? DBRefSource.PROTEIN_MASK : DBRefSource.DNA_CODING_MASK, bsSelect))
 +                      return;
 +
 +//      selfs.addAll(selfArray);
 +//    }
 +
 +              // filter non-primary refs
 +              for (int ip = pr.size(); --ip >= 0;) {
 +                      DBRefEntry p = pr.get(ip);
 +                      for (int i = bsSelect.nextSetBit(0); i >= 0; i = bsSelect.nextSetBit(i + 1)) {
 +                              if (dbrefs.get(i) == p)
 +                                      bsSelect.clear(i);
 +                      }
 +//      while (selfs.contains(p))
 +//      {
 +//        selfs.remove(p);
 +//      }
 +              }
 +//    List<DBRefEntry> toPromote = new ArrayList<DBRefEntry>();
 +
 +              for (int ip = pr.size(), keys = 0; --ip >= 0 && keys != DBRefSource.PRIMARY_MASK;) {
 +                      DBRefEntry p = pr.get(ip);
 +                      if (isProtein) {
 +                              switch (getCanonicalName(p.getSource())) {
 +                              case DBRefSource.UNIPROT:
 +                                      keys |= DBRefSource.UNIPROT_MASK;
 +                                      break;
 +                              case DBRefSource.ENSEMBL:
 +                                      keys |= DBRefSource.ENSEMBL_MASK;
 +                                      break;
 +                              }
 +                      } else {
 +                              // TODO: promote transcript refs ??
 +                      }
 +                      if (keys == 0 || !selectRefsBS(dbrefs, keys, bsSelect))
 +                              return;
 +//      if (candidates != null)
 +                      {
 +                              for (int ic = bsSelect.nextSetBit(0); ic >= 0; ic = bsSelect.nextSetBit(ic + 1))
 +//        for (int ic = 0, n = candidates.size(); ic < n; ic++)
 +                              {
 +                                      DBRefEntry cand = dbrefs.get(ic);// candidates.get(ic);
 +                                      if (cand.hasMap()) {
 +                                              Mapping map = cand.getMap();
 +                                              SequenceI cto = map.getTo();
 +                                              if (cto != null && cto != sequence) {
 +                                                      // can't promote refs with mappings to other sequences
 +                                                      continue;
 +                                              }
 +                                              MapList mlist = map.getMap();
 +                                              if (mlist.getFromLowest() != sstart && mlist.getFromHighest() != send) {
 +                                                      // can't promote refs with mappings from a region of this sequence
 +                                                      // - eg CDS
 +                                                      continue;
 +                                              }
 +                                      }
 +                                      // and promote - not that version must be non-null here, 
 +                                      // as p must have passed isPrimaryCandidate()
 +                                      cand.setVersion(p.getVersion() + " (promoted)");
 +                                      bsSelect.clear(ic);
 +                                      // selfs.remove(cand);
 +//          toPromote.add(cand);
 +                                      if (!cand.isPrimaryCandidate()) {
 +                                              System.out.println("Warning: Couldn't promote dbref " + cand.toString() + " for sequence "
 +                                                              + sequence.toString());
 +                                      }
 +                              }
 +                      }
 +              }
 +      }
  
  }
@@@ -24,7 -24,6 +24,7 @@@ import jalview.analysis.AnnotationSorte
  import jalview.analysis.Conservation;
  import jalview.analysis.TreeModel;
  import jalview.api.AlignCalcManagerI;
 +import jalview.api.AlignExportSettingsI;
  import jalview.api.AlignViewportI;
  import jalview.api.AlignmentViewPanel;
  import jalview.api.FeaturesDisplayedI;
@@@ -32,7 -31,6 +32,7 @@@ import jalview.api.ViewStyleI
  import jalview.commands.CommandI;
  import jalview.datamodel.AlignedCodonFrame;
  import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.AlignmentExportData;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.AlignmentView;
  import jalview.datamodel.Annotation;
@@@ -664,7 -662,8 +664,8 @@@ public abstract class AlignmentViewpor
           * retain any colour thresholds per group while
           * changing choice of colour scheme (JAL-2386)
           */
-         sg.setColourScheme(cs);
+         sg.setColourScheme(
+                 cs == null ? null : cs.getInstance(this, sg));
          if (cs != null)
          {
            sg.getGroupColourScheme().alignmentChanged(sg,
    public void invertColumnSelection()
    {
      colSel.invertColumnSelection(0, alignment.getWidth(), alignment);
+     isColSelChanged(true);
    }
  
    @Override
      return currentTree;
    }
  
 +  @Override
 +  public AlignmentExportData getAlignExportData(AlignExportSettingsI options)
 +  {
 +    AlignmentI alignmentToExport = null;
 +    String[] omitHidden = null;
 +    alignmentToExport = null;
 +
 +    if (hasHiddenColumns() && !options.isExportHiddenColumns())
 +    {
 +      omitHidden = getViewAsString(false,
 +              options.isExportHiddenSequences());
 +    }
 +
 +    int[] alignmentStartEnd = new int[2];
 +    if (hasHiddenRows() && options.isExportHiddenSequences())
 +    {
 +      alignmentToExport = getAlignment().getHiddenSequences()
 +              .getFullAlignment();
 +    }
 +    else
 +    {
 +      alignmentToExport = getAlignment();
 +    }
 +    alignmentStartEnd = getAlignment().getHiddenColumns()
 +            .getVisibleStartAndEndIndex(alignmentToExport.getWidth());
 +    AlignmentExportData ed = new AlignmentExportData(alignmentToExport,
 +            omitHidden, alignmentStartEnd);
 +    return ed;
 +  }
++  
+   /**
+    * flag set to indicate if structure views might be out of sync with sequences
+    * in the alignment
+    */
+   private boolean needToUpdateStructureViews = false;
+   @Override
+   public boolean isUpdateStructures()
+   {
+     return needToUpdateStructureViews;
+   }
+   @Override
+   public void setUpdateStructures(boolean update)
+   {
+     needToUpdateStructureViews = update;
+   }
+   @Override
+   public boolean needToUpdateStructureViews()
+   {
+     boolean update = needToUpdateStructureViews;
+     needToUpdateStructureViews = false;
+     return update;
+   }
+   @Override
+   public void addSequenceGroup(SequenceGroup sequenceGroup)
+   {
+     alignment.addGroup(sequenceGroup);
+     Color col = sequenceGroup.idColour;
+     if (col != null)
+     {
+       col = col.brighter();
+       for (SequenceI sq : sequenceGroup.getSequences())
+       {
+         setSequenceColour(sq, col);
+       }
+     }
+     if (codingComplement != null)
+     {
+       SequenceGroup mappedGroup = MappingUtils
+               .mapSequenceGroup(sequenceGroup, this, codingComplement);
+       if (mappedGroup.getSequences().size() > 0)
+       {
+         codingComplement.getAlignment().addGroup(mappedGroup);
+         if (col != null)
+         {
+           for (SequenceI seq : mappedGroup.getSequences())
+           {
+             codingComplement.setSequenceColour(seq, col);
+           }
+         }
+       }
+       // propagate the structure view update flag according to our own setting
+       codingComplement.setUpdateStructures(needToUpdateStructureViews);
+     }
+   }
  }
@@@ -37,7 -37,6 +37,7 @@@ import java.beans.PropertyChangeListene
  import java.beans.PropertyChangeSupport;
  import java.util.ArrayList;
  import java.util.Arrays;
 +import java.util.Comparator;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Hashtable;
@@@ -613,7 -612,7 +613,7 @@@ public abstract class FeatureRendererMo
     * @param type
     * @return
     */
 -  protected boolean showFeatureOfType(String type)
 +  public boolean showFeatureOfType(String type)
    {
      return type == null ? false : (av.getFeaturesDisplayed() == null ? true
              : av.getFeaturesDisplayed().isVisible(type));
    }
  
    /**
-    * Removes from the list of features any that duplicate the location of a
-    * feature of the same type. Should be used only for features of the same,
-    * simple, feature colour (which normally implies the same feature type). Does
-    * not check visibility settings for feature type or feature group. No
-    * filtering is done if transparency, or any feature filters, are in force.
+    * Removes from the list of features any whose group is not shown, or that are
+    * visible and duplicate the location of a visible feature of the same type.
+    * Should be used only for features of the same, simple, feature colour (which
+    * normally implies the same feature type). No filtering is done if
+    * transparency, or any feature filters, are in force.
     * 
     * @param features
     */
      while (it.hasNext())
      {
        SequenceFeature sf = it.next();
+       if (featureGroupNotShown(sf))
+       {
+         it.remove();
+         continue;
+       }
  
        /*
         * a feature is redundant for rendering purposes if it has the
         * (checking type and isContactFeature as a fail-safe here, although
         * currently they are guaranteed to match in this context)
         */
-       if (lastFeature != null && sf.getBegin() == lastFeature.getBegin()
+       if (lastFeature != null
+               && sf.getBegin() == lastFeature.getBegin()
                && sf.getEnd() == lastFeature.getEnd()
                && sf.isContactFeature() == lastFeature.isContactFeature()
                && sf.getType().equals(lastFeature.getType()))
      return filter == null ? true : filter.matches(sf);
    }
  
 +  /**
 +   * Answers true unless the specified group is set to hidden. Defaults to true
 +   * if group visibility is not set.
 +   * 
 +   * @param group
 +   * @return
 +   */
 +  public boolean isGroupVisible(String group)
 +  {
 +    if (!featureGroups.containsKey(group))
 +    {
 +      return true;
 +    }
 +    return featureGroups.get(group);
 +  }
 +
 +  /**
 +   * Orders features in render precedence (last in order is last to render, so
 +   * displayed on top of other features)
 +   * 
 +   * @param order
 +   */
 +  public void orderFeatures(Comparator<String> order)
 +  {
 +    Arrays.sort(renderOrder, order);
 +  }
++
+   @Override
+   public boolean isVisible(SequenceFeature feature)
+   {
+     if (feature == null)
+     {
+       return false;
+     }
+     if (getFeaturesDisplayed() == null
+             || !getFeaturesDisplayed().isVisible(feature.getType()))
+     {
+       return false;
+     }
+     if (featureGroupNotShown(feature))
+     {
+       return false;
+     }
+     FeatureColourI fc = featureColours.get(feature.getType());
+     if (fc != null && fc.isOutwithThreshold(feature))
+     {
+       return false;
+     }
+     if (!featureMatchesFilters(feature))
+     {
+       return false;
+     }
+     return true;
+   }
 -
  }
@@@ -134,7 -134,8 +134,7 @@@ public class DBRefFetcher implements Ru
      }
      this.dataset = ds;
      // TODO Jalview 2.5 lots of this code should be in the gui package!
 -    sfetcher = jalview.gui.SequenceFetcher
 -            .getSequenceFetcherSingleton(progressIndicatorFrame);
 +    sfetcher = jalview.gui.SequenceFetcher.getSequenceFetcherSingleton();
      // set default behaviour for transferring excess sequence data to the
      // dataset
      trimDsSeqs = Cache.getDefault(TRIM_RETRIEVED_SEQUENCES, true);
                    && (i < 50); seqIndex++, i++)
            {
              SequenceI sequence = dataset[seqIndex];
 -            DBRefEntry[] uprefs = DBRefUtils
 +            List<DBRefEntry> uprefs = DBRefUtils
                      .selectRefs(sequence.getDBRefs(), new String[]
                      { dbsource.getDbSource() }); // jalview.datamodel.DBRefSource.UNIPROT
              // });
              // check for existing dbrefs to use
 -            if (uprefs != null && uprefs.length > 0)
 +            if (uprefs != null && uprefs.size() > 0)
              {
 -              for (int j = 0; j < uprefs.length; j++)
 +              for (int j = 0, n = uprefs.size(); j < n; j++)
                {
 -                addSeqId(sequence, uprefs[j].getAccessionId());
 +              DBRefEntry upref = uprefs.get(j);
 +                addSeqId(sequence, upref.getAccessionId());
                  queries.addElement(
 -                        uprefs[j].getAccessionId().toUpperCase());
 +                        upref.getAccessionId().toUpperCase());
                }
              }
              else
        // taking into account all accessionIds and names in the file
        Vector<SequenceI> sequenceMatches = new Vector<>();
        // look for corresponding accession ids
 -      DBRefEntry[] entryRefs = DBRefUtils
 +      List<DBRefEntry> entryRefs = DBRefUtils
                .selectRefs(retrievedSeq.getDBRefs(), new String[]
                { dbSource });
        if (entryRefs == null)
                          + dbSource + " on " + retrievedSeq.getName());
          continue;
        }
 -      for (int j = 0; j < entryRefs.length; j++)
 +      for (int j = 0, n = entryRefs.size(); j < n; j++)
        {
 -        String accessionId = entryRefs[j].getAccessionId();
 +      DBRefEntry ref = entryRefs.get(j);
 +        String accessionId = ref.getAccessionId();
          // match up on accessionId
          if (seqRefs.containsKey(accessionId.toUpperCase()))
          {
        // could be useful to extend this so we try to find any 'significant'
        // information in common between two sequence objects.
        /*
 -       * DBRefEntry[] entryRefs =
 +       * List<DBRefEntry> entryRefs =
         * jalview.util.DBRefUtils.selectRefs(entry.getDBRef(), new String[] {
         * dbSource }); for (int j = 0; j < entry.getName().size(); j++) { String
         * name = entry.getName().elementAt(j).toString(); if
          // TODO: test for legacy where uniprot or EMBL refs exist but no
          // mappings are made (but content matches retrieved set)
          boolean updateRefFrame = sequence.getDBRefs() == null
 -                || sequence.getDBRefs().length == 0;
 +                || sequence.getDBRefs().size() == 0;
          // TODO:
          // verify sequence against the entry sequence
  
          // and remove it from the rest
          // TODO: decide if we should remove annotated sequence from set
          sdataset.remove(sequence);
-         // TODO: should we make a note of sequences that have received new DB
-         // ids, so we can query all enabled DAS servers for them ?
        }
      }
      return modified;
     */
    private SequenceI[] recoverDbSequences(SequenceI[] sequencesArray)
    {
 -    Vector<SequenceI> nseq = new Vector<>();
 -    for (int i = 0; sequencesArray != null
 -            && i < sequencesArray.length; i++)
 +      int n;
 +      if (sequencesArray == null || (n = sequencesArray.length) == 0)
 +        return sequencesArray;
 +    ArrayList<SequenceI> nseq = new ArrayList<>();
 +    for (int i = 0;i < n; i++)
      {
 -      nseq.addElement(sequencesArray[i]);
 -      DBRefEntry[] dbr = sequencesArray[i].getDBRefs();
 +      nseq.add(sequencesArray[i]);
 +      List<DBRefEntry> dbr = sequencesArray[i].getDBRefs();
        Mapping map = null;
 -      for (int r = 0; (dbr != null) && r < dbr.length; r++)
 +      if (dbr != null)
        {
 -        if ((map = dbr[r].getMap()) != null)
 +        for (int r = 0, rn = dbr.size(); r < rn; r++)
          {
 -          if (map.getTo() != null && !nseq.contains(map.getTo()))
 +          if ((map = dbr.get(r).getMap()) != null)
            {
 -            nseq.addElement(map.getTo());
 -          }
 +            if (map.getTo() != null && !nseq.contains(map.getTo()))
 +            {
 +              nseq.add(map.getTo());
 +            }
 +          }  
          }
        }
      }
 +    // BH 2019.01.25 question here if this is the right logic. Return the original if nothing found?
      if (nseq.size() > 0)
      {
 -      sequencesArray = new SequenceI[nseq.size()];
 -      nseq.toArray(sequencesArray);
 +      return nseq.toArray(new SequenceI[nseq.size()]);
      }
      return sequencesArray;
    }
@@@ -32,7 -32,6 +32,7 @@@ import jalview.datamodel.SequenceI
  import jalview.schemes.ResidueProperties;
  import jalview.util.StringUtils;
  import jalview.ws.seqfetcher.DbSourceProxyImpl;
 +import jalview.xml.binding.embl.ROOT;
  import jalview.xml.binding.uniprot.DbReferenceType;
  import jalview.xml.binding.uniprot.Entry;
  import jalview.xml.binding.uniprot.FeatureType;
@@@ -48,7 -47,6 +48,7 @@@ 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;
@@@ -180,16 -178,12 +180,12 @@@ public class Uniprot extends DbSourcePr
    SequenceI uniprotEntryToSequence(Entry entry)
    {
      String id = getUniprotEntryId(entry);
-     String seqString = entry.getSequence().getValue();
      /*
-      * for backwards compatibility with Castor processing,
-      * remove any internal spaces
+      * Sequence should not include any whitespace, but JAXB leaves these in
       */
-     if (seqString.indexOf(' ') > -1)
-     {
-       seqString = seqString.replace(" ", "");
-     }
+     String seqString = entry.getSequence().getValue().replaceAll("\\s*",
+             "");
      SequenceI sequence = new Sequence(id,
              seqString);
      sequence.setDescription(getUniprotEntryDescription(entry));
        XMLStreamReader streamReader = XMLInputFactory.newInstance()
                .createXMLStreamReader(is);
        javax.xml.bind.Unmarshaller um = jc.createUnmarshaller();
 -      jalview.xml.binding.uniprot.Uniprot uniprot = (jalview.xml.binding.uniprot.Uniprot) um.unmarshal(streamReader);
 +      JAXBElement<jalview.xml.binding.uniprot.Uniprot> uniprotElement = 
 +                um.unmarshal(streamReader, jalview.xml.binding.uniprot.Uniprot.class);
 +      jalview.xml.binding.uniprot.Uniprot uniprot = uniprotElement.getValue();
 +      
        if (uniprot != null && !uniprot.getEntry().isEmpty())
        {
          entries = uniprot.getEntry();
@@@ -42,10 -42,8 +42,10 @@@ import jalview.util.DBRefUtils
  import jalview.util.MapList;
  import jalview.ws.SequenceFetcher;
  import jalview.ws.SequenceFetcherFactory;
 +import jalview.ws.params.InvalidArgumentException;
  
  import java.util.ArrayList;
 +import java.util.Arrays;
  import java.util.List;
  
  import org.testng.annotations.AfterClass;
@@@ -75,28 -73,28 +75,28 @@@ public class CrossRefTes
      DBRefEntry ref8 = new DBRefEntry("PFAM", "1", "A123");
      // ENSEMBL is a source of either dna or protein sequence data
      DBRefEntry ref9 = new DBRefEntry("ENSEMBL", "1", "A123");
 -    DBRefEntry[] refs = new DBRefEntry[] { ref1, ref2, ref3, ref4, ref5,
 -        ref6, ref7, ref8, ref9 };
 +    List<DBRefEntry> refs = Arrays.asList(new DBRefEntry[] { ref1, ref2, ref3, ref4, ref5,
 +            ref6, ref7, ref8, ref9 });
  
      /*
       * Just the DNA refs:
       */
 -    DBRefEntry[] found = DBRefUtils.selectDbRefs(true, refs);
 -    assertEquals(4, found.length);
 -    assertSame(ref5, found[0]);
 -    assertSame(ref6, found[1]);
 -    assertSame(ref7, found[2]);
 -    assertSame(ref9, found[3]);
 +    List<DBRefEntry> found = DBRefUtils.selectDbRefs(true, refs);
 +    assertEquals(4, found.size());
 +    assertSame(ref5, found.get(0));
 +    assertSame(ref6, found.get(1));
 +    assertSame(ref7, found.get(2));
 +    assertSame(ref9, found.get(3));
  
      /*
       * Just the protein refs:
       */
      found = DBRefUtils.selectDbRefs(false, refs);
 -    assertEquals(4, found.length);
 -    assertSame(ref1, found[0]);
 -    assertSame(ref2, found[1]);
 -    assertSame(ref4, found[2]);
 -    assertSame(ref9, found[3]);
 +    assertEquals(4, found.size());
 +    assertSame(ref1, found.get(0));
 +    assertSame(ref2, found.get(1));
 +    assertSame(ref4, found.get(2));
 +    assertSame(ref9, found.get(3));
    }
  
    /**
       * and others to dna coding databases
       */
      sources.clear();
 -    seq.setDBRefs(null);
 +      seq.setDBRefs(null);
      seq.addDBRef(new DBRefEntry("UNIPROT", "0", "A1234"));
      seq.addDBRef(new DBRefEntry("EMBLCDS", "0", "E2347"));
      SequenceI seq2 = new Sequence("Seq2", "MGKYQARLSS");
      CrossRef testee = new CrossRef(al.getSequencesArray(), al);
      AlignedCodonFrame acf = new AlignedCodonFrame();
      boolean found = testee.searchDataset(true, dna1, dbref, result, acf,
 -            true);
 +            true, DBRefUtils.SEARCH_MODE_FULL);
      assertFalse(found);
      assertTrue(result.isEmpty());
      assertTrue(acf.isEmpty());
      acf = new AlignedCodonFrame();
      dbref = new DBRefEntry("UNIPROT", "0", "Q9ZTS2");
      found = testee.searchDataset(!dna1.isProtein(), dna1, dbref, result,
 -            acf, false); // search dataset with a protein xref from a dna
 +            acf, false, DBRefUtils.SEARCH_MODE_FULL); // search dataset with a protein xref from a dna
                           // sequence to locate the protein product
      assertTrue(found);
      assertEquals(1, result.size());
      acf = new AlignedCodonFrame();
      dbref = new DBRefEntry("UNIPROT", "0", "Q9ZTS2");
      found = testee.searchDataset(!pep1.isProtein(), pep1, dbref, result,
 -            acf, false); // search dataset with a protein's direct dbref to
 +            acf, false, DBRefUtils.SEARCH_MODE_FULL); // search dataset with a protein's direct dbref to
                           // locate dna sequences with matching xref
      assertTrue(found);
      assertEquals(1, result.size());
      assertSame(pep2, xrefs.getSequenceAt(1));
    }
  
-   @AfterClass
+   @AfterClass(alwaysRun = true)
    public void tearDown()
    {
      SequenceFetcherFactory.setSequenceFetcher(null);
      /*
       * verify mappings added to Uniprot-to-EMBL dbrefs
       */
 -    Mapping mapping = p0ce19.getDBRefs()[0].getMap();
 +    Mapping mapping = p0ce19.getDBRefs().get(0).getMap();
      assertSame(j03321, mapping.getTo());
 -    mapping = p0ce19.getDBRefs()[1].getMap();
 +    mapping = p0ce19.getDBRefs().get(1).getMap();
      assertSame(x06707, mapping.getTo());
 -    mapping = p0ce20.getDBRefs()[0].getMap();
 +    mapping = p0ce20.getDBRefs().get(0).getMap();
      assertSame(j03321, mapping.getTo());
 -    mapping = p0ce20.getDBRefs()[1].getMap();
 +    mapping = p0ce20.getDBRefs().get(1).getMap();
      assertSame(x06707, mapping.getTo());
  
      /*
       * verify dbrefs on EMBL are mapped to alignment seqs
       */
 -    assertSame(p0ce19, j03321.getDBRefs()[0].getMap().getTo());
 -    assertSame(p0ce20, j03321.getDBRefs()[1].getMap().getTo());
 -    assertSame(p0ce19, x06707.getDBRefs()[0].getMap().getTo());
 -    assertSame(p0ce20, x06707.getDBRefs()[1].getMap().getTo());
 +    
 +    assertSame(p0ce19, j03321.getDBRefs().get(0).getMap().getTo());
 +    assertSame(p0ce20, j03321.getDBRefs().get(1).getMap().getTo());
 +    assertSame(p0ce19, x06707.getDBRefs().get(0).getMap().getTo());
 +    assertSame(p0ce20, x06707.getDBRefs().get(1).getMap().getTo());
  
      /*
       * verify new dbref on EMBL dbref mapping is copied to the
       * original Uniprot sequence
       */
 -    assertEquals(4, p0ce19.getDBRefs().length);
 -    assertEquals("PIR", p0ce19.getDBRefs()[3].getSource());
 -    assertEquals("S01875", p0ce19.getDBRefs()[3].getAccessionId());
 +    assertEquals(4, p0ce19.getDBRefs().size());
 +    assertEquals("PIR", p0ce19.getDBRefs().get(3).getSource());
 +    assertEquals("S01875", p0ce19.getDBRefs().get(3).getAccessionId());
    }
  
    @Test(groups = "Functional")
@@@ -1173,14 -1173,14 +1173,14 @@@ public class AlignmentTes
      /*
       * verify peptide.cdsdbref.peptidedbref is now mapped to peptide dataset
       */
 -    DBRefEntry[] dbRefs = pep.getDBRefs();
 -    assertEquals(2, dbRefs.length);
 -    assertSame(dna, dbRefs[0].map.to);
 -    assertSame(cds, dbRefs[1].map.to);
 -    assertEquals(1, dna.getDBRefs().length);
 -    assertSame(pep.getDatasetSequence(), dna.getDBRefs()[0].map.to);
 -    assertEquals(1, cds.getDBRefs().length);
 -    assertSame(pep.getDatasetSequence(), cds.getDBRefs()[0].map.to);
 +    List<DBRefEntry> dbRefs = pep.getDBRefs();
 +    assertEquals(2, dbRefs.size());
 +    assertSame(dna, dbRefs.get(0).map.to);
 +    assertSame(cds, dbRefs.get(1).map.to);
 +    assertEquals(1, dna.getDBRefs().size());
 +    assertSame(pep.getDatasetSequence(), dna.getDBRefs().get(0).map.to);
 +    assertEquals(1, cds.getDBRefs().size());
 +    assertSame(pep.getDatasetSequence(), cds.getDBRefs().get(0).map.to);
    }
  
    @Test(groups = { "Functional" })
      assertEquals(".JKLMNO", seq2.getSequenceAsString());
      assertEquals(".PQR...", seq3.getSequenceAsString());
    }
+   /**
+    * Test for setHiddenColumns, to check it returns true if the hidden columns
+    * have changed, else false
+    */
+   @Test(groups = { "Functional" })
+   public void testSetHiddenColumns()
+   {
+     AlignmentI al = new Alignment(new SequenceI[] {});
+     assertFalse(al.getHiddenColumns().hasHiddenColumns());
+     HiddenColumns hc = new HiddenColumns();
+     assertFalse(al.setHiddenColumns(hc)); // no change
+     assertSame(hc, al.getHiddenColumns());
+     hc.hideColumns(2, 4);
+     assertTrue(al.getHiddenColumns().hasHiddenColumns());
+     /*
+      * set a different object but with the same columns hidden
+      */
+     HiddenColumns hc2 = new HiddenColumns();
+     hc2.hideColumns(2, 4);
+     assertFalse(al.setHiddenColumns(hc2)); // no change
+     assertSame(hc2, al.getHiddenColumns());
+     assertTrue(al.setHiddenColumns(null));
+     assertNull(al.getHiddenColumns());
+     assertTrue(al.setHiddenColumns(hc));
+     assertSame(hc, al.getHiddenColumns());
+     al.getHiddenColumns().hideColumns(10, 12);
+     hc2.hideColumns(10, 12);
+     assertFalse(al.setHiddenColumns(hc2)); // no change
+     /*
+      * hide columns 15-16 then 17-18 in hc
+      * hide columns 15-18 in hc2
+      * these are not now 'equal' objects even though they
+      * represent the same set of columns
+      */
+     assertSame(hc2, al.getHiddenColumns());
+     hc.hideColumns(15, 16);
+     hc.hideColumns(17, 18);
+     hc2.hideColumns(15, 18);
+     assertFalse(hc.equals(hc2));
+     assertTrue(al.setHiddenColumns(hc)); // 'changed'
+   }
+   @Test(groups = { "Functional" })
+   public void testGetWidth()
+   {
+     SequenceI seq1 = new Sequence("seq1", "ABCDEF--");
+     SequenceI seq2 = new Sequence("seq2", "-JKLMNO--");
+     SequenceI seq3 = new Sequence("seq2", "-PQR");
+     AlignmentI a = new Alignment(new SequenceI[] { seq1, seq2, seq3 });
+     assertEquals(9, a.getWidth());
+     // width includes hidden columns
+     a.getHiddenColumns().hideColumns(2, 5);
+     assertEquals(9, a.getWidth());
+   }
+   @Test(groups = { "Functional" })
+   public void testGetVisibleWidth()
+   {
+     SequenceI seq1 = new Sequence("seq1", "ABCDEF--");
+     SequenceI seq2 = new Sequence("seq2", "-JKLMNO--");
+     SequenceI seq3 = new Sequence("seq2", "-PQR");
+     AlignmentI a = new Alignment(new SequenceI[] { seq1, seq2, seq3 });
+     assertEquals(9, a.getVisibleWidth());
+     // width excludes hidden columns
+     a.getHiddenColumns().hideColumns(2, 5);
+     assertEquals(5, a.getVisibleWidth());
+   }
  }
@@@ -34,7 -34,6 +34,7 @@@ import jalview.commands.EditCommand.Act
  import jalview.datamodel.PDBEntry.Type;
  import jalview.gui.JvOptionPane;
  import jalview.util.MapList;
 +import jalview.ws.params.InvalidArgumentException;
  
  import java.io.File;
  import java.util.ArrayList;
@@@ -53,7 -52,6 +53,6 @@@ import junit.extensions.PA
  
  public class SequenceTest
  {
    @BeforeClass(alwaysRun = true)
    public void setUpJvOptionPane()
    {
      assertNotNull(newDs);
      assertNotSame(ds, newDs);
      assertNotNull(sq.getDBRefs());
 -    assertEquals(1, sq.getDBRefs().length);
 -    assertNotSame(dbr1, sq.getDBRefs()[0]);
 -    assertEquals(dbr1, sq.getDBRefs()[0]);
 +    assertEquals(1, sq.getDBRefs().size());
 +    assertNotSame(dbr1, sq.getDBRefs().get(0));
 +    assertEquals(dbr1, sq.getDBRefs().get(0));
  
      /*
       * internal delete with sequence features
      assertEquals(4, sq.getEnd());
      assertSame(ds, PA.getValue(sq, "datasetSequence"));
      assertNotNull(sq.getDBRefs());
 -    assertEquals(1, sq.getDBRefs().length);
 -    assertSame(dbr1, sq.getDBRefs()[0]);
 +    assertEquals(1, sq.getDBRefs().size());
 +    assertSame(dbr1, sq.getDBRefs().get(0));
    }
  
    @Test(groups = { "Functional" })
              new AlignmentAnnotation("Test annot", "Test annot description",
                      annots));
      Assert.assertEquals(sq.getDescription(), "Test sequence description..");
 -    Assert.assertEquals(sq.getDBRefs().length, 5); // DBRefs are on dataset
 +    Assert.assertEquals(sq.getDBRefs().size(), 5); // DBRefs are on dataset
                                                     // sequence
      Assert.assertEquals(sq.getAllPDBEntries().size(), 4);
      Assert.assertNotNull(sq.getAnnotation());
      Assert.assertEquals(sq.getAnnotation()[0].annotations.length, 2);
 -    Assert.assertEquals(sq.getDatasetSequence().getDBRefs().length, 5); // same
 +    Assert.assertEquals(sq.getDatasetSequence().getDBRefs().size(), 5); // same
                                                                          // as
                                                                          // sq.getDBRefs()
      Assert.assertEquals(sq.getDatasetSequence().getAllPDBEntries().size(),
  
      Assert.assertEquals(derived.getDescription(),
              "Test sequence description..");
 -    Assert.assertEquals(derived.getDBRefs().length, 5); // come from dataset
 +    Assert.assertEquals(derived.getDBRefs().size(), 5); // come from dataset
      Assert.assertEquals(derived.getAllPDBEntries().size(), 4);
      Assert.assertNotNull(derived.getAnnotation());
      Assert.assertEquals(derived.getAnnotation()[0].annotations.length, 2);
 -    Assert.assertEquals(derived.getDatasetSequence().getDBRefs().length, 5);
 +    Assert.assertEquals(derived.getDatasetSequence().getDBRefs().size(), 5);
      Assert.assertEquals(derived.getDatasetSequence().getAllPDBEntries()
              .size(), 4);
      Assert.assertNotNull(derived.getDatasetSequence().getAnnotation());
      // but that doesn't distinguish it from an aligned sequence
      // which has not yet generated a dataset sequence
      // NB getDBRef looks inside dataset sequence if not null
 -    DBRefEntry[] dbrefs = copy.getDBRefs();
 -    assertEquals(1, dbrefs.length);
 -    assertFalse(dbrefs[0] == seq1.getDBRefs()[0]);
 -    assertTrue(dbrefs[0].equals(seq1.getDBRefs()[0]));
 +    List<DBRefEntry> dbrefs = copy.getDBRefs();
 +    assertEquals(1, dbrefs.size());
 +    assertFalse(dbrefs.get(0) == seq1.getDBRefs().get(0));
 +    assertTrue(dbrefs.get(0).equals(seq1.getDBRefs().get(0)));
    }
  
    @Test(groups = { "Functional" })
  
      // getDBRef looks inside dataset sequence and this is shared,
      // so holds the same dbref objects
 -    DBRefEntry[] dbrefs = copy.getDBRefs();
 -    assertEquals(1, dbrefs.length);
 -    assertSame(dbrefs[0], seq1.getDBRefs()[0]);
 +    List<DBRefEntry> dbrefs = copy.getDBRefs();
 +    assertEquals(1, dbrefs.size());
 +    assertSame(dbrefs.get(0), seq1.getDBRefs().get(0));
    }
  
    /**
      assertNull(sq.getDBRefs());
      DBRefEntry dbref = new DBRefEntry("Uniprot", "1", "P00340");
      sq.addDBRef(dbref);
 -    assertEquals(1, sq.getDBRefs().length);
 -    assertSame(dbref, sq.getDBRefs()[0]);
 +    assertEquals(1, sq.getDBRefs().size());
 +    assertSame(dbref, sq.getDBRefs().get(0));
  
      /*
       * change of version - new entry
       */
      DBRefEntry dbref2 = new DBRefEntry("Uniprot", "2", "P00340");
      sq.addDBRef(dbref2);
 -    assertEquals(2, sq.getDBRefs().length);
 -    assertSame(dbref, sq.getDBRefs()[0]);
 -    assertSame(dbref2, sq.getDBRefs()[1]);
 +    assertEquals(2, sq.getDBRefs().size());
 +    assertSame(dbref, sq.getDBRefs().get(0));
 +    assertSame(dbref2, sq.getDBRefs().get(1));
  
      /*
       * matches existing entry - not added
       */
      sq.addDBRef(new DBRefEntry("UNIPROT", "1", "p00340"));
 -    assertEquals(2, sq.getDBRefs().length);
 +    assertEquals(2, sq.getDBRefs().size());
  
      /*
       * different source = new entry
       */
      DBRefEntry dbref3 = new DBRefEntry("UniRef", "1", "p00340");
      sq.addDBRef(dbref3);
 -    assertEquals(3, sq.getDBRefs().length);
 -    assertSame(dbref3, sq.getDBRefs()[2]);
 +    assertEquals(3, sq.getDBRefs().size());
 +    assertSame(dbref3, sq.getDBRefs().get(2));
  
      /*
       * different ref = new entry
       */
      DBRefEntry dbref4 = new DBRefEntry("UniRef", "1", "p00341");
      sq.addDBRef(dbref4);
 -    assertEquals(4, sq.getDBRefs().length);
 -    assertSame(dbref4, sq.getDBRefs()[3]);
 +    assertEquals(4, sq.getDBRefs().size());
 +    assertSame(dbref4, sq.getDBRefs().get(3));
  
      /*
       * matching ref with a mapping - map updated
          1, 1 }, 3, 1));
      dbref5.setMap(map);
      sq.addDBRef(dbref5);
 -    assertEquals(4, sq.getDBRefs().length);
 -    assertSame(dbref4, sq.getDBRefs()[3]);
 +    assertEquals(4, sq.getDBRefs().size());
 +    assertSame(dbref4, sq.getDBRefs().get(3));
      assertSame(map, dbref4.getMap());
  
      /*
      DBRefEntry dbref6 = new DBRefEntry(dbref2.getSource(), "3",
              dbref2.getAccessionId());
      sq.addDBRef(dbref6);
 -    assertEquals(4, sq.getDBRefs().length);
 -    assertSame(dbref2, sq.getDBRefs()[1]);
 +    assertEquals(4, sq.getDBRefs().size());
 +    assertSame(dbref2, sq.getDBRefs().get(1));
      assertEquals("3", dbref2.getVersion());
  
      /*
      DBRefEntry dbref7 = new DBRefEntry(dbref3.getSource(), "3",
              dbref3.getAccessionId());
      sq.addDBRef(dbref7);
 -    assertEquals(4, sq.getDBRefs().length);
 -    assertSame(dbref3, sq.getDBRefs()[2]);
 +    assertEquals(4, sq.getDBRefs().size());
 +    assertSame(dbref3, sq.getDBRefs().get(2));
      assertEquals("3", dbref2.getVersion());
    }
  
      assertTrue(primaryDBRefs.isEmpty());
  
      // empty dbrefs
 -    sq.setDBRefs(new DBRefEntry[] {});
 +      sq.setDBRefs(null);
      primaryDBRefs = sq.getPrimaryDBRefs();
      assertTrue(primaryDBRefs.isEmpty());
  
@@@ -29,9 -29,8 +29,9 @@@ import jalview.datamodel.SequenceI
  import jalview.gui.AlignFrame;
  import jalview.gui.CrossRefAction;
  import jalview.gui.Desktop;
- import jalview.gui.Jalview2XML;
  import jalview.gui.JvOptionPane;
 +import jalview.gui.SequenceFetcher;
+ import jalview.project.Jalview2XML;
  import jalview.util.DBRefUtils;
  
  import java.io.File;
@@@ -89,10 -88,9 +89,10 @@@ public class CrossRef2xmlTests extends 
      List<String> failedXrefMenuItems = new ArrayList<>();
      List<String> failedProjectRecoveries = new ArrayList<>();
      // only search for ensembl or Uniprot crossrefs
 -    List<String> limit=Arrays.asList(new String[] {
 -        DBRefUtils.getCanonicalName("ENSEMBL"), 
 -        DBRefUtils.getCanonicalName("Uniprot")});
 +    List<String> limit = Arrays
 +            .asList(new String[]
 +            { DBRefUtils.getCanonicalName("ENSEMBL"),
 +                DBRefUtils.getCanonicalName("Uniprot") });
      // for every set of db queries
      // retrieve db query
      // verify presence of expected xrefs
      List<String> keyseq = new ArrayList<>();
      Map<String, File> savedProjects = new HashMap<>();
  
 -//    for (String[] did : new String[][] { { "UNIPROT", "P00338" } })
 -//    {
 -      // pass counters - 0 - first pass, 1 means retrieve project rather than
 -      // perform action
 -      int pass1 = 0, pass2 = 0, pass3 = 0;
 -      // each do loop performs two iterations in the first outer loop pass, but
 -      // only performs one iteration on the second outer loop
 -      // ie. pass 1 = 0 {pass 2= 0 { pass 3 = 0,1 }, pass 2=1 { pass 3 = 0 }}, 1
 -      // { pass 2 = 0 { pass 3 = 0 } }
 -      do
 +    // for (String[] did : new String[][] { { "UNIPROT", "P00338" } })
 +    // {
 +    // pass counters - 0 - first pass, 1 means retrieve project rather than
 +    // perform action
 +    int pass1 = 0, pass2 = 0, pass3 = 0;
 +    // each do loop performs two iterations in the first outer loop pass, but
 +    // only performs one iteration on the second outer loop
 +    // ie. pass 1 = 0 {pass 2= 0 { pass 3 = 0,1 }, pass 2=1 { pass 3 = 0 }}, 1
 +    // { pass 2 = 0 { pass 3 = 0 } }
 +    do
 +    {
 +      String first = forSource + " " + forAccession;// did[0] + " " + did[1];
 +      AlignFrame af = null;
 +      boolean dna;
 +      AlignmentI retral;
 +      AlignmentI dataset;
 +      SequenceI[] seqs;
 +      List<String> ptypes = null;
 +      if (pass1 == 0)
        {
 -        String first = forSource + " " + forAccession;//did[0] + " " + did[1];
 -        AlignFrame af = null;
 -        boolean dna;
 -        AlignmentI retral;
 -        AlignmentI dataset;
 -        SequenceI[] seqs;
 -        List<String> ptypes = null;
 -        if (pass1 == 0)
 -        {
 -          // retrieve dbref
 +        // retrieve dbref
  
 -          List<AlignFrame> afs = jalview.gui.SequenceFetcher.fetchAndShow(
 +        SequenceFetcher sf = new SequenceFetcher(Desktop.instance,
                  forSource, forAccession);
 -        // did[0], did[1]);
 -          if (afs.size() == 0)
 -          {
 -            failedDBRetr.add("Didn't retrieve " + first);
 -            break;
 -          }
 -          keyseq.add(first);
 -          af = afs.get(0);
 -
 -          // verify references for retrieved data
 -          AlignmentTest.assertAlignmentDatasetRefs(af.getViewport()
 -                  .getAlignment(), "Pass (" + pass1 + "," + pass2 + ","
 -                  + pass3 + "): Fetch " + first + ":");
 -          assertDatasetIsNormalisedKnownDefect(af.getViewport()
 -                  .getAlignment(), "Pass (" + pass1 + "," + pass2 + ","
 -                  + pass3 + "): Fetch " + first + ":");
 -          dna = af.getViewport().getAlignment().isNucleotide();
 -          retral = af.getViewport().getAlignment();
 -          dataset = retral.getDataset();
 -          seqs = retral.getSequencesArray();
 -
 -        }
 -        else
 +        sf.run();
 +        AlignFrame[] afs = Desktop.getAlignFrames();
 +        if (afs.length == 0)
          {
 -          Desktop.instance.closeAll_actionPerformed(null);
 -          // recover stored project
 -          af = new FileLoader(false).LoadFileWaitTillLoaded(savedProjects
 -                  .get(first).toString(), DataSourceType.FILE);
 -          System.out.println("Recovered view for '" + first + "' from '"
 -                  + savedProjects.get(first).toString() + "'");
 -          dna = af.getViewport().getAlignment().isNucleotide();
 -          retral = af.getViewport().getAlignment();
 -          dataset = retral.getDataset();
 -          seqs = retral.getSequencesArray();
 -
 -          // verify references for recovered data
 -          AlignmentTest.assertAlignmentDatasetRefs(af.getViewport()
 -                  .getAlignment(), "Pass (" + pass1 + "," + pass2 + ","
 -                  + pass3 + "): Recover " + first + ":");
 -          assertDatasetIsNormalisedKnownDefect(af.getViewport()
 -                  .getAlignment(), "Pass (" + pass1 + "," + pass2 + ","
 -                  + pass3 + "): Recover " + first + ":");
 -
 +          failedDBRetr.add("Didn't retrieve " + first);
 +          break;
          }
 +        keyseq.add(first);
 +        af = afs[0];
 +
 +        // verify references for retrieved data
 +        AlignmentTest.assertAlignmentDatasetRefs(
 +                af.getViewport().getAlignment(), "Pass (" + pass1 + ","
 +                        + pass2 + "," + pass3 + "): Fetch " + first + ":");
 +        assertDatasetIsNormalisedKnownDefect(
 +                af.getViewport().getAlignment(), "Pass (" + pass1 + ","
 +                        + pass2 + "," + pass3 + "): Fetch " + first + ":");
 +        dna = af.getViewport().getAlignment().isNucleotide();
 +        retral = af.getViewport().getAlignment();
 +        dataset = retral.getDataset();
 +        seqs = retral.getSequencesArray();
  
 -        // store project on first pass, compare next pass
 -        stringify(dbtoviewBit, savedProjects, first, af.alignPanel);
 +      }
 +      else
 +      {
 +        Desktop.instance.closeAll_actionPerformed(null);
 +        // recover stored project
 +        af = new FileLoader(false).LoadFileWaitTillLoaded(
 +                savedProjects.get(first).toString(), DataSourceType.FILE);
 +        System.out.println("Recovered view for '" + first + "' from '"
 +                + savedProjects.get(first).toString() + "'");
 +        dna = af.getViewport().getAlignment().isNucleotide();
 +        retral = af.getViewport().getAlignment();
 +        dataset = retral.getDataset();
 +        seqs = retral.getSequencesArray();
 +
 +        // verify references for recovered data
 +        AlignmentTest.assertAlignmentDatasetRefs(
 +                af.getViewport().getAlignment(),
 +                "Pass (" + pass1 + "," + pass2 + "," + pass3 + "): Recover "
 +                        + first + ":");
 +        assertDatasetIsNormalisedKnownDefect(
 +                af.getViewport().getAlignment(),
 +                "Pass (" + pass1 + "," + pass2 + "," + pass3 + "): Recover "
 +                        + first + ":");
  
 -        ptypes = (seqs == null || seqs.length == 0) ? null : new CrossRef(
 -                seqs, dataset).findXrefSourcesForSequences(dna);
 -        filterDbRefs(ptypes, limit);
 -        
 -        // start of pass2: retrieve each cross-ref for fetched or restored
 -        // project.
 -        do // first cross ref and recover crossref loop
 -        {
 +      }
 +
 +      // store project on first pass, compare next pass
 +      stringify(dbtoviewBit, savedProjects, first, af.alignPanel);
 +
 +      ptypes = (seqs == null || seqs.length == 0) ? null
 +              : new CrossRef(seqs, dataset)
 +                      .findXrefSourcesForSequences(dna);
 +      filterDbRefs(ptypes, limit);
  
 -          for (String db : ptypes)
 +      // start of pass2: retrieve each cross-ref for fetched or restored
 +      // project.
 +      do // first cross ref and recover crossref loop
 +      {
 +
 +        for (String db : ptypes)
 +        {
 +          // counter for splitframe views retrieved via crossref
 +          int firstcr_ap = 0;
 +          // build next key so we an retrieve all views
 +          String nextxref = first + " -> " + db + "{" + firstcr_ap + "}";
 +          // perform crossref action, or retrieve stored project
 +          List<AlignmentViewPanel> cra_views = new ArrayList<>();
 +          CrossRefAction cra = null;
 +
 +          if (pass2 == 0)
 +          { // retrieve and show cross-refs in this thread
 +            cra = CrossRefAction.getHandlerFor(seqs, dna, db, af);
 +            cra.run();
 +            cra_views = (List<AlignmentViewPanel>) PA.getValue(cra,
 +                    "xrefViews");
 +            if (cra_views.size() == 0)
 +            {
 +              failedXrefMenuItems.add(
 +                      "No crossrefs retrieved for " + first + " -> " + db);
 +              continue;
 +            }
 +            assertNucleotide(cra_views.get(0),
 +                    "Nucleotide panel included proteins for " + first
 +                            + " -> " + db);
 +            assertProtein(cra_views.get(1),
 +                    "Protein panel included nucleotides for " + first
 +                            + " -> " + db);
 +          }
 +          else
            {
 -            // counter for splitframe views retrieved via crossref
 -            int firstcr_ap = 0;
 -            // build next key so we an retrieve all views
 -            String nextxref = first + " -> " + db + "{" + firstcr_ap + "}";
 -            // perform crossref action, or retrieve stored project
 -            List<AlignmentViewPanel> cra_views = new ArrayList<>();
 -            CrossRefAction cra = null;
 -
 -            if (pass2 == 0)
 -            { // retrieve and show cross-refs in this thread
 -              cra = CrossRefAction.getHandlerFor(seqs, dna, db, af);
 -              cra.run();
 -              cra_views = (List<AlignmentViewPanel>) PA.getValue(cra,
 -                      "xrefViews");
 -              if (cra_views.size() == 0)
 -              {
 -                failedXrefMenuItems.add("No crossrefs retrieved for "
 -                        + first + " -> " + db);
 -                continue;
 -              }
 -              assertNucleotide(cra_views.get(0),
 -                      "Nucleotide panel included proteins for " + first
 -                              + " -> " + db);
 -              assertProtein(cra_views.get(1),
 -                      "Protein panel included nucleotides for " + first
 -                              + " -> " + db);
 +            Desktop.instance.closeAll_actionPerformed(null);
 +            pass3 = 0;
 +            // recover stored project
 +            File storedProject = savedProjects.get(nextxref);
 +            if (storedProject == null)
 +            {
 +              failedProjectRecoveries
 +                      .add("Failed to store a view for '" + nextxref + "'");
 +              continue;
              }
 -            else
 +
 +            // recover stored project
 +            AlignFrame af2 = new FileLoader(false).LoadFileWaitTillLoaded(
 +                    savedProjects.get(nextxref).toString(),
 +                    DataSourceType.FILE);
 +            System.out
 +                    .println("Recovered view for '" + nextxref + "' from '"
 +                            + savedProjects.get(nextxref).toString() + "'");
 +            // gymnastics to recover the alignPanel/Complementary alignPanel
 +            if (af2.getViewport().isNucleotide())
              {
 -              Desktop.instance.closeAll_actionPerformed(null);
 -              pass3 = 0;
 -              // recover stored project
 -              File storedProject = savedProjects.get(nextxref);
 -              if (storedProject == null)
 -              {
 -                failedProjectRecoveries.add("Failed to store a view for '"
 -                        + nextxref + "'");
 -                continue;
 -              }
 -
 -              // recover stored project
 -              AlignFrame af2 = new FileLoader(false)
 -                      .LoadFileWaitTillLoaded(savedProjects.get(nextxref)
 -                              .toString(), DataSourceType.FILE);
 -              System.out.println("Recovered view for '" + nextxref
 -                      + "' from '" + savedProjects.get(nextxref).toString()
 -                      + "'");
 -              // gymnastics to recover the alignPanel/Complementary alignPanel
 -              if (af2.getViewport().isNucleotide())
 -              {
 -                // top view, then bottom
 -                cra_views.add(af2.getViewport().getAlignPanel());
 -                cra_views.add(((jalview.gui.AlignViewport) af2
 -                        .getViewport().getCodingComplement())
 -                        .getAlignPanel());
 -
 -              }
 -              else
 -              {
 -                // bottom view, then top
 -                cra_views.add(((jalview.gui.AlignViewport) af2
 -                        .getViewport().getCodingComplement())
 -                        .getAlignPanel());
 -                cra_views.add(af2.getViewport().getAlignPanel());
 +              // top view, then bottom
 +              cra_views.add(af2.getViewport().getAlignPanel());
 +              cra_views.add(((jalview.gui.AlignViewport) af2.getViewport()
 +                      .getCodingComplement()).getAlignPanel());
  
 -              }
              }
 -            HashMap<String, List<String>> xrptypes = new HashMap<>();
 -            // first save/verify views.
 -            for (AlignmentViewPanel avp : cra_views)
 +            else
              {
 -              nextxref = first + " -> " + db + "{" + firstcr_ap++ + "}";
 -              // verify references for this panel
 -              AlignmentTest.assertAlignmentDatasetRefs(avp.getAlignment(),
 -                      "Pass (" + pass1 + "," + pass2 + "," + pass3
 -                              + "): before start of pass3: " + nextxref
 -                              + ":");
 -              assertDatasetIsNormalisedKnownDefect(avp.getAlignment(),
 -                      "Pass (" + pass1 + "," + pass2 + "," + pass3
 -                              + "): before start of pass3: " + nextxref
 -                              + ":");
 -
 -              SequenceI[] xrseqs = avp.getAlignment().getSequencesArray();
 -
 -              List<String> _xrptypes = (seqs == null || seqs.length == 0) ? null
 -                      : new CrossRef(xrseqs, dataset)
 -                              .findXrefSourcesForSequences(avp
 -                                      .getAlignViewport().isNucleotide());
 -
 -              stringify(dbtoviewBit, savedProjects, nextxref, avp);
 -              xrptypes.put(nextxref, _xrptypes);
 +              // bottom view, then top
 +              cra_views.add(((jalview.gui.AlignViewport) af2.getViewport()
 +                      .getCodingComplement()).getAlignPanel());
 +              cra_views.add(af2.getViewport().getAlignPanel());
  
              }
 +          }
 +          HashMap<String, List<String>> xrptypes = new HashMap<>();
 +          // first save/verify views.
 +          for (AlignmentViewPanel avp : cra_views)
 +          {
 +            nextxref = first + " -> " + db + "{" + firstcr_ap++ + "}";
 +            // verify references for this panel
 +            AlignmentTest.assertAlignmentDatasetRefs(avp.getAlignment(),
 +                    "Pass (" + pass1 + "," + pass2 + "," + pass3
 +                            + "): before start of pass3: " + nextxref
 +                            + ":");
 +            assertDatasetIsNormalisedKnownDefect(avp.getAlignment(),
 +                    "Pass (" + pass1 + "," + pass2 + "," + pass3
 +                            + "): before start of pass3: " + nextxref
 +                            + ":");
 +
 +            SequenceI[] xrseqs = avp.getAlignment().getSequencesArray();
 +
 +            List<String> _xrptypes = (seqs == null || seqs.length == 0)
 +                    ? null
 +                    : new CrossRef(xrseqs, dataset)
 +                            .findXrefSourcesForSequences(
 +                                    avp.getAlignViewport().isNucleotide());
 +
 +            stringify(dbtoviewBit, savedProjects, nextxref, avp);
 +            xrptypes.put(nextxref, _xrptypes);
  
 -            // now do the second xref pass starting from either saved or just
 -            // recovered split pane, in sequence
 -            do // retrieve second set of cross refs or recover and verify
 +          }
 +
 +          // now do the second xref pass starting from either saved or just
 +          // recovered split pane, in sequence
 +          do // retrieve second set of cross refs or recover and verify
 +          {
 +            firstcr_ap = 0;
 +            for (AlignmentViewPanel avp : cra_views)
              {
 -              firstcr_ap = 0;
 -              for (AlignmentViewPanel avp : cra_views)
 +              nextxref = first + " -> " + db + "{" + firstcr_ap++ + "}";
 +              for (String xrefdb : xrptypes.get(nextxref))
                {
 -                nextxref = first + " -> " + db + "{" + firstcr_ap++ + "}";
 -                for (String xrefdb : xrptypes.get(nextxref))
 -                {
 -                  List<AlignmentViewPanel> cra_views2 = new ArrayList<>();
 -                  int q = 0;
 -                  String nextnextxref = nextxref + " -> " + xrefdb + "{"
 -                          + q + "}";
 +                List<AlignmentViewPanel> cra_views2 = new ArrayList<>();
 +                int q = 0;
 +                String nextnextxref = nextxref + " -> " + xrefdb + "{" + q
 +                        + "}";
  
 -                  if (pass3 == 0)
 +                if (pass3 == 0)
 +                {
 +                  SequenceI[] xrseqs = avp.getAlignment()
 +                          .getSequencesArray();
 +                  AlignFrame nextaf = Desktop
 +                          .getAlignFrameFor(avp.getAlignViewport());
 +
 +                  cra = CrossRefAction.getHandlerFor(xrseqs,
 +                          avp.getAlignViewport().isNucleotide(), xrefdb,
 +                          nextaf);
 +                  cra.run();
 +                  cra_views2 = (List<AlignmentViewPanel>) PA.getValue(cra,
 +                          "xrefViews");
 +                  if (cra_views2.size() == 0)
                    {
 -                    SequenceI[] xrseqs = avp.getAlignment()
 -                            .getSequencesArray();
 -                    AlignFrame nextaf = Desktop.getAlignFrameFor(avp
 -                            .getAlignViewport());
 -
 -                    cra = CrossRefAction.getHandlerFor(xrseqs, avp
 -                            .getAlignViewport().isNucleotide(), xrefdb,
 -                            nextaf);
 -                    cra.run();
 -                    cra_views2 = (List<AlignmentViewPanel>) PA.getValue(
 -                            cra, "xrefViews");
 -                    if (cra_views2.size() == 0)
 -                    {
 -                      failedXrefMenuItems
 -                              .add("No crossrefs retrieved for '"
 -                                      + nextxref + "' to " + xrefdb
 -                                      + " via '" + nextaf.getTitle() + "'");
 -                      continue;
 -                    }
 -                    assertNucleotide(cra_views2.get(0),
 -                            "Nucleotide panel included proteins for '"
 -                                    + nextxref + "' to " + xrefdb
 -                                    + " via '" + nextaf.getTitle() + "'");
 -                    assertProtein(cra_views2.get(1),
 -                            "Protein panel included nucleotides for '"
 -                                    + nextxref + "' to " + xrefdb
 -                                    + " via '" + nextaf.getTitle() + "'");
 -
 +                    failedXrefMenuItems.add("No crossrefs retrieved for '"
 +                            + nextxref + "' to " + xrefdb + " via '"
 +                            + nextaf.getTitle() + "'");
 +                    continue;
                    }
 -                  else
 +                  assertNucleotide(cra_views2.get(0),
 +                          "Nucleotide panel included proteins for '"
 +                                  + nextxref + "' to " + xrefdb + " via '"
 +                                  + nextaf.getTitle() + "'");
 +                  assertProtein(cra_views2.get(1),
 +                          "Protein panel included nucleotides for '"
 +                                  + nextxref + "' to " + xrefdb + " via '"
 +                                  + nextaf.getTitle() + "'");
 +
 +                }
 +                else
 +                {
 +                  Desktop.instance.closeAll_actionPerformed(null);
 +                  // recover stored project
 +                  File storedProject = savedProjects.get(nextnextxref);
 +                  if (storedProject == null)
                    {
 -                    Desktop.instance.closeAll_actionPerformed(null);
 -                    // recover stored project
 -                    File storedProject = savedProjects.get(nextnextxref);
 -                    if (storedProject == null)
 -                    {
 -                      failedProjectRecoveries
 -                              .add("Failed to store a view for '"
 -                                      + nextnextxref + "'");
 -                      continue;
 -                    }
 -                    AlignFrame af2 = new FileLoader(false)
 -                            .LoadFileWaitTillLoaded(
 -                                    savedProjects.get(nextnextxref)
 -                                            .toString(),
 -                                    DataSourceType.FILE);
 -                    System.out.println("Recovered view for '"
 -                            + nextnextxref + "' from '"
 -                            + savedProjects.get(nextnextxref).toString()
 -                            + "'");
 -                    // gymnastics to recover the alignPanel/Complementary
 -                    // alignPanel
 -                    if (af2.getViewport().isNucleotide())
 -                    {
 -                      // top view, then bottom
 -                      cra_views2.add(af2.getViewport().getAlignPanel());
 -                      cra_views2.add(((jalview.gui.AlignViewport) af2
 -                              .getViewport().getCodingComplement())
 -                              .getAlignPanel());
 -
 -                    }
 -                    else
 -                    {
 -                      // bottom view, then top
 -                      cra_views2.add(((jalview.gui.AlignViewport) af2
 -                              .getViewport().getCodingComplement())
 -                              .getAlignPanel());
 -                      cra_views2.add(af2.getViewport().getAlignPanel());
 -                    }
 -                    Assert.assertEquals(cra_views2.size(), 2);
 -                    Assert.assertNotNull(cra_views2.get(0));
 -                    Assert.assertNotNull(cra_views2.get(1));
 +                    failedProjectRecoveries
 +                            .add("Failed to store a view for '"
 +                                    + nextnextxref + "'");
 +                    continue;
                    }
 +                  AlignFrame af2 = new FileLoader(false)
 +                          .LoadFileWaitTillLoaded(savedProjects
 +                                  .get(nextnextxref).toString(),
 +                                  DataSourceType.FILE);
 +                  System.out
 +                          .println("Recovered view for '" + nextnextxref
 +                                  + "' from '" + savedProjects
 +                                          .get(nextnextxref).toString()
 +                                  + "'");
 +                  // gymnastics to recover the alignPanel/Complementary
 +                  // alignPanel
 +                  if (af2.getViewport().isNucleotide())
 +                  {
 +                    // top view, then bottom
 +                    cra_views2.add(af2.getViewport().getAlignPanel());
 +                    cra_views2.add(((jalview.gui.AlignViewport) af2
 +                            .getViewport().getCodingComplement())
 +                                    .getAlignPanel());
  
 -                  for (AlignmentViewPanel nextavp : cra_views2)
 +                  }
 +                  else
                    {
 -                    nextnextxref = nextxref + " -> " + xrefdb + "{" + q++
 -                            + "}";
 -
 -                    // verify references for this panel
 -                    AlignmentTest.assertAlignmentDatasetRefs(
 -                            nextavp.getAlignment(), "" + "Pass (" + pass1
 -                                    + "," + pass2 + "): For "
 -                                    + nextnextxref + ":");
 -                    assertDatasetIsNormalisedKnownDefect(
 -                            nextavp.getAlignment(), "" + "Pass (" + pass1
 -                                    + "," + pass2 + "): For "
 -                                    + nextnextxref + ":");
 -
 -                    stringify(dbtoviewBit, savedProjects, nextnextxref,
 -                            nextavp);
 -                    keyseq.add(nextnextxref);
 +                    // bottom view, then top
 +                    cra_views2.add(((jalview.gui.AlignViewport) af2
 +                            .getViewport().getCodingComplement())
 +                                    .getAlignPanel());
 +                    cra_views2.add(af2.getViewport().getAlignPanel());
                    }
 -                } // end of loop around showing all xrefdb for crossrf2
 -
 -              } // end of loop around all viewpanels from crossrf1
 -            } while (pass2 == 2 && pass3++ < 2);
 -            // fetchdb->crossref1->crossref-2->verify for xrefs we
 -            // either loop twice when pass2=0, or just once when pass2=1
 -            // (recovered project from previous crossref)
 -
 -          } // end of loop over db-xrefs for crossref-2
 -
 -          // fetchdb-->crossref1
 -          // for each xref we try to retrieve xref, store and verify when
 -          // pass1=0, or just retrieve and verify when pass1=1
 -        } while (pass1 == 1 && pass2++ < 2);
 -        // fetchdb
 -        // for each ref we
 -        // loop twice: first, do the retrieve, second recover from saved project
 -
 -        // increment pass counters, so we repeat traversal starting from the
 -        // oldest saved project first.
 -        if (pass1 == 0)
 -        {
 -          // verify stored projects for first set of cross references
 -          pass1 = 1;
 -          // and verify cross-references retrieved from stored projects
 -          pass2 = 0;
 -          pass3 = 0;
 -        }
 -        else
 -        {
 -          pass1++;
 -        }
 -      } while (pass1 < 3);
 +                  Assert.assertEquals(cra_views2.size(), 2);
 +                  Assert.assertNotNull(cra_views2.get(0));
 +                  Assert.assertNotNull(cra_views2.get(1));
 +                }
 +
 +                for (AlignmentViewPanel nextavp : cra_views2)
 +                {
 +                  nextnextxref = nextxref + " -> " + xrefdb + "{" + q++
 +                          + "}";
 +
 +                  // verify references for this panel
 +                  AlignmentTest.assertAlignmentDatasetRefs(
 +                          nextavp.getAlignment(),
 +                          "" + "Pass (" + pass1 + "," + pass2 + "): For "
 +                                  + nextnextxref + ":");
 +                  assertDatasetIsNormalisedKnownDefect(
 +                          nextavp.getAlignment(),
 +                          "" + "Pass (" + pass1 + "," + pass2 + "): For "
 +                                  + nextnextxref + ":");
 +
 +                  stringify(dbtoviewBit, savedProjects, nextnextxref,
 +                          nextavp);
 +                  keyseq.add(nextnextxref);
 +                }
 +              } // end of loop around showing all xrefdb for crossrf2
 +
 +            } // end of loop around all viewpanels from crossrf1
 +          } while (pass2 == 2 && pass3++ < 2);
 +          // fetchdb->crossref1->crossref-2->verify for xrefs we
 +          // either loop twice when pass2=0, or just once when pass2=1
 +          // (recovered project from previous crossref)
 +
 +        } // end of loop over db-xrefs for crossref-2
 +
 +        // fetchdb-->crossref1
 +        // for each xref we try to retrieve xref, store and verify when
 +        // pass1=0, or just retrieve and verify when pass1=1
 +      } while (pass1 == 1 && pass2++ < 2);
 +      // fetchdb
 +      // for each ref we
 +      // loop twice: first, do the retrieve, second recover from saved project
 +
 +      // increment pass counters, so we repeat traversal starting from the
 +      // oldest saved project first.
 +      if (pass1 == 0)
 +      {
 +        // verify stored projects for first set of cross references
 +        pass1 = 1;
 +        // and verify cross-references retrieved from stored projects
 +        pass2 = 0;
 +        pass3 = 0;
 +      }
 +      else
 +      {
 +        pass1++;
 +      }
 +    } while (pass1 < 3);
  
      if (failedXrefMenuItems.size() > 0)
      {
        {
          System.err.println(s);
        }
 -      Assert.fail("Didn't recover projects for some retrievals (did they retrieve ?) ("
 -              + failedProjectRecoveries.size() + " counts)");
 +      Assert.fail(
 +              "Didn't recover projects for some retrievals (did they retrieve ?) ("
 +                      + failedProjectRecoveries.size() + " counts)");
      }
      if (failedDBRetr.size() > 0)
      {
        }
        else
        {
 -        System.out
 -                .println("Ignored exception for known defect: JAL-2179 : "
 -                        + message);
 +        System.out.println("Ignored exception for known defect: JAL-2179 : "
 +                + message);
        }
  
      }
            AlignmentViewPanel alignmentViewPanel, String message)
    {
      List<SequenceI> nonType = new ArrayList<>();
 -    for (SequenceI sq : alignmentViewPanel.getAlignViewport()
 -            .getAlignment().getSequences())
 +    for (SequenceI sq : alignmentViewPanel.getAlignViewport().getAlignment()
 +            .getSequences())
      {
        if (sq.isProtein() != expectProtein)
        {
      }
      else
      {
 -      Assert.assertEquals(sbr.toString(), dbt, "stringify mismatch for "
 -              + xrefpath);
 +      Assert.assertEquals(sbr.toString(), dbt,
 +              "stringify mismatch for " + xrefpath);
      }
    }
  }
@@@ -66,7 -66,7 +66,7 @@@ public class VCFLoaderTes
        // insertion G/GA is transferred to nucleotide but not to peptide
        "17\t45051613\t.\tG\tGA,C\t1666.64\tRF\tAC=15;AF=3.0e-03,2.0e-03" };
  
-   @BeforeClass
+   @BeforeClass(alwaysRun = true)
    public void setUp()
    {
      /*
      assertEquals(sf.getFeatureGroup(), "VCF");
      assertEquals(sf.getBegin(), 2);
      assertEquals(sf.getEnd(), 2);
-     assertEquals(sf.getScore(), 4.0e-03, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 4.0e-03,
+             DELTA);
      assertEquals(sf.getValue(Gff3Helper.ALLELES), "A,C");
      assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
      sf = geneFeatures.get(1);
      assertEquals(sf.getBegin(), 2);
      assertEquals(sf.getEnd(), 2);
      assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
-     assertEquals(sf.getScore(), 5.0e-03, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 5.0e-03,
+             DELTA);
      assertEquals(sf.getValue(Gff3Helper.ALLELES), "A,T");
  
      sf = geneFeatures.get(2);
      assertEquals(sf.getBegin(), 4);
      assertEquals(sf.getEnd(), 4);
      assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
-     assertEquals(sf.getScore(), 2.0e-03, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 2.0e-03,
+             DELTA);
      assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,C");
  
      sf = geneFeatures.get(3);
      assertEquals(sf.getBegin(), 4);
      assertEquals(sf.getEnd(), 4);
      assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
-     assertEquals(sf.getScore(), 3.0e-03, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 3.0e-03,
+             DELTA);
      assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GA");
  
      /*
      assertEquals(sf.getBegin(), 2);
      assertEquals(sf.getEnd(), 2);
      assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
-     assertEquals(sf.getScore(), 2.0e-03, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 2.0e-03,
+             DELTA);
      assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,C");
      sf = transcriptFeatures.get(1);
      assertEquals(sf.getFeatureGroup(), "VCF");
      assertEquals(sf.getBegin(), 2);
      assertEquals(sf.getEnd(), 2);
      assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
-     assertEquals(sf.getScore(), 3.0e-03, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 3.0e-03,
+             DELTA);
      assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GA");
  
      /*
       * verify SNP variant feature(s) computed and added to protein
       * first codon AGC varies to ACC giving S/T
       */
 -    DBRefEntry[] dbRefs = al.getSequenceAt(1).getDBRefs();
 +    List<DBRefEntry> dbRefs = al.getSequenceAt(1).getDBRefs();
      SequenceI peptide = null;
      for (DBRefEntry dbref : dbRefs)
      {
      assertEquals(sf.getBegin(), 24);
      assertEquals(sf.getEnd(), 24);
      assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
-     assertEquals(sf.getScore(), 5.0e-03, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 5.0e-03,
+             DELTA);
      assertEquals(sf.getValue(Gff3Helper.ALLELES), "T,A");
  
      /*
      assertEquals(sf.getBegin(), 24);
      assertEquals(sf.getEnd(), 24);
      assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
-     assertEquals(sf.getScore(), 4.0e-03, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 4.0e-03,
+             DELTA);
      assertEquals(sf.getValue(Gff3Helper.ALLELES), "T,G");
  
      /*
      assertEquals(sf.getBegin(), 22);
      assertEquals(sf.getEnd(), 22);
      assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
-     assertEquals(sf.getScore(), 2.0e-03, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 2.0e-03,
+             DELTA);
      assertEquals(sf.getValue(Gff3Helper.ALLELES), "C,G");
  
      /*
      assertEquals(sf.getBegin(), 21);
      assertEquals(sf.getEnd(), 21);
      assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
-     assertEquals(sf.getScore(), 3.0e-03, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 3.0e-03,
+             DELTA);
      assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GT");
  
      /*
      assertEquals(sf.getBegin(), 16);
      assertEquals(sf.getEnd(), 16);
      assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
-     assertEquals(sf.getScore(), 3.0e-03, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 3.0e-03,
+             DELTA);
      assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GT");
  
      /*
      assertEquals(sf.getBegin(), 17);
      assertEquals(sf.getEnd(), 17);
      assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
-     assertEquals(sf.getScore(), 2.0e-03, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 2.0e-03,
+             DELTA);
      assertEquals(sf.getValue(Gff3Helper.ALLELES), "C,G");
  
      /*
       * verify variant feature(s) computed and added to protein
       * last codon GCT varies to GGT giving A/G in the last peptide position
       */
 -    DBRefEntry[] dbRefs = al.getSequenceAt(3).getDBRefs();
 +    List<DBRefEntry> dbRefs = al.getSequenceAt(3).getDBRefs();
      SequenceI peptide = null;
      for (DBRefEntry dbref : dbRefs)
      {
      SequenceFeature sf = geneFeatures.get(0);
      assertEquals(sf.getBegin(), 1);
      assertEquals(sf.getEnd(), 1);
-     assertEquals(sf.getScore(), 0.1f, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 0.1f, DELTA);
      assertEquals(sf.getValue("alleles"), "C,A");
      // gene features include Consequence for all transcripts
      Map map = (Map) sf.getValue("CSQ");
      sf = geneFeatures.get(1);
      assertEquals(sf.getBegin(), 5);
      assertEquals(sf.getEnd(), 5);
-     assertEquals(sf.getScore(), 0.2f, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 0.2f, DELTA);
      assertEquals(sf.getValue("alleles"), "C,T");
      map = (Map) sf.getValue("CSQ");
      assertEquals(map.size(), 9);
      sf = geneFeatures.get(2);
      assertEquals(sf.getBegin(), 9);
      assertEquals(sf.getEnd(), 11); // deletion over 3 positions
-     assertEquals(sf.getScore(), 0.3f, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 0.3f, DELTA);
      assertEquals(sf.getValue("alleles"), "CGG,C");
      map = (Map) sf.getValue("CSQ");
      assertEquals(map.size(), 9);
      sf = geneFeatures.get(3);
      assertEquals(sf.getBegin(), 13);
      assertEquals(sf.getEnd(), 13);
-     assertEquals(sf.getScore(), 0.5f, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 0.5f, DELTA);
      assertEquals(sf.getValue("alleles"), "C,T");
      map = (Map) sf.getValue("CSQ");
      assertEquals(map.size(), 9);
      sf = geneFeatures.get(4);
      assertEquals(sf.getBegin(), 13);
      assertEquals(sf.getEnd(), 13);
-     assertEquals(sf.getScore(), 0.4f, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 0.4f, DELTA);
      assertEquals(sf.getValue("alleles"), "C,G");
      map = (Map) sf.getValue("CSQ");
      assertEquals(map.size(), 9);
      sf = geneFeatures.get(5);
      assertEquals(sf.getBegin(), 17);
      assertEquals(sf.getEnd(), 17);
-     assertEquals(sf.getScore(), 0.7f, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 0.7f, DELTA);
      assertEquals(sf.getValue("alleles"), "A,G");
      map = (Map) sf.getValue("CSQ");
      assertEquals(map.size(), 9);
      sf = geneFeatures.get(6);
      assertEquals(sf.getBegin(), 17);
      assertEquals(sf.getEnd(), 17); // insertion
-     assertEquals(sf.getScore(), 0.6f, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 0.6f, DELTA);
      assertEquals(sf.getValue("alleles"), "A,AC");
      map = (Map) sf.getValue("CSQ");
      assertEquals(map.size(), 9);
      sf = transcriptFeatures.get(0);
      assertEquals(sf.getBegin(), 3);
      assertEquals(sf.getEnd(), 3);
-     assertEquals(sf.getScore(), 0.2f, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 0.2f, DELTA);
      assertEquals(sf.getValue("alleles"), "C,T");
      // transcript features only have Consequence for that transcripts
      map = (Map) sf.getValue("CSQ");
      sf = transcriptFeatures.get(1);
      assertEquals(sf.getBegin(), 11);
      assertEquals(sf.getEnd(), 11);
-     assertEquals(sf.getScore(), 0.7f, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 0.7f, DELTA);
      assertEquals(sf.getValue("alleles"), "A,G");
      assertEquals(map.size(), 9);
      assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript3");
      sf = transcriptFeatures.get(2);
      assertEquals(sf.getBegin(), 11);
      assertEquals(sf.getEnd(), 11);
-     assertEquals(sf.getScore(), 0.6f, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 0.6f, DELTA);
      assertEquals(sf.getValue("alleles"), "A,AC");
      assertEquals(map.size(), 9);
      assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript3");
       * and GAG/GGG which is E/G in position 4
       * the insertion variant is not transferred to the peptide
       */
 -    DBRefEntry[] dbRefs = al.findName("transcript3").getDBRefs();
 +    List<DBRefEntry> dbRefs = al.findName("transcript3").getDBRefs();
      SequenceI peptide = null;
      for (DBRefEntry dbref : dbRefs)
      {
      sf = transcriptFeatures.get(0);
      assertEquals(sf.getBegin(), 7);
      assertEquals(sf.getEnd(), 7);
-     assertEquals(sf.getScore(), 0.5f, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 0.5f, DELTA);
      assertEquals(sf.getValue("alleles"), "C,T");
      assertEquals(map.size(), 9);
      assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
      sf = transcriptFeatures.get(1);
      assertEquals(sf.getBegin(), 7);
      assertEquals(sf.getEnd(), 7);
-     assertEquals(sf.getScore(), 0.4f, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 0.4f, DELTA);
      assertEquals(sf.getValue("alleles"), "C,G");
      assertEquals(map.size(), 9);
      assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
      sf = transcriptFeatures.get(2);
      assertEquals(sf.getBegin(), 11);
      assertEquals(sf.getEnd(), 11);
-     assertEquals(sf.getScore(), 0.7f, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 0.7f, DELTA);
      assertEquals(sf.getValue("alleles"), "A,G");
      assertEquals(map.size(), 9);
      assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
      sf = transcriptFeatures.get(3);
      assertEquals(sf.getBegin(), 11);
      assertEquals(sf.getEnd(), 11);
-     assertEquals(sf.getScore(), 0.6f, DELTA);
+     assertEquals(sf.getScore(), 0f);
+     assertEquals(Float.parseFloat((String) sf.getValue("AF")), 0.6f, DELTA);
      assertEquals(sf.getValue("alleles"), "A,AC");
      assertEquals(map.size(), 9);
      assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
@@@ -27,6 -27,7 +27,7 @@@ import static org.testng.Assert.assertN
  import static org.testng.Assert.assertSame;
  import static org.testng.Assert.assertTrue;
  
+ import jalview.analysis.scoremodels.SimilarityParams;
  import jalview.api.AlignViewportI;
  import jalview.api.AlignmentViewPanel;
  import jalview.api.FeatureColourI;
@@@ -49,6 -50,7 +50,7 @@@ import jalview.gui.AlignmentPanel
  import jalview.gui.Desktop;
  import jalview.gui.FeatureRenderer;
  import jalview.gui.JvOptionPane;
+ import jalview.gui.PCAPanel;
  import jalview.gui.PopupMenu;
  import jalview.gui.SliderPanel;
  import jalview.io.DataSourceType;
@@@ -77,6 -79,8 +79,8 @@@ import java.util.HashMap
  import java.util.List;
  import java.util.Map;
  
+ import javax.swing.JInternalFrame;
  import org.testng.Assert;
  import org.testng.AssertJUnit;
  import org.testng.annotations.BeforeClass;
@@@ -105,67 -109,73 +109,75 @@@ public class Jalview2xmlTests extends J
      assertNotNull(af, "Didn't read input file " + inFile);
      int olddsann = countDsAnn(af.getViewport());
      assertTrue(olddsann > 0, "Didn't find any dataset annotations");
-     af.changeColour_actionPerformed(JalviewColourScheme.RNAHelices
-             .toString());
+     af.changeColour_actionPerformed(
+             JalviewColourScheme.RNAHelices.toString());
      assertTrue(
-             af.getViewport().getGlobalColourScheme() instanceof RNAHelicesColour,
+             af.getViewport()
+                     .getGlobalColourScheme() instanceof RNAHelicesColour,
              "Couldn't apply RNA helices colourscheme");
 -    assertTrue(af.saveAlignment(tfile, FileFormat.Jalview),
 +    af.saveAlignment(tfile, FileFormat.Jalview);
 +    assertTrue(af.isSaveAlignmentSuccessful(),
              "Failed to store as a project.");
      af.closeMenuItem_actionPerformed(true);
      af = null;
-     af = new FileLoader()
-             .LoadFileWaitTillLoaded(tfile, DataSourceType.FILE);
+     af = new FileLoader().LoadFileWaitTillLoaded(tfile,
+             DataSourceType.FILE);
      assertNotNull(af, "Failed to import new project");
      int newdsann = countDsAnn(af.getViewport());
      assertEquals(olddsann, newdsann,
              "Differing numbers of dataset sequence annotation\nOriginally "
                      + olddsann + " and now " + newdsann);
-     System.out
-             .println("Read in same number of annotations as originally present ("
+     System.out.println(
+             "Read in same number of annotations as originally present ("
                      + olddsann + ")");
      assertTrue(
  
-     af.getViewport().getGlobalColourScheme() instanceof RNAHelicesColour,
+             af.getViewport()
+                     .getGlobalColourScheme() instanceof RNAHelicesColour,
              "RNA helices colourscheme was not applied on import.");
    }
  
    @Test(groups = { "Functional" })
    public void testTCoffeeScores() throws Exception
    {
-     String inFile = "examples/uniref50.fa", inAnnot = "examples/uniref50.score_ascii";
+     String inFile = "examples/uniref50.fa",
+             inAnnot = "examples/uniref50.score_ascii";
      String tfile = File.createTempFile("JalviewTest", ".jvp")
              .getAbsolutePath();
      AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(inFile,
              DataSourceType.FILE);
      assertNotNull(af, "Didn't read input file " + inFile);
      af.loadJalviewDataFile(inAnnot, DataSourceType.FILE, null, null);
-     assertSame(af.getViewport().getGlobalColourScheme().getClass(),
+     AlignViewport viewport = af.getViewport();
+     assertSame(viewport.getGlobalColourScheme().getClass(),
              TCoffeeColourScheme.class, "Didn't set T-coffee colourscheme");
-     assertNotNull(ColourSchemeProperty.getColourScheme(af.getViewport()
-             .getAlignment(), af.getViewport().getGlobalColourScheme()
-             .getSchemeName()), "Recognise T-Coffee score from string");
+     assertNotNull(
+             ColourSchemeProperty.getColourScheme(viewport,
+                     viewport.getAlignment(),
+                     viewport.getGlobalColourScheme()
+                             .getSchemeName()),
+             "Recognise T-Coffee score from string");
  
 -    assertTrue(af.saveAlignment(tfile, FileFormat.Jalview),
 +    af.saveAlignment(tfile, FileFormat.Jalview);
 +    assertTrue(af.isSaveAlignmentSuccessful(),
              "Failed to store as a project.");
      af.closeMenuItem_actionPerformed(true);
      af = null;
-     af = new FileLoader()
-             .LoadFileWaitTillLoaded(tfile, DataSourceType.FILE);
+     af = new FileLoader().LoadFileWaitTillLoaded(tfile,
+             DataSourceType.FILE);
      assertNotNull(af, "Failed to import new project");
      assertSame(af.getViewport().getGlobalColourScheme().getClass(),
              TCoffeeColourScheme.class,
              "Didn't set T-coffee colourscheme for imported project.");
-     System.out
-             .println("T-Coffee score shading successfully recovered from project.");
+     System.out.println(
+             "T-Coffee score shading successfully recovered from project.");
    }
  
    @Test(groups = { "Functional" })
    public void testColourByAnnotScores() throws Exception
    {
-     String inFile = "examples/uniref50.fa", inAnnot = "examples/testdata/uniref50_iupred.jva";
+     String inFile = "examples/uniref50.fa",
+             inAnnot = "examples/testdata/uniref50_iupred.jva";
      String tfile = File.createTempFile("JalviewTest", ".jvp")
              .getAbsolutePath();
      AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(inFile,
              .getSequenceAt(0).getAnnotation("IUPredWS (Short)");
      assertTrue(
  
-     aa != null && aa.length > 0,
+             aa != null && aa.length > 0,
              "Didn't find any IUPred annotation to use to shade alignment.");
      AnnotationColourGradient cs = new AnnotationColourGradient(aa[0], null,
              AnnotationColourGradient.ABOVE_THRESHOLD);
-     AnnotationColourGradient gcs = new AnnotationColourGradient(aa[0],
-             null, AnnotationColourGradient.BELOW_THRESHOLD);
+     AnnotationColourGradient gcs = new AnnotationColourGradient(aa[0], null,
+             AnnotationColourGradient.BELOW_THRESHOLD);
      cs.setSeqAssociated(true);
      gcs.setSeqAssociated(true);
      af.changeColour(cs);
      sg.addSequence(af.getViewport().getAlignment().getSequenceAt(1), false);
      sg.addSequence(af.getViewport().getAlignment().getSequenceAt(2), true);
      af.alignPanel.alignmentChanged();
 -    assertTrue(af.saveAlignment(tfile, FileFormat.Jalview),
 +    af.saveAlignment(tfile, FileFormat.Jalview);
 +    assertTrue(af.isSaveAlignmentSuccessful(),
              "Failed to store as a project.");
      af.closeMenuItem_actionPerformed(true);
      af = null;
-     af = new FileLoader()
-             .LoadFileWaitTillLoaded(tfile, DataSourceType.FILE);
+     af = new FileLoader().LoadFileWaitTillLoaded(tfile,
+             DataSourceType.FILE);
      assertNotNull(af, "Failed to import new project");
  
      // check for group and alignment colourschemes
  
      ColourSchemeI _rcs = af.getViewport().getGlobalColourScheme();
-     ColourSchemeI _rgcs = af.getViewport().getAlignment().getGroups()
-             .get(0).getColourScheme();
+     ColourSchemeI _rgcs = af.getViewport().getAlignment().getGroups().get(0)
+             .getColourScheme();
      assertNotNull(_rcs, "Didn't recover global colourscheme");
      assertTrue(_rcs instanceof AnnotationColourGradient,
              "Didn't recover annotation colour global scheme");
  
      boolean diffseqcols = false, diffgseqcols = false;
      SequenceI[] sqs = af.getViewport().getAlignment().getSequencesArray();
-     for (int p = 0, pSize = af.getViewport().getAlignment().getWidth(); p < pSize
-             && (!diffseqcols || !diffgseqcols); p++)
+     for (int p = 0, pSize = af.getViewport().getAlignment()
+             .getWidth(); p < pSize && (!diffseqcols || !diffgseqcols); p++)
      {
        if (_rcs.findColour(sqs[0].getCharAt(p), p, sqs[0], null, 0f) != _rcs
                .findColour(sqs[5].getCharAt(p), p, sqs[5], null, 0f))
        }
      }
      assertTrue(diffseqcols, "Got Different sequence colours");
-     System.out
-             .println("Per sequence colourscheme (Background) successfully applied and recovered.");
+     System.out.println(
+             "Per sequence colourscheme (Background) successfully applied and recovered.");
  
      assertNotNull(_rgcs, "Didn't recover group colourscheme");
      assertTrue(_rgcs instanceof AnnotationColourGradient,
      assertTrue(__rcs.isSeqAssociated(),
              "Group Annotation colourscheme wasn't sequence associated");
  
-     for (int p = 0, pSize = af.getViewport().getAlignment().getWidth(); p < pSize
-             && (!diffseqcols || !diffgseqcols); p++)
+     for (int p = 0, pSize = af.getViewport().getAlignment()
+             .getWidth(); p < pSize && (!diffseqcols || !diffgseqcols); p++)
      {
-       if (_rgcs.findColour(sqs[1].getCharAt(p), p, sqs[1], null, 0f) != _rgcs
-               .findColour(sqs[2].getCharAt(p), p, sqs[2], null, 0f))
+       if (_rgcs.findColour(sqs[1].getCharAt(p), p, sqs[1], null,
+               0f) != _rgcs.findColour(sqs[2].getCharAt(p), p, sqs[2], null,
+                       0f))
        {
          diffgseqcols = true;
        }
      }
      assertTrue(diffgseqcols, "Got Different group sequence colours");
-     System.out
-             .println("Per sequence (Group) colourscheme successfully applied and recovered.");
+     System.out.println(
+             "Per sequence (Group) colourscheme successfully applied and recovered.");
    }
  
    @Test(groups = { "Functional" })
    public void gatherViewsHere() throws Exception
    {
-     int origCount = Desktop.getAlignFrames() == null ? 0 : Desktop
-             .getAlignFrames().length;
+     int origCount = Desktop.getAlignFrames() == null ? 0
+             : Desktop.getAlignFrames().length;
      AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
              "examples/exampleFile_2_7.jar", DataSourceType.FILE);
      assertNotNull(af, "Didn't read in the example file correctly.");
            sq.findPosition(p);
            try
            {
-             assertTrue(
-                     (alaa.annotations[p] == null && refan.annotations[p] == null)
-                             || alaa.annotations[p].value == refan.annotations[p].value,
+             assertTrue((alaa.annotations[p] == null
+                     && refan.annotations[p] == null)
+                     || alaa.annotations[p].value == refan.annotations[p].value,
                      "Mismatch at alignment position " + p);
            } catch (NullPointerException q)
            {
-             Assert.fail("Mismatch of alignment annotations at position "
-                     + p + " Ref seq ann: " + refan.annotations[p]
+             Assert.fail("Mismatch of alignment annotations at position " + p
+                     + " Ref seq ann: " + refan.annotations[p]
                      + " alignment " + alaa.annotations[p]);
            }
          }
      AssertJUnit.assertFalse(structureStyle.sameStyle(groupStyle));
  
      groups.getAlignViewport().setViewStyle(structureStyle);
-     AssertJUnit.assertFalse(groupStyle.sameStyle(groups.getAlignViewport()
-             .getViewStyle()));
-     Assert.assertTrue(structureStyle.sameStyle(groups.getAlignViewport()
-             .getViewStyle()));
+     AssertJUnit.assertFalse(
+             groupStyle.sameStyle(groups.getAlignViewport().getViewStyle()));
+     Assert.assertTrue(structureStyle
+             .sameStyle(groups.getAlignViewport().getViewStyle()));
  
    }
  
  
      // check FileLoader returned a reference to the one alignFrame that is
      // actually on the Desktop
-     assertSame(
-             af,
-             Desktop.getAlignFrameFor(af.getViewport()),
+     assertSame(af, Desktop.getAlignFrameFor(af.getViewport()),
              "Jalview2XML.loadAlignFrame() didn't return correct AlignFrame reference for multiple view window");
  
      Desktop.explodeViews(af);
      af = new FileLoader().LoadFileWaitTillLoaded(tfile.getAbsolutePath(),
              DataSourceType.FILE);
      Assert.assertNotNull(af);
+     Assert.assertEquals(Desktop.getAlignFrames().length,
+             Desktop.getAlignmentPanels(
+                     af.getViewport().getSequenceSetId()).length);
      Assert.assertEquals(
-             Desktop.getAlignFrames().length,
-             Desktop.getAlignmentPanels(af.getViewport().getSequenceSetId()).length);
-     Assert.assertEquals(
-             Desktop.getAlignmentPanels(af.getViewport().getSequenceSetId()).length,
+             Desktop.getAlignmentPanels(
+                     af.getViewport().getSequenceSetId()).length,
              oldviews);
    }
  
      assertTrue(Jalview2XML.isVersionStringLaterThan(null, "Test"));
      assertTrue(Jalview2XML.isVersionStringLaterThan(null, "TEST"));
      assertTrue(Jalview2XML.isVersionStringLaterThan("2.8.3", "Test"));
-     assertTrue(Jalview2XML
-             .isVersionStringLaterThan(null, "Automated Build"));
+     assertTrue(
+             Jalview2XML.isVersionStringLaterThan(null, "Automated Build"));
      assertTrue(Jalview2XML.isVersionStringLaterThan("2.8.3",
              "Automated Build"));
      assertTrue(Jalview2XML.isVersionStringLaterThan("2.8.3",
  
        n++;
      }
-     File tfile = File
-             .createTempFile("testStoreAndRecoverGroupReps", ".jvp");
+     File tfile = File.createTempFile("testStoreAndRecoverGroupReps",
+             ".jvp");
      try
      {
        new Jalview2XML(false).saveState(tfile);
         */
        List<String> hidden = hiddenSeqNames.get(ap.getViewName());
        HiddenSequences hs = alignment.getHiddenSequences();
-       assertEquals(
-               hidden.size(),
-               hs.getSize(),
+       assertEquals(hidden.size(), hs.getSize(),
                "wrong number of restored hidden sequences in "
                        + ap.getViewName());
      }
      pdbEntries[1] = new PDBEntry("3W5V", "B", Type.PDB, testFile);
      pdbEntries[2] = new PDBEntry("3W5V", "C", Type.PDB, testFile);
      pdbEntries[3] = new PDBEntry("3W5V", "D", Type.PDB, testFile);
-     Assert.assertEquals(seqs[0].getDatasetSequence().getAllPDBEntries()
-             .get(0), pdbEntries[0]);
-     Assert.assertEquals(seqs[1].getDatasetSequence().getAllPDBEntries()
-             .get(0), pdbEntries[1]);
-     Assert.assertEquals(seqs[2].getDatasetSequence().getAllPDBEntries()
-             .get(0), pdbEntries[2]);
-     Assert.assertEquals(seqs[3].getDatasetSequence().getAllPDBEntries()
-             .get(0), pdbEntries[3]);
+     Assert.assertEquals(
+             seqs[0].getDatasetSequence().getAllPDBEntries().get(0),
+             pdbEntries[0]);
+     Assert.assertEquals(
+             seqs[1].getDatasetSequence().getAllPDBEntries().get(0),
+             pdbEntries[1]);
+     Assert.assertEquals(
+             seqs[2].getDatasetSequence().getAllPDBEntries().get(0),
+             pdbEntries[2]);
+     Assert.assertEquals(
+             seqs[3].getDatasetSequence().getAllPDBEntries().get(0),
+             pdbEntries[3]);
  
      File tfile = File.createTempFile("testStoreAndRecoverPDBEntry", ".jvp");
      try
      /*
       * Colour alignment by Buried Index, Above 10% PID, By Conservation 20%
       */
+     av.setColourAppliesToAllGroups(false);
      af.changeColour_actionPerformed(JalviewColourScheme.Buried.toString());
      assertTrue(av.getGlobalColourScheme() instanceof BuriedColourScheme);
      af.abovePIDThreshold_actionPerformed(true);
      sg.setEndRes(25);
      av.setSelectionGroup(sg);
      PopupMenu popupMenu = new PopupMenu(af.alignPanel, null, null);
-     popupMenu.changeColour_actionPerformed(JalviewColourScheme.Strand
-             .toString());
+     popupMenu.changeColour_actionPerformed(
+             JalviewColourScheme.Strand.toString());
      assertTrue(sg.getColourScheme() instanceof StrandColourScheme);
      assertEquals(al.getGroups().size(), 1);
      assertSame(al.getGroups().get(0), sg);
      fr.setColour("type2", byLabel);
  
      // type3: by score above threshold
-     FeatureColourI byScore = new FeatureColour(Color.BLACK, Color.BLUE, 1,
-             10);
+     FeatureColourI byScore = new FeatureColour(null, Color.BLACK,
+             Color.BLUE, null, 1, 10);
      byScore.setAboveThreshold(true);
      byScore.setThreshold(2f);
      fr.setColour("type3", byScore);
      fr.setColour("type4", byAF);
  
      // type5: by attribute CSQ:PolyPhen below threshold
-     FeatureColourI byPolyPhen = new FeatureColour(Color.BLACK, Color.BLUE,
-             1, 10);
+     FeatureColourI byPolyPhen = new FeatureColour(null, Color.BLACK,
+             Color.BLUE, null, 1, 10);
      byPolyPhen.setBelowThreshold(true);
      byPolyPhen.setThreshold(3f);
      byPolyPhen.setAttributeName("CSQ", "PolyPhen");
      File tfile = File.createTempFile("JalviewTest", ".jvp");
      tfile.deleteOnExit();
      String filePath = tfile.getAbsolutePath();
 -    assertTrue(af.saveAlignment(filePath, FileFormat.Jalview),
 +    af.saveAlignment(filePath, FileFormat.Jalview);
 +    assertTrue(af.isSaveAlignmentSuccessful(),
              "Failed to store as a project.");
  
      /*
       */
      af.closeMenuItem_actionPerformed(true);
      af = null;
-     af = new FileLoader()
-             .LoadFileWaitTillLoaded(filePath, DataSourceType.FILE);
+     af = new FileLoader().LoadFileWaitTillLoaded(filePath,
+             DataSourceType.FILE);
      assertNotNull(af, "Failed to import new project");
  
      /*
      addFeature(seq, featureType, score++);
      addFeature(seq, featureType, score);
    }
+   /**
+    * pre 2.11 - jalview 2.10 erroneously created new dataset entries for each
+    * view (JAL-3171) this test ensures we can import and merge those views
+    */
+   @Test(groups = { "Functional" })
+   public void testMergeDatasetsforViews() throws IOException
+   {
+     // simple project - two views on one alignment
+     AlignFrame af = new FileLoader(false).LoadFileWaitTillLoaded(
+             "examples/testdata/projects/twoViews.jvp", DataSourceType.FILE);
+     assertNotNull(af);
+     assertTrue(af.getAlignPanels().size() > 1);
+     verifyDs(af);
+   }
+   /**
+    * pre 2.11 - jalview 2.10 erroneously created new dataset entries for each
+    * view (JAL-3171) this test ensures we can import and merge those views This
+    * is a more complex project
+    */
+   @Test(groups = { "Functional" })
+   public void testMergeDatasetsforManyViews() throws IOException
+   {
+     Desktop.instance.closeAll_actionPerformed(null);
+     // complex project - one dataset, several views on several alignments
+     AlignFrame af = new FileLoader(false).LoadFileWaitTillLoaded(
+             "examples/testdata/projects/manyViews.jvp",
+             DataSourceType.FILE);
+     assertNotNull(af);
+     AlignmentI ds = null;
+     for (AlignFrame alignFrame : Desktop.getAlignFrames())
+     {
+       if (ds == null)
+       {
+         ds = verifyDs(alignFrame);
+       }
+       else
+       {
+         // check that this frame's dataset matches the last
+         assertTrue(ds == verifyDs(alignFrame));
+       }
+     }
+   }
+   private AlignmentI verifyDs(AlignFrame af)
+   {
+     AlignmentI ds = null;
+     for (AlignmentViewPanel ap : af.getAlignPanels())
+     {
+       if (ds == null)
+       {
+         ds = ap.getAlignment().getDataset();
+       }
+       else
+       {
+         assertTrue(ap.getAlignment().getDataset() == ds,
+                 "Dataset was not the same for imported 2.10.5 project with several alignment views");
+       }
+     }
+     return ds;
+   }
+   @Test(groups = "Functional")
+   public void testPcaViewAssociation() throws IOException
+   {
+     Desktop.instance.closeAll_actionPerformed(null);
+     final String PCAVIEWNAME = "With PCA";
+     // create a new tempfile
+     File tempfile = File.createTempFile("jvPCAviewAssoc", "jvp");
+     {
+       String exampleFile = "examples/uniref50.fa";
+       AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(exampleFile,
+               DataSourceType.FILE);
+       assertNotNull(af, "Didn't read in the example file correctly.");
+       AlignmentPanel origView = (AlignmentPanel) af.getAlignPanels().get(0);
+       AlignmentPanel newview = af.newView(PCAVIEWNAME, true);
+       // create another for good measure
+       af.newView("Not the PCA View", true);
+       PCAPanel pcaPanel = new PCAPanel(origView, "BLOSUM62",
+               new SimilarityParams(true, true, true, false));
+       // we're in the test exec thread, so we can just run synchronously here
+       pcaPanel.run();
+       // now switch the linked view
+       pcaPanel.selectAssociatedView(newview);
+       assertTrue(pcaPanel.getAlignViewport() == newview.getAlignViewport(),
+               "PCA should be associated with 'With PCA' view: test is broken");
+       // now save and reload project
+       Jalview2XML jv2xml = new jalview.project.Jalview2XML(false);
+       tempfile.delete();
+       jv2xml.saveState(tempfile);
+       assertTrue(jv2xml.errorMessage == null,
+               "Failed to save dummy project with PCA: test broken");
+     }
+     // load again.
+     Desktop.instance.closeAll_actionPerformed(null);
+     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+             tempfile.getCanonicalPath(), DataSourceType.FILE);
+     JInternalFrame[] frames = Desktop.instance.getAllFrames();
+     // PCA and the tabbed alignment view should be the only two windows on the
+     // desktop
+     assertEquals(frames.length, 2,
+             "PCA and the tabbed alignment view should be the only two windows on the desktop");
+     PCAPanel pcaPanel = (PCAPanel) frames[frames[0] == af ? 1 : 0];
+     AlignmentViewPanel restoredNewView = null;
+     for (AlignmentViewPanel alignpanel : Desktop.getAlignmentPanels(null))
+     {
+       if (alignpanel.getAlignViewport() == pcaPanel.getAlignViewport())
+       {
+         restoredNewView = alignpanel;
+       }
+     }
+     assertEquals(restoredNewView.getViewName(), PCAVIEWNAME);
+     assertTrue(
+             restoredNewView.getAlignViewport() == pcaPanel
+                     .getAlignViewport(),
+             "Didn't restore correct view association for the PCA view");
+   }
  }
@@@ -21,7 -21,6 +21,6 @@@
  package jalview.util;
  
  import static org.testng.AssertJUnit.assertEquals;
- import static org.testng.AssertJUnit.assertFalse;
  import static org.testng.AssertJUnit.assertNull;
  import static org.testng.AssertJUnit.assertSame;
  import static org.testng.AssertJUnit.assertTrue;
@@@ -34,7 -33,6 +33,7 @@@ import jalview.datamodel.Sequence
  import jalview.datamodel.SequenceI;
  import jalview.gui.JvOptionPane;
  
 +import java.util.Arrays;
  import java.util.List;
  
  import org.testng.annotations.BeforeClass;
@@@ -64,25 -62,25 +63,25 @@@ public class DBRefUtilsTes
      DBRefEntry ref2 = new DBRefEntry("UNIPROT", "1.2", "A12346");
      // Source is converted to upper-case by this constructor!
      DBRefEntry ref3 = new DBRefEntry("Uniprot", "1.2", "A12347");
 -    DBRefEntry[] dbrefs = new DBRefEntry[] { ref1, ref2, ref3 };
 +    List<DBRefEntry> dbrefs = Arrays.asList(new DBRefEntry[] { ref1, ref2, ref3 });
      String[] sources = new String[] { "EMBL", "UNIPROT" };
  
 -    DBRefEntry[] selected = DBRefUtils.selectRefs(dbrefs, sources);
 -    assertEquals(3, selected.length);
 -    assertSame(ref1, selected[0]);
 -    assertSame(ref2, selected[1]);
 -    assertSame(ref3, selected[2]);
 +    List<DBRefEntry> selected = DBRefUtils.selectRefs(dbrefs, sources);
 +    assertEquals(3, selected.size());
 +    assertSame(ref1, selected.get(0));
 +    assertSame(ref2, selected.get(1));
 +    assertSame(ref3, selected.get(2));
  
      sources = new String[] { "EMBL" };
      selected = DBRefUtils.selectRefs(dbrefs, sources);
 -    assertEquals(1, selected.length);
 -    assertSame(ref1, selected[0]);
 +    assertEquals(1, selected.size());
 +    assertSame(ref1, selected.get(0));
  
      sources = new String[] { "UNIPROT" };
      selected = DBRefUtils.selectRefs(dbrefs, sources);
 -    assertEquals(2, selected.length);
 -    assertSame(ref2, selected[0]);
 -    assertSame(ref3, selected[1]);
 +    assertEquals(2, selected.size());
 +    assertSame(ref2, selected.get(0));
 +    assertSame(ref3, selected.get(1));
  
      sources = new String[] { "EMBLCDS" };
      selected = DBRefUtils.selectRefs(dbrefs, sources);
  
      sources = new String[] { "embl", "uniprot" };
      selected = DBRefUtils.selectRefs(dbrefs, sources);
 -    assertEquals(3, selected.length);
 -    assertSame(ref1, selected[0]);
 -    assertSame(ref2, selected[1]);
 -    assertSame(ref3, selected[2]);
 +    assertEquals(3, selected.size());
 +    assertSame(ref1, selected.get(0));
 +    assertSame(ref2, selected.get(1));
 +    assertSame(ref3, selected.get(2));
    }
  
    /**
      assertEquals("pfam", DBRefUtils.getCanonicalName("pfam"));
  
    }
-   @Test(groups = { "Functional" })
-   public void testIsDasCoordinateSystem()
-   {
-     assertFalse(DBRefUtils.isDasCoordinateSystem(null, null));
-     assertFalse(DBRefUtils.isDasCoordinateSystem("pdbresnum", null));
-     assertFalse(DBRefUtils.isDasCoordinateSystem(null, new DBRefEntry(
-             "PDB", "v1", "a1")));
-     assertTrue(DBRefUtils.isDasCoordinateSystem("pdbresnum",
-             new DBRefEntry("PDB", "v1", "a1")));
-     assertTrue(DBRefUtils.isDasCoordinateSystem("PDBRESNUM",
-             new DBRefEntry("PDB", "v1", "a1")));
-     // "pdb" is converted to upper-case in DBRefEntry constructor
-     assertTrue(DBRefUtils.isDasCoordinateSystem("pdbresnum",
-             new DBRefEntry("pdb", "v1", "a1")));
-     assertFalse(DBRefUtils.isDasCoordinateSystem("pdb", new DBRefEntry(
-             "pdb", "v1", "a1")));
-     assertTrue(DBRefUtils.isDasCoordinateSystem("UNIPROT", new DBRefEntry(
-             "Uniprot", "v1", "a1")));
-     assertTrue(DBRefUtils.isDasCoordinateSystem("Uniprot", new DBRefEntry(
-             "UNIPROT", "v1", "a1")));
-     assertFalse(DBRefUtils.isDasCoordinateSystem("UNIPROTKB",
-             new DBRefEntry("pdb", "v1", "a1")));
-     assertTrue(DBRefUtils.isDasCoordinateSystem("EMBL", new DBRefEntry(
-             "EMBL", "v1", "a1")));
-     assertTrue(DBRefUtils.isDasCoordinateSystem("embl", new DBRefEntry(
-             "embl", "v1", "a1")));
-   }
    /**
     * Test 'parsing' a DBRef - non PDB case
     */
    {
      SequenceI seq = new Sequence("Seq1", "ABCD");
      DBRefEntry ref = DBRefUtils.parseToDbRef(seq, "EMBL", "1.2", "a7890");
 -    DBRefEntry[] refs = seq.getDBRefs();
 -    assertEquals(1, refs.length);
 -    assertSame(ref, refs[0]);
 +    List<DBRefEntry> refs = seq.getDBRefs();
 +    assertEquals(1, refs.size());
 +    assertSame(ref, refs.get(0));
      assertEquals("EMBL", ref.getSource());
      assertEquals("1.2", ref.getVersion());
      assertEquals("a7890", ref.getAccessionId());
      // TODO: correct PDBEntry and PDB DBRef accessions need to be generated for
      // PDB ref in Stockholm
  
 -    DBRefEntry[] refs = seq.getDBRefs();
 -    assertEquals(1, refs.length);
 -    assertSame(ref, refs[0]);
 +    List<DBRefEntry> refs = seq.getDBRefs();
 +    assertEquals(1, refs.size());
 +    assertSame(ref, refs.get(0));
      assertEquals("PDB", ref.getSource());
      assertEquals("1.2", ref.getVersion());
      // DBRef id is pdbId + chain code
      ref5.setMap(new Mapping(new MapList(new int[] { 1, 1 }, new int[] { 1,
          1 }, 1, 1)));
  
 -    List<DBRefEntry> matches = DBRefUtils.searchRefs(new DBRefEntry[] {
 -        ref1, ref2, ref3, ref4, ref5 }, target);
 +    List<DBRefEntry> matches = DBRefUtils.searchRefs(
 +              Arrays.asList(new DBRefEntry[] {
 +        ref1, ref2, ref3, ref4, ref5 }), target, DBRefUtils.SEARCH_MODE_FULL);
      assertEquals(3, matches.size());
      assertSame(ref1, matches.get(0));
      assertSame(ref2, matches.get(1));
              new int[] { 1, 1 }, 2, 2));
      ref3.setMap(map3);
  
 -    List<DBRefEntry> matches = DBRefUtils.searchRefs(new DBRefEntry[] {
 -        ref1, ref2, ref3 }, target);
 +    List<DBRefEntry> matches = DBRefUtils.searchRefs(
 +              Arrays.asList(new DBRefEntry[] {
 +        ref1, ref2, ref3 }), target, DBRefUtils.SEARCH_MODE_FULL);
      assertEquals(2, matches.size());
      assertSame(ref1, matches.get(0));
      assertSame(ref2, matches.get(1));
      ref5.setMap(new Mapping(new MapList(new int[] { 1, 1 }, new int[] { 1,
          1 }, 1, 1)));
  
 -    DBRefEntry[] dbrefs = new DBRefEntry[] { ref1, ref2, ref3, ref4, ref5 };
 +    List<DBRefEntry> dbrefs = Arrays.asList(new DBRefEntry[] {
 +               ref1, ref2, ref3, ref4, ref5 });
      List<DBRefEntry> matches = DBRefUtils.searchRefs(dbrefs, "A1234");
      assertEquals(3, matches.size());
      assertSame(ref1, matches.get(0));
      ref5.setMap(new Mapping(new MapList(new int[] { 1, 1 }, new int[] { 1,
          1 }, 1, 1)));
  
 -    List<DBRefEntry> matches = DBRefUtils.searchRefs(new DBRefEntry[] {
 -        ref1, ref2, ref3, ref4, ref5 }, target);
 +    List<DBRefEntry> matches = DBRefUtils.searchRefs(
 +              Arrays.asList(new DBRefEntry[] {
 +        ref1, ref2, ref3, ref4, ref5 }), target, DBRefUtils.SEARCH_MODE_FULL);
      assertEquals(4, matches.size());
      assertSame(ref1, matches.get(0));
      assertSame(ref2, matches.get(1));
@@@ -215,8 -215,10 +215,9 @@@ public class UniprotTes
              is).get(0);
      SequenceI seq = new Uniprot().uniprotEntryToSequence(entry);
      assertNotNull(seq);
 -    assertEquals(6, seq.getDBRefs().length); // 2*Uniprot, PDB, PDBsum, 2*EMBL
 +    assertEquals(6, seq.getDBRefs().size()); // 2*Uniprot, PDB, PDBsum, 2*EMBL
+     assertEquals(seq.getSequenceAsString(),
+             seq.createDatasetSequence().getSequenceAsString());
 -
    }
  
    /**