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

@@@ -4,15 -3,12 +4,15 @@@ buildPropertiesFil
  buildTests
  buildextclients
  buildindices
- castorbinding
  clean
 +clean-site
 +compile-site
  compileApplet
  distclean
 +eclipse-install
  help
  init
+ jaxb-bindings
  linkcheck
  makeApplet
  makedist
diff --cc .classpath
@@@ -70,7 -67,6 +67,8 @@@
        <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 --cc .gitignore
Simple merge
diff --cc build.xml
Simple merge
@@@ -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
@@@ -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
Simple merge
@@@ -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);
  }
Simple merge
@@@ -616,9 -591,27 +607,27 @@@ public class Cach
      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)
    {
Simple merge
@@@ -1475,14 -1476,20 +1476,20 @@@ public class EditCommand implements Com
  
      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)
      {
Simple merge
Simple merge
Simple merge
@@@ -533,9 -548,10 +549,9 @@@ public abstract class GFTSPanel extend
        }
      });
  
-     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)
  
      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);
@@@ -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;
@@@ -83,6 -85,6 +85,7 @@@ 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;
@@@ -137,11 -136,10 +140,12 @@@ 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;
@@@ -1237,78 -1155,91 +1246,84 @@@ public class AlignFrame extends GAlignF
      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
      {
Simple merge
@@@ -334,11 -333,11 +334,11 @@@ public class AlignmentPanel extends GAl
     */
    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);
    }
  
    /**
          }
          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();
Simple merge
Simple merge
@@@ -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;
@@@ -1645,24 -1600,49 +1647,49 @@@ public class Desktop extends jalview.jb
    }
  
    /**
 -   * 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
                      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
@@@ -283,23 -249,19 +251,18 @@@ public class FeatureSettings extends JP
          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,
@@@ -941,14 -900,9 +915,9 @@@ public class FeatureTypeSettings extend
      {
        // 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)
      {
@@@ -208,14 -211,10 +209,14 @@@ public class Finder extends GFinde
    @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";
  
      /*
      }
      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();
    }
  
    /**
Simple merge
@@@ -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;
  
  /**
Simple merge
@@@ -29,9 -30,8 +30,10 @@@ 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,7 -49,7 +51,6 @@@ 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;
@@@ -210,20 -144,17 +145,20 @@@ public class PCAPanel extends GPCAPane
    }
  
    @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);
    }
  
    /**
      }
    }
  
 -  /**
 -   * 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
@@@ -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!
@@@ -819,10 -800,28 +826,31 @@@ public class Preferences extends GPrefe
      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
@@@ -276,12 -278,9 +276,11 @@@ public class ScalePanel extends JPane
      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()
@@@ -52,8 -53,13 +52,13 @@@ import javax.swing.JPanel
   * 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;
  
        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);
    }
  
@@@ -882,11 -996,37 +1005,39 @@@ public class SeqPanel extends JPane
      }
    }
  
-   
+   /**
+    * 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 mouseExited(MouseEvent e)
    {
-     if (mouseDragging)
+     ap.alignFrame.setStatus(" ");
+     if (av.getWrapAlignment())
+     {
+       return;
+     }
+     if (mouseDragging && scrollThread == null)
      {
 -      scrollThread = new ScrollThread();
 +      startScrolling(e.getPoint());
      }
    }
  
       * (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;
      }
  
Simple merge
@@@ -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;
Simple merge
Simple merge
Simple merge
@@@ -31,15 -30,13 +31,17 @@@ 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;
  
@@@ -49,6 -48,9 +53,7 @@@ 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
     * 
    }
  
    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
      {
     * @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;
  
Simple merge
Simple merge
Simple merge
@@@ -140,10 -132,11 +131,10 @@@ public class GFinder extends JPane
                @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)
      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();
  
@@@ -337,43 -287,23 +288,19 @@@ public class GPCAPanel extends JInterna
      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)
    {
    }
  
@@@ -23,10 -24,14 +24,15 @@@ 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;
@@@ -29,9 -38,9 +38,10 @@@ import jalview.datamodel.AlignedCodonFr
  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;
@@@ -38,625 -38,668 +39,656 @@@ 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());
 +                                      }
 +                              }
 +                      }
 +              }
 +      }
  
  }
@@@ -2957,33 -2957,67 +2959,97 @@@ public abstract class AlignmentViewpor
      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);
+     }
+   }
  }
@@@ -1151,30 -1156,32 +1157,58 @@@ public abstract class FeatureRendererMo
      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;
+   }
 -
  }
Simple merge
Simple merge
@@@ -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;
@@@ -105,13 -109,13 +109,14 @@@ 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;
              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;
Simple merge
@@@ -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());
 -
    }
  
    /**