JAL-1953 2.11.2 with Archeopteryx!
authorJim Procter <j.procter@dundee.ac.uk>
Fri, 3 Jun 2022 10:37:14 +0000 (11:37 +0100)
committerJim Procter <j.procter@dundee.ac.uk>
Fri, 3 Jun 2022 11:24:33 +0000 (12:24 +0100)
Minimal patches and Merge branch 'kjvdh/features/PhylogenyViewer' into merge/2_11_2/PhylogenyViewer
also restored last kjvdh build of forester to j8 and j11lib directories
 Conflicts:
.classpath
j8lib/forester.jar
resources/lang/Messages.properties
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
src/jalview/fts/core/GFTSPanel.java
src/jalview/fts/service/pdb/PDBFTSPanel.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/DasSourceBrowser.java
src/jalview/gui/Desktop.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/SplashScreen.java
src/jalview/gui/StructureViewerBase.java
src/jalview/io/BioJsHTMLOutput.java
src/jalview/io/FileLoader.java
src/jalview/io/HtmlSvgOutput.java
src/jalview/project/Jalview2XML.java
src/jalview/schemabinding/version2/JalviewModelSequence.java
src/jalview/util/DBRefUtils.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/ws/DBRefFetcher.java
src/jalview/ws/DasSequenceFeatureFetcher.java
src/jalview/ws/jws2/Jws2Discoverer.java
utils/jalviewjs/buildxml/build.xml

38 files changed:
1  2 
j11lib/forester.jar
j8lib/forester.jar
resources/lang/Messages.properties
src/jalview/analysis/AlignmentSorter.java
src/jalview/appletgui/OverviewPanel.java
src/jalview/appletgui/PCAPanel.java
src/jalview/bin/Jalview.java
src/jalview/ext/archaeopteryx/AptxFrame.java
src/jalview/ext/treeviewer/JalviewBinding.java
src/jalview/fts/core/GFTSPanel.java
src/jalview/fts/service/pdb/PDBFTSPanel.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/AppJmol.java
src/jalview/gui/CalculationChooser.java
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/Console.java
src/jalview/gui/Desktop.java
src/jalview/gui/JalviewDialog.java
src/jalview/gui/OverviewPanel.java
src/jalview/gui/RedundancyPanel.java
src/jalview/gui/StructureViewerBase.java
src/jalview/gui/TreeCanvas.java
src/jalview/gui/VamsasApplication.java
src/jalview/gui/WebserviceInfo.java
src/jalview/gui/WsPreferences.java
src/jalview/io/FileFormat.java
src/jalview/io/FileLoader.java
src/jalview/io/IdentifyFile.java
src/jalview/jbgui/GAlignFrame.java
src/jalview/project/Jalview2XML.java
src/jalview/util/AWTConsole.java
src/jalview/util/DBRefUtils.java
src/jalview/util/MappingUtils.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/ws/DBRefFetcher.java
src/jalview/ws/jws2/Jws2Discoverer.java
test/jalview/gui/FreeUpMemoryTest.java

diff --combined j11lib/forester.jar
index afe77b2,534564c..534564c
Binary files differ
diff --combined j8lib/forester.jar
index afe77b2,0000000..534564c
mode 100644,000000..100644
Binary files differ
@@@ -30,9 -30,7 +30,9 @@@ action.minimize_associated_windows = Mi
  action.close_all = Close all
  action.load_project = Load Project
  action.save_project = Save Project
 +action.save_project_as = Save Project as...
  action.quit = Quit
 +label.quit_jalview = Quit Jalview?
  action.expand_views = Expand Views
  action.gather_views = Gather Views
  action.page_setup = Page Setup...
@@@ -118,11 -116,12 +118,11 @@@ action.paste_annotations = Paste Annota
  action.format = Format
  action.select = Select
  action.new_view = New View
 +action.new_structure_view_with = Open new structure view with {0}
  action.close = Close
  action.add = Add
 -action.save_as_default = Save as default
  action.save_as = Save as...
  action.save = Save
 -action.cancel_fetch = Cancel Fetch
  action.change_font = Change Font
  action.change_font_tree_panel = Change Font (Tree Panel)
  action.colour = Colour
@@@ -133,8 -132,6 +133,8 @@@ tooltip.select_highlighted_columns = Pr
  action.deselect_all = Deselect all
  action.invert_selection = Invert selection
  action.using_jmol = Using Jmol
 +action.undo_changes_to_feature_settings = Undo all unapplied changes to feature settings
 +action.undo_changes_to_feature_settings_and_close_the_dialog = Undo all pending changes and close the feature settings dialog
  action.link = Link
  action.group_link = Group Link
  action.show_chain = Show Chain
@@@ -143,6 -140,7 +143,6 @@@ action.fetch_db_references = Fetch DB R
  action.view_flanking_regions = Show flanking regions
  label.view_flanking_regions = Show sequence data either side of the subsequences involved in this alignment
  label.structures_manager = Structures Manager
 -label.nickname = Nickname:
  label.url = URL
  label.url\: = URL:
  label.input_file_url = Enter URL or Input File
@@@ -164,6 -162,7 +164,6 @@@ label.current_parameter_set_name = Curr
  label.service_action = Service Action:
  label.post_url = POST URL:
  label.url_suffix = URL Suffix
 -label.sequence_source = Sequence Source
  label.per_seq = per Sequence
  label.result_vertically_separable = Results are vertically separable
  label.amend = Amend
@@@ -173,9 -172,12 +173,12 @@@ label.principal_component_analysis = Pr
  label.average_distance_identity = Average Distance Using % Identity
  label.neighbour_joining_identity = Neighbour Joining Using % Identity
  label.choose_calculation = Choose Calculation
 +label.calc_title = {0} Using {1}
+ label.treecalc_title = {0} Using {1}
+ label.aptx_title = Archaeopteryx Tree View 
+ label.of_x = of {0}
  label.tree_calc_av = Average Distance
  label.tree_calc_nj = Neighbour Joining
 -label.select_score_model = Select score model
  label.score_model_pid = % Identity
  label.score_model_blosum62 = BLOSUM62
  label.score_model_pam250 = PAM 250
@@@ -188,22 -190,21 +191,22 @@@ label.out_to_textbox = Output to Textbo
  label.occupancy = Occupancy
  # delete Clustal - use FileFormat name instead
  label.clustal = Clustal
 -# label.colourScheme_<schemeName> as in JalviewColourScheme
 +# label.colourScheme_<schemeName> as in JalviewColourScheme, spaces removed
  label.colourScheme_clustal = Clustalx
  label.colourScheme_blosum62 = BLOSUM62 Score
 -label.colourScheme_%_identity = Percentage Identity
 +label.colourScheme_%identity = Percentage Identity
  label.colourScheme_zappo = Zappo
  label.colourScheme_taylor = Taylor
  label.colourScheme_hydrophobic = Hydrophobicity
 -label.colourScheme_helix_propensity = Helix Propensity
 -label.colourScheme_strand_propensity = Strand Propensity
 -label.colourScheme_turn_propensity = Turn Propensity
 -label.colourScheme_buried_index = Buried Index
 +label.colourScheme_helixpropensity = Helix Propensity
 +label.colourScheme_strandpropensity = Strand Propensity
 +label.colourScheme_turnpropensity = Turn Propensity
 +label.colourScheme_buriedindex = Buried Index
  label.colourScheme_purine/pyrimidine = Purine/Pyrimidine
  label.colourScheme_nucleotide = Nucleotide
 -label.colourScheme_t-coffee_scores = T-Coffee Scores
 -label.colourScheme_rna_helices = By RNA Helices
 +label.colourScheme_t-coffeescores = T-Coffee Scores
 +label.colourScheme_rnahelices = By RNA Helices
 +label.colourScheme_sequenceid = Sequence ID Colour
  label.blc = BLC
  label.fasta = Fasta
  label.msf = MSF
@@@ -231,7 -232,6 +234,7 @@@ label.nucleotide = Nucleotid
  label.protein = Protein
  label.nucleotides = Nucleotides
  label.proteins = Proteins
 +label.CDS = CDS
  label.to_new_alignment = To New Alignment
  label.to_this_alignment = Add To This Alignment
  label.apply_colour_to_all_groups = Apply Colour To All Groups
@@@ -244,6 -244,7 +247,6 @@@ label.documentation = Documentatio
  label.about = About...
  label.show_sequence_limits = Show Sequence Limits
  action.feature_settings = Feature Settings...
 -label.feature_settings = Feature Settings
  label.all_columns = All Columns
  label.all_sequences = All Sequences
  label.selected_columns = Selected Columns 
@@@ -268,15 -269,13 +271,15 @@@ label.use_rnaview = Use RNAView for sec
  label.autoadd_secstr = Add secondary structure annotation to alignment
  label.autoadd_temp = Add Temperature Factor annotation to alignment
  label.structure_viewer = Default structure viewer
 -label.chimera_path = Path to Chimera program
 -label.chimera_path_tip = Jalview will first try any path entered here, else standard installation locations.<br>Double-click to browse for file.
 -label.invalid_chimera_path = Chimera path not found or not executable
 -label.chimera_missing = Chimera structure viewer not found.<br/>Please enter the path to Chimera (if installed),<br/>or download and install UCSF Chimera.
 -label.chimera_failed = Error opening Chimera - is it installed?\nCheck path in Preferences, Structure
 +label.double_click_to_browse = Double-click to browse for file
 +label.viewer_path = Path to {0} program
 +label.viewer_path_tip = Jalview will first try any path entered here, else standard installation locations.<br>Double-click to browse for file.
 +label.invalid_viewer_path = Path not found or not executable
 +label.viewer_missing = Structure viewer not found.<br/>Please enter the path to the executable (if installed),<br/>or download and install the program.
 +label.open_viewer_failed = Error opening {0} - is it installed?\nCheck configured path in Structure tab of Jalview''s Preferences 
  label.min_colour = Minimum Colour
  label.max_colour = Maximum Colour
 +label.no_colour = No Colour
  label.use_original_colours = Use Original Colours
  label.threshold_minmax = Threshold is min/max
  label.represent_group_with = Represent Group with {0}
@@@ -284,9 -283,9 +287,9 @@@ label.selection = Selectio
  label.group_colour = Group Colour
  label.sequence = Sequence
  label.view_pdb_structure = View PDB Structure
 -label.min = Min:
 -label.max = Max:
 -label.colour_by_label = Colour by label
 +label.min_value = Min value
 +label.max_value = Max value
 +label.no_value = No value
  label.new_feature = New Feature
  label.match_case = Match Case
  label.view_alignment_editor = View in alignment editor
@@@ -303,6 -302,10 +306,10 @@@ label.mark_unassociated_leaves = Mark U
  label.fit_to_window = Fit To Window
  label.newick_format = Newick Format
  label.select_tree_file = Select a tree file
+ label.treebase_study = TreeBASE Study
+ label.treebase = TreeBASE
+ label.treefam = TreeFam
+ label.tree_of_life = Tree of Life
  label.colours = Colours
  label.view_mapping = View Mapping
  label.wireframe = Wireframe
@@@ -329,7 -332,6 +336,7 @@@ label.successfully_pasted_alignment_fil
  label.paste_your_alignment_file = Paste your alignment file here
  label.paste_your = Paste your
  label.finished_searching = Finished searching
 +label.subsequence_matches_found = {0} subsequence matches found
  label.search_results= Search results {0} : {1}
  label.found_match_for = Found match for {0}
  label.font = Font:
@@@ -356,25 -358,30 +363,25 @@@ label.status = Statu
  label.channels = Channels
  label.channel_title_item_count = {0} ({1})
  label.blog_item_published_on_date = {0} {1} 
  label.groovy_console = Groovy Console...
  label.lineart = Lineart
  label.dont_ask_me_again = Don't ask me again
 -label.select_eps_character_rendering_style = Select EPS character rendering style
 +label.select_character_rendering_style = {0} character rendering style
 +label.select_character_style_title = {0} Rendering options
  label.invert_selection = Invert Selection
  label.optimise_order = Optimise Order
  label.seq_sort_by_score = Sequence sort by Score
  label.load_colours = Load Colours
  label.save_colours = Save Colours
 -label.fetch_das_features = Fetch DAS Features
 +label.load_colours_tooltip = Load feature colours and filters from file
 +label.save_colours_tooltip = Save feature colours and filters to file
  label.selected_database_to_fetch_from = Selected {0} database {1} to fetch from {2} 
  label.database_param = Database: {0}
  label.example = Example
  label.example_param = Example: {0}
  label.select_file_format_before_saving = You must select a file format before saving!
  label.file_format_not_specified = File format not specified
 -label.couldnt_save_file = Couldn't save file: {0}
 +label.couldnt_save_file = Couldn''t save file: {0}
  label.error_saving_file = Error Saving File
  label.remove_from_default_list = Remove from default list?
  label.remove_user_defined_colour = Remove user defined colour
@@@ -386,7 -393,9 +393,9 @@@ label.not_enough_sequences = Not enoug
  label.selected_region_to_tree_may_only_contain_residues_or_gaps =  The selected region to create a tree may\nonly contain residues or gaps.\nTry using the Pad function in the edit menu,\nor one of the multiple sequence alignment web services.
  label.sequences_selection_not_aligned = Sequences in selection are not aligned
  label.problem_reading_tree_file =  Problem reading tree file
+ label.tabs_detected_archaeopteryx = Warning, multiple trees detected in a single tree viewer instance. This will cause problems!
  label.possible_problem_with_tree_file = Possible problem with tree file
+ label.aptx_config_not_found = Warning: tree viewer configuration file not found, continue anyway? (this WILL cause the viewer to look different)
  label.tree_url_example = Please enter a complete URL, for example \"http://www.jalview.org/examples/ferredoxin.nw\"
  label.from_database = From Database...
  label.load_tree_url = Tree from URL
@@@ -402,18 -411,28 +411,18 @@@ label.view_name_original = Origina
  label.enter_view_name = Enter View Name
  label.enter_label = Enter label
  label.enter_label_for_the_structure = Enter a label for the structure
 -label.pdb_entry_is_already_displayed = {0} is already displayed.\nDo you want to re-use this viewer ?
 -label.map_sequences_to_visible_window = Map Sequences to Visible Window: {0}
 -label.add_pdbentry_to_view = Do you want to add {0} to the view called\n{1}\n
 -label.align_to_existing_structure_view = Align to existing structure view
  label.pdb_entries_couldnt_be_retrieved = The following pdb entries could not be retrieved from the PDB\:\n{0}\nPlease retry, or try downloading them manually.
  label.couldnt_load_file = Couldn't load file
  label.couldnt_find_pdb_id_in_file = Couldn't find a PDB id in the file supplied. Please enter an Id to identify this structure.
  label.no_pdb_id_in_file = No PDB Id in File
 -label.couldnt_read_pasted_text = Couldn't read the pasted text {0}
 +label.couldnt_read_pasted_text = Couldn''t read the pasted text {0}
  label.error_parsing_text = Error parsing text
 -label.enter_local_das_source = Enter Nickname & URL of Local DAS Source
 -label.you_can_only_edit_or_remove_local_das_sources = You can only edit or remove local DAS Sources!
 -label.public_das_source = Public DAS source - not editable
  label.input_alignment_from_url = Input Alignment From URL
  label.input_alignment = Input Alignment
 -label.couldnt_import_as_vamsas_session = Couldn't import {0} as a new vamsas session.
  label.vamsas_document_import_failed = Vamsas Document Import Failed
 -label.couldnt_locate = Could not locate {0}
 +label.couldnt_locate = Couldn''t locate {0}
  label.url_not_found = URL not found
  label.new_sequence_url_link = New sequence URL link
 -label.cannot_edit_annotations_in_wrapped_view = Cannot edit annotations in wrapped view
 -label.wrapped_view_no_edit = Wrapped view - no edit
  label.error_retrieving_data = Error Retrieving Data
  label.user_colour_scheme_must_have_name = User colour scheme must have a name
  label.no_name_colour_scheme = No name for colour scheme
@@@ -421,6 -440,8 +430,6 @@@ label.invalid_url = Invalid URL 
  label.error_loading_file = Error loading file
  label.problems_opening_file = Encountered problems opening {0}!!
  label.file_open_error = File open error
 -label.no_das_sources_selected_warn = No das sources were selected.\nPlease select some sources and\ntry again.
 -label.no_das_sources_selected_title = No DAS Sources Selected
  label.colour_scheme_exists_overwrite = Colour scheme {0} exists.\nContinue saving colour scheme as {1}?"
  label.duplicate_scheme_name = Duplicate scheme name
  label.jalview_new_questionnaire = There is a new Questionnaire available. Would you like to complete it now ?\n
@@@ -480,10 -501,6 +489,10 @@@ label.settings_for_type = Settings for 
  label.view_full_application = View in Full Application
  label.load_associated_tree = Load Associated Tree...
  label.load_features_annotations = Load Features/Annotations...
 +label.load_vcf = Load SNP variants from plain text or indexed VCF data
 +label.load_vcf_file = Load VCF File
 +label.searching_vcf = Loading VCF variants...
 +label.added_vcf = Added {0} VCF variants to {1} sequence(s)
  label.export_features = Export Features...
  label.export_annotations = Export Annotations...
  label.to_upper_case = To Upper Case
@@@ -493,14 -510,11 +502,14 @@@ label.edit_name_description = Edit Name
  label.create_sequence_feature = Create Sequence Feature...
  label.edit_sequence = Edit Sequence
  label.edit_sequences = Edit Sequences
 +label.insert_gap = Insert 1 gap
 +label.insert_gaps = Insert {0} gaps
 +label.delete_gap = Delete 1 gap
 +label.delete_gaps = Delete {0} gaps
  label.sequence_details = Sequence Details
 -label.jmol_help = Jmol Help
 -label.chimera_help = Chimera Help
 +label.viewer_help = {0} Help
  label.close_viewer = Close Viewer
 -label.confirm_close_chimera = This will close Jalview''s connection to {0}.<br>Do you want to close the Chimera window as well?
 +label.confirm_close_viewer = This will close Jalview''s connection to {0}.<br>Do you want to close the {1} window as well?
  label.all = All
  label.sort_by = Sort alignment by
  label.sort_by_score = Sort by Score
@@@ -516,20 -530,16 +525,20 @@@ label.load_tree_file = Load a tree fil
  label.retrieve_parse_sequence_database_records_alignment_or_selected_sequences = Retrieve and parse sequence database records for the alignment or the currently selected sequences
  label.standard_databases = Standard Databases
  label.fetch_embl_uniprot = Fetch from EMBL/EMBLCDS or Uniprot/PDB and any selected DAS sources
 +label.fetch_uniprot_references = Fetch Uniprot references
 +label.search_3dbeacons = 3D-Beacons Search
 +label.find_models_from_3dbeacons = Search 3D-Beacons for 3D structures and models
 +label.3dbeacons = 3D-Beacons
 +label.fetch_references_for = Fetch database references for {0} sequences ?
 +label.fetch_references_for_3dbeacons = 3D Beacons needs Uniprot References. Fetch database references for {0} sequences ?
  label.reset_min_max_colours_to_defaults = Reset min and max colours to defaults from user preferences.
  label.align_structures_using_linked_alignment_views = Superpose structures using {0} selected alignment view(s)
 -label.connect_to_session = Connect to session {0}
  label.threshold_feature_display_by_score = Threshold the feature display by score.
  label.threshold_feature_no_threshold = No Threshold
  label.threshold_feature_above_threshold = Above Threshold
  label.threshold_feature_below_threshold = Below Threshold
  label.adjust_threshold = Adjust threshold
  label.toggle_absolute_relative_display_threshold = Toggle between absolute and relative display threshold.
 -label.display_features_same_type_different_label_using_different_colour = Display features of the same type with a different label using a different colour. (e.g. domain features)
  label.select_colour_minimum_value = Select Colour for Minimum Value
  label.select_colour_maximum_value = Select Colour for Maximum Value
  label.open_url_param = Open URL {0}
@@@ -557,7 -567,10 +566,7 @@@ label.right_align_sequence_id = Right A
  label.sequence_id_tooltip = Sequence ID Tooltip
  label.no_services = <No Services>
  label.select_copy_raw_html = Select this if you want to copy raw html
 -label.share_data_vamsas_applications = Share data with other vamsas applications
 -label.connect_to = Connect to
 -label.join_existing_vamsas_session = Join an existing vamsas session
 -label.from_url = From URL
 +label.from_url = from URL
  label.any_trees_calculated_or_loaded_alignment_automatically_sort = When selected, any trees calculated or loaded onto the alignment will automatically sort the alignment
  label.sort_with_new_tree = Sort With New Tree
  label.from_textbox = From Textbox
@@@ -566,6 -579,7 +575,6 @@@ label.preferences = Preference
  label.tools = Tools
  label.fetch_sequences = Fetch Sequences
  action.fetch_sequences = Fetch Sequences...
 -label.stop_vamsas_session = Stop Vamsas Session
  label.collect_garbage = Collect Garbage
  label.show_memory_usage = Show Memory Usage
  label.show_java_console = Show Java Console
@@@ -588,22 -602,14 +597,22 @@@ label.gap_symbol = Gap Symbo
  label.prot_alignment_colour = Protein Alignment Colour
  label.nuc_alignment_colour = Nucleotide Alignment Colour
  label.address = Address
 +label.host = Host
  label.port = Port
 -label.default_browser_unix = Default Browser (Unix)
 +label.default_browser_unix_windows = Default Browser (Unix, Windows)
  label.send_usage_statistics = Send usage statistics
  label.check_for_questionnaires = Check for questionnaires
  label.check_for_latest_version = Check for latest version
  label.url_linkfrom_sequence_id = URL link from Sequence ID
 -label.use_proxy_server = Use a proxy server
 -label.eps_rendering_style = EPS rendering style
 +label.no_proxy = No proxy servers
 +label.system_proxy = System proxy servers (http={0}; https={1})
 +label.use_proxy_server = Use these proxy servers
 +label.auth_required = Authentication required
 +label.username = Username
 +label.password = Password
 +label.proxy_password_required = Proxy password required
 +label.not_stored = not stored in Preferences file
 +label.rendering_style = {0} rendering style
  label.append_start_end = Append /start-end (/15-380)
  label.full_sequence_id = Full Sequence Id
  label.smooth_font = Smooth Font
@@@ -625,11 -631,13 +634,11 @@@ label.visual = Visua
  label.connections = Connections
  label.output = Output
  label.editing = Editing
  label.web_services = Web Services
  label.right_click_to_edit_currently_selected_parameter = Right click to edit currently selected parameter.
  label.let_jmol_manage_structure_colours = Let Jmol manage structure colours
 -label.let_chimera_manage_structure_colours = Let Chimera manage structure colours
 -label.fetch_chimera_attributes = Fetch Chimera attributes
 -label.fetch_chimera_attributes_tip = Copy Chimera attribute to Jalview feature
 +label.fetch_viewer_attributes = Fetch {0} attributes
 +label.fetch_viewer_attributes_tip = Copy {0} attribute to Jalview feature
  label.marks_leaves_tree_not_associated_with_sequence = Marks leaves of tree not associated with a sequence
  label.index_web_services_menu_by_host_site = Index web services in menu by the host site
  label.option_want_informed_web_service_URL_cannot_be_accessed_jalview_when_starts_up = Check this option if you want to be informed<br>when a web service URL cannot be accessed by Jalview<br>when it starts up
@@@ -639,7 -647,11 +648,7 @@@ label.delete_service_url = Delete Servi
  label.details = Details
  label.options = Options
  label.parameters = Parameters
 -label.available_das_sources = Available DAS Sources
 -label.full_details = Full Details
 -label.authority = Authority
 -label.type = Type
 -label.proxy_server = Proxy Server
 +label.proxy_servers = Proxy Servers
  label.file_output = File Output
  label.select_input_type = Select input type
  label.set_options_for_type = Set options for type
@@@ -683,7 -695,7 +692,7 @@@ label.sequence_details_for = Sequence D
  label.sequence_name = Sequence Name
  label.sequence_description = Sequence Description
  label.edit_sequence_name_description = Edit Sequence Name/Description
 -label.spaces_converted_to_backslashes = Spaces have been converted to _
 +label.spaces_converted_to_underscores = Spaces have been converted to _
  label.no_spaces_allowed_sequence_name = No spaces allowed in Sequence Name
  label.select_outline_colour = Select Outline Colour
  label.web_browser_not_found_unix = Unixers\: Couldn't find default web browser.\nAdd the full path to your browser in Preferences."
@@@ -707,6 -719,9 +716,6 @@@ label.sort_alignment_new_tree = Sort Al
  label.add_sequences = Add Sequences
  label.new_window = New Window
  label.split_window = Split Window
 -label.refresh_available_sources = Refresh Available Sources
 -label.use_registry = Use Registry
 -label.add_local_source = Add Local Source
  label.set_as_default = Set as Default
  label.show_labels = Show labels
  action.background_colour = Background Colour...
@@@ -714,14 -729,15 +723,14 @@@ label.associate_nodes_with = Associate 
  label.link_name = Link Name
  label.pdb_file = PDB file
  label.colour_with_jmol = Colour with Jmol
 -label.colour_with_chimera = Colour with Chimera
 +label.let_viewer_manage_structure_colours = Let viewer manage structure colours
 +label.colour_with_viewer = Colour in structure viewer
  label.superpose_structures = Superpose Structures
  error.superposition_failed = Superposition failed: {0}
  label.insufficient_residues = Not enough aligned residues ({0}) to perform superposition
 -label.jmol = Jmol
 -label.chimera = Chimera
 -label.create_chimera_attributes = Write Jalview features
 -label.create_chimera_attributes_tip = Set Chimera residue attributes for visible features
 -label.attributes_set = {0} attribute values set on Chimera
 +label.create_viewer_attributes = Write Jalview features
 +label.create_viewer_attributes_tip = Set structure residue attributes for Jalview features
 +label.attributes_set = {0} attribute values set on {1}
  label.sort_alignment_by_tree = Sort Alignment By Tree
  label.mark_unlinked_leaves = Mark Unlinked Leaves
  label.associate_leaves_with = Associate Leaves With
@@@ -764,21 -780,18 +773,21 @@@ label.run_with_preset_params = Run {0} 
  label.view_and_change_parameters_before_running_calculation = View and change parameters before running calculation
  label.view_documentation = View documentation
  label.select_return_type = Select return type
 -label.translation_of_params = Translation of {0}
 +label.translation_of_params = Translation of {0} (Table {1})
  label.features_for_params = Features for - {0}
  label.annotations_for_params = Annotations for - {0}
  label.generating_features_for_params = Generating features for - {0}
  label.generating_annotations_for_params = Generating annotations for - {0}
  label.varna_params = VARNA - {0}
  label.sequence_feature_settings = Sequence Feature Settings
 +label.sequence_feature_settings_for = Sequence Feature Settings for {0}
 +label.sequence_feature_settings_for_view = Sequence Feature Settings for view "{0}"
 +label.sequence_feature_settings_for_CDS_and_Protein = Sequence Feature Settings for CDS and Protein
  label.pairwise_aligned_sequences = Pairwise Aligned Sequences
  label.original_data_for_params = Original Data for {0}
  label.points_for_params = Points for {0}
  label.transformed_points_for_params = Transformed points for {0}
 -label.graduated_color_for_params = Graduated Feature Colour for {0}
 +label.variable_color_for = Variable Feature Colour for {0}
  label.select_background_colour = Select Background Colour
  label.invalid_font = Invalid Font
  label.separate_multiple_accession_ids = Enter one or more accession IDs separated by a semi-colon ";"
@@@ -849,6 -862,7 +858,6 @@@ label.multiharmony = Multi-Harmon
  label.unable_start_web_service_analysis = Unable to start web service analysis
  label.job_couldnt_be_started_check_input = The Job couldn't be started. Please check your input, and the Jalview console for any warning messages.
  label.prompt_each_time = Prompt each time
 -label.use_source = Use Source
  label.couldnt_save_project = Couldn't save project
  label.error_whilst_saving_current_state_to = Error whilst saving current state to {0}
  label.error_whilst_loading_project_from = Error whilst loading project from {0}
@@@ -858,13 -872,13 +867,13 @@@ label.invalid_name = Invalid nam
  label.set_proxy_settings = Please set up your proxy settings in the 'Connections' tab of the Preferences window
  label.proxy_authorization_failed = Proxy Authorization Failed
  label.internal_jalview_error = Internal Jalview Error
 -label.secondary_structure_prediction_service_couldnt_be_located = The Secondary Structure Prediction Service named {0} at {1} couldn't be located.
 +label.secondary_structure_prediction_service_couldnt_be_located = The Secondary Structure Prediction Service named {0} at {1} couldn''t be located.
  label.service_called_is_not_msa_service = The Service called \n{0}\nis not a \nMultiple Sequence Alignment Service\!
  label.msa_service_is_unknown = The Multiple Sequence Alignment Service named {0} is unknown
  label.service_called_is_not_seq_search_service = The Service called \n{0}\nis not a \nSequence Search Service\!
  label.seq_search_service_is_unknown = The Sequence Search Service named {0} is unknown
  label.feature_type = Feature Type
 -label.display = Display
 +label.show = Show
  label.service_url = Service URL
  label.copied_sequences = Copied sequences
  label.cut_sequences = Cut Sequences
@@@ -874,16 -888,22 +883,16 @@@ label.error_unsupported_owwner_user_col
  label.save_alignment_to_file = Save Alignment to file
  label.save_features_to_file = Save Features to File
  label.save_annotation_to_file = Save Annotation to File
 -label.no_features_on_alignment = No features found on alignment
  label.save_pdb_file = Save PDB File
  label.save_text_to_file = Save Text to File
  label.save_state = Save State
  label.restore_state = Restore State
  label.saving_jalview_project = Saving jalview project {0}
 -label.loading_jalview_project = Loading jalview project {0}
 -label.save_vamsas_document_archive = Save Vamsas Document Archive
 -label.saving_vamsas_doc = Saving VAMSAS Document to {0}
  label.load_feature_colours = Load Feature Colours
  label.save_feature_colours = Save Feature Colour Scheme
  label.select_startup_file = Select startup file
  label.select_default_browser = Select default web browser
  label.save_tree_as_newick = Save tree as newick file
 -label.create_eps_from_tree = Create EPS file from tree
 -label.create_png_from_tree = Create PNG image from tree
  label.save_colour_scheme = Save colour scheme
  label.edit_params_for = Edit parameters for {0}
  label.choose_filename_for_param_file = Choose a filename for this parameter file
@@@ -898,9 -918,12 +907,11 @@@ label.visible = Visibl
  label.select_unselect_visible_regions_from = select and unselected {0} regions from {1}
  label.visible_region_of = visible region of
  label.webservice_job_title_on = {0} using {1} on {2}
  label.loading_file = Loading File: {0}
  label.edit_params = Edit {0}
  label.as_percentage = As Percentage
+ error.database_id_has_letters = Database identifier ({0}) should contain only digits
+ error.phyloxml_validation = phyloXML XSD-based validation is turned off (enable with line 'validate_against_phyloxml_xsd_schem: true' in configuration file)
  error.not_implemented = Not implemented
  error.no_such_method_as_clone1_for = No such method as clone1 for {0}
  error.null_from_clone1 = Null from clone1!
@@@ -938,20 -961,29 +949,20 @@@ error.call_setprogressbar_before_regist
  label.cancelled_params = Cancelled {0}
  error.implementation_error_cannot_show_view_alignment_frame = Implementation error: cannot show a view from another alignment in an AlignFrame.
  error.implementation_error_dont_know_about_threshold_setting = Implementation error: don't know about threshold setting for current AnnotationColourGradient.
 -error.eps_generation_not_implemented = EPS Generation not yet implemented
 -error.png_generation_not_implemented = PNG Generation not yet implemented
 -error.try_join_vamsas_session_another = Trying to join a vamsas session when another is already connected
 -error.invalid_vamsas_session_id = Invalid vamsas session id
  label.groovy_support_failed = Jalview Groovy Support Failed
  label.couldnt_create_groovy_shell = Couldn't create the groovy Shell. Check the error log for the details of what went wrong.
  error.unsupported_version_calcIdparam = Unsupported Version for calcIdparam {0}
  error.implementation_error_cant_reorder_tree = Implementation Error: Can't reorder this tree. Not DefaultMutableTreeNode.
 -error.invalid_value_for_option = Invalid value {0} for option {1}
 +error.invalid_value_for_option = Invalid value ''{0}'' for option ''{1}''
  error.implementation_error_cannot_import_vamsas_doc = Implementation Error - cannot import existing vamsas document into an existing session, Yet!
  label.vamsas_doc_couldnt_be_opened_as_new_session = VAMSAS Document could not be opened as a new session - please choose another
 -error.implementation_error_vamsas_operation_not_init = Impementation error! Vamsas Operations when client not initialised and connected
 -error.jalview_no_connected_vamsas_session = Jalview not connected to Vamsas session
 -error.implementation_error_cannot_recover_vamsas_object_mappings = IMPLEMENTATION ERROR: Cannot recover vamsas object mappings - no backup was made
  error.setstatus_called_non_existent_job_pane = setStatus called for non-existent job pane {0}
  error.implementation_error_cannot_find_marshaller_for_param_set =Implementation error: Can't find a marshaller for the parameter set
  error.implementation_error_old_jalview_object_not_bound =IMPLEMENTATION ERROR: old jalview object is not bound ! ({0})
  error.implementation_error_vamsas_doc_class_should_bind_to_type = Implementation Error: Vamsas Document Class {0} should bind to a {1} (found a {2})
  error.invalid_vamsas_rangetype_cannot_resolve_lists = Invalid vamsas RangeType - cannot resolve both lists of Pos and Seg from choice!
 -error.implementation_error_maplist_is_null = Implementation error. MapList is null for initMapType.
  error.implementation_error_cannot_have_null_alignment = Implementation error: Cannot have null alignment property key
  error.implementation_error_null_fileparse = Implementation error. Null FileParse in copy constructor
 -error.implementation_error_cannot_map_alignment_sequences = IMPLEMENTATION ERROR: Cannot map an alignment of sequences from different datasets into a single alignment in the vamsas document.
  error.implementation_error_structure_selection_manager_null = Implementation error. Structure selection manager's context is 'null'
  exception.ssm_context_is_null = SSM context is null
  error.idstring_seqstrings_only_one_per_sequence = idstrings and seqstrings contain one string each per sequence
@@@ -966,13 -998,14 +977,13 @@@ error.implementation_error_minlen_must_
  error.implementation_error_msawbjob_called = Implementation error - StartJob(MsaWSJob) called on a WSJobInstance {0}
  error.implementation_error_cannot_attach_ws_menu_entry = IMPLEMENTATION ERROR: cannot attach WS Menu Entry without service handle reference!
  error.parameter_migration_not_implemented_yet = Parameter migration not implemented yet
 -error.implementation_error_cannot_set_jaba_option = Implementation error: cannot set Jaba Option to a value outside its allowed value range!
  error.implementation_error_valuetype_doesnt_support_jabaws_type = IMPLEMENTATION ERROR: jalview.ws.params.ValueConstrainI.ValueType does not support the JABAWS type : {0}
  error.cannot_create_jabaws_param_set = Cannot create a JabaWSParamSet from non-JabaWS parameters
  error.cannot_set_arguments_to_jabaws_param_set = Cannot set arguments to a JabaWSParamSet that are not JabaWS arguments
  error.implementation_error_runner_config_not_available = Implementation Error: Runner Config not available for a JABAWS service of type {0} ({1})
  error.implementation_error_cannot_handle_jaba_param = Implementation Error: Cannot handle Jaba parameter object {0}
  error.implementation_error_attempt_to_delete_service_preset = Implementation error: Attempt to delete a service preset!
 -error.implementation_error_cannot_locate_oldname_presetname = Implementation error: Can't locate either oldname ({0}) or presetName ({1}in the datastore!"
 +error.implementation_error_cannot_locate_oldname_presetname = Implementation error: Can''t locate either oldname ({0}) or presetName ({1}) in the datastore!"
  error.implementation_error_jabaws_param_set_only_handled_by = Implementation error: JabaWsParamSets can only be handled by JabaParamStore
  error.cannot_set_source_file_for = Cannot set source file for {0}
  error.mismatch_service_instance_preset = Probable mismatch between service instance and preset!
@@@ -980,20 -1013,18 +991,20 @@@ error.cannot_set_params_for_ws_preset 
  error.implementation_error_can_only_instantiate_jaba_param_sets = Implementation error: Can only instantiate Jaba parameter sets
  error.no_aacon_service_found = No AACon service found
  error.implementation_error_couldnt_copy_value_constraint = Implementation error: could not copy ValueConstrain!
 -error.couldnt_encode_as_utf8 = Couldn't encode {0} as UTF-8.
 +error.couldnt_encode_as_utf8 = Couldn''t encode {0} as UTF-8.
  error.tree_inputtype_not_yet_implemented = Tree InputType not yet implemented
  error.implementation_error_need_to_have_httpresponse = Implementation Error: need to have an HttpResponse to process
  error.dbrefsource_implementation_exception =DBRefSource Implementation Exception
  error.implementation_error_dbinstance_must_implement_interface = Implmentation Error - getDbInstances must be given a class that implements jalview.ws.seqfetcher.DbSourceProxy (was given{0})
  error.implementation_error_must_init_dbsources =Implementation error. Must initialise dbSources
  label.view_controller_toggled_marked = {0} {1} columns {2} features of type {3}  across {4} sequence(s)
 +label.no_highlighted_regions_marked = No highlighted regions marked
  label.toggled = Toggled
  label.marked = Marked
  label.containing = containing
  label.not_containing = not containing
 -label.no_feature_of_type_found = No features of type {0} found.
 +label.no_feature_of_type_found = No features of type {0} found
 +label.no_feature_found_selection = No features of type {0} found in selection
  label.submission_params = Submission {0}
  label.empty_alignment_job = Empty Alignment Job
  label.add_new_sbrs_service = Add a new Simple Bioinformatics Rest Service
@@@ -1002,7 -1033,7 +1013,7 @@@ label.pca_recalculating = Recalculatin
  label.pca_calculating = Calculating PCA
  label.select_foreground_colour = Choose foreground colour
  label.select_colour_for_text = Select Colour for Text
 -label.adjunst_foreground_text_colour_threshold = Adjust Foreground Text Colour Threshold
 +label.adjust_foreground_text_colour_threshold = Adjust Foreground Text Colour Threshold
  label.select_subtree_colour = Select Sub-Tree Colour
  label.create_new_sequence_features = Create New Sequence Feature(s)
  label.amend_delete_features = Amend/Delete Features for {0}
@@@ -1037,24 -1068,22 +1048,24 @@@ error.implementation_error_reset_called
  exception.number_of_residues_in_query_sequence_differ_from_prediction = Number of residues in {0} supposed query sequence ({1}\n{2})\ndiffer from number of prediction sites in prediction ({3})
  label.mapped = mapped
  exception.jpredconcide_entry_has_unexpected_number_of_columns = JPredConcise: Entry ({0}) has an unexpected number of columns
 -exception.couldnt_parse_concise_annotation_for_prediction = Couldn't parse concise annotation for prediction profile.\n{0}
 +exception.couldnt_parse_concise_annotation_for_prediction = Couldn''t parse concise annotation for prediction profile.\n{0}
  exception.newfile = NewickFile\: {0}\n
  label.no_tree_read_in = No Tree read in
 -exception.rnaml_couldnt_access_datasource = Couldn't access datasource ({0})
 -exception.ranml_couldnt_process_data = Couldn't process data as RNAML file ({0})
 +exception.rnaml_couldnt_access_datasource = Couldn''t access datasource ({0})
 +exception.ranml_couldnt_process_data = Couldn''t process data as RNAML file ({0})
  exception.ranml_invalid_file = Invalid RNAML file ({0})
  exception.ranml_problem_parsing_data = Problem parsing data as RNAML ({0})
  exception.pfam_no_sequences_found = No sequences found (PFAM input)
  exception.stockholm_invalid_format = This file is not in valid STOCKHOLM format: First line does not contain '# STOCKHOLM'
  exception.couldnt_parse_sequence_line = Could not parse sequence line: {0}
  exception.unknown_annotation_detected = Unknown annotation detected: {0} {1}
 -exception.couldnt_store_sequence_mappings = Couldn't store sequence mappings for {0}
 +exception.couldnt_store_sequence_mappings = Couldn''t store sequence mappings for {0}
  exception.matrix_too_many_iteration = Too many iterations in {0} (max is {1})
  exception.invalid_matrix_identifier = Sequence identifier {0} not found in distance matrix.
  exception.browser_not_found = Exception in finding browser: {0}
 +exception.browser_unable_to_launch = Unable to launch browser: {0}
  exception.browser_unable_to_locate = Unable to locate browser: {0}
 +exception.browser_os_not_supported = Launching browser on this operating system not supported.  Use URL\n{0}
  exception.invocation_target_exception_creating_aedesc = InvocationTargetException while creating AEDesc: {0}
  exception.illegal_access_building_apple_evt= IllegalAccessException while building AppleEvent: {0}
  exception.unable_to_launch_url = Unable to launch URL: {0}
@@@ -1062,6 -1091,9 +1073,6 @@@ exception.unable_to_create_internet_con
  exception.invocation_target_calling_url = InvocationTargetException while calling openURL: {0}
  exception.illegal_access_calling_url = IllegalAccessException while calling openURL: {0}
  exception.interrupted_launching_browser = InterruptedException while launching browser: {0}
 -exception.das_source_doesnt_support_sequence_command = Source {0} does not support the sequence command.
 -exception.invalid_das_source = Invalid das source: {0}
 -exception.ebiembl_retrieval_failed_on = EBI EMBL XML retrieval failed on {0}:{1}
  exception.no_pdb_records_for_chain = No PDB Records for {0} chain {1}
  exception.unexpected_handling_rnaml_translation_for_pdb = Unexpected exception when handling RNAML translation of PDB data
  exception.couldnt_recover_sequence_properties_for_alignment = Couldn't recover sequence properties for alignment
@@@ -1076,6 -1108,7 +1087,6 @@@ error.implementation_error_cannot_find_
  exception.jobsubmission_invalid_params_set = Invalid parameter set. Check Jalview implementation
  exception.notvaliddata_group_contains_less_than_min_seqs = Group contains less than {0} sequences.
  exception.outofmemory_loading_pdb_file = Out of memory loading PDB File
 -exception.eps_coudnt_write_output_file = Could not write to the output file: {0}
  exception.eps_method_not_supported = Method not currently supported by EpsGraphics2D version {0}
  exception.eps_unable_to_get_inverse_matrix = Unable to get inverse of matrix: {0}
  warn.job_cannot_be_cancelled_close_window = This job cannot be cancelled.\nJust close the window.
@@@ -1083,7 -1116,7 +1094,7 @@@ warn.service_not_supported = Service no
  warn.input_is_too_big = Input is too big!
  warn.invalid_job_param_set = Invalid job parameter set!
  warn.oneseq_msainput_selection = The current selection only contains a single sequence. Do you want to submit all sequences for alignment instead ?   
 -info.job_couldnt_be_run_server_doesnt_support_program = Job could not be run because the server doesn't support this program.\n{0}
 +info.job_couldnt_be_run_server_doesnt_support_program = Job could not be run because the server doesn''t support this program.\n{0}
  info.job_couldnt_be_run_exceeded_hard_limit = Job could not be run because it exceeded a hard limit on the server.\n{0}
  info.job_couldnt_be_run_incorrect_param_setting = Job could not be run because some of the parameter settings are not supported by the server.\n{0}\nPlease check to make sure you have used the correct parameter set for this service\!\n
  info.no_jobs_ran = No jobs ran
@@@ -1100,36 -1133,48 +1111,36 @@@ status.searching_for_sequences_from = S
  status.finished_searching_for_sequences_from = Finished searching for sequences from {0}
  label.eps_file = EPS file
  label.png_image = PNG image
 -status.saving_file = Saving {0}
 -status.export_complete = {0} Export completed.
 +status.export_complete = {0} Export completed
  status.fetching_pdb = Fetching PDB {0}
  status.refreshing_news = Refreshing news
 -status.importing_vamsas_session_from = Importing VAMSAS session from {0}
  status.opening_params = Opening {0}
 -status.waiting_sequence_database_fetchers_init = Waiting for Sequence Database Fetchers to initialise
 -status.init_sequence_database_fetchers = Initialising Sequence Database Fetchers
  status.fetching_sequence_queries_from = Fetching {0} sequence queries from {1}
  status.finshed_querying = Finished querying
  status.parsing_results = Parsing results.
  status.processing = Processing...
  status.refreshing_web_service_menus = Refreshing Web Service Menus
  status.collecting_job_results = Collecting job results.
  status.fetching_db_refs = Fetching db refs
  status.loading_cached_pdb_entries = Loading Cached PDB Entries
  status.searching_for_pdb_structures = Searching for PDB Structures
 +status.searching_3d_beacons = Searching 3D Beacons
 +status.no_structures_discovered_from_3d_beacons = No models discovered from 3D Beacons
  status.opening_file_for = opening file for
 -status.colouring_chimera = Colouring Chimera
 +status.colouring_structures = Colouring structures
  label.font_doesnt_have_letters_defined = Font doesn't have letters defined\nso cannot be used\nwith alignment data
  label.font_too_small = Font size is too small
 -label.error_loading_file_params = Error loading file {0}
 -label.error_loading_jalview_file = Error loading Jalview file
  warn.out_of_memory_when_action = Out of memory when {0}\!\!\nSee help files for increasing Java Virtual Machine memory.
  warn.out_of_memory_loading_file = Out of memory loading file {0}\!\!\nSee help files for increasing Java Virtual Machine memory.
  label.out_of_memory = Out of memory
  label.invalid_id_column_width = Invalid ID Column width
  warn.user_defined_width_requirements = The user defined width for the\nannotation and sequence ID columns\nin exported figures must be\nat least 12 pixels wide.
 -label.couldnt_create_sequence_fetcher = Couldn't create SequenceFetcher
 -warn.couldnt_create_sequence_fetcher_client = Could not create the sequence fetcher client. Check error logs for details.
  warn.server_didnt_pass_validation = Service did not pass validation.\nCheck the Jalview Console for more details.
  warn.url_must_contain = Sequence URL must contain $SEQUENCE_ID$, $DB_ACCESSION$, or a regex
  warn.urls_not_contacted = URLs that could not be contacted
  warn.urls_no_jaba = URLs without any JABA Services
  info.validate_jabaws_server = Validate JabaWS Server ?\n(Look in console output for results)
  label.test_server = Test Server?
 -info.you_want_jalview_to_find_uniprot_accessions = Do you want Jalview to find\nUniprot Accession ids for given sequence names?
 -label.find_uniprot_accession_ids = Find Uniprot Accession Ids
  label.new_sequence_fetcher = New Sequence Fetcher
  label.additional_sequence_fetcher = Additional Sequence Fetcher
  label.select_database_retrieval_source = Select Database Retrieval Source
@@@ -1148,7 -1193,6 +1159,7 @@@ label.add_annotations_for = Add annotat
  action.choose_annotations = Choose Annotations...
  label.choose_annotations = Choose Annotations
  label.find = Find
 +label.in = in
  label.invalid_search = Search string invalid
  error.invalid_regex = Invalid regular expression
  label.ignore_gaps_consensus = Ignore Gaps In Consensus
@@@ -1183,6 -1227,7 +1194,6 @@@ label.pdb_sequence_fetcher = PDB Sequen
  label.result = result
  label.results = results
  label.structure_chooser = Structure Chooser
 -label.select = Select : 
  label.invert = Invert 
  label.select_pdb_file = Select PDB File
  info.select_filter_option = Select Filter Option/Manual Entry
@@@ -1216,10 -1261,14 +1227,10 @@@ label.structure_chooser_filter_time = S
  label.structure_chooser_no_of_structures = Structure Chooser - {0} Found ({1})
  info.no_pdb_entry_found_for = No PDB entry found for {0}
  exception.unable_to_detect_internet_connection = Jalview is unable to detect an internet connection
 -exception.fts_rest_service_no_longer_available = {0} rest services no longer available!
 -exception.resource_not_be_found = The requested resource could not be found
 -exception.fts_server_error = There seems to be an error from the {0} server
  exception.fts_server_unreachable = Jalview is unable to reach the {0} server. \nPlease ensure that you are connected to the internet and try again.
  label.nw_mapping = Needleman & Wunsch Alignment
  label.sifts_mapping = SIFTs Mapping
  label.mapping_method = Sequence \u27f7 Structure mapping method
 -status.waiting_for_user_to_select_output_file = Waiting for user to select {0} file
  status.cancelled_image_export_operation = Cancelled {0} export operation
  info.error_creating_file = Error creating {0} file
  exception.outofmemory_loading_mmcif_file = Out of memory loading mmCIF File
@@@ -1231,6 -1280,8 +1242,6 @@@ action.next_page= >
  action.prev_page= << 
  label.next_page_tooltip=Next Page
  label.prev_page_tooltip=Previous Page
 -exception.bad_request=Bad request. There is a problem with your input.
 -exception.service_not_available=Service not available. The server is being updated, try again later.
  status.launching_3d_structure_viewer = Launching 3D Structure viewer...
  status.fetching_3d_structures_for_selected_entries = Fetching 3D Structures for selected entries...
  status.fetching_dbrefs_for_sequences_without_valid_refs = Fetching db refs for {0} sequence(s) without valid db ref required for SIFTS mapping
@@@ -1246,6 -1297,7 +1257,6 @@@ label.SEQUENCE_ID_for_DB_ACCESSION1 = P
  label.SEQUENCE_ID_for_DB_ACCESSION2 = URL links using '$SEQUENCE_ID$' for DB accessions now use '$DB_ACCESSION$'.
  label.do_not_display_again = Do not display this message again
  exception.url_cannot_have_duplicate_id = {0} cannot be used as a label for more than one line
 -label.filter = Filter text:
  action.customfilter = Custom only
  action.showall = Show All
  label.insert = Insert:
@@@ -1260,6 -1312,7 +1271,6 @@@ label.edit_sequence_url_link = Edit seq
  warn.name_cannot_be_duplicate = User-defined URL names must be unique and cannot be MIRIAM ids
  label.output_seq_details = Output Sequence Details to list all database references
  label.urllinks = Links
 -label.default_cache_size = Default Cache Size
  action.clear_cached_items = Clear Cached Items
  label.togglehidden = Show hidden regions
  label.quality_descr = Alignment Quality based on Blosum62 scores
@@@ -1281,40 -1334,9 +1292,40 @@@ label.select_hidden_colour = Select hid
  label.overview = Overview
  label.reset_to_defaults = Reset to defaults
  label.oview_calc = Recalculating overview...
 +label.feature_details = Feature details
 +label.matchCondition_contains = Contains
 +label.matchCondition_notcontains = Does not contain
 +label.matchCondition_matches = Matches
 +label.matchCondition_notmatches = Does not match
 +label.matchCondition_present = Is present
 +label.matchCondition_notpresent = Is not present
 +label.matchCondition_eq = =
 +label.matchCondition_ne = not =
 +label.matchCondition_lt = <
 +label.matchCondition_le = <=
 +label.matchCondition_gt = >
 +label.matchCondition_ge = >=
 +label.numeric_required = The value should be numeric
 +label.filter = Filter
 +label.filters = Filters
 +label.join_conditions = Join conditions with
 +label.delete_condition = Delete this condition
 +label.score = Score
 +label.colour_by_label = Colour by label
 +label.variable_colour = Variable colour...
 +label.select_colour_for = Select colour for {0}
  option.enable_disable_autosearch = When ticked, search is performed automatically
  option.autosearch = Autosearch
  label.retrieve_ids = Retrieve IDs
 +label.display_settings_for = Display settings for {0} features
 +label.simple_colour = Simple Colour
 +label.colour_by_text = Colour by text
 +label.graduated_colour = Graduated Colour
 +label.by_text_of = By text of
 +label.by_range_of = By range of
 +label.or = Or
 +label.and = And
 +label.sequence_feature_colours = Sequence Feature Colours
  label.best_quality = Best Quality
  label.best_resolution = Best Resolution
  label.most_protein_chain = Most Protein Chain
@@@ -1322,97 -1344,3 +1333,97 @@@ 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.continue_operation = Continue operation?
 +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.scheme_examples = Scheme examples
 +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 = Auto-delete 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.custom = Custom
 +label.default = Default
 +label.single_file = Single backup
 +label.keep_all_versions = Keep all versions
 +label.rolled_backups = Rolled backup files
 +label.customise_description = Select Customise, make changes, and click on OK to save your own custom scheme
 +label.custom_description = Your own saved scheme
 +label.default_description = Keep the last three versions of the file
 +label.single_file_description = Keep the last version of the file
 +label.keep_all_versions_description = Keep all previous versions of the file
 +label.rolled_backups_description = Keep the last nine versions of the file from _bak.1 (newest) to _bak.9 (oldest)
 +label.cancel_changes_description = Cancel changes made to your last saved Custom 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.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
 +label.show_linked_features = Show {0} features
 +label.on_top = on top
 +label.include_linked_features = Include {0} features
 +label.include_linked_tooltip = Include visible {0} features<br>converted to local sequence coordinates
 +label.features_not_shown = {0} feature(s) not shown
 +label.no_features_to_sort_by = No features to sort by
 +label.ignore_hidden = Ignore hidden columns
 +label.ignore_hidden_tooltip = Ignore any characters in hidden columns when matching
 +label.log_level = Log level
 +label.log_level_tooltip = Temporarily set the log level for this console. The log level will revert to {0} when this Java console is closed.
 +label.copy_to_clipboard = Copy to clipboard
 +label.copy_to_clipboard_tooltip = Copy all of the log text in this console to the system clipboard
 +label.startup = Startup
 +label.memory = Memory
 +label.customise_memory_settings = Customise maximum memory settings
 +label.memory_setting_text = New memory settings will only come into effect the next time you start Jalview
 +label.maximum_memory_used = Maximum memory limited to both
 +label.percent_of_physical_memory = Maximum percent of physical memory
 +label.maximum_memory = Maximum absolute memory
 +label.maximum_memory_tooltip = Enter memory as an integer number optionally followed by 'b', 'k', 'm', 'g' or 't'
 +label.adjustments_for_this_computer = Adjustments for this computer
 +label.memory_example_text = Maximum memory that would be used with these settings on this computer
 +label.memory_example_tooltip = The memory allocated to Jalview is the smaller of the percentage of physical memory (default 90%) and the maximum absolute memory (default 32GB). If your computer's memory cannot be ascertained then the maximum absolute memory defaults to 8GB (if not customised).<br>Jalview will always try and reserve 512MB for the OS and at least 512MB for itself.
@@@ -29,12 -29,15 +29,15 @@@ import jalview.datamodel.SequenceFeatur
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  import jalview.datamodel.SequenceNode;
+ import jalview.ext.treeviewer.TreeI;
+ import jalview.ext.treeviewer.TreeNodeI;
  import jalview.util.QuickSort;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.Iterator;
  import java.util.List;
+ import java.util.Map;
  
  /**
   * Routines for manipulating the order of a multiple sequence alignment TODO:
@@@ -69,6 -72,8 +72,8 @@@ public class AlignmentSorte
  
    static TreeModel lastTree = null;
  
+   static TreeI lastExternalTree = null;
    static boolean sortTreeAscending = true;
  
    /*
      }
  
      // NOTE: DO NOT USE align.setSequenceAt() here - it will NOT work
 -    List<SequenceI> asq;
 -    synchronized (asq = align.getSequences())
 +    List<SequenceI> asq = align.getSequences();
 +    synchronized (asq)
      {
        for (int i = 0; i < len; i++)
        {
    public static void setOrder(AlignmentI align, SequenceI[] seqs)
    {
      // NOTE: DO NOT USE align.setSequenceAt() here - it will NOT work
 -    List<SequenceI> algn;
 -    synchronized (algn = align.getSequences())
 +    List<SequenceI> algn = align.getSequences();
 +    synchronized (algn)
      {
        List<SequenceI> tmp = new ArrayList<>();
  
  
      List<SequenceI> tmp = new ArrayList<>();
  
-     tmp = _sortByTree(tree.getTopNode(), tmp, align.getSequences());
+     tmp = _sortByTree(tree.getTopNode(), tmp);
  
      if (tmp.size() != nSeq)
      {
      return tmp;
    }
  
+   private static List<SequenceI> getOrderByTree(TreeI aptxTree,
+           Map<TreeNodeI, SequenceI> nodesWithBoundSeqs)
+   {
+     List<SequenceI> seqsByTreeOrder = new ArrayList<>();
+     if (!aptxTree.isEmpty())
+     {
+       for (final Iterator<TreeNodeI> iter = aptxTree
+               .iterateInPreOrder(); iter.hasNext();)
+       {
+         TreeNodeI treeNode = iter.next();
+         seqsByTreeOrder.add(nodesWithBoundSeqs.get(treeNode));
+       }
+     }
+     return seqsByTreeOrder;
+   }
    /**
     * Sorts the alignment by a given tree
     * 
    }
  
    /**
+    * Sorts the alignment by a given tree from Archaeopteryx
+    * 
+    * @param align
+    *          alignment to order
+    * @param tree
+    *          tree which has
+    */
+   public static void sortByTree(AlignmentI align,
+           Map<TreeNodeI, SequenceI> nodesBoundToSequences,
+           TreeI treeI) throws IllegalArgumentException
+   {
+     List<SequenceI> tmp = getOrderByTree(treeI, nodesBoundToSequences);
+     if (!tmp.isEmpty())
+     {
+       if (lastExternalTree != treeI)
+       {
+         sortTreeAscending = true;
+         lastExternalTree = treeI;
+       }
+       else
+       {
+         sortTreeAscending = !sortTreeAscending;
+       }
+       if (sortTreeAscending)
+       {
+         setOrder(align, tmp);
+       }
+       else
+       {
+         setReverseOrder(align,
+                 vectorSubsetToArray(tmp, align.getSequences()));
+       }
+     }
+     else
+     {
+       throw new IllegalArgumentException();
+     }
+   }
+   /**
     * DOCUMENT ME!
     * 
     * @param align
     * @return DOCUMENT ME!
     */
    private static List<SequenceI> _sortByTree(SequenceNode node,
-           List<SequenceI> tmp, List<SequenceI> seqset)
+           List<SequenceI> tmp)
    {
      if (node == null)
      {
      }
      else
      {
-       _sortByTree(left, tmp, seqset);
-       _sortByTree(right, tmp, seqset);
+       _sortByTree(left, tmp);
+       _sortByTree(right, tmp);
      }
  
      return tmp;
    }
  
    // Ordering Objects
    // Alignment.sortBy(OrderObj) - sequence of sequence pointer refs in
    // appropriate order
  
      for (int i = 0; i < alignment.length; i++)
      {
 -      ids[i] = (new Float(alignment[i].getName().substring(8)))
 +      ids[i] = (Float.valueOf(alignment[i].getName().substring(8)))
                .floatValue();
      }
  
      if (method != FEATURE_SCORE && method != FEATURE_LABEL
              && method != FEATURE_DENSITY)
      {
 -      String msg = String
 -              .format("Implementation Error - sortByFeature method must be either '%s' or '%s'",
 -                      FEATURE_SCORE, FEATURE_DENSITY);
 +      String msg = String.format(
 +              "Implementation Error - sortByFeature method must be either '%s' or '%s'",
 +              FEATURE_SCORE, FEATURE_DENSITY);
        System.err.println(msg);
        return;
      }
  
 -    flipFeatureSortIfUnchanged(method, featureTypes, groups, startCol, endCol);
 +    flipFeatureSortIfUnchanged(method, featureTypes, groups, startCol,
 +            endCol);
  
      SequenceI[] seqs = alignment.getSequencesArray();
  
         * get sequence residues overlapping column region
         * and features for residue positions and specified types
         */
 -      String[] types = featureTypes == null ? null : featureTypes
 -              .toArray(new String[featureTypes.size()]);
 +      String[] types = featureTypes == null ? null
 +              : featureTypes.toArray(new String[featureTypes.size()]);
        List<SequenceFeature> sfs = seqs[i].findFeatures(startCol + 1,
                endCol + 1, types);
  
@@@ -44,8 -44,6 +44,8 @@@ import java.awt.event.MouseListener
  import java.awt.event.MouseMotionListener;
  import java.beans.PropertyChangeEvent;
  
 +import javax.swing.SwingUtilities;
 +
  public class OverviewPanel extends Panel implements Runnable,
          MouseMotionListener, MouseListener, ViewportListenerI
  {
    @Override
    public void mouseClicked(MouseEvent evt)
    {
 -    if ((evt.getModifiers()
 -            & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
 +    if ((evt.getModifiersEx()
 +            & InputEvent.BUTTON3_DOWN_MASK) == InputEvent.BUTTON3_DOWN_MASK)
      {
        showPopupMenu(evt);
      }
    {
      if (od.isPositionInBox(evt.getX(), evt.getY()))
      {
 -      // display drag cursor at mouse position
 -      setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
 +      this.getParent()
 +              .setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
      }
      else
      {
 -      // reset cursor
 -      setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
 +      this.getParent().setCursor(
 +              Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
      }
    }
  
    @Override
    public void mousePressed(MouseEvent evt)
    {
 -    if ((evt.getModifiers()
 -            & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
 +    if ((evt.getModifiersEx()
 +            & InputEvent.BUTTON3_DOWN_MASK) == InputEvent.BUTTON3_DOWN_MASK)
      {
 -      if (!Platform.isAMac())
 +      if (!Platform.isMac()) // BH was excluding JavaScript
        {
          showPopupMenu(evt);
        }
        // (wait to see if it's a drag instead)
        // otherwise update the viewport
        if (!od.isPositionInBox(evt.getX(), evt.getY()))
 -      { 
 -      draggingBox = false;
 +      {
 +        draggingBox = false;
 +
 +        // display drag cursor at mouse position
 +        setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
 +
          od.updateViewportFromMouse(evt.getX(), evt.getY(),
                  av.getAlignment().getHiddenSequences(),
                  av.getAlignment().getHiddenColumns());
 +        getParent()
 +                .setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        }
        else
        {
    @Override
    public void mouseReleased(MouseEvent evt)
    {
 +    draggingBox = false;
    }
  
    @Override
    public void mouseDragged(MouseEvent evt)
    {
 -    if ((evt.getModifiers()
 -            & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
 +    if (Platform.isWinRightButton(evt))
      {
 -      if (!Platform.isAMac())
 -      {
 -        showPopupMenu(evt);
 -      }
 +      showPopupMenu(evt);
 +      return;
 +    }
 +
 +    if (SwingUtilities.isRightMouseButton(evt))
 +    {
 +      return;
 +    }
 +
 +    if (draggingBox)
 +    {
 +      // set the mouse position as a fixed point in the box
 +      // and drag relative to that position
 +      od.adjustViewportFromMouse(evt.getX(), evt.getY(),
 +              av.getAlignment().getHiddenSequences(),
 +              av.getAlignment().getHiddenColumns());
      }
      else
      {
 -      if (draggingBox)
 -      {
 -        // set the mouse position as a fixed point in the box
 -        // and drag relative to that position
 -        od.adjustViewportFromMouse(evt.getX(), evt.getY(),
 -                av.getAlignment().getHiddenSequences(),
 -                av.getAlignment().getHiddenColumns());
 -      }
 -      else
 -      {
 -        od.updateViewportFromMouse(evt.getX(), evt.getY(),
 -                av.getAlignment().getHiddenSequences(),
 -                av.getAlignment().getHiddenColumns());
 -      }
 -      ap.paintAlignment(false, false);
 +      od.updateViewportFromMouse(evt.getX(), evt.getY(),
 +              av.getAlignment().getHiddenSequences(),
 +              av.getAlignment().getHiddenColumns());
      }
 +    ap.paintAlignment(false, false);
    }
  
    /**
  
        updateRunning = true;
      }
-     Thread thread = new Thread(this, "OverviewUpdateThread");
+     Thread thread = new Thread(this, "OverviewUpdate");
      thread.start();
      repaint();
      updateRunning = false;
      } finally
      {
        av = null;
 +      if (oviewCanvas != null)
 +      {
 +        oviewCanvas.dispose();
 +      }
        oviewCanvas = null;
        ap = null;
        od = null;
@@@ -117,7 -117,7 +117,7 @@@ public class PCAPanel extends EmbmenuFr
              MessageManager.getString("label.principal_component_analysis"),
              475, 400);
  
-     Thread worker = new Thread(this, "PCACalcThread");
+     Thread worker = new Thread(this, "PCACalc");
      worker.start();
    }
  
      {
        nuclSetting.setState(pcaModel.isNucleotide());
        protSetting.setState(!pcaModel.isNucleotide());
 -      pcaModel.run();
 +      pcaModel.calculate();
        // ////////////////
        xCombobox.select(0);
        yCombobox.select(1);
      int dim2 = top - yCombobox.getSelectedIndex();
      int dim3 = top - zCombobox.getSelectedIndex();
      pcaModel.updateRcView(dim1, dim2, dim3);
 -    rc.img = null;
 -    rc.rotmat.setIdentity();
 -    rc.initAxes();
 +    rc.resetView();
      rc.paint(rc.getGraphics());
    }
  
          ScoreModelI scoreModel = ScoreModels.getInstance()
                  .getDefaultModel(false);
          pcaModel.setScoreModel(scoreModel);
-         new Thread(this, "PCARecalcThread").start();
+         new Thread(this, "PCARecalc").start();
        }
      }
      else if (evt.getSource() == protSetting)
          ScoreModelI scoreModel = ScoreModels.getInstance()
                  .getDefaultModel(true);
          pcaModel.setScoreModel(scoreModel);
-         new Thread(this, "PCARecalcThread").start();
+         new Thread(this, "PCARecalc").start();
        }
      }
    }
      {
      }
      ;
 -    Object[] alAndColsel = pcaModel.getSeqtrings()
 +    Object[] alAndColsel = pcaModel.getInputData()
              .getAlignmentAndHiddenColumns(gc);
  
      if (alAndColsel != null && alAndColsel[0] != null)
   */
  package jalview.bin;
  
 -import jalview.ext.so.SequenceOntology;
 -import jalview.gui.AlignFrame;
 -import jalview.gui.Desktop;
 -import jalview.gui.PromptUserConfig;
 -import jalview.io.AppletFormatAdapter;
 -import jalview.io.BioJsHTMLOutput;
 -import jalview.io.DataSourceType;
 -import jalview.io.FileFormat;
 -import jalview.io.FileFormatException;
 -import jalview.io.FileFormatI;
 -import jalview.io.FileLoader;
 -import jalview.io.HtmlSvgOutput;
 -import jalview.io.IdentifyFile;
 -import jalview.io.NewickFile;
 -import jalview.io.gff.SequenceOntologyFactory;
 -import jalview.schemes.ColourSchemeI;
 -import jalview.schemes.ColourSchemeProperty;
 -import jalview.util.MessageManager;
 -import jalview.util.Platform;
 -import jalview.ws.jws2.Jws2Discoverer;
 -
  import java.io.BufferedReader;
  import java.io.File;
  import java.io.FileOutputStream;
@@@ -37,44 -58,10 +37,44 @@@ import java.security.PermissionCollecti
  import java.security.Permissions;
  import java.security.Policy;
  import java.util.HashMap;
 +import java.util.Locale;
  import java.util.Map;
  import java.util.Vector;
 +import java.util.logging.ConsoleHandler;
 +import java.util.logging.Level;
 +import java.util.logging.Logger;
  
  import javax.swing.UIManager;
 +import javax.swing.UIManager.LookAndFeelInfo;
 +
 +import com.threerings.getdown.util.LaunchUtil;
 +
 +//import edu.stanford.ejalbert.launching.IBrowserLaunching;
 +import groovy.lang.Binding;
 +import groovy.util.GroovyScriptEngine;
 +import jalview.ext.so.SequenceOntology;
 +import jalview.gui.AlignFrame;
 +import jalview.gui.Desktop;
 +import jalview.gui.PromptUserConfig;
 +import jalview.io.AppletFormatAdapter;
 +import jalview.io.BioJsHTMLOutput;
 +import jalview.io.DataSourceType;
 +import jalview.io.FileFormat;
 +import jalview.io.FileFormatException;
 +import jalview.io.FileFormatI;
 +import jalview.io.FileFormats;
 +import jalview.io.FileLoader;
 +import jalview.io.HtmlSvgOutput;
 +import jalview.io.IdentifyFile;
 +import jalview.io.NewickFile;
 +import jalview.io.gff.SequenceOntologyFactory;
 +import jalview.schemes.ColourSchemeI;
 +import jalview.schemes.ColourSchemeProperty;
 +import jalview.util.ChannelProperties;
 +import jalview.util.HttpUtils;
 +import jalview.util.MessageManager;
 +import jalview.util.Platform;
 +import jalview.ws.jws2.Jws2Discoverer;
  
  import groovy.lang.Binding;
  import groovy.util.GroovyScriptEngine;
  /**
   * Main class for Jalview Application <br>
   * <br>
 - * start with java -Djava.ext.dirs=$PATH_TO_LIB$ jalview.bin.Jalview
 + * start with: java -classpath "$PATH_TO_LIB$/*:$PATH_TO_CLASSES$" \
 + * jalview.bin.Jalview
 + * 
 + * or on Windows: java -classpath "$PATH_TO_LIB$/*;$PATH_TO_CLASSES$" \
 + * jalview.bin.Jalview jalview.bin.Jalview
 + * 
 + * (ensure -classpath arg is quoted to avoid shell expansion of '*' and do not
 + * embellish '*' to e.g. '*.jar')
   * 
   * @author $author$
   * @version $Revision$
   */
  public class Jalview
  {
 +  static
 +  {
 +    Platform.getURLCommandArguments();
 +    Platform.addJ2SDirectDatabaseCall("https://www.jalview.org");
 +    Platform.addJ2SDirectDatabaseCall("http://www.jalview.org");
 +    Platform.addJ2SDirectDatabaseCall("http://www.compbio.dundee.ac.uk");
 +    Platform.addJ2SDirectDatabaseCall("https://www.compbio.dundee.ac.uk");
 +  }
 +
    /*
     * singleton instance of this class
     */
  
    static
    {
 -    // grab all the rights we can the JVM
 -    Policy.setPolicy(new Policy()
 +    if (!Platform.isJS())
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
      {
 -      @Override
 -      public PermissionCollection getPermissions(CodeSource codesource)
 +      // grab all the rights we can for the JVM
 +      Policy.setPolicy(new Policy()
        {
 -        Permissions perms = new Permissions();
 -        perms.add(new AllPermission());
 -        return (perms);
 -      }
 +        @Override
 +        public PermissionCollection getPermissions(CodeSource codesource)
 +        {
 +          Permissions perms = new Permissions();
 +          perms.add(new AllPermission());
 +          return (perms);
 +        }
  
 -      @Override
 -      public void refresh()
 -      {
 -      }
 -    });
 +        @Override
 +        public void refresh()
 +        {
 +        }
 +      });
 +    }
    }
  
    /**
    class FeatureFetcher
    {
      /*
 -     * TODO: generalise to track all jalview events to orchestrate batch
 -     * processing events.
 +     * TODO: generalise to track all jalview events to orchestrate batch processing
 +     * events.
       */
  
      private int queued = 0;
            af.setProgressBar(MessageManager
                    .getString("status.das_features_being_retrived"), id);
            af.featureSettings_actionPerformed(null);
 -          af.featureSettings.fetchDasFeatures(dasSources, true);
            af.setProgressBar(null, id);
            synchronized (us)
            {
              running--;
            }
          }
-       }, "FeatureFetcherThread").start();
+       }, "FeatureFetcher").start();
      }
  
      public synchronized boolean allFinished()
     */
    public static void main(String[] args)
    {
 +    // setLogging(); // BH - for event debugging in JavaScript
      instance = new Jalview();
      instance.doMain(args);
    }
  
 +  private static void logClass(String name)
 +  {
 +    // BH - for event debugging in JavaScript
 +    ConsoleHandler consoleHandler = new ConsoleHandler();
 +    consoleHandler.setLevel(Level.ALL);
 +    Logger logger = Logger.getLogger(name);
 +    logger.setLevel(Level.ALL);
 +    logger.addHandler(consoleHandler);
 +  }
 +
 +  @SuppressWarnings("unused")
 +  private static void setLogging()
 +  {
 +
 +    /**
 +     * @j2sIgnore
 +     * 
 +     */
 +    {
 +      System.out.println("not in js");
 +    }
 +
 +    // BH - for event debugging in JavaScript (Java mode only)
 +    if (!Platform.isJS())
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
 +    {
 +      Logger.getLogger("").setLevel(Level.ALL);
 +      logClass("java.awt.EventDispatchThread");
 +      logClass("java.awt.EventQueue");
 +      logClass("java.awt.Component");
 +      logClass("java.awt.focus.Component");
 +      logClass("java.awt.focus.DefaultKeyboardFocusManager");
 +    }
 +
 +  }
 +
    /**
     * @param args
     */
    void doMain(String[] args)
    {
 -    System.setSecurityManager(null);
 +
 +    if (!Platform.isJS())
 +    {
 +      System.setSecurityManager(null);
 +    }
 +
      System.out
              .println("Java version: " + System.getProperty("java.version"));
 +    System.out.println("Java Home: " + System.getProperty("java.home"));
      System.out.println(System.getProperty("os.arch") + " "
              + System.getProperty("os.name") + " "
              + System.getProperty("os.version"));
 -
 -    ArgsParser aparser = new ArgsParser(args);
 -    boolean headless = false;
 -
 -    if (aparser.contains("help") || aparser.contains("h"))
 +    String val = System.getProperty("sys.install4jVersion");
 +    if (val != null)
      {
 -      showUsage();
 -      System.exit(0);
 +      System.out.println("Install4j version: " + val);
      }
 -    if (aparser.contains("nodisplay") || aparser.contains("nogui")
 -            || aparser.contains("headless"))
 +    val = System.getProperty("installer_template_version");
 +    if (val != null)
      {
 -      System.setProperty("java.awt.headless", "true");
 -      headless = true;
 +      System.out.println("Install4j template version: " + val);
      }
 +    val = System.getProperty("launcher_version");
 +    if (val != null)
 +    {
 +      System.out.println("Launcher version: " + val);
 +    }
 +
 +    // report Jalview version
 +    Cache.loadBuildProperties(true);
 +
 +    ArgsParser aparser = new ArgsParser(args);
 +    boolean headless = false;
 +
      String usrPropsFile = aparser.getValue("props");
      Cache.loadProperties(usrPropsFile); // must do this before
      if (usrPropsFile != null)
                "CMD [-props " + usrPropsFile + "] executed successfully!");
      }
  
 -    // anything else!
 -
 -    final String jabawsUrl = aparser.getValue("jabaws");
 -    if (jabawsUrl != null)
 +    if (!Platform.isJS())
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
      {
 -      try
 +      if (aparser.contains("help") || aparser.contains("h"))
        {
 -        Jws2Discoverer.getDiscoverer().setPreferredUrl(jabawsUrl);
 -        System.out.println(
 -                "CMD [-jabaws " + jabawsUrl + "] executed successfully!");
 -      } catch (MalformedURLException e)
 +        showUsage();
 +        System.exit(0);
 +      }
 +      if (aparser.contains("nodisplay") || aparser.contains("nogui")
 +              || aparser.contains("headless"))
        {
 -        System.err.println(
 -                "Invalid jabaws parameter: " + jabawsUrl + " ignored");
 +        System.setProperty("java.awt.headless", "true");
 +        headless = true;
 +      }
 +      // anything else!
 +
 +      // allow https handshakes to download intermediate certs if necessary
 +      System.setProperty("com.sun.security.enableAIAcaIssuers", "true");
 +
 +      final String jabawsUrl = aparser.getValue("jabaws");
 +      if (jabawsUrl != null)
 +      {
 +        try
 +        {
 +          Jws2Discoverer.getDiscoverer().setPreferredUrl(jabawsUrl);
 +          System.out.println(
 +                  "CMD [-jabaws " + jabawsUrl + "] executed successfully!");
 +        } catch (MalformedURLException e)
 +        {
 +          System.err.println(
 +                  "Invalid jabaws parameter: " + jabawsUrl + " ignored");
 +        }
        }
      }
  
        else
        {
          System.out.println("Executing setprop argument: " + defs);
 +        if (Platform.isJS())
 +        {
 +          Cache.setProperty(defs.substring(0, p), defs.substring(p + 1));
 +        }
          // DISABLED FOR SECURITY REASONS
          // TODO: add a property to allow properties to be overriden by cli args
          // Cache.setProperty(defs.substring(0,p), defs.substring(p+1));
      }
      System.setProperty("http.agent",
              "Jalview Desktop/" + Cache.getDefault("VERSION", "Unknown"));
 +
      try
      {
 -      Cache.initLogger();
 +      Console.initLogger();
      } catch (NoClassDefFoundError error)
      {
        error.printStackTrace();
        System.out.println("\nEssential logging libraries not found."
 -              + "\nUse: java -Djava.ext.dirs=$PATH_TO_LIB$ jalview.bin.Jalview");
 +              + "\nUse: java -classpath \"$PATH_TO_LIB$/*:$PATH_TO_CLASSES$\" jalview.bin.Jalview");
        System.exit(0);
      }
  
      desktop = null;
  
 -    try
 -    {
 -      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
 -    } catch (Exception ex)
 -    {
 -    }
 -    if (Platform.isAMac())
 -    {
 -      System.setProperty("com.apple.mrj.application.apple.menu.about.name",
 -              "Jalview");
 -      System.setProperty("apple.laf.useScreenMenuBar", "true");
 -      try
 -      {
 -        UIManager.setLookAndFeel(
 -                ch.randelshofer.quaqua.QuaquaManager.getLookAndFeel());
 -      } catch (Throwable e)
 -      {
 -        System.err.println(
 -                "Failed to set QuaQua look and feel: " + e.toString());
 -      }
 -    }
 +    setLookAndFeel();
  
      /*
 -     * configure 'full' SO model if preferences say to, 
 -     * else use the default (SO Lite)
 +     * configure 'full' SO model if preferences say to, else use the default (full SO)
 +     * - as JS currently doesn't have OBO parsing, it must use 'Lite' version
       */
 -    if (Cache.getDefault("USE_FULL_SO", false))
 +    boolean soDefault = !Platform.isJS();
 +    if (Cache.getDefault("USE_FULL_SO", soDefault))
      {
        SequenceOntologyFactory.setInstance(new SequenceOntology());
      }
  
      if (!headless)
      {
 +      Desktop.nosplash = aparser.contains("nosplash");
        desktop = new Desktop();
        desktop.setInBatchMode(true); // indicate we are starting up
 -      desktop.setVisible(true);
 -      desktop.startServiceDiscovery();
 -      if (!aparser.contains("nousagestats"))
 +
 +      try
        {
 -        startUsageStats(desktop);
 -      }
 -      else
 +        JalviewTaskbar.setTaskbar(this);
 +      } catch (Exception e)
        {
 -        System.err.println("CMD [-nousagestats] executed successfully!");
 +        Console.info("Cannot set Taskbar");
 +        Console.error(e.getMessage());
 +        // e.printStackTrace();
 +      } catch (Throwable t)
 +      {
 +        Console.info("Cannot set Taskbar");
 +        Console.error(t.getMessage());
 +        // t.printStackTrace();
        }
  
 -      if (!aparser.contains("noquestionnaire"))
 +      // set Proxy settings before all the internet calls
 +      Cache.setProxyPropertiesFromPreferences();
 +
 +      desktop.setVisible(true);
 +
 +      if (!Platform.isJS())
 +      /**
 +       * Java only
 +       * 
 +       * @j2sIgnore
 +       */
        {
 -        String url = aparser.getValue("questionnaire");
 -        if (url != null)
 +        if (!aparser.contains("nowebservicediscovery"))
          {
 -          // Start the desktop questionnaire prompter with the specified
 -          // questionnaire
 -          Cache.log.debug("Starting questionnaire url at " + url);
 -          desktop.checkForQuestionnaire(url);
 -          System.out.println(
 -                  "CMD questionnaire[-" + url + "] executed successfully!");
 +          desktop.startServiceDiscovery();
 +        }
 +        if (!aparser.contains("nousagestats"))
 +        {
 +          startUsageStats(desktop);
          }
          else
          {
 -          if (Cache.getProperty("NOQUESTIONNAIRES") == null)
 +          System.err.println("CMD [-nousagestats] executed successfully!");
 +        }
 +
 +        if (!aparser.contains("noquestionnaire"))
 +        {
 +          String url = aparser.getValue("questionnaire");
 +          if (url != null)
            {
              // Start the desktop questionnaire prompter with the specified
              // questionnaire
 -            // String defurl =
 -            // "http://anaplog.compbio.dundee.ac.uk/cgi-bin/questionnaire.pl";
 -            // //
 -            String defurl = "http://www.jalview.org/cgi-bin/questionnaire.pl";
 -            Cache.log.debug(
 -                    "Starting questionnaire with default url: " + defurl);
 -            desktop.checkForQuestionnaire(defurl);
 +            Console.debug("Starting questionnaire url at " + url);
 +            desktop.checkForQuestionnaire(url);
 +            System.out.println("CMD questionnaire[-" + url
 +                    + "] executed successfully!");
 +          }
 +          else
 +          {
 +            if (Cache.getProperty("NOQUESTIONNAIRES") == null)
 +            {
 +              // Start the desktop questionnaire prompter with the specified
 +              // questionnaire
 +              // String defurl =
 +              // "http://anaplog.compbio.dundee.ac.uk/cgi-bin/questionnaire.pl";
 +              // //
 +              String defurl = "https://www.jalview.org/cgi-bin/questionnaire.pl";
 +              Console.debug(
 +                      "Starting questionnaire with default url: " + defurl);
 +              desktop.checkForQuestionnaire(defurl);
 +            }
            }
          }
 -      }
 -      else
 -      {
 -        System.err.println("CMD [-noquestionnaire] executed successfully!");
 -      }
 +        else
 +        {
 +          System.err
 +                  .println("CMD [-noquestionnaire] executed successfully!");
 +        }
  
 -      if (!aparser.contains("nonews"))
 -      {
 -        desktop.checkForNews();
 +        if (!aparser.contains("nonews")
 +                || Cache.getProperty("NONEWS") == null)
 +        {
 +          desktop.checkForNews();
 +        }
 +
 +        if (!aparser.contains("nohtmltemplates")
 +                || Cache.getProperty("NOHTMLTEMPLATES") == null)
 +        {
 +          BioJsHTMLOutput.updateBioJS();
 +        }
        }
 +    }
  
 -      BioJsHTMLOutput.updateBioJS();
 +    // Move any new getdown-launcher-new.jar into place over old
 +    // getdown-launcher.jar
 +    String appdirString = System.getProperty("getdownappdir");
 +    if (appdirString != null && appdirString.length() > 0)
 +    {
 +      final File appdir = new File(appdirString);
 +      new Thread()
 +      {
 +        @Override
 +        public void run()
 +        {
 +          LaunchUtil.upgradeGetdown(
 +                  new File(appdir, "getdown-launcher-old.jar"),
 +                  new File(appdir, "getdown-launcher.jar"),
 +                  new File(appdir, "getdown-launcher-new.jar"));
 +        }
 +      }.start();
      }
  
      String file = null, data = null;
      FileFormatI format = null;
      DataSourceType protocol = null;
      FileLoader fileLoader = new FileLoader(!headless);
 -    Vector<String> getFeatures = null; // vector of das source nicknames to
 -                                       // fetch
 -    // features from
 -    // loading is done.
 +
      String groovyscript = null; // script to execute after all loading is
      // completed one way or another
      // extract groovy argument and execute if necessary
        System.out.println("No files to open!");
        System.exit(1);
      }
 -    String vamsasImport = aparser.getValue("vdoc");
 -    String vamsasSession = aparser.getValue("vsess");
 -    if (vamsasImport != null || vamsasSession != null)
 -    {
 -      if (desktop == null || headless)
 -      {
 -        System.out.println(
 -                "Headless vamsas sessions not yet supported. Sorry.");
 -        System.exit(1);
 -      }
 -      // if we have a file, start a new session and import it.
 -      boolean inSession = false;
 -      if (vamsasImport != null)
 -      {
 -        try
 -        {
 -          DataSourceType viprotocol = AppletFormatAdapter
 -                  .checkProtocol(vamsasImport);
 -          if (viprotocol == DataSourceType.FILE)
 -          {
 -            inSession = desktop.vamsasImport(new File(vamsasImport));
 -          }
 -          else if (viprotocol == DataSourceType.URL)
 -          {
 -            inSession = desktop.vamsasImport(new URL(vamsasImport));
 -          }
 -
 -        } catch (Exception e)
 -        {
 -          System.err.println("Exeption when importing " + vamsasImport
 -                  + " as a vamsas document.");
 -          e.printStackTrace();
 -        }
 -        if (!inSession)
 -        {
 -          System.err.println("Failed to import " + vamsasImport
 -                  + " as a vamsas document.");
 -        }
 -        else
 -        {
 -          System.out.println("Imported Successfully into new session "
 -                  + desktop.getVamsasApplication().getCurrentSession());
 -        }
 -      }
 -      if (vamsasSession != null)
 -      {
 -        if (vamsasImport != null)
 -        {
 -          // close the newly imported session and import the Jalview specific
 -          // remnants into the new session later on.
 -          desktop.vamsasStop_actionPerformed(null);
 -        }
 -        // now join the new session
 -        try
 -        {
 -          if (desktop.joinVamsasSession(vamsasSession))
 -          {
 -            System.out.println(
 -                    "Successfully joined vamsas session " + vamsasSession);
 -          }
 -          else
 -          {
 -            System.err.println("WARNING: Failed to join vamsas session "
 -                    + vamsasSession);
 -          }
 -        } catch (Exception e)
 -        {
 -          System.err.println(
 -                  "ERROR: Failed to join vamsas session " + vamsasSession);
 -          e.printStackTrace();
 -        }
 -        if (vamsasImport != null)
 -        {
 -          // the Jalview specific remnants can now be imported into the new
 -          // session at the user's leisure.
 -          Cache.log.info(
 -                  "Skipping Push for import of data into existing vamsas session."); // TODO:
 -          // enable
 -          // this
 -          // when
 -          // debugged
 -          // desktop.getVamsasApplication().push_update();
 -        }
 -      }
 -    }
      long progress = -1;
      // Finally, deal with the remaining input data.
      if (file != null)
        }
        System.out.println("CMD [-open " + file + "] executed successfully!");
  
 -      if (!file.startsWith("http://"))
 +      if (!Platform.isJS())
 +      /**
 +       * ignore in JavaScript -- can't just file existence - could load it?
 +       * 
 +       * @j2sIgnore
 +       */
        {
 -        if (!(new File(file)).exists())
 +        if (!HttpUtils.startsWithHttpOrHttps(file))
          {
 -          System.out.println("Can't find " + file);
 -          if (headless)
 +          if (!(new File(file)).exists())
            {
 -            System.exit(1);
 +            System.out.println("Can't find " + file);
 +            if (headless)
 +            {
 +              System.exit(1);
 +            }
            }
          }
        }
          {
            data.replaceAll("%20", " ");
  
 -          ColourSchemeI cs = ColourSchemeProperty
 -                  .getColourScheme(af.getViewport().getAlignment(), data);
 +          ColourSchemeI cs = ColourSchemeProperty.getColourScheme(
 +                  af.getViewport(), af.getViewport().getAlignment(), data);
  
            if (cs != null)
            {
          // TODO - load PDB structure(s) to alignment JAL-629
          // (associate with identical sequence in alignment, or a specified
          // sequence)
 -
 -        getFeatures = checkDasArguments(aparser);
 -        if (af != null && getFeatures != null)
 -        {
 -          FeatureFetcher ff = startFeatureFetching(getFeatures);
 -          if (ff != null)
 -          {
 -            while (!ff.allFinished() || af.operationInProgress())
 -            {
 -              // wait around until fetching is finished.
 -              try
 -              {
 -                Thread.sleep(100);
 -              } catch (Exception e)
 -              {
 -
 -              }
 -            }
 -          }
 -          getFeatures = null; // have retrieved features - forget them now.
 -        }
          if (groovyscript != null)
          {
            // Execute the groovy script after we've done all the rendering stuff
              af.createEPS(outputFile);
              continue;
            }
 -
 -          if (af.saveAlignment(file, format))
 +          FileFormatI outFormat = null;
 +          try
            {
 -            System.out.println("Written alignment in " + format
 -                    + " format to " + file);
 +            outFormat = FileFormats.getInstance().forName(outputFormat);
 +          } catch (Exception formatP)
 +          {
 +            System.out.println("Couldn't parse " + outFormat
 +                    + " as a valid Jalview format string.");
            }
 -          else
 +          if (outFormat != null)
            {
 -            System.out.println("Error writing file " + file + " in "
 -                    + format + " format!!");
 +            if (!outFormat.isWritable())
 +            {
 +              System.out.println(
 +                      "This version of Jalview does not support alignment export as "
 +                              + outputFormat);
 +            }
 +            else
 +            {
 +              af.saveAlignment(file, outFormat);
 +              if (af.isSaveAlignmentSuccessful())
 +              {
 +                System.out.println("Written alignment in "
 +                        + outFormat.getName() + " format to " + file);
 +              }
 +              else
 +              {
 +                System.out.println("Error writing file " + file + " in "
 +                        + outFormat.getName() + " format!!");
 +              }
 +            }
            }
  
          }
      // And the user
      // ////////////////////
  
 -    if (!headless && file == null && vamsasImport == null
 -            && jalview.bin.Cache.getDefault("SHOW_STARTUP_FILE", true))
 +    if (!Platform.isJS() && !headless && file == null
 +            && Cache.getDefault("SHOW_STARTUP_FILE", true))
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
      {
 -      file = jalview.bin.Cache.getDefault("STARTUP_FILE",
 -              jalview.bin.Cache.getDefault("www.jalview.org",
 -                      "http://www.jalview.org")
 -                      + "/examples/exampleFile_2_7.jar");
 -      if (file.equals(
 -              "http://www.jalview.org/examples/exampleFile_2_3.jar"))
 +      file = Cache.getDefault("STARTUP_FILE",
 +              Cache.getDefault("www.jalview.org", "https://www.jalview.org")
 +                      + "/examples/exampleFile_2_7.jvp");
 +      if (file.equals("http://www.jalview.org/examples/exampleFile_2_3.jar")
 +              || file.equals(
 +                      "http://www.jalview.org/examples/exampleFile_2_7.jar"))
        {
 +        file.replace("http:", "https:");
          // hardwire upgrade of the startup file
 -        file.replace("_2_3.jar", "_2_7.jar");
 +        file.replace("_2_3", "_2_7");
 +        file.replace("2_7.jar", "2_7.jvp");
          // and remove the stale setting
 -        jalview.bin.Cache.removeProperty("STARTUP_FILE");
 +        Cache.removeProperty("STARTUP_FILE");
        }
  
 -      protocol = DataSourceType.FILE;
 -
 -      if (file.indexOf("http:") > -1)
 -      {
 -        protocol = DataSourceType.URL;
 -      }
 +      protocol = AppletFormatAdapter.checkProtocol(file);
  
        if (file.endsWith(".jar"))
        {
  
        startUpAlframe = fileLoader.LoadFileWaitTillLoaded(file, protocol,
                format);
 -      getFeatures = checkDasArguments(aparser);
        // extract groovy arguments before anything else.
      }
 -    // If the user has specified features to be retrieved,
 -    // or a groovy script to be executed, do them if they
 -    // haven't been done already
 -    // fetch features for the default alignment
 -    if (getFeatures != null)
 -    {
 -      if (startUpAlframe != null)
 -      {
 -        startFeatureFetching(getFeatures);
 -      }
 -    }
 +
      // Once all other stuff is done, execute any groovy scripts (in order)
      if (groovyscript != null)
      {
      }
    }
  
 +  private static void setLookAndFeel()
 +  {
 +    // property laf = "crossplatform", "system", "gtk", "metal", "nimbus" or
 +    // "mac"
 +    // If not set (or chosen laf fails), use the normal SystemLaF and if on Mac,
 +    // try Quaqua/Vaqua.
 +    String lafProp = System.getProperty("laf");
 +    String lafSetting = Cache.getDefault("PREFERRED_LAF", null);
 +    String laf = "none";
 +    if (lafProp != null)
 +    {
 +      laf = lafProp;
 +    }
 +    else if (lafSetting != null)
 +    {
 +      laf = lafSetting;
 +    }
 +    boolean lafSet = false;
 +    switch (laf)
 +    {
 +    case "crossplatform":
 +      lafSet = setCrossPlatformLookAndFeel();
 +      if (!lafSet)
 +      {
 +        Console.error("Could not set requested laf=" + laf);
 +      }
 +      break;
 +    case "system":
 +      lafSet = setSystemLookAndFeel();
 +      if (!lafSet)
 +      {
 +        Console.error("Could not set requested laf=" + laf);
 +      }
 +      break;
 +    case "gtk":
 +      lafSet = setGtkLookAndFeel();
 +      if (!lafSet)
 +      {
 +        Console.error("Could not set requested laf=" + laf);
 +      }
 +      break;
 +    case "metal":
 +      lafSet = setMetalLookAndFeel();
 +      if (!lafSet)
 +      {
 +        Console.error("Could not set requested laf=" + laf);
 +      }
 +      break;
 +    case "nimbus":
 +      lafSet = setNimbusLookAndFeel();
 +      if (!lafSet)
 +      {
 +        Console.error("Could not set requested laf=" + laf);
 +      }
 +      break;
 +    case "quaqua":
 +      lafSet = setQuaquaLookAndFeel();
 +      if (!lafSet)
 +      {
 +        Console.error("Could not set requested laf=" + laf);
 +      }
 +      break;
 +    case "vaqua":
 +      lafSet = setVaquaLookAndFeel();
 +      if (!lafSet)
 +      {
 +        Console.error("Could not set requested laf=" + laf);
 +      }
 +      break;
 +    case "mac":
 +      lafSet = setMacLookAndFeel();
 +      if (!lafSet)
 +      {
 +        Console.error("Could not set requested laf=" + laf);
 +      }
 +      break;
 +    case "none":
 +      break;
 +    default:
 +      Console.error("Requested laf=" + laf + " not implemented");
 +    }
 +    if (!lafSet)
 +    {
 +      setSystemLookAndFeel();
 +      if (Platform.isLinux())
 +      {
 +        setMetalLookAndFeel();
 +      }
 +      if (Platform.isMac())
 +      {
 +        setMacLookAndFeel();
 +      }
 +    }
 +  }
 +
 +  private static boolean setCrossPlatformLookAndFeel()
 +  {
 +    boolean set = false;
 +    try
 +    {
 +      UIManager.setLookAndFeel(
 +              UIManager.getCrossPlatformLookAndFeelClassName());
 +      set = true;
 +    } catch (Exception ex)
 +    {
 +      Console.error("Unexpected Look and Feel Exception");
 +      Console.error(ex.getMessage());
 +      Console.debug(Cache.getStackTraceString(ex));
 +    }
 +    return set;
 +  }
 +
 +  private static boolean setSystemLookAndFeel()
 +  {
 +    boolean set = false;
 +    try
 +    {
 +      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
 +      set = true;
 +    } catch (Exception ex)
 +    {
 +      Console.error("Unexpected Look and Feel Exception");
 +      Console.error(ex.getMessage());
 +      Console.debug(Cache.getStackTraceString(ex));
 +    }
 +    return set;
 +  }
 +
 +  private static boolean setSpecificLookAndFeel(String name,
 +          String className, boolean nameStartsWith)
 +  {
 +    boolean set = false;
 +    try
 +    {
 +      for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels())
 +      {
 +        if (info.getName() != null && nameStartsWith
 +                ? info.getName().toLowerCase(Locale.ROOT)
 +                        .startsWith(name.toLowerCase(Locale.ROOT))
 +                : info.getName().toLowerCase(Locale.ROOT)
 +                        .equals(name.toLowerCase(Locale.ROOT)))
 +        {
 +          className = info.getClassName();
 +          break;
 +        }
 +      }
 +      UIManager.setLookAndFeel(className);
 +      set = true;
 +    } catch (Exception ex)
 +    {
 +      Console.error("Unexpected Look and Feel Exception");
 +      Console.error(ex.getMessage());
 +      Console.debug(Cache.getStackTraceString(ex));
 +    }
 +    return set;
 +  }
 +
 +  private static boolean setGtkLookAndFeel()
 +  {
 +    return setSpecificLookAndFeel("gtk",
 +            "com.sun.java.swing.plaf.gtk.GTKLookAndFeel", true);
 +  }
 +
 +  private static boolean setMetalLookAndFeel()
 +  {
 +    return setSpecificLookAndFeel("metal",
 +            "javax.swing.plaf.metal.MetalLookAndFeel", false);
 +  }
 +
 +  private static boolean setNimbusLookAndFeel()
 +  {
 +    return setSpecificLookAndFeel("nimbus",
 +            "javax.swing.plaf.nimbus.NimbusLookAndFeel", false);
 +  }
 +
 +  private static boolean setQuaquaLookAndFeel()
 +  {
 +    return setSpecificLookAndFeel("quaqua",
 +            ch.randelshofer.quaqua.QuaquaManager.getLookAndFeel().getClass()
 +                    .getName(),
 +            false);
 +  }
 +
 +  private static boolean setVaquaLookAndFeel()
 +  {
 +    return setSpecificLookAndFeel("vaqua",
 +            "org.violetlib.aqua.AquaLookAndFeel", false);
 +  }
 +
 +  private static boolean setMacLookAndFeel()
 +  {
 +    boolean set = false;
 +    System.setProperty("com.apple.mrj.application.apple.menu.about.name",
 +            ChannelProperties.getProperty("app_name"));
 +    System.setProperty("apple.laf.useScreenMenuBar", "true");
 +    set = setQuaquaLookAndFeel();
 +    if ((!set) || !UIManager.getLookAndFeel().getClass().toString()
 +            .toLowerCase(Locale.ROOT).contains("quaqua"))
 +    {
 +      set = setVaquaLookAndFeel();
 +    }
 +    return set;
 +  }
 +
    private static void showUsage()
    {
      System.out.println(
                      // (quote the 'PROPERTY=VALUE' pair to ensure spaces are
                      // passed in correctly)"
                      + "-jabaws URL\tSpecify URL for Jabaws services (e.g. for a local installation).\n"
 -                    + "-dasserver nickname=URL\tAdd and enable a das server with given nickname\n\t\t\t(alphanumeric or underscores only) for retrieval of features for all alignments.\n"
 -                    + "\t\t\tSources that also support the sequence command may be specified by prepending the URL with sequence:\n"
 -                    + "\t\t\t e.g. sequence:http://localdas.somewhere.org/das/source)\n"
                      + "-fetchfrom nickname\tQuery nickname for features for the alignments and display them.\n"
 -                    // +
 -                    // "-vdoc vamsas-document\tImport vamsas document into new
 -                    // session or join existing session with same URN\n"
 -                    // + "-vses vamsas-session\tJoin session with given URN\n"
                      + "-groovy FILE\tExecute groovy script in FILE, after all other arguments have been processed (if FILE is the text 'STDIN' then the file will be read from STDIN)\n"
 -                    + "\n~Read documentation in Application or visit http://www.jalview.org for description of Features and Annotations file~\n\n");
 +                    + "-jvmmempc=PERCENT\tOnly available with standalone executable jar or jalview.bin.Launcher. Limit maximum heap size (memory) to PERCENT% of total physical memory detected. This defaults to 90 if total physical memory can be detected. See https://www.jalview.org/help/html/memory.html for more details.\n"
 +                    + "-jvmmemmax=MAXMEMORY\tOnly available with standalone executable jar or jalview.bin.Launcher. Limit maximum heap size (memory) to MAXMEMORY. MAXMEMORY can be specified in bytes, kilobytes(k), megabytes(m), gigabytes(g) or if you're lucky enough, terabytes(t). This defaults to 32g if total physical memory can be detected, or to 8g if total physical memory cannot be detected. See https://www.jalview.org/help/html/memory.html for more details.\n"
 +                    + "\n~Read documentation in Application or visit https://www.jalview.org for description of Features and Annotations file~\n\n");
    }
  
    private static void startUsageStats(final Desktop desktop)
                @Override
                public void run()
                {
 -                Cache.log.debug(
 +                Console.debug(
                          "Initialising googletracker for usage stats.");
                  Cache.initGoogleTracker();
 -                Cache.log.debug("Tracking enabled.");
 +                Console.debug("Tracking enabled.");
                }
              }, new Runnable()
              {
                @Override
                public void run()
                {
 -                Cache.log.debug("Not enabling Google Tracking.");
 +                Console.debug("Not enabling Google Tracking.");
                }
              }, null, true);
      desktop.addDialogThread(prompter);
      }
      try
      {
 -      Map<String, Object> vbinding = new HashMap<>();
 +      Map<String, java.lang.Object> vbinding = new HashMap<>();
        vbinding.put("Jalview", this);
        if (af != null)
        {
      }
    }
  
 -  /**
 -   * Check commandline for any das server definitions or any fetchfrom switches
 -   * 
 -   * @return vector of DAS source nicknames to retrieve from
 -   */
 -  private static Vector<String> checkDasArguments(ArgsParser aparser)
 -  {
 -    Vector<String> source = null;
 -    String data;
 -    String locsources = Cache.getProperty(Cache.DAS_LOCAL_SOURCE);
 -    while ((data = aparser.getValue("dasserver", true)) != null)
 -    {
 -      String nickname = null;
 -      String url = null;
 -      int pos = data.indexOf('=');
 -      // determine capabilities
 -      if (pos > 0)
 -      {
 -        nickname = data.substring(0, pos);
 -      }
 -      url = data.substring(pos + 1);
 -      if (url != null && (url.startsWith("http:")
 -              || url.startsWith("sequence:http:")))
 -      {
 -        if (nickname == null)
 -        {
 -          nickname = url;
 -        }
 -        if (locsources == null)
 -        {
 -          locsources = "";
 -        }
 -        else
 -        {
 -          locsources += "\t";
 -        }
 -        locsources = locsources + nickname + "|" + url;
 -        System.err.println(
 -                "NOTE! dasserver parameter not yet really supported (got args of "
 -                        + nickname + "|" + url);
 -        if (source == null)
 -        {
 -          source = new Vector<>();
 -        }
 -        source.addElement(nickname);
 -      }
 -      System.out.println(
 -              "CMD [-dasserver " + data + "] executed successfully!");
 -    } // loop until no more server entries are found.
 -    if (locsources != null && locsources.indexOf('|') > -1)
 -    {
 -      Cache.log.debug("Setting local source list in properties file to:\n"
 -              + locsources);
 -      Cache.setProperty(Cache.DAS_LOCAL_SOURCE, locsources);
 -    }
 -    while ((data = aparser.getValue("fetchfrom", true)) != null)
 -    {
 -      System.out.println("adding source '" + data + "'");
 -      if (source == null)
 -      {
 -        source = new Vector<>();
 -      }
 -      source.addElement(data);
 -    }
 -    return source;
 -  }
 -
 -  /**
 -   * start a feature fetcher for every alignment frame
 -   * 
 -   * @param dasSources
 -   */
 -  private FeatureFetcher startFeatureFetching(
 -          final Vector<String> dasSources)
 -  {
 -    FeatureFetcher ff = new FeatureFetcher();
 -    AlignFrame afs[] = Desktop.getAlignFrames();
 -    if (afs == null || afs.length == 0)
 -    {
 -      return null;
 -    }
 -    for (int i = 0; i < afs.length; i++)
 -    {
 -      ff.addFetcher(afs[i], dasSources);
 -    }
 -    return ff;
 -  }
 -
    public static boolean isHeadlessMode()
    {
      String isheadless = System.getProperty("java.awt.headless");
index 0000000,e321c90..fca6e49
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,466 +1,510 @@@
+ package jalview.ext.archaeopteryx;
+ import jalview.bin.Cache;
+ import jalview.ext.treeviewer.TreeControlsI;
+ import jalview.ext.treeviewer.TreeFrameI;
+ import jalview.ext.treeviewer.TreeI;
+ import jalview.ext.treeviewer.TreePanelI;
+ import jalview.ext.treeviewer.TreeViewerBindingI;
+ import jalview.ext.treeviewer.TreeViewerUtils;
+ import jalview.gui.Desktop;
 -import jalview.gui.EPSOptions;
++import jalview.gui.LineartOptions;
+ import jalview.io.JalviewFileChooser;
+ import jalview.io.JalviewFileView;
+ import jalview.util.ImageMaker;
+ import jalview.util.MessageManager;
++import jalview.util.Platform;
++import jalview.util.ImageMaker.TYPE;
+ import java.awt.Component;
+ import java.awt.Container;
+ import java.awt.Dimension;
+ import java.awt.Event;
+ import java.awt.Font;
+ import java.awt.Image;
+ import java.awt.MenuComponent;
+ import java.awt.event.ActionEvent;
+ import java.awt.event.ActionListener;
+ import java.io.FileOutputStream;
++import java.util.concurrent.atomic.AtomicBoolean;
+ import javax.accessibility.AccessibleContext;
+ import javax.swing.JLayeredPane;
+ import javax.swing.JMenu;
+ import javax.swing.JMenuBar;
+ import javax.swing.JMenuItem;
+ import javax.swing.JRootPane;
+ import javax.swing.JSeparator;
+ import javax.swing.event.InternalFrameListener;
+ import org.forester.archaeopteryx.Archaeopteryx;
+ import org.forester.archaeopteryx.Configuration;
+ import org.forester.archaeopteryx.MainFrame;
+ import org.forester.phylogeny.Phylogeny;
+ import org.jibble.epsgraphics.EpsGraphics2D;
+ public class AptxFrame implements TreeFrameI
+ {
+   private final MainFrame aptxFrame;
+   private TreeViewerBindingI viewBinding;
+   private TreePanelI aptxPanel;
+   private TreeControlsI aptxControls;
+   public AptxFrame(Phylogeny tree, Configuration aptxConfig,
+           String treeTitle)
+   {
+     this(Archaeopteryx.createApplication(tree,
+             aptxConfig,
+             treeTitle));
+   }
+   public AptxFrame(MainFrame aptx)
+   {
+     aptxFrame = aptx;
+     aptxPanel = new AptxTreePanel(
+             aptxFrame.getMainPanel().getCurrentTreePanel());
+     aptxControls = new AptxControlPanel(
+             aptxFrame.getMainPanel().getControlPanel());
+     adaptAptxGui(aptxFrame);
+   }
+   
+   /**
+    * Hides certain redundant Archaeopteryx GUI elements such as the menu items
+    * for reading in trees and adds extra items related to Jalview such as the
+    * tree sorting menu item.
+    * 
+    * 
+    * @param aptxFrame
+    */
+   protected void adaptAptxGui(MainFrame aptxFrame)
+   {
+     JMenuBar frameBar = aptxFrame.getJMenuBar();
+     boolean epsAdded = false;
+     for (int i = 0; i < frameBar.getMenuCount();i++) {
+       JMenu menu = frameBar.getMenu(i);
+       int menuCount = menu.getMenuComponentCount();
+       if (menu.getText().contains("File"))
+       {
+         // hide all "Read from ..." and "New" menu items and any Separators that
+         // come directly after them
+         Component previousComp = null;
+         for (int x = 0; x < menuCount; x++)
+         {
+           Component menuItem = menu.getMenuComponent(x);
+           if (previousComp instanceof JMenuItem)
+           {
+             JMenuItem previousMenuItem = (JMenuItem) previousComp;
+             if (previousMenuItem.getText().startsWith("Read")
+                     || previousMenuItem.getText()
+                             .startsWith("New")
+                     || previousMenuItem.getText()
+                             .startsWith("Close Tab"))
+             {
+               previousComp.setVisible(false);
+               if (menuItem instanceof JSeparator)
+               {
+                 menuItem.setVisible(false);
+               }
+             }
+             if ((!epsAdded) && previousMenuItem.getText()
+                     .startsWith("Export to"))
+             {
+               JMenuItem exportEps = new JMenuItem("Export to EPS file...");
+               menu.add(exportEps, x);
+               exportEps.addActionListener(new ActionListener()
+               {
+                 @Override
+                 public void actionPerformed(ActionEvent e)
+                 {
+                   epsTree_actionPerformed(e);
+                 }
+                 
+               });
+               epsAdded = true;
+             }
+           }
+           previousComp = menuItem;
+         }
+       }
+       else if (menu.getText().contains("Inference"))
+       {
+         menu.setVisible(false);
+       }
+       else if (menu.getText().contains("View"))
+       {
+         menu.addSeparator();
+         JMenuItem sortByTree = new JMenuItem("Sort alignment by tree");
+         JMenuItem refreshJalview = new JMenuItem(
+                 "Filter alignment to show only currently visible sequences");
+         JMenuItem hideCollapsed = new JMenuItem(
+                 "Hide sequences of collapsed nodes");
+         refreshJalview.setFont(menu.getFont());
+         refreshJalview.addActionListener(new ActionListener() {
+           
+           @Override
+           public void actionPerformed(ActionEvent e) {
+             TreeViewerBindingI bindingManager = TreeViewerUtils
+                     .getActiveTreeViews().get(AptxFrame.this);
+             bindingManager.actionPerformed(e);
+           }
+         });
+                 
+         sortByTree.addActionListener(new ActionListener()
+         {
+           @Override
+           public void actionPerformed(ActionEvent e)
+           {
+             TreeViewerBindingI bindingManager = TreeViewerUtils
+                     .getActiveTreeViews().get(AptxFrame.this);
+             bindingManager.sortByTree_actionPerformed();
+           }
+         });
+         hideCollapsed.addActionListener(new ActionListener()
+         {
+           @Override
+           public void actionPerformed(ActionEvent e)
+           {
+             TreeViewerBindingI bindingManager = TreeViewerUtils
+                     .getActiveTreeViews().get(AptxFrame.this);
+             bindingManager.hideCollapsedSequences_actionPerformed();
+             
+           }
+         });
+         menu.add(sortByTree);
+         menu.add(refreshJalview);
+         menu.add(hideCollapsed);
+         sortByTree.setFont(menu.getFont());
+       }
+     }
+     // aptxFrame.validate();
+   }
+   public void epsTree_actionPerformed(ActionEvent e)
+   {
+     boolean accurateText = true;
++    final long messageId = System.currentTimeMillis();
+     String renderStyle = jalview.bin.Cache.getDefault("EPS_RENDERING",
+             "Prompt each time");
++    if (Platform.isJS())
++    {
++      renderStyle = "Text";
++    }
++
++    AtomicBoolean textSelected = new AtomicBoolean(
++            !"Lineart".equals(renderStyle));
+     // If we need to prompt, and if the GUI is visible then
+     // Prompt for EPS rendering style
+     if (renderStyle.equalsIgnoreCase("Prompt each time")
 -            && !(System.getProperty("java.awt.headless") != null && System
 -                    .getProperty("java.awt.headless").equals("true")))
++            && !Platform.isHeadless())
++    // && !(System.getProperty("java.awt.headless") != null && System
++    // .getProperty("java.awt.headless").equals("true")))
+     {
 -      EPSOptions eps = new EPSOptions();
 -      renderStyle = eps.getValue();
 -
 -      if (renderStyle == null || eps.cancelled)
++      LineartOptions epsOption = new LineartOptions(TYPE.EPS.getName(),
++              textSelected);
++      epsOption.setResponseAction(1, new Runnable()
+       {
 -        return;
 -      }
++        @Override
++        public void run()
++        {
++          // report canceled
++          // setStatus(MessageManager.formatMessage(
++          // "status.cancelled_image_export_operation",
++          // TYPE.EPS.getName()), messageId);
++        }
++      });
++      epsOption.setResponseAction(0, new Runnable()
++      {
++        @Override
++        public void run()
++        {
++          // TODO Auto-generated method stub
 -    }
++          String renderStyle = epsOption.getValue();
 -    if (renderStyle.equalsIgnoreCase("text"))
++          if (renderStyle == null)
++          {
++            return;
++          }
++
++          boolean accurateText = true;
++          if (renderStyle.equalsIgnoreCase("text"))
++          {
++            accurateText = false;
++          }
++          doExport(accurateText);
++
++        }
++
++      });
++      epsOption.showDialog();
++    }
++    else
+     {
 -      accurateText = false;
++      doExport(accurateText);
+     }
++  }
++  protected void doExport(boolean accurateText)
++  {
+     int width = getTreePanel().getWidth();
+     int height = getTreePanel().getHeight();
+     try
+     {
+       JalviewFileChooser chooser = new JalviewFileChooser(
+               ImageMaker.EPS_EXTENSION, ImageMaker.EPS_EXTENSION);
+       chooser.setFileView(new JalviewFileView());
+       chooser.setDialogTitle(
+               MessageManager.getString("label.create_eps_from_tree"));
+       chooser.setToolTipText(MessageManager.getString("action.save"));
+       int value = chooser.showSaveDialog(aptxFrame);
+       if (value != JalviewFileChooser.APPROVE_OPTION)
+       {
+         return;
+       }
+       Cache.setProperty("LAST_DIRECTORY",
+               chooser.getSelectedFile().getParent());
+       FileOutputStream out = new FileOutputStream(
+               chooser.getSelectedFile());
+       EpsGraphics2D pg = new EpsGraphics2D("Tree", out, 0, 0, width,
+               height);
+       pg.setAccurateTextMode(accurateText);
+       getTreePanel().paintToFile(pg, width, height);
+       pg.flush();
+       pg.close();
+     } catch (Exception ex)
+     {
+       ex.printStackTrace();
+     }
+   }
+   @Override
+   public TreePanelI getTreePanel()
+   {
+     return aptxPanel;
+   }
+   @Override
+   public TreeI getTree()
+   {
+     return aptxPanel.getTree();
+   }
+   @Override
+   public void enableMultipleTrees()
+   {
+     aptxFrame.activateSaveAllIfNeeded();
+   }
+   @Override
+   public int getNumberOfTrees()
+   {
+     return aptxFrame.getMainPanel().getTabbedPane().getTabCount();
+   }
+   @Override
+   public TreeControlsI getTreeControls()
+   {
+     return aptxControls;
+   }
+   @Override
+   public AccessibleContext getAccessibleContext()
+   {
+     return aptxFrame.getAccessibleContext();
+   }
+   @Override
+   public JRootPane getRootPane()
+   {
+     return aptxFrame.getRootPane();
+   }
+   @Override
+   public void setContentPane(Container contentPane)
+   {
+     aptxFrame.setContentPane(contentPane);
+   }
+   @Override
+   public Container getContentPane()
+   {
+     return aptxFrame.getContentPane();
+   }
+   @Override
+   public void setLayeredPane(JLayeredPane layeredPane)
+   {
+     aptxFrame.setLayeredPane(layeredPane);
+   }
+   @Override
+   public JLayeredPane getLayeredPane()
+   {
+     return aptxFrame.getLayeredPane();
+   }
+   @Override
+   public void setGlassPane(Component glassPane)
+   {
+     aptxFrame.setGlassPane(glassPane);
+   }
+   @Override
+   public Component getGlassPane()
+   {
+     return aptxFrame.getGlassPane();
+   }
+   @Override
+   public boolean imageUpdate(Image img, int infoflags, int x, int y,
+           int width, int height)
+   {
+     return aptxFrame.imageUpdate(img, infoflags, x, y, width, height);
+   }
+   @Override
+   public Font getFont()
+   {
+     return aptxFrame.getFont();
+   }
+   @Override
+   public void remove(MenuComponent comp)
+   {
+     aptxFrame.remove(comp);
+   }
+   @Deprecated
+   @Override
+   public boolean postEvent(Event evt)
+   {
+     return aptxFrame.postEvent(evt);
+   }
+   @Override
+   public void addFrameListener(InternalFrameListener listener)
+   {
+     aptxFrame.addInternalFrameListener(listener);
+   }
+   @Override
+   public void removeFrameListener(InternalFrameListener listener)
+   {
+     aptxFrame.removeInternalFrameListener(listener);
+   }
+   @Override
+   public InternalFrameListener[] getFrameListeners()
+   {
+     return aptxFrame.getInternalFrameListeners();
+   }
+   @Override
+   public void repaint()
+   {
+     aptxFrame.repaint();
+   }
+   @Override
+   public void setMinimumSize(Dimension dimension)
+   {
+     aptxFrame.setMinimumSize(dimension);
+   }
+   @Override
+   public boolean isShowing()
+   {
+     return aptxFrame.isShowing();
+   }
+   @Override
+   public Container getTopLevelAncestor()
+   {
+     return aptxFrame.getTopLevelAncestor();
+   }
+   @Override
+   public void addFrameToJalview(String title, boolean makeVisible,
+           int width, int height, boolean resizable, boolean ignoreMinSize)
+   {
+     Desktop.addInternalFrame(aptxFrame, title, makeVisible, width, height,
+             resizable, ignoreMinSize);
+   }
+   @Override
+   public TreeViewerBindingI getViewBinding()
+   {
+     return viewBinding;
+   }
+   @Override
+   public void setViewBinding(TreeViewerBindingI alignmentBinding)
+   {
+     viewBinding = alignmentBinding;
+   }
+   @Override
+   public void setMaximumSize(Dimension maximumSize)
+   {
+     aptxFrame.setMaximumSize(maximumSize);
+   }
+   @Override
+   public void setPreferredSize(Dimension preferredSize)
+   {
+     aptxFrame.setPreferredSize(preferredSize);
+   }
+ }
index 0000000,987e3a6..fd1735e
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,763 +1,763 @@@
+ package jalview.ext.treeviewer;
+ import jalview.analysis.AlignmentSorter;
+ import jalview.analysis.Conservation;
+ import jalview.api.AlignViewportI;
+ import jalview.commands.CommandI;
+ import jalview.commands.OrderCommand;
+ import jalview.datamodel.AlignmentI;
+ import jalview.datamodel.ColumnSelection;
+ import jalview.datamodel.HiddenColumns;
+ import jalview.datamodel.SequenceGroup;
+ import jalview.datamodel.SequenceI;
+ import jalview.gui.AlignViewport;
+ import jalview.gui.AlignmentPanel;
+ import jalview.gui.Desktop;
+ import jalview.gui.JvOptionPane;
+ import jalview.gui.PaintRefresher;
+ import jalview.schemes.ColourSchemeI;
+ import jalview.schemes.ColourSchemeProperty;
+ import jalview.schemes.UserColourScheme;
+ import jalview.structure.SelectionSource;
+ import jalview.structure.StructureSelectionManager;
+ import jalview.util.MappingUtils;
+ import jalview.util.MessageManager;
+ import jalview.viewmodel.AlignmentViewport;
+ import java.awt.Color;
+ import java.awt.Rectangle;
+ import java.awt.event.ActionEvent;
+ import java.awt.event.InputEvent;
+ import java.awt.event.MouseEvent;
+ import java.util.ArrayList;
+ import java.util.Collection;
+ import java.util.HashSet;
+ import java.util.List;
+ import java.util.Map;
+ import javax.swing.SwingUtilities;
+ import javax.swing.event.InternalFrameAdapter;
+ import javax.swing.event.InternalFrameEvent;
+ /**
+  * Class for binding the tree viewer to the Jalview alignment that it originates
+  * from, meaning that selecting sequences in the tree viewer also selects them
+  * in the alignment view and vice versa.
+  * 
+  * @author kjvanderheide
+  *
+  */
+ public final class JalviewBinding
+         implements TreeViewerBindingI
+ {
+   private final TreeFrameI aptxFrame;
+   private TreePanelI treeView;
+   private AlignmentViewport parentAvport;
+   private final StructureSelectionManager ssm;
+   private Map<SequenceI, TreeNodeI> sequencesBoundToNodes;
+   private Map<TreeNodeI, SequenceI> nodesBoundToSequences;
+   private float rootX;
+   private float furthestNodeX;
+   private int nrTreeGroups = 0;
+   private boolean applyToAllViews = false;
+   /**
+    * 
+    * @param archaeopteryx
+    * 
+    * @param jalviewAlignmentViewport
+    *          alignment viewport from which the tree was calculated.
+    * 
+    * @param alignMappedToNodes
+    *          map with sequences used to calculate the tree and matching tree
+    *          nodes as key, value pair respectively.
+    * 
+    * @param nodesMappedToAlign
+    *          map with tree nodes and matching sequences used to calculate the
+    *          tree as key, value pair respectively.
+    */
+   public JalviewBinding(final TreeFrameI archaeopteryx,
+           final AlignmentViewport jalviewAlignmentViewport,
+           final Map<SequenceI, TreeNodeI> alignMappedToNodes,
+           final Map<TreeNodeI, SequenceI> nodesMappedToAlign)
+   {
+     if (archaeopteryx.getNumberOfTrees() > 1)
+     {
+       JvOptionPane.showMessageDialog(Desktop.desktop,
+               MessageManager.getString("label.tabs_detected_archaeopteryx"),
+               MessageManager.getString("label.problem_reading_tree_file"),
+               JvOptionPane.WARNING_MESSAGE);
+     }
+     // deal with/prohibit null values here as that will cause problems
+     aptxFrame = archaeopteryx;
+     parentAvport = jalviewAlignmentViewport;
+     sequencesBoundToNodes = alignMappedToNodes;
+     nodesBoundToSequences = nodesMappedToAlign;
+     treeView = archaeopteryx.getTreePanel();
+     ssm = parentAvport.getStructureSelectionManager();
+     
+     aptxFrame.setViewBinding(this);
+     ssm.addSelectionListener(this);
+     treeView.addMouseListener(this);
+     treeView.registerWithPaintRefresher(
+             parentAvport.getSequenceSetId());
+     aptxFrame.addFrameListener(new InternalFrameAdapter()
+     {
+       @Override
+       public void internalFrameClosed(InternalFrameEvent e)
+       {
+         TreeViewerUtils.getActiveTreeViews().remove(aptxFrame);
+         ssm.removeSelectionListener(JalviewBinding.this);
+       }
+     });
+     // treeTabs.addChangeListener(new ChangeListener()
+     // {
+     //
+     // @Override
+     // public void stateChanged(ChangeEvent e)
+     // {
+     //
+     // SwingUtilities.invokeLater(new Runnable()
+     // {
+     //
+     // @Override
+     // /**
+     // * Resend the selection to the tree view when tabs get switched, this
+     // * has to be buried in invokeLater as Forester first resets the tree
+     // * view on switching tabs, without invokeLater this would get called
+     // * before Forester resets which would nullify the selection.
+     // */
+     // public void run()
+     // {
+     // treeView = archaeopteryx.getMainPanel().getCurrentTreePanel();
+     // parentAvport.sendSelection();
+     // // PaintRefresher.Refresh(treeView,
+     // // parentAvport.getSequenceSetId());
+     //
+     // }
+     // });
+     //
+     // }
+     //
+     // });
+   }
+   @Override
+   public void actionPerformed(ActionEvent e)
+   {
+     // reset hidden sequences first
+     parentAvport.showAllHiddenSeqs();
+     if (treeView.showingSubTree())
+     {
+       LoadedTreeSequenceAssociation bindAptxNodes = new LoadedTreeSequenceAssociation(
+               parentAvport.getAlignment().getSequencesArray(),
+               treeView.getTree());
+       bindAptxNodes.associateNodesToSequences();
+       sequencesBoundToNodes = bindAptxNodes.getAlignmentWithNodes();
+       nodesBoundToSequences = bindAptxNodes.getNodesWithAlignment();
+       TreeViewerUtils.associateNodesWithJalviewSequences(aptxFrame,
+               parentAvport, sequencesBoundToNodes, nodesBoundToSequences);
+       for (SequenceI seq : parentAvport.getAlignment().getSequencesArray())
+       {
+         if (!sequencesBoundToNodes.containsKey(seq))
+         {
+           parentAvport.hideSequence(new SequenceI[] { seq });
+         }
+       }
+     }
+     else
+     {
+       Rectangle visibleView = treeView.getVisibleArea();
+       for (TreeNodeI node : treeView.getTree().getRoot()
+               .getAllDescendants())
+       {
+         if (!(node.getXcoord() > visibleView.getMinX()
+                 && node.getXcoord() < visibleView.getMaxX()
+                 && node.getYcoord() > visibleView.getMinY()
+                 && node.getYcoord() < visibleView.getMaxY()))
+         {
+           parentAvport
+                   .hideSequence(new SequenceI[]
+                   { nodesBoundToSequences.get(node) });
+         }
+       }
+     }
+   }
+   @Override
+   public void mouseClicked(MouseEvent e)
+   {
+     SwingUtilities.invokeLater(new Runnable() {
+       @Override
+       /**
+        * invokeLater so that this always runs after Forester's mouseClicked
+        */
+       public void run()
+       {
+         final TreeNodeI node = treeView.findNode(e.getX(),
+                 e.getY());
+         if (node != null)
+         {
+           if ((e.getModifiers() & InputEvent.SHIFT_MASK) == 0) // clear previous
+           // selection if shift
+           // IS NOT pressed
+           {
+             parentAvport.setSelectionGroup(null);
+           }
+           showNodeSelectionOnAlign(node);
+         }
+         else
+         {
+           partitionTree(e.getX());
+       }
+         treeView.notifyPaintRefresher(parentAvport.getSequenceSetId(),
+                 false, false);
+         treeView.repaint();
+       }
+     });
+   }
+   @Override
+   public void mousePressed(final MouseEvent e)
+   {
+   }
+   @Override
+   public void mouseReleased(MouseEvent e)
+   {
+   }
+   @Override
+   public void mouseEntered(MouseEvent e)
+   {
+   }
+   @Override
+   public void mouseExited(MouseEvent e)
+   {
+   }
+   @Override
+   public void selection(final SequenceGroup seqsel,
+           final ColumnSelection colsel, final HiddenColumns hidden,
+           final SelectionSource source)
+   {
+     if (source == parentAvport) // check if source is alignment from where the
+     // tree originates
+     {
+       treeView.setMatchingNodes(
+               new HashSet<Long>(seqsel.getSequences().size()));
+       for (SequenceI selectedSequence : seqsel.getSequences())
+       {
+         TreeNodeI matchingNode = sequencesBoundToNodes
+                 .get(selectedSequence);
+         if (matchingNode != null)
+         {
+           treeView.addToMatchingNodes(matchingNode);
+           // if (!matchingNode.getBranchData().isHasBranchColor())
+           // {
+           // // Color foundNodesColour = treeView.getTreeColorSet()
+           // // .getFoundColor0();
+           // // matchingNode.getBranchData()
+           // // .setBranchColor(new BranchColor(foundNodesColour));
+           //
+           // }
+         }
+       }
+       treeView.repaint();
+     }
+   }
+   /**
+    * Partially refactored from TreeCanvas
+    */
+   @Override
+   public void partitionTree(final int x)
+   {
+     TreeI tree = treeView.getTree();
+     if (!tree.isEmpty())
+     {
+       // should be calculated on each partition as the tree can theoretically
+       // change in the meantime
+       TreeNodeI furthestNode = tree.getFurthestNode();
+       furthestNodeX = furthestNode.getXcoord();
+       rootX = tree.getRoot().getXcoord();
+       // don't bother if 0 distance tree or clicked x lies outside of tree
+       // if (furthestNodeX != rootX && !(x > furthestNodeX))
+         float threshold = (x - rootX) / (furthestNodeX - rootX);
+         List<TreeNodeI> foundNodes = getNodesAboveThreshold(
+                 threshold,
+                 tree.getRoot());
+     }
+   }
+   public List<TreeNodeI> getNodesAboveThreshold(float threshold,
+           TreeNodeI node)
+   {
+     List<TreeNodeI> nodesAboveThreshold = new ArrayList<>();
+     parentAvport.setSelectionGroup(null);
+     parentAvport.getAlignment().deleteAllGroups();
+     parentAvport.clearSequenceColours();
+     if (parentAvport.getCodingComplement() != null)
+     {
+       parentAvport.getCodingComplement().setSelectionGroup(null);
+       parentAvport.getCodingComplement().getAlignment().deleteAllGroups();
+       parentAvport.getCodingComplement().clearSequenceColours();
+     }
+     colourNodesAboveThreshold(nodesAboveThreshold, threshold,
+             node);
+     return nodesAboveThreshold;
+   }
+   /**
+    * Partially refactored from TreeCanvas colourGroups (can be made nicer).
+    * 
+    * @param nodeList
+    * @param threshold
+    * @param treeLength
+    * @param node
+    * @return
+    */
+   private List<TreeNodeI> colourNodesAboveThreshold(
+           List<TreeNodeI> nodeList, float threshold,
+           TreeNodeI node)
+   {
+     for (TreeNodeI childNode : node.getDirectChildren())
+     {
+       childNode.setBranchColor(Color.black);
+       float nodeCutoff = (childNode.getXcoord() - rootX)
+               / (furthestNodeX - rootX);
+       if (nodeCutoff > threshold)
+       {
+         nodeList.add(childNode);
+         Color randomColour = new Color((int) (Math.random() * 255),
+                 (int) (Math.random() * 255), (int) (Math.random() * 255));
+         childNode.setBranchColor(randomColour);
+         List<SequenceI> groupSeqs = new ArrayList<>();
+         SequenceI seq = nodesBoundToSequences.get(childNode);
+         if (seq != null)
+         {
+           groupSeqs.add(seq);
+           parentAvport.setSequenceColour(seq, randomColour);
+         }
+         List<TreeNodeI> descendantNodes = childNode
+                 .getAllDescendants();
+         // .forEach instead?
+         for (TreeNodeI descNode : descendantNodes)
+         {
+           seq = nodesBoundToSequences.get(descNode);
+           if (seq != null)
+           {
+             groupSeqs.add(seq);
+             parentAvport.setSequenceColour(seq, randomColour);
+           }
+           descNode.setBranchColor(randomColour);
+         }
+         if (groupSeqs != null)
+         {
+           nrTreeGroups++;
+           groupThresholdSequences(groupSeqs, randomColour);
+         }}
+       else
+       {
+         colourNodesAboveThreshold(nodeList, threshold, childNode);
+       }
+     }
+     for (AlignmentPanel associatedPanel : getAssociatedPanels())
+     {
+         associatedPanel.updateAnnotation();
+         final AlignViewportI codingComplement = associatedPanel.getAlignViewport()
+                 .getCodingComplement();
+         if (codingComplement != null)
+         {
+           // GROSS
+           ((AlignViewport) codingComplement).getAlignPanel()
+                   .updateAnnotation();
+         }
+       }
+     return nodeList;
+   }
+   public void groupThresholdSequences(List<SequenceI> groupedSeqs,
+           Color groupColour)
+   {
+     SequenceGroup treeGroup = new SequenceGroup(groupedSeqs, null, null,
+             true, true, false, 0,
+             parentAvport.getAlignment().getWidth() - 1);
+     ColourSchemeI cs = null;
+     if (parentAvport.getGlobalColourScheme() != null)
+     {
+       if (parentAvport.getGlobalColourScheme() instanceof UserColourScheme)
+       {
+         cs = new UserColourScheme(
+                 ((UserColourScheme) parentAvport.getGlobalColourScheme())
+                         .getColours());
+       }
+       else
+       {
 -        cs = ColourSchemeProperty.getColourScheme(treeGroup,
++        cs = ColourSchemeProperty.getColourScheme(parentAvport,treeGroup,
+                 ColourSchemeProperty.getColourName(
+                         parentAvport.getGlobalColourScheme()));
+       }
+     }
+     treeGroup.setColourScheme(cs);
+     treeGroup.getGroupColourScheme().setThreshold(
+             parentAvport.getResidueShading().getThreshold(),
+             parentAvport.isIgnoreGapsConsensus());
+     treeGroup.setName("Tree Group " + nrTreeGroups);
+     treeGroup.setIdColour(groupColour);
+     for (AlignmentPanel associatedPanel : getAssociatedPanels())
+     {
+       AlignViewportI altViewport = associatedPanel
+               .getAlignViewport();
+       if (altViewport.getGlobalColourScheme() != null
+               && altViewport.getResidueShading()
+                       .conservationApplied())
+       {
+         Conservation conserv = new Conservation(treeGroup.getName(),
+                 treeGroup.getSequences(null), treeGroup.getStartRes(),
+                 treeGroup.getEndRes());
+         conserv.calculate();
+         conserv.verdict(false, altViewport.getConsPercGaps());
+         treeGroup.getGroupColourScheme().setConservation(conserv);
+       }
+       altViewport.getAlignment().addGroup(treeGroup);
+       // TODO can we push all of the below into AlignViewportI?
+       final AlignViewportI codingComplement = altViewport
+               .getCodingComplement();
+       if (codingComplement != null)
+       {
+         SequenceGroup mappedGroup = MappingUtils.mapSequenceGroup(treeGroup,
+                 parentAvport, codingComplement);
+         if (mappedGroup.getSequences().size() > 0)
+         {
+           codingComplement.getAlignment().addGroup(mappedGroup);
+           for (SequenceI seq : mappedGroup.getSequences())
+           {
+             codingComplement.setSequenceColour(seq, groupColour.brighter());
+           }
+         }
+       }
+     }
+   }
+   @Override
+   public void showNodeSelectionOnAlign(final TreeNodeI node)
+   {
+       if (node.isInternal())
+       {
+         showMatchingChildSequences(node);
+       }
+       else
+       {
+         showMatchingSequence(node);
+       }
+     }
+   @Override
+   public void showMatchingSequence(final TreeNodeI nodeToMatch)
+   {
+     SequenceI matchingSequence = nodesBoundToSequences.get(nodeToMatch);
+     if (matchingSequence != null)
+     {
+       long nodeId = nodeToMatch.getId();
+       addOrRemoveInCollection(treeView.getMatchingNodesIds(), nodeId);
+       treeSelectionChanged(matchingSequence);
+       parentAvport.sendSelection();
+     }
+   }
+   @Override
+   public void showMatchingChildSequences(final TreeNodeI parentNode)
+   {
+     // redundancy here, Forester already iterates through tree to get all
+     // descendants
+     List<TreeNodeI> childNodes = parentNode.getAllDescendants();
+     for (TreeNodeI childNode : childNodes)
+     {
+       SequenceI matchingSequence = nodesBoundToSequences.get(childNode);
+       if (matchingSequence != null)
+       {
+         long nodeId = childNode.getId();
+         addOrRemoveInCollection(treeView.getMatchingNodesIds(), nodeId);
+         treeSelectionChanged(matchingSequence);
+       }
+     }
+     parentAvport.sendSelection();
+   }
+   @Override
+   public void treeSelectionChanged(final SequenceI sequence)
+   {
+     if (!parentAvport.isClosed()) // alignment view could be closed
+     {
+       SequenceGroup selected = parentAvport.getSelectionGroup();
+       if (selected == null)
+       {
+         selected = new SequenceGroup();
+         parentAvport.setSelectionGroup(selected);
+       }
+       selected.setEndRes(parentAvport.getAlignment().getWidth() - 1);
+         selected.addOrRemove(sequence, true);
+     }
+   }
+   @Override
+   public void sortByTree_actionPerformed()
+   {
+     // if (applyToAllViews)
+       final ArrayList<CommandI> commands = new ArrayList<>();
+       for (AlignmentPanel ap : PaintRefresher
+               .getAssociatedPanels(parentAvport.getSequenceSetId()))
+       {
+         commands.add(sortAlignmentIn(ap.av.getAlignPanel()));
+         ap.alignFrame.addHistoryItem(new CommandI()
+         {
+           @Override
+           public void undoCommand(AlignmentI[] views)
+           {
+             for (CommandI tsort : commands)
+             {
+               tsort.undoCommand(views);
+             }
+           }
+           @Override
+           public int getSize()
+           {
+             return commands.size();
+           }
+           @Override
+           public String getDescription()
+           {
+             return "Tree Sort (many views)";
+           }
+           @Override
+           public void doCommand(AlignmentI[] views)
+           {
+             for (CommandI tsort : commands)
+             {
+               tsort.doCommand(views);
+             }
+           }
+         });
+         ap.alignFrame.updateEditMenuBar();
+       }
+     }
+   // else
+   // {
+   // alignPanel.alignFrame.addHistoryItem(sortAlignmentIn(alignPanel));
+   // }
+   @Override
+   public CommandI sortAlignmentIn(AlignmentPanel ap)
+   {
+     AlignmentViewport viewport = ap.av;
+     SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
+     try
+     {
+     AlignmentSorter.sortByTree(viewport.getAlignment(),
+             nodesBoundToSequences,
+               treeView.getTree());
+       CommandI undo;
+       undo = new OrderCommand("Tree Sort", oldOrder,
+               viewport.getAlignment());
+       ap.paintAlignment(true, false);
+       return undo;
+     } catch (Exception e)
+     {
+       System.err.println(e.getMessage());
+     }
+     return null;
+   }
+   
+   /**
+    * TO BE MOVED
+    * 
+    * @param collection
+    * @param nodeId
+    */
+   public static <E> void addOrRemoveInCollection(Collection<Long> collection,
+           long nodeId)
+   {
+     if (collection.contains(nodeId))
+     {
+       collection.remove(nodeId);
+     }
+     else
+     {
+       collection.add(nodeId);
+     }
+   }
+   public AlignmentViewport getParentAvport()
+   {
+     return parentAvport;
+   }
+   public void setParentAvport(final AlignmentViewport parentAvport)
+   {
+     this.parentAvport = parentAvport;
+   }
+   public AlignmentPanel[] getAssociatedPanels()
+   {
+     return PaintRefresher
+             .getAssociatedPanels(parentAvport.getSequenceSetId());
+   }
+   @Override
+   public Map<SequenceI, TreeNodeI> getAlignmentWithNodes()
+   {
+     return sequencesBoundToNodes;
+   }
+   @Override
+   public Map<TreeNodeI, SequenceI> getNodesWithAlignment()
+   {
+     return nodesBoundToSequences;
+   }
+   @Override
+   public void hideCollapsedSequences_actionPerformed()
+   {
+     parentAvport.showAllHiddenSeqs();
+     for (TreeNodeI node : treeView.getTree().getAllNodes())
+     {
+       if (node.isCollapsed())
+       {
+         SequenceI seqToHide = nodesBoundToSequences.get(node);
+         if (seqToHide != null)
+         {
+           parentAvport.hideSequence(new SequenceI[] { seqToHide });
+         }
+       }
+     }
+   }
+ }
@@@ -21,7 -21,6 +21,7 @@@
  
  package jalview.fts.core;
  
 +import jalview.bin.Cache;
  import jalview.fts.api.FTSDataColumnI;
  import jalview.fts.api.GFTSPanelI;
  import jalview.fts.core.FTSDataColumnPreferences.PreferenceSource;
@@@ -31,12 -30,10 +31,12 @@@ import jalview.gui.JvSwingUtils
  import jalview.gui.SequenceFetcher;
  import jalview.io.cache.JvCacheableInputBox;
  import jalview.util.MessageManager;
 +import jalview.util.Platform;
  
  import java.awt.BorderLayout;
  import java.awt.CardLayout;
  import java.awt.Dimension;
 +import java.awt.Font;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.FocusAdapter;
@@@ -76,6 -73,7 +76,6 @@@ import javax.swing.event.DocumentListen
  import javax.swing.event.InternalFrameEvent;
  import javax.swing.table.DefaultTableModel;
  import javax.swing.table.TableColumn;
 -import javax.swing.text.JTextComponent;
  
  /**
   * This class provides the swing GUI layout for FTS Panel and implements most of
  @SuppressWarnings("serial")
  public abstract class GFTSPanel extends JPanel implements GFTSPanelI
  {
 +  private static final Font VERDANA_12 = new Font("Verdana", 0, 12);
 +
    protected JInternalFrame mainFrame = new JInternalFrame(
            getFTSFrameTitle());
  
    protected JTabbedPane tabs = new JTabbedPane();
 +
    protected IProgressIndicator progressIndicator;
  
    protected JComboBox<FTSDataColumnI> cmb_searchTarget = new JComboBox<>();
  
    protected JLabel lbl_blank = new JLabel(balnkPlaceholderImage);
  
 -  private JTabbedPane tabbedPane = new JTabbedPane();
 +  JTabbedPane tabbedPane = new JTabbedPane();
  
    private JPanel pnl_actions = new JPanel();
  
        {
          tabs.addTab(MessageManager.getString("label.retrieve_ids"),
                  fetcher);
 -        fetcher.setDatabaseChooserVisible(false);
 -        fetcher.embedWithFTSPanel(this);
 +        fetcher.embedIn(this);
        }
        mainFrame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
        final JPanel ftsPanel = this;
          public void focusGained(FocusEvent e)
          {
            // TODO: make selected tab gain focus in correct widget
 -          if (tabs != null
 -                  && tabs.getSelectedComponent() == ftsPanel)
 +          if (tabs != null && tabs.getSelectedComponent() == ftsPanel)
            {
 -            txt_search.requestFocusInWindow();
 +            txt_search.getComponent().requestFocusInWindow();
            }
          }
        });
    private void jbInit() throws Exception
    {
  
 -    txt_search = new JvCacheableInputBox<>(getCacheKey());
 +    txt_search = new JvCacheableInputBox<>(getCacheKey(), 45);
      populateCmbSearchTargetOptions();
      Integer width = getTempUserPrefs().get("FTSPanel.width") == null ? 800
              : getTempUserPrefs().get("FTSPanel.width");
      Integer height = getTempUserPrefs().get("FTSPanel.height") == null ? 400
              : getTempUserPrefs().get("FTSPanel.height");
      lbl_warning.setVisible(false);
 -    lbl_warning.setFont(new java.awt.Font("Verdana", 0, 12));
 +    lbl_warning.setFont(VERDANA_12);
      lbl_loading.setVisible(false);
 -    lbl_loading.setFont(new java.awt.Font("Verdana", 0, 12));
 +    lbl_loading.setFont(VERDANA_12);
      lbl_blank.setVisible(true);
 -    lbl_blank.setFont(new java.awt.Font("Verdana", 0, 12));
 +    lbl_blank.setFont(VERDANA_12);
  
      tbl_summary.setAutoCreateRowSorter(true);
      tbl_summary.getTableHeader().setReorderingAllowed(false);
        }
      });
  
 +    JButton txt_help = new JButton("?");
 +    txt_help.setFont(VERDANA_12);
 +    txt_help.setPreferredSize(new Dimension(15, 15));
 +    txt_help.setToolTipText(MessageManager.getString("action.help"));
 +    txt_help.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        showHelp();
 +      }
 +    });
 +
      btn_autosearch.setText(MessageManager.getString("option.autosearch"));
      btn_autosearch.setToolTipText(
              MessageManager.getString("option.enable_disable_autosearch"));
 -    btn_autosearch.setSelected(
 -            jalview.bin.Cache.getDefault(getAutosearchPreference(), true));
 +    // disable autosearch by default
 +    btn_autosearch.setSelected(!Platform.isJS()
 +            && Cache.getDefault(getAutosearchPreference(), false));
      btn_autosearch.addActionListener(new java.awt.event.ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        jalview.bin.Cache.setProperty(getAutosearchPreference(),
 +        Cache.setProperty(getAutosearchPreference(),
                  Boolean.toString(btn_autosearch.isSelected()));
        }
      });
 -    btn_back.setFont(new java.awt.Font("Verdana", 0, 12));
 +    btn_back.setFont(VERDANA_12);
      btn_back.setText(MessageManager.getString("action.back"));
      btn_back.addActionListener(new java.awt.event.ActionListener()
      {
      });
  
      btn_ok.setEnabled(false);
 -    btn_ok.setFont(new java.awt.Font("Verdana", 0, 12));
 +    btn_ok.setFont(VERDANA_12);
      btn_ok.setText(MessageManager.getString("action.ok"));
      btn_ok.addActionListener(new java.awt.event.ActionListener()
      {
      btn_next_page.setEnabled(false);
      btn_next_page.setToolTipText(
              MessageManager.getString("label.next_page_tooltip"));
 -    btn_next_page.setFont(new java.awt.Font("Verdana", 0, 12));
 +    btn_next_page.setFont(VERDANA_12);
      btn_next_page.setText(MessageManager.getString("action.next_page"));
      btn_next_page.addActionListener(new java.awt.event.ActionListener()
      {
      btn_prev_page.setEnabled(false);
      btn_prev_page.setToolTipText(
              MessageManager.getString("label.prev_page_tooltip"));
 -    btn_prev_page.setFont(new java.awt.Font("Verdana", 0, 12));
 +    btn_prev_page.setFont(VERDANA_12);
      btn_prev_page.setText(MessageManager.getString("action.prev_page"));
      btn_prev_page.addActionListener(new java.awt.event.ActionListener()
      {
        btn_next_page.setVisible(false);
      }
  
 -    btn_cancel.setFont(new java.awt.Font("Verdana", 0, 12));
 +    btn_cancel.setFont(VERDANA_12);
      btn_cancel.setText(MessageManager.getString("action.cancel"));
      btn_cancel.addActionListener(new java.awt.event.ActionListener()
      {
      });
      scrl_searchResult.setPreferredSize(new Dimension(width, height));
  
 -    cmb_searchTarget.setFont(new java.awt.Font("Verdana", 0, 12));
 +    cmb_searchTarget.setFont(VERDANA_12);
      cmb_searchTarget.addItemListener(new ItemListener()
      {
        @Override
                      "label.separate_multiple_query_values", new Object[]
                      { getCmbSearchTarget().getSelectedItem().toString() });
            }
 -          txt_search.setToolTipText(
 +          txt_search.getComponent().setToolTipText(
                    JvSwingUtils.wrapTooltip(true, tooltipText));
            searchAction(true);
          }
        }
      });
  
 -    txt_search.setFont(new java.awt.Font("Verdana", 0, 12));
 +    txt_search.getComponent().setFont(VERDANA_12);
  
 -    txt_search.getEditor().getEditorComponent()
 -            .addKeyListener(new KeyAdapter()
 -            {
 -              @Override
 -              public void keyPressed(KeyEvent e)
 -              {
 -                if (e.getKeyCode() == KeyEvent.VK_ENTER)
 -                {
 -                  if (getTypedText() == null || getTypedText().isEmpty())
 -                  {
 -                    return;
 -                  }
 -                  String primaryKeyName = getFTSRestClient()
 -                          .getPrimaryKeyColumn().getName();
 -                  if (primaryKeyName.equalsIgnoreCase(getCmbSearchTarget()
 -                          .getSelectedItem().toString()))
 -                  {
 -                    // TODO: nicer to show the list in the result set before
 -                    // viewing in Jalview perhaps ?
 -                    transferToSequenceFetcher(getTypedText());
 -                  }
 -                  else
 -                  {
 -                    performSearchAction();
 -                  }
 -                }
 -              }
 -            });
 +    txt_search.addKeyListener(new KeyAdapter()
 +    {
 +      @Override
 +      public void keyPressed(KeyEvent e)
 +      {
 +        if (e.getKeyCode() == KeyEvent.VK_ENTER)
 +        {
 +          if (getTypedText() == null || getTypedText().isEmpty())
 +          {
 +            return;
 +          }
 +          String primaryKeyName = getFTSRestClient().getPrimaryKeyColumn()
 +                  .getName();
 +          if (primaryKeyName.equalsIgnoreCase(
 +                  getCmbSearchTarget().getSelectedItem().toString()))
 +          {
 +            // TODO: nicer to show the list in the result set before
 +            // viewing in Jalview perhaps ?
 +            transferToSequenceFetcher(getTypedText());
 +          }
 +          else
 +          {
 +            performSearchAction();
 +          }
 +        }
 +      }
 +    });
      final DeferredTextInputListener listener = new DeferredTextInputListener(
              1500, new ActionListener()
              {
                  }
                }
              }, false);
 -    ((JTextComponent) txt_search.getEditor().getEditorComponent())
 -            .getDocument().addDocumentListener(listener);
 +    txt_search.addDocumentListener(listener);
  
      txt_search.addFocusListener(new FocusListener()
      {
  
      txt_search.addActionListener(new ActionListener()
      {
 -
        @Override
        public void actionPerformed(ActionEvent e)
        {
            btn_ok.setEnabled(false);
            btn_next_page.setEnabled(false);
            btn_prev_page.setEnabled(false);
 -          txt_search.setEnabled(false);
 +          txt_search.getComponent().setEnabled(false);
            cmb_searchTarget.setEnabled(false);
            previousWantedFields = getFTSRestClient()
                    .getAllDefaultDisplayedFTSDataColumns()
            btn_back.setEnabled(true);
            btn_cancel.setEnabled(true);
            refreshPaginatorState();
 -          txt_search.setEnabled(true);
 +          txt_search.getComponent().setEnabled(true);
            cmb_searchTarget.setEnabled(true);
            if (wantedFieldsUpdated())
            {
  
      pnl_results.add(tabbedPane);
      pnl_inputs.add(cmb_searchTarget);
 -    pnl_inputs.add(txt_search);
 +    pnl_inputs.add(txt_search.getComponent());
 +    pnl_inputs.add(txt_help);
      pnl_inputs.add(btn_autosearch);
      pnl_inputs.add(lbl_loading);
      pnl_inputs.add(lbl_warning);
      Desktop.addInternalFrame(mainFrame, getFTSFrameTitle(), width, height);
    }
  
 +  abstract protected void showHelp();
 +
    protected void closeAction()
    {
      getTempUserPrefs().put("FTSPanel.width", this.getWidth());
      return cmb_searchTarget;
    }
  
 -  public JComboBox<String> getTxtSearch()
 -  {
 -    return txt_search;
 -  }
 -
    public JInternalFrame getMainFrame()
    {
      return mainFrame;
      }
    }
  
 +  /**
 +   * Action on Back button is to close this panel and open a new Sequence
 +   * Fetcher panel
 +   */
    public void btn_back_ActionPerformed()
    {
      closeAction();
  
    public void transferToSequenceFetcher(String ids)
    {
 -    seqFetcher.getTextArea().setText(ids);
 +    seqFetcher.setQuery(ids);
-     Thread worker = new Thread(seqFetcher);
+     Thread worker = new Thread(seqFetcher, "GFTSSeqFetcher");
      worker.start();
    }
  
      lbl_blank.setVisible(true);
      btn_ok.setEnabled(false);
      mainFrame.setTitle(getFTSFrameTitle());
 -    referesh();
 +    refresh();
      tbl_summary.setModel(new DefaultTableModel());
      tbl_summary.setVisible(false);
    }
      }
    }
  
 -  public void referesh()
 +  public void refresh()
    {
      mainFrame.setTitle(getFTSFrameTitle());
    }
  
 +  @Override
 +  public abstract void okAction();
  }
@@@ -26,8 -26,6 +26,8 @@@ import jalview.fts.api.FTSRestClientI
  import jalview.fts.core.FTSRestRequest;
  import jalview.fts.core.FTSRestResponse;
  import jalview.fts.core.GFTSPanel;
 +import jalview.gui.Help;
 +import jalview.gui.Help.HelpId;
  import jalview.gui.SequenceFetcher;
  import jalview.util.MessageManager;
  
@@@ -35,8 -33,6 +35,8 @@@ import java.util.HashMap
  import java.util.HashSet;
  import java.util.Map;
  
 +import javax.help.HelpSetException;
 +
  @SuppressWarnings("serial")
  public class PDBFTSPanel extends GFTSPanel
  {
  
            if (isPaginationEnabled() && resultSetCount > 0)
            {
 +            String f1 = totalNumberformatter
 +                    .format(Integer.valueOf(offSet + 1));
 +            String f2 = totalNumberformatter
 +                    .format(Integer.valueOf(offSet + resultSetCount));
 +            String f3 = totalNumberformatter
 +                    .format(Integer.valueOf(totalResultSetCount));
              updateSearchFrameTitle(defaultFTSFrameTitle + " - " + result
 -                    + " "
 -                    + totalNumberformatter.format((Number) (offSet + 1))
 -                    + " to "
 -                    + totalNumberformatter
 -                            .format((Number) (offSet + resultSetCount))
 -                    + " of "
 -                    + totalNumberformatter
 -                            .format((Number) totalResultSetCount)
 -                    + " " + " (" + (endTime - startTime) + " milli secs)");
 +                    + " " + f1 + " to " + f2 + " of " + f3 + " " + " ("
 +                    + (endTime - startTime) + " milli secs)");
            }
            else
            {
      }
  
      String ids = selectedIds.toString();
 -    // System.out.println(">>>>>>>>>>>>>>>> selected Ids: " + ids);
 -    seqFetcher.getTextArea().setText(ids);
 +    seqFetcher.setQuery(ids);
-     Thread worker = new Thread(seqFetcher, "PDBFTSSeqFetcherThread");
+     Thread worker = new Thread(seqFetcher, "PDBFTSSeqFetcher");
      worker.start();
      delayAndEnableActionButtons();
    }
    {
      return PDB_AUTOSEARCH;
    }
 -}
 +
 +  @Override
 +  protected void showHelp()
 +  {
 +    try
 +    {
 +      Help.showHelpWindow(HelpId.PdbFts);
 +    } catch (HelpSetException e1)
 +    {
 +      e1.printStackTrace();
 +    }
 +  }
 +}
   */
  package jalview.gui;
  
 +import java.awt.BorderLayout;
 +import java.awt.Color;
 +import java.awt.Component;
 +import java.awt.Dimension;
 +import java.awt.GridLayout;
 +import java.awt.Rectangle;
 +import java.awt.Toolkit;
 +import java.awt.datatransfer.Clipboard;
 +import java.awt.datatransfer.DataFlavor;
 +import java.awt.datatransfer.StringSelection;
 +import java.awt.datatransfer.Transferable;
 +import java.awt.dnd.DnDConstants;
 +import java.awt.dnd.DropTargetDragEvent;
 +import java.awt.dnd.DropTargetDropEvent;
 +import java.awt.dnd.DropTargetEvent;
 +import java.awt.dnd.DropTargetListener;
 +import java.awt.event.ActionEvent;
 +import java.awt.event.ActionListener;
 +import java.awt.event.FocusAdapter;
 +import java.awt.event.FocusEvent;
 +import java.awt.event.ItemEvent;
 +import java.awt.event.ItemListener;
 +import java.awt.event.KeyAdapter;
 +import java.awt.event.KeyEvent;
 +import java.awt.event.MouseEvent;
 +import java.awt.print.PageFormat;
 +import java.awt.print.PrinterJob;
 +import java.beans.PropertyChangeEvent;
 +import java.io.File;
 +import java.io.FileWriter;
 +import java.io.IOException;
 +import java.io.PrintWriter;
 +import java.net.URL;
 +import java.util.ArrayList;
 +import java.util.Arrays;
 +import java.util.Deque;
 +import java.util.Enumeration;
 +import java.util.Hashtable;
 +import java.util.List;
 +import java.util.Locale;
++import java.util.Map;
++import java.util.Map.Entry;
 +import java.util.StringTokenizer;
 +import java.util.Vector;
 +
 +import javax.swing.ButtonGroup;
 +import javax.swing.JCheckBoxMenuItem;
 +import javax.swing.JComboBox;
 +import javax.swing.JComponent;
 +import javax.swing.JEditorPane;
 +import javax.swing.JInternalFrame;
 +import javax.swing.JLabel;
 +import javax.swing.JLayeredPane;
 +import javax.swing.JMenu;
 +import javax.swing.JMenuItem;
 +import javax.swing.JPanel;
 +import javax.swing.JScrollPane;
 +import javax.swing.SwingUtilities;
 +
++import org.forester.archaeopteryx.webservices.PhylogeniesWebserviceClient;
++import org.forester.archaeopteryx.webservices.WebservicesManager;
++
 +import ext.vamsas.ServiceHandle;
  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.analysis.TreeModel;
 -import jalview.api.AlignExportSettingI;
 +import jalview.api.AlignExportSettingsI;
  import jalview.api.AlignViewControllerGuiI;
  import jalview.api.AlignViewControllerI;
  import jalview.api.AlignViewportI;
  import jalview.api.AlignmentViewPanel;
  import jalview.api.FeatureSettingsControllerI;
 +import jalview.api.FeatureSettingsModelI;
  import jalview.api.SplitContainerI;
  import jalview.api.ViewStyleI;
  import jalview.bin.Cache;
 +import jalview.bin.Console;
  import jalview.bin.Jalview;
  import jalview.commands.CommandI;
  import jalview.commands.EditCommand;
@@@ -106,7 -45,6 +111,7 @@@ import jalview.commands.RemoveGapColCom
  import jalview.commands.RemoveGapsCommand;
  import jalview.commands.SlideSequencesCommand;
  import jalview.commands.TrimRegionCommand;
 +import jalview.datamodel.AlignExportSettingsAdapter;
  import jalview.datamodel.AlignedCodonFrame;
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentAnnotation;
@@@ -115,7 -53,9 +120,8 @@@ import jalview.datamodel.AlignmentI
  import jalview.datamodel.AlignmentOrder;
  import jalview.datamodel.AlignmentView;
  import jalview.datamodel.ColumnSelection;
+ import jalview.datamodel.DBRefEntry;
  import jalview.datamodel.HiddenColumns;
 -import jalview.datamodel.HiddenSequences;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.SeqCigar;
  import jalview.datamodel.Sequence;
@@@ -123,12 -63,13 +129,14 @@@ import jalview.datamodel.SequenceGroup
  import jalview.datamodel.SequenceI;
  import jalview.ext.archaeopteryx.AptxInit;
  import jalview.ext.forester.io.SupportedTreeFileFilter;
- import jalview.ext.forester.io.TreeParser;
+ import jalview.ext.treeviewer.TreeFrameI;
+ import jalview.ext.treeviewer.TreeViewerBindingI;
+ import jalview.ext.treeviewer.TreeViewerUtils;
  import jalview.gui.ColourMenuHelper.ColourChangeListener;
  import jalview.gui.ViewSelectionMenu.ViewSetProvider;
  import jalview.io.AlignmentProperties;
  import jalview.io.AnnotationFile;
 +import jalview.io.BackupFiles;
  import jalview.io.BioJsHTMLOutput;
  import jalview.io.DataSourceType;
  import jalview.io.FileFormat;
@@@ -146,17 -87,13 +154,18 @@@ import jalview.io.JnetAnnotationMaker
  import jalview.io.NewickFile;
  import jalview.io.ScoreMatrixFile;
  import jalview.io.TCoffeeScoreFile;
 +import jalview.io.vcf.VCFLoader;
  import jalview.jbgui.GAlignFrame;
 +import jalview.project.Jalview2XML;
  import jalview.schemes.ColourSchemeI;
  import jalview.schemes.ColourSchemes;
  import jalview.schemes.ResidueColourScheme;
  import jalview.schemes.TCoffeeColourScheme;
+ import jalview.util.DBRefUtils;
 +import jalview.util.HttpUtils;
 +import jalview.util.ImageMaker.TYPE;
  import jalview.util.MessageManager;
 +import jalview.util.Platform;
  import jalview.viewmodel.AlignmentViewport;
  import jalview.viewmodel.ViewportRanges;
  import jalview.ws.DBRefFetcher;
@@@ -166,13 -103,70 +175,13 @@@ import jalview.ws.jws2.Jws2Discoverer
  import jalview.ws.jws2.jabaws2.Jws2Instance;
  import jalview.ws.seqfetcher.DbSourceProxy;
  
 -import java.awt.BorderLayout;
 -import java.awt.Component;
 -import java.awt.Dimension;
 -import java.awt.GridLayout;
 -import java.awt.Rectangle;
 -import java.awt.Toolkit;
 -import java.awt.datatransfer.Clipboard;
 -import java.awt.datatransfer.DataFlavor;
 -import java.awt.datatransfer.StringSelection;
 -import java.awt.datatransfer.Transferable;
 -import java.awt.dnd.DnDConstants;
 -import java.awt.dnd.DropTargetDragEvent;
 -import java.awt.dnd.DropTargetDropEvent;
 -import java.awt.dnd.DropTargetEvent;
 -import java.awt.dnd.DropTargetListener;
 -import java.awt.event.ActionEvent;
 -import java.awt.event.ActionListener;
 -import java.awt.event.FocusAdapter;
 -import java.awt.event.FocusEvent;
 -import java.awt.event.ItemEvent;
 -import java.awt.event.ItemListener;
 -import java.awt.event.KeyAdapter;
 -import java.awt.event.KeyEvent;
 -import java.awt.event.MouseEvent;
 -import java.awt.print.PageFormat;
 -import java.awt.print.PrinterJob;
 -import java.beans.PropertyChangeEvent;
 -import java.io.File;
 -import java.io.FileWriter;
 -import java.io.IOException;
 -import java.io.PrintWriter;
 -import java.net.URL;
 -import java.util.ArrayList;
 -import java.util.Arrays;
 -import java.util.Deque;
 -import java.util.Enumeration;
 -import java.util.Hashtable;
 -import java.util.List;
 -import java.util.Map;
 -import java.util.Map.Entry;
 -import java.util.StringTokenizer;
 -import java.util.Vector;
 -
 -import javax.swing.JCheckBoxMenuItem;
 -import javax.swing.JComboBox;
 -import javax.swing.JEditorPane;
 -import javax.swing.JInternalFrame;
 -import javax.swing.JLabel;
 -import javax.swing.JLayeredPane;
 -import javax.swing.JMenu;
 -import javax.swing.JMenuItem;
 -import javax.swing.JPanel;
 -import javax.swing.JScrollPane;
 -import javax.swing.SwingUtilities;
 -
 -import org.forester.archaeopteryx.webservices.PhylogeniesWebserviceClient;
 -import org.forester.archaeopteryx.webservices.WebservicesManager;
 -
  /**
   * DOCUMENT ME!
   * 
   * @author $author$
   * @version $Revision$
   */
 +@SuppressWarnings("serial")
  public class AlignFrame extends GAlignFrame implements DropTargetListener,
          IProgressIndicator, AlignViewControllerGuiI, ColourChangeListener
  {
     */
    String fileName = null;
  
 +  File fileObject;
 +
    /**
     * Creates a new AlignFrame object with specific width and height.
     * 
     */
    void init()
    {
 +    // setBackground(Color.white); // BH 2019
 +
      if (!Jalview.isHeadlessMode())
      {
        progressBar = new ProgressBar(this.statusPanel, this.statusBar);
        // modifyPID.setEnabled(false);
      }
  
 -    String sortby = jalview.bin.Cache.getDefault("SORT_ALIGNMENT",
 -            "No sort");
 +    String sortby = Cache.getDefault("SORT_ALIGNMENT", "No sort");
  
      if (sortby.equals("Id"))
      {
      if (Desktop.desktop != null)
      {
        this.setDropTarget(new java.awt.dnd.DropTarget(this, this));
 -      addServiceListeners();
 +      if (!Platform.isJS())
 +      {
 +        addServiceListeners();
 +      }
        setGUINucleotide();
      }
  
        wrapMenuItem_actionPerformed(null);
      }
  
 -    if (jalview.bin.Cache.getDefault("SHOW_OVERVIEW", false))
 +    if (Cache.getDefault("SHOW_OVERVIEW", false))
      {
        this.overviewMenuItem_actionPerformed(null);
      }
  
      addKeyListener();
  
 -    final List<AlignmentPanel> selviews = new ArrayList<>();
 +    final List<AlignmentViewPanel> selviews = new ArrayList<>();
      final List<AlignmentPanel> origview = new ArrayList<>();
      final String menuLabel = MessageManager
              .getString("label.copy_format_from");
                  }
                }
              });
 -    if (Cache.getDefault("VERSION", "DEVELOPMENT").toLowerCase()
 +    if (Cache.getDefault("VERSION", "DEVELOPMENT").toLowerCase(Locale.ROOT)
              .indexOf("devel") > -1
 -            || Cache.getDefault("VERSION", "DEVELOPMENT").toLowerCase()
 -                    .indexOf("test") > -1)
 +            || Cache.getDefault("VERSION", "DEVELOPMENT")
 +                    .toLowerCase(Locale.ROOT).indexOf("test") > -1)
      {
        formatMenu.add(vsel);
      }
    }
  
    /**
 +   * JavaScript will have this, maybe others. More dependable than a file name
 +   * and maintains a reference to the actual bytes loaded.
 +   * 
 +   * @param file
 +   */
 +  public void setFileObject(File file)
 +  {
 +    this.fileObject = file;
 +  }
 +
 +  /**
     * Add a KeyListener with handlers for various KeyPressed and KeyReleased
     * events
     */
            }
            if (viewport.cursorMode)
            {
 -            alignPanel.getSeqPanel().moveCursor(0, 1);
 +            alignPanel.getSeqPanel().moveCursor(0, 1, evt.isShiftDown());
            }
            break;
  
            }
            if (viewport.cursorMode)
            {
 -            alignPanel.getSeqPanel().moveCursor(0, -1);
 +            alignPanel.getSeqPanel().moveCursor(0, -1, evt.isShiftDown());
            }
  
            break;
            }
            else
            {
 -            alignPanel.getSeqPanel().moveCursor(-1, 0);
 +            alignPanel.getSeqPanel().moveCursor(-1, 0, evt.isShiftDown());
            }
  
            break;
            }
            else
            {
 -            alignPanel.getSeqPanel().moveCursor(1, 0);
 +            alignPanel.getSeqPanel().moveCursor(1, 0, evt.isShiftDown());
            }
            break;
  
          case KeyEvent.VK_BACK_SPACE:
            if (!viewport.cursorMode)
            {
 -            cut_actionPerformed(null);
 +            cut_actionPerformed();
            }
            else
            {
  
          case KeyEvent.VK_F2:
            viewport.cursorMode = !viewport.cursorMode;
 -          statusBar.setText(MessageManager
 +          setStatus(MessageManager
                    .formatMessage("label.keyboard_editing_mode", new String[]
                    { (viewport.cursorMode ? "on" : "off") }));
            if (viewport.cursorMode)
  
      int aSize = alignPanels.size();
  
 -    tabbedPane.setVisible(aSize > 1 || ap.av.viewName != null);
 +    tabbedPane.setVisible(aSize > 1 || ap.av.getViewName() != null);
  
 -    if (aSize == 1 && ap.av.viewName == null)
 +    if (aSize == 1 && ap.av.getViewName() == null)
      {
        this.getContentPane().add(ap, BorderLayout.CENTER);
      }
  
        expandViews.setEnabled(true);
        gatherViews.setEnabled(true);
 -      tabbedPane.addTab(ap.av.viewName, ap);
 +      tabbedPane.addTab(ap.av.getViewName(), ap);
  
        ap.setVisible(false);
      }
      gatherViews.setEnabled(true);
      tabbedPane.setVisible(true);
      AlignmentPanel first = alignPanels.get(0);
 -    tabbedPane.addTab(first.av.viewName, first);
 +    tabbedPane.addTab(first.av.getViewName(), first);
      this.getContentPane().add(tabbedPane, BorderLayout.CENTER);
    }
  
          Desktop.instance.removeJalviewPropertyChangeListener("services",
                  thisListener);
          closeMenuItem_actionPerformed(true);
 -      };
 +      }
      });
      // Finally, build the menu once to get current service state
      new Thread(new Runnable()
      AlignmentI al = getViewport().getAlignment();
      boolean nucleotide = al.isNucleotide();
  
 +    loadVcf.setVisible(nucleotide);
      showTranslation.setVisible(nucleotide);
      showReverse.setVisible(nucleotide);
      showReverseComplement.setVisible(nucleotide);
     * @param av
     *          AlignViewport
     */
 -  void setMenusFromViewport(AlignViewport av)
 +  public void setMenusFromViewport(AlignViewport av)
    {
      padGapsMenuitem.setSelected(av.isPadGaps());
      colourTextMenuItem.setSelected(av.isShowColourText());
      return progressBar.operationInProgress();
    }
  
 +  /**
 +   * Sets the text of the status bar. Note that setting a null or empty value
 +   * will cause the status bar to be hidden, with possibly undesirable flicker
 +   * of the screen layout.
 +   */
    @Override
    public void setStatus(String text)
    {
 -    statusBar.setText(text);
 +    statusBar.setText(text == null || text.isEmpty() ? " " : text);
    }
  
    /*
     */
    public String getVersion()
    {
 -    return jalview.bin.Cache.getProperty("VERSION");
 +    return Cache.getProperty("VERSION");
    }
  
    public FeatureRenderer getFeatureRenderer()
    }
  
    @Override
 -  public void fetchSequence_actionPerformed(ActionEvent e)
 +  public void fetchSequence_actionPerformed()
    {
 -    new jalview.gui.SequenceFetcher(this);
 +    new SequenceFetcher(this);
    }
  
    @Override
          Desktop.instance.closeAssociatedWindows();
  
          FileLoader loader = new FileLoader();
 -        DataSourceType protocol = fileName.startsWith("http:")
 +        DataSourceType protocol = HttpUtils.startsWithHttpOrHttps(fileName)
                  ? DataSourceType.URL
                  : DataSourceType.FILE;
          loader.LoadFile(viewport, fileName, protocol, currentFileFormat);
          Rectangle bounds = this.getBounds();
  
          FileLoader loader = new FileLoader();
 -        DataSourceType protocol = fileName.startsWith("http:")
 -                ? DataSourceType.URL
 -                : DataSourceType.FILE;
 -        AlignFrame newframe = loader.LoadFileWaitTillLoaded(fileName,
 -                protocol, currentFileFormat);
 +
 +        AlignFrame newframe = null;
 +
 +        if (fileObject == null)
 +        {
 +
 +          DataSourceType protocol = HttpUtils.startsWithHttpOrHttps(
 +                  fileName) ? DataSourceType.URL : DataSourceType.FILE;
 +          newframe = loader.LoadFileWaitTillLoaded(fileName, protocol,
 +                  currentFileFormat);
 +        }
 +        else
 +        {
 +          newframe = loader.LoadFileWaitTillLoaded(fileObject,
 +                  DataSourceType.FILE, currentFileFormat);
 +        }
  
          newframe.setBounds(bounds);
          if (featureSettings != null && featureSettings.isShowing())
    public void save_actionPerformed(ActionEvent e)
    {
      if (fileName == null || (currentFileFormat == null)
 -            || fileName.startsWith("http"))
 +            || HttpUtils.startsWithHttpOrHttps(fileName))
      {
 -      saveAs_actionPerformed(null);
 +      saveAs_actionPerformed();
      }
      else
      {
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Saves the alignment to a file with a name chosen by the user, if necessary
 +   * warning if a file would be overwritten
     */
    @Override
 -  public void saveAs_actionPerformed(ActionEvent e)
 +  public void saveAs_actionPerformed()
    {
      String format = currentFileFormat == null ? null
              : currentFileFormat.getName();
  
      int value = chooser.showSaveDialog(this);
  
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    if (value != JalviewFileChooser.APPROVE_OPTION)
 +    {
 +      return;
 +    }
 +    currentFileFormat = chooser.getSelectedFormat();
 +    // todo is this (2005) test now obsolete - value is never null?
 +    while (currentFileFormat == null)
      {
 +      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 +              MessageManager
 +                      .getString("label.select_file_format_before_saving"),
 +              MessageManager.getString("label.file_format_not_specified"),
 +              JvOptionPane.WARNING_MESSAGE);
        currentFileFormat = chooser.getSelectedFormat();
 -      while (currentFileFormat == null)
 +      value = chooser.showSaveDialog(this);
 +      if (value != JalviewFileChooser.APPROVE_OPTION)
        {
 -        JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 -                MessageManager.getString(
 -                        "label.select_file_format_before_saving"),
 -                MessageManager.getString("label.file_format_not_specified"),
 -                JvOptionPane.WARNING_MESSAGE);
 -        currentFileFormat = chooser.getSelectedFormat();
 -        value = chooser.showSaveDialog(this);
 -        if (value != JalviewFileChooser.APPROVE_OPTION)
 -        {
 -          return;
 -        }
 +        return;
        }
 +    }
 +
 +    fileName = chooser.getSelectedFile().getPath();
  
 -      fileName = chooser.getSelectedFile().getPath();
 +    Cache.setProperty("DEFAULT_FILE_FORMAT", currentFileFormat.getName());
 +    Cache.setProperty("LAST_DIRECTORY", fileName);
 +    saveAlignment(fileName, currentFileFormat);
 +  }
  
 -      Cache.setProperty("DEFAULT_FILE_FORMAT", currentFileFormat.getName());
 +  boolean lastSaveSuccessful = false;
 +
 +  FileFormatI lastFormatSaved;
 +
 +  String lastFilenameSaved;
 +
 +  /**
 +   * Raise a dialog or status message for the last call to saveAlignment.
 +   *
 +   * @return true if last call to saveAlignment(file, format) was successful.
 +   */
 +  public boolean isSaveAlignmentSuccessful()
 +  {
 +
 +    if (!lastSaveSuccessful)
 +    {
 +      if (!Platform.isHeadless())
 +      {
 +        JvOptionPane.showInternalMessageDialog(this, MessageManager
 +                .formatMessage("label.couldnt_save_file", new Object[]
 +                { lastFilenameSaved }),
 +                MessageManager.getString("label.error_saving_file"),
 +                JvOptionPane.WARNING_MESSAGE);
 +      }
 +      else
 +      {
 +        Console.error(MessageManager
 +                .formatMessage("label.couldnt_save_file", new Object[]
 +                { lastFilenameSaved }));
 +      }
 +    }
 +    else
 +    {
 +
 +      setStatus(MessageManager.formatMessage(
 +              "label.successfully_saved_to_file_in_format", new Object[]
 +              { lastFilenameSaved, lastFormatSaved }));
  
 -      Cache.setProperty("LAST_DIRECTORY", fileName);
 -      saveAlignment(fileName, currentFileFormat);
      }
 +    return lastSaveSuccessful;
    }
  
 -  public boolean saveAlignment(String file, FileFormatI format)
 +  /**
 +   * Saves the alignment to the specified file path, in the specified format,
 +   * which may be an alignment format, or Jalview project format. If the
 +   * alignment has hidden regions, or the format is one capable of including
 +   * non-sequence data (features, annotations, groups), then the user may be
 +   * prompted to specify what to include in the output.
 +   * 
 +   * @param file
 +   * @param format
 +   */
 +  public void saveAlignment(String file, FileFormatI format)
    {
 -    boolean success = true;
 +    lastSaveSuccessful = true;
 +    lastFilenameSaved = file;
 +    lastFormatSaved = format;
  
      if (FileFormat.Jalview.equals(format))
      {
        String shortName = title;
 -
 -      if (shortName.indexOf(java.io.File.separatorChar) > -1)
 +      if (shortName.indexOf(File.separatorChar) > -1)
        {
 -        shortName = shortName.substring(
 -                shortName.lastIndexOf(java.io.File.separatorChar) + 1);
 +        shortName = shortName
 +                .substring(shortName.lastIndexOf(File.separatorChar) + 1);
        }
 -
 -      success = new 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 }));
 +              { file, 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()
        {
 -        try
 -        {
 -          PrintWriter out = new PrintWriter(new FileWriter(file));
 -
 -          out.print(output);
 -          out.close();
 -          this.setTitle(file);
 -          statusBar.setText(MessageManager.formatMessage(
 -                  "label.successfully_saved_to_file_in_format", new Object[]
 -                  { fileName, format.getName() }));
 -        } catch (Exception ex)
 +        // 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)
          {
 -          success = false;
 -          ex.printStackTrace();
 +          lastSaveSuccessful = false;
          }
 -      }
 -    }
 +        else
 +        {
 +          // create backupfiles object and get new temp filename destination
 +          boolean doBackup = BackupFiles.getEnabled();
 +          BackupFiles backupfiles = null;
 +          if (doBackup)
 +          {
 +            Console.trace(
 +                    "ALIGNFRAME making backupfiles object for " + file);
 +            backupfiles = new BackupFiles(file);
 +          }
 +          try
 +          {
 +            String tempFilePath = doBackup ? backupfiles.getTempFilePath()
 +                    : file;
 +            Console.trace("ALIGNFRAME setting PrintWriter");
 +            PrintWriter out = new PrintWriter(new FileWriter(tempFilePath));
  
 -    if (!success)
 -    {
 -      JvOptionPane.showInternalMessageDialog(this, MessageManager
 -              .formatMessage("label.couldnt_save_file", new Object[]
 -              { fileName }),
 -              MessageManager.getString("label.error_saving_file"),
 -              JvOptionPane.WARNING_MESSAGE);
 -    }
 +            if (backupfiles != null)
 +            {
 +              Console.trace("ALIGNFRAME about to write to temp file "
 +                      + backupfiles.getTempFilePath());
 +            }
  
 -    return success;
 -  }
 +            out.print(output);
 +            Console.trace("ALIGNFRAME about to close file");
 +            out.close();
 +            Console.trace("ALIGNFRAME closed file");
 +            AlignFrame.this.setTitle(file);
 +            statusBar.setText(MessageManager.formatMessage(
 +                    "label.successfully_saved_to_file_in_format",
 +                    new Object[]
 +                    { fileName, format.getName() }));
 +            lastSaveSuccessful = true;
 +          } catch (IOException e)
 +          {
 +            lastSaveSuccessful = false;
 +            Console.error(
 +                    "ALIGNFRAME Something happened writing the temp file");
 +            Console.error(e.getMessage());
 +            Console.debug(Cache.getStackTraceString(e));
 +          } catch (Exception ex)
 +          {
 +            lastSaveSuccessful = false;
 +            Console.error(
 +                    "ALIGNFRAME Something unexpected happened writing the temp file");
 +            Console.error(ex.getMessage());
 +            Console.debug(Cache.getStackTraceString(ex));
 +          }
  
 -  private void warningMessage(String warning, String title)
 -  {
 -    if (new jalview.util.Platform().isHeadless())
 -    {
 -      System.err.println("Warning: " + title + "\nWarning: " + warning);
 +          if (doBackup)
 +          {
 +            backupfiles.setWriteSuccess(lastSaveSuccessful);
 +            Console.debug("ALIGNFRAME writing temp file was "
 +                    + (lastSaveSuccessful ? "" : "NOT ") + "successful");
 +            // do the backup file roll and rename the temp file to actual file
 +            Console.trace(
 +                    "ALIGNFRAME about to rollBackupsAndRenameTempFile");
 +            lastSaveSuccessful = backupfiles.rollBackupsAndRenameTempFile();
 +            Console.debug(
 +                    "ALIGNFRAME performed rollBackupsAndRenameTempFile "
 +                            + (lastSaveSuccessful ? "" : "un")
 +                            + "successfully");
 +          }
 +        }
 +      }
 +    };
  
 +    /*
 +     * show dialog with export options if applicable; else just do it
 +     */
 +    if (AlignExportOptions.isNeeded(viewport, format))
 +    {
 +      AlignExportOptions choices = new AlignExportOptions(
 +              alignPanel.getAlignViewport(), format, options);
 +      choices.setResponseAction(0, outputAction);
 +      choices.setResponseAction(1, cancelAction);
 +      choices.showDialog();
      }
      else
      {
 -      JvOptionPane.showInternalMessageDialog(this, warning, title,
 -              JvOptionPane.WARNING_MESSAGE);
 +      outputAction.run();
      }
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Outputs the alignment to textbox in the requested format, if necessary
 +   * first prompting the user for whether to include hidden regions or
 +   * non-sequence data
     * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * @param fileFormatName
     */
    @Override
 -  protected void outputText_actionPerformed(ActionEvent e)
 +  protected void outputText_actionPerformed(String fileFormatName)
    {
      FileFormatI fileFormat = FileFormats.getInstance()
 -            .forName(e.getActionCommand());
 -    AlignmentExportData exportData = getAlignmentForExport(fileFormat,
 -            viewport, null);
 -    if (exportData.getSettings().isCancelled())
 -    {
 -      return;
 -    }
 -    CutAndPasteTransfer cap = new CutAndPasteTransfer();
 -    cap.setForInput(null);
 -    try
 +            .forName(fileFormatName);
 +    AlignExportSettingsI options = new AlignExportSettingsAdapter(false);
 +    Runnable outputAction = new Runnable()
      {
 -      FileFormatI format = fileFormat;
 -      cap.setText(new FormatAdapter(alignPanel, exportData.getSettings())
 -              .formatSequences(format, exportData.getAlignment(),
 -                      exportData.getOmitHidden(),
 -                      exportData.getStartEndPostions(),
 -                      viewport.getAlignment().getHiddenColumns()));
 -      Desktop.addInternalFrame(cap, MessageManager
 -              .formatMessage("label.alignment_output_command", new Object[]
 -              { e.getActionCommand() }), 600, 500);
 -    } catch (OutOfMemoryError oom)
 -    {
 -      new OOMWarning("Outputting alignment as " + e.getActionCommand(),
 -              oom);
 -      cap.dispose();
 -    }
 -
 -  }
 -
 -  public static AlignmentExportData getAlignmentForExport(
 -          FileFormatI format, AlignViewportI viewport,
 -          AlignExportSettingI exportSettings)
 -  {
 -    AlignmentI alignmentToExport = null;
 -    AlignExportSettingI settings = exportSettings;
 -    String[] omitHidden = null;
 -
 -    HiddenSequences hiddenSeqs = viewport.getAlignment()
 -            .getHiddenSequences();
 -
 -    alignmentToExport = viewport.getAlignment();
 -
 -    boolean hasHiddenSeqs = hiddenSeqs.getSize() > 0;
 -    if (settings == null)
 -    {
 -      settings = new AlignExportSettings(hasHiddenSeqs,
 -              viewport.hasHiddenColumns(), format);
 -    }
 -    // settings.isExportAnnotations();
 -
 -    if (viewport.hasHiddenColumns() && !settings.isExportHiddenColumns())
 -    {
 -      omitHidden = viewport.getViewAsString(false,
 -              settings.isExportHiddenSequences());
 -    }
 +      @Override
 +      public void run()
 +      {
 +        // todo defer this to inside formatSequences (or later)
 +        AlignmentExportData exportData = viewport
 +                .getAlignExportData(options);
 +        CutAndPasteTransfer cap = new CutAndPasteTransfer();
 +        cap.setForInput(null);
 +        try
 +        {
 +          FileFormatI format = fileFormat;
 +          cap.setText(new FormatAdapter(alignPanel, options)
 +                  .formatSequences(format, exportData.getAlignment(),
 +                          exportData.getOmitHidden(),
 +                          exportData.getStartEndPostions(),
 +                          viewport.getAlignment().getHiddenColumns()));
 +          Desktop.addInternalFrame(cap, MessageManager.formatMessage(
 +                  "label.alignment_output_command", new Object[]
 +                  { fileFormat.getName() }), 600, 500);
 +        } catch (OutOfMemoryError oom)
 +        {
 +          new OOMWarning("Outputting alignment as " + fileFormat.getName(),
 +                  oom);
 +          cap.dispose();
 +        }
 +      }
 +    };
  
 -    int[] alignmentStartEnd = new int[2];
 -    if (hasHiddenSeqs && settings.isExportHiddenSequences())
 +    /*
 +     * show dialog with export options if applicable; else just do it
 +     */
 +    if (AlignExportOptions.isNeeded(viewport, fileFormat))
      {
 -      alignmentToExport = hiddenSeqs.getFullAlignment();
 +      AlignExportOptions choices = new AlignExportOptions(
 +              alignPanel.getAlignViewport(), fileFormat, options);
 +      choices.setResponseAction(0, outputAction);
 +      choices.showDialog();
      }
      else
      {
 -      alignmentToExport = viewport.getAlignment();
 +      outputAction.run();
      }
 -    alignmentStartEnd = viewport.getAlignment().getHiddenColumns()
 -            .getVisibleStartAndEndIndex(alignmentToExport.getWidth());
 -    AlignmentExportData ed = new AlignmentExportData(alignmentToExport,
 -            omitHidden, alignmentStartEnd, settings);
 -    return ed;
    }
  
    /**
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Creates a PNG image of the alignment and writes it to the given file. If
 +   * the file is null, the user is prompted to choose a file.
     * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * @param f
     */
    @Override
    public void createPNG(File f)
    {
 -    alignPanel.makePNG(f);
 +    alignPanel.makeAlignmentImage(TYPE.PNG, f);
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Creates an EPS image of the alignment and writes it to the given file. If
 +   * the file is null, the user is prompted to choose a file.
     * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * @param f
     */
    @Override
    public void createEPS(File f)
    {
 -    alignPanel.makeEPS(f);
 +    alignPanel.makeAlignmentImage(TYPE.EPS, f);
    }
  
 +  /**
 +   * Creates an SVG image of the alignment and writes it to the given file. If
 +   * the file is null, the user is prompted to choose a file.
 +   * 
 +   * @param f
 +   */
    @Override
    public void createSVG(File f)
    {
 -    alignPanel.makeSVG(f);
 +    alignPanel.makeAlignmentImage(TYPE.SVG, f);
    }
  
    @Override
    @Override
    public void exportFeatures_actionPerformed(ActionEvent e)
    {
 -    new AnnotationExporter().exportFeatures(alignPanel);
 +    new AnnotationExporter(alignPanel).exportFeatures();
    }
  
    @Override
    public void exportAnnotations_actionPerformed(ActionEvent e)
    {
 -    new AnnotationExporter().exportAnnotations(alignPanel);
 +    new AnnotationExporter(alignPanel).exportAnnotations();
    }
  
    @Override
    public void associatedData_actionPerformed(ActionEvent e)
    {
 -    // Pick the tree file
 -    JalviewFileChooser chooser = new JalviewFileChooser(
 -            jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
 +    final JalviewFileChooser chooser = new JalviewFileChooser(
 +            Cache.getProperty("LAST_DIRECTORY"));
      chooser.setFileView(new JalviewFileView());
 -    chooser.setDialogTitle(
 -            MessageManager.getString("label.load_jalview_annotations"));
 -    chooser.setToolTipText(
 -            MessageManager.getString("label.load_jalview_annotations"));
 -
 -    int value = chooser.showOpenDialog(null);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    String tooltip = MessageManager
 +            .getString("label.load_jalview_annotations");
 +    chooser.setDialogTitle(tooltip);
 +    chooser.setToolTipText(tooltip);
 +    chooser.setResponseHandler(0, new Runnable()
      {
 -      String choice = chooser.getSelectedFile().getPath();
 -      jalview.bin.Cache.setProperty("LAST_DIRECTORY", choice);
 -      loadJalviewDataFile(choice, null, null, null);
 -    }
 +      @Override
 +      public void run()
 +      {
 +        String choice = chooser.getSelectedFile().getPath();
 +        Cache.setProperty("LAST_DIRECTORY", choice);
 +        loadJalviewDataFile(chooser.getSelectedFile(), null, null, null);
 +      }
 +    });
  
 +    chooser.showOpenDialog(this);
    }
  
    /**
            closeView(alignPanel);
          }
        }
 -
        if (closeAllTabs)
        {
 +        if (featureSettings != null && featureSettings.isOpen())
 +        {
 +          featureSettings.close();
 +          featureSettings = null;
 +        }
          /*
           * this will raise an INTERNAL_FRAME_CLOSED event and this method will
           * be called recursively, with the frame now in 'closed' state
    /**
     * DOCUMENT ME!
     */
-   void updateEditMenuBar()
+   public void updateEditMenuBar()
    {
  
      if (viewport.getHistoryList().size() > 0)
      {
        if (originalSource != viewport)
        {
 -        Cache.log.warn(
 +        Console.warn(
                  "Implementation worry: mismatch of viewport origin for undo");
        }
        originalSource.updateHiddenColumns();
  
        if (originalSource != viewport)
        {
 -        Cache.log.warn(
 +        Console.warn(
                  "Implementation worry: mismatch of viewport origin for redo");
        }
        originalSource.updateHiddenColumns();
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Calls AlignmentI.moveSelectedSequencesByOne with current sequence selection
 +   * or the sequence under cursor in keyboard mode
     * 
     * @param up
 -   *          DOCUMENT ME!
 +   *          or down (if !up)
     */
    public void moveSelectedSequences(boolean up)
    {
  
      if (sg == null)
      {
 +      if (viewport.cursorMode)
 +      {
 +        sg = new SequenceGroup();
 +        sg.addSequence(viewport.getAlignment().getSequenceAt(
 +                alignPanel.getSeqPanel().seqCanvas.cursorY), false);
 +      }
 +      else
 +      {
 +        return;
 +      }
 +    }
 +
 +    if (sg.getSize() < 1)
 +    {
        return;
      }
 +
 +    // TODO: JAL-3733 - add an event to the undo buffer for this !
 +
      viewport.getAlignment().moveSelectedSequencesByOne(sg,
              viewport.getHiddenRepSequences(), up);
      alignPanel.paintAlignment(true, false);
     *          DOCUMENT ME!
     */
    @Override
 -  protected void copy_actionPerformed(ActionEvent e)
 +  protected void copy_actionPerformed()
    {
 -    System.gc();
      if (viewport.getSelectionGroup() == null)
      {
        return;
        return;
      }
  
 -    ArrayList<int[]> hiddenColumns = null;
 +    HiddenColumns hiddenColumns = null;
      if (viewport.hasHiddenColumns())
      {
 -      hiddenColumns = new ArrayList<>();
        int hiddenOffset = viewport.getSelectionGroup().getStartRes();
        int hiddenCutoff = viewport.getSelectionGroup().getEndRes();
 -      ArrayList<int[]> hiddenRegions = viewport.getAlignment()
 -              .getHiddenColumns().getHiddenColumnsCopy();
 -      for (int[] region : hiddenRegions)
 -      {
 -        if (region[0] >= hiddenOffset && region[1] <= hiddenCutoff)
 -        {
 -          hiddenColumns
 -                  .add(new int[]
 -                  { region[0] - hiddenOffset, region[1] - hiddenOffset });
 -        }
 -      }
 +
 +      // create new HiddenColumns object with copy of hidden regions
 +      // between startRes and endRes, offset by startRes
 +      hiddenColumns = new HiddenColumns(
 +              viewport.getAlignment().getHiddenColumns(), hiddenOffset,
 +              hiddenCutoff, hiddenOffset);
      }
  
      Desktop.jalviewClipboard = new Object[] { seqs,
          viewport.getAlignment().getDataset(), hiddenColumns };
 -    statusBar.setText(MessageManager.formatMessage(
 +    setStatus(MessageManager.formatMessage(
              "label.copied_sequences_to_clipboard", new Object[]
              { Integer.valueOf(seqs.length).toString() }));
    }
                  && Desktop.jalviewClipboard[1] != alignment.getDataset();
          // importDs==true instructs us to copy over new dataset sequences from
          // an existing alignment
 -        Vector newDs = (importDs) ? new Vector() : null; // used to create
 +        Vector<SequenceI> newDs = (importDs) ? new Vector<>() : null; // used to
 +                                                                      // create
          // minimum dataset set
  
          for (int i = 0; i < sequences.length; i++)
                      newGraphGroups.add(q, null);
                    }
                    newGraphGroups.set(newann.graphGroup,
 -                          new Integer(++fgroup));
 +                          Integer.valueOf(++fgroup));
                  }
                  newann.graphGroup = newGraphGroups.get(newann.graphGroup)
                          .intValue();
                      newGraphGroups.add(q, null);
                    }
                    newGraphGroups.set(newann.graphGroup,
 -                          new Integer(++fgroup));
 +                          Integer.valueOf(++fgroup));
                  }
                  newann.graphGroup = newGraphGroups.get(newann.graphGroup)
                          .intValue();
        {
  
          // propagate alignment changed.
 -        viewport.getRanges().setEndSeq(alignment.getHeight());
 +        viewport.getRanges().setEndSeq(alignment.getHeight() - 1);
          if (annotationAdded)
          {
            // Duplicate sequence annotation in all views.
          if (Desktop.jalviewClipboard != null
                  && Desktop.jalviewClipboard[2] != null)
          {
 -          List<int[]> hc = (List<int[]>) Desktop.jalviewClipboard[2];
 -          for (int[] region : hc)
 -          {
 -            af.viewport.hideColumns(region[0], region[1]);
 -          }
 +          HiddenColumns hc = (HiddenColumns) Desktop.jalviewClipboard[2];
 +          af.viewport.setHiddenColumns(hc);
          }
  
          // >>>This is a fix for the moment, until a better solution is
        if (Desktop.jalviewClipboard != null
                && Desktop.jalviewClipboard[2] != null)
        {
 -        List<int[]> hc = (List<int[]>) Desktop.jalviewClipboard[2];
 -        for (int region[] : hc)
 -        {
 -          af.viewport.hideColumns(region[0], region[1]);
 -        }
 +        HiddenColumns hc = (HiddenColumns) Desktop.jalviewClipboard[2];
 +        af.viewport.setHiddenColumns(hc);
        }
  
        // >>>This is a fix for the moment, until a better solution is
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Action Cut (delete and copy) the selected region
     */
    @Override
 -  protected void cut_actionPerformed(ActionEvent e)
 +  protected void cut_actionPerformed()
    {
 -    copy_actionPerformed(null);
 -    delete_actionPerformed(null);
 +    copy_actionPerformed();
 +    delete_actionPerformed();
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Performs menu option to Delete the currently selected region
     */
    @Override
 -  protected void delete_actionPerformed(ActionEvent evt)
 +  protected void delete_actionPerformed()
    {
  
      SequenceGroup sg = viewport.getSelectionGroup();
        return;
      }
  
 -    /*
 -     * If the cut affects all sequences, warn, remove highlighted columns
 -     */
 -    if (sg.getSize() == viewport.getAlignment().getHeight())
 +    Runnable okAction = new Runnable()
      {
 -      boolean isEntireAlignWidth = (((sg.getEndRes() - sg.getStartRes())
 -              + 1) == viewport.getAlignment().getWidth()) ? true : false;
 -      if (isEntireAlignWidth)
 +      @Override
 +      public void run()
        {
 -        int confirm = JvOptionPane.showConfirmDialog(this,
 -                MessageManager.getString("warn.delete_all"), // $NON-NLS-1$
 -                MessageManager.getString("label.delete_all"), // $NON-NLS-1$
 -                JvOptionPane.OK_CANCEL_OPTION);
 +        SequenceI[] cut = sg.getSequences()
 +                .toArray(new SequenceI[sg.getSize()]);
 +
 +        addHistoryItem(new EditCommand(
 +                MessageManager.getString("label.cut_sequences"), Action.CUT,
 +                cut, sg.getStartRes(),
 +                sg.getEndRes() - sg.getStartRes() + 1,
 +                viewport.getAlignment()));
 +
 +        viewport.setSelectionGroup(null);
 +        viewport.sendSelection();
 +        viewport.getAlignment().deleteGroup(sg);
  
 -        if (confirm == JvOptionPane.CANCEL_OPTION
 -                || confirm == JvOptionPane.CLOSED_OPTION)
 +        viewport.firePropertyChange("alignment", null,
 +                viewport.getAlignment().getSequences());
 +        if (viewport.getAlignment().getHeight() < 1)
          {
 -          return;
 +          try
 +          {
 +            AlignFrame.this.setClosed(true);
 +          } catch (Exception ex)
 +          {
 +          }
          }
        }
 -      viewport.getColumnSelection().removeElements(sg.getStartRes(),
 -              sg.getEndRes() + 1);
 -    }
 -    SequenceI[] cut = sg.getSequences()
 -            .toArray(new SequenceI[sg.getSize()]);
 -
 -    addHistoryItem(new EditCommand(
 -            MessageManager.getString("label.cut_sequences"), Action.CUT,
 -            cut, sg.getStartRes(), sg.getEndRes() - sg.getStartRes() + 1,
 -            viewport.getAlignment()));
 -
 -    viewport.setSelectionGroup(null);
 -    viewport.sendSelection();
 -    viewport.getAlignment().deleteGroup(sg);
 +    };
  
 -    viewport.firePropertyChange("alignment", null,
 -            viewport.getAlignment().getSequences());
 -    if (viewport.getAlignment().getHeight() < 1)
 +    /*
 +     * If the cut affects all sequences, prompt for confirmation
 +     */
 +    boolean wholeHeight = sg.getSize() == viewport.getAlignment()
 +            .getHeight();
 +    boolean wholeWidth = (((sg.getEndRes() - sg.getStartRes())
 +            + 1) == viewport.getAlignment().getWidth()) ? true : false;
 +    if (wholeHeight && wholeWidth)
 +    {
 +      JvOptionPane dialog = JvOptionPane.newOptionDialog(Desktop.desktop);
 +      dialog.setResponseHandler(0, okAction); // 0 = OK_OPTION
 +      Object[] options = new Object[] {
 +          MessageManager.getString("action.ok"),
 +          MessageManager.getString("action.cancel") };
 +      dialog.showDialog(MessageManager.getString("warn.delete_all"),
 +              MessageManager.getString("label.delete_all"),
 +              JvOptionPane.DEFAULT_OPTION, JvOptionPane.PLAIN_MESSAGE, null,
 +              options, options[0]);
 +    }
 +    else
      {
 -      try
 -      {
 -        this.setClosed(true);
 -      } catch (Exception ex)
 -      {
 -      }
 +      okAction.run();
      }
    }
  
    @Override
    public void selectAllSequenceMenuItem_actionPerformed(ActionEvent e)
    {
 -    SequenceGroup sg = new SequenceGroup();
 -
 -    for (int i = 0; i < viewport.getAlignment().getSequences().size(); i++)
 -    {
 -      sg.addSequence(viewport.getAlignment().getSequenceAt(i), false);
 -    }
 +    SequenceGroup sg = new SequenceGroup(
 +            viewport.getAlignment().getSequences());
  
      sg.setEndRes(viewport.getAlignment().getWidth() - 1);
      viewport.setSelectionGroup(sg);
 +    viewport.isSelectionGroupChanged(true);
      viewport.sendSelection();
      // JAL-2034 - should delegate to
      // alignPanel to decide if overview needs
      }
      viewport.setSelectionGroup(null);
      viewport.getColumnSelection().clear();
 -    viewport.setSelectionGroup(null);
 +    viewport.setSearchResults(null);
      alignPanel.getIdPanel().getIdCanvas().searchResults = null;
      // JAL-2034 - should delegate to
      // alignPanel to decide if overview needs
                  column, viewport.getAlignment());
        }
  
 -      statusBar.setText(MessageManager
 -              .formatMessage("label.removed_columns", new String[]
 +      setStatus(MessageManager.formatMessage("label.removed_columns",
 +              new String[]
                { Integer.valueOf(trimRegion.getSize()).toString() }));
  
        addHistoryItem(trimRegion);
  
      addHistoryItem(removeGapCols);
  
 -    statusBar.setText(MessageManager
 -            .formatMessage("label.removed_empty_columns", new Object[]
 +    setStatus(MessageManager.formatMessage("label.removed_empty_columns",
 +            new Object[]
              { Integer.valueOf(removeGapCols.getSize()).toString() }));
  
      // This is to maintain viewport position on first residue
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Opens a Finder dialog
     * 
     * @param e
 -   *          DOCUMENT ME!
     */
    @Override
    public void findMenuItem_actionPerformed(ActionEvent e)
    {
 -    new Finder();
 +    new Finder(alignPanel, false, null);
    }
  
    /**
      /*
       * Create a new AlignmentPanel (with its own, new Viewport)
       */
 -    AlignmentPanel newap = new Jalview2XML().copyAlignPanel(alignPanel);
 +    AlignmentPanel newap = new jalview.project.Jalview2XML()
 +            .copyAlignPanel(alignPanel);
      if (!copyAnnotation)
      {
        /*
  
      newap.av.setGatherViewsHere(false);
  
 -    if (viewport.viewName == null)
 +    if (viewport.getViewName() == null)
      {
 -      viewport.viewName = MessageManager
 -              .getString("label.view_name_original");
 +      viewport.setViewName(
 +              MessageManager.getString("label.view_name_original"));
      }
  
      /*
      newap.av.setRedoList(viewport.getRedoList());
  
      /*
 +     * copy any visualisation settings that are not saved in the project
 +     */
 +    newap.av.setColourAppliesToAllGroups(
 +            viewport.getColourAppliesToAllGroups());
 +
 +    /*
       * Views share the same mappings; need to deregister any new mappings
       * created by copyAlignPanel, and register the new reference to the shared
       * mappings
        newap.refresh(true); // adjust layout of annotations
      }
  
 -    newap.av.viewName = getNewViewName(viewTitle);
 +    newap.av.setViewName(getNewViewName(viewTitle));
  
      addAlignmentPanel(newap, true);
      newap.alignmentChanged();
        if (comp instanceof AlignmentPanel)
        {
          AlignmentPanel ap = (AlignmentPanel) comp;
 -        if (!existingNames.contains(ap.av.viewName))
 +        if (!existingNames.contains(ap.av.getViewName()))
          {
 -          existingNames.add(ap.av.viewName);
 +          existingNames.add(ap.av.getViewName());
          }
        }
      }
      viewport.setFollowHighlight(state);
      if (state)
      {
 -      alignPanel.scrollToPosition(viewport.getSearchResults(), false);
 +      alignPanel.scrollToPosition(viewport.getSearchResults());
      }
    }
  
     * @param toggleSeqs
     * @param toggleCols
     */
 -  private void toggleHiddenRegions(boolean toggleSeqs, boolean toggleCols)
 +  protected void toggleHiddenRegions(boolean toggleSeqs, boolean toggleCols)
    {
  
      boolean hide = false;
      viewport.expandColSelection(sg, false);
      viewport.hideAllSelectedSeqs();
      viewport.hideSelectedColumns();
 +    alignPanel.updateLayout();
      alignPanel.paintAlignment(true, true);
      viewport.sendSelection();
    }
    public void hideSelColumns_actionPerformed(ActionEvent e)
    {
      viewport.hideSelectedColumns();
 +    alignPanel.updateLayout();
      alignPanel.paintAlignment(true, true);
      viewport.sendSelection();
    }
    protected void scaleAbove_actionPerformed(ActionEvent e)
    {
      viewport.setScaleAboveWrapped(scaleAbove.isSelected());
 -    // TODO: do we actually need to update overview for scale above change ?
 +    alignPanel.updateLayout();
      alignPanel.paintAlignment(true, false);
    }
  
    protected void scaleLeft_actionPerformed(ActionEvent e)
    {
      viewport.setScaleLeftWrapped(scaleLeft.isSelected());
 +    alignPanel.updateLayout();
      alignPanel.paintAlignment(true, false);
    }
  
    protected void scaleRight_actionPerformed(ActionEvent e)
    {
      viewport.setScaleRightWrapped(scaleRight.isSelected());
 +    alignPanel.updateLayout();
      alignPanel.paintAlignment(true, false);
    }
  
    @Override
    public void featureSettings_actionPerformed(ActionEvent e)
    {
 +    showFeatureSettingsUI();
 +  }
 +
 +  @Override
 +  public FeatureSettingsControllerI showFeatureSettingsUI()
 +  {
      if (featureSettings != null)
      {
 -      featureSettings.close();
 +      featureSettings.closeOldSettings();
        featureSettings = null;
      }
      if (!showSeqFeatures.isSelected())
        showSeqFeatures_actionPerformed(null);
      }
      featureSettings = new FeatureSettings(this);
 +    return featureSettings;
    }
  
    /**
    @Override
    public void alignmentProperties()
    {
 -    JEditorPane editPane = new JEditorPane("text/html", "");
 -    editPane.setEditable(false);
 +    JComponent pane;
      StringBuffer contents = new AlignmentProperties(viewport.getAlignment())
 +
              .formatAsHtml();
 -    editPane.setText(
 -            MessageManager.formatMessage("label.html_content", new Object[]
 -            { contents.toString() }));
 +    String content = MessageManager.formatMessage("label.html_content",
 +            new Object[]
 +            { contents.toString() });
 +    contents = null;
 +
 +    if (Platform.isJS())
 +    {
 +      JLabel textLabel = new JLabel();
 +      textLabel.setText(content);
 +      textLabel.setBackground(Color.WHITE);
 +
 +      pane = new JPanel(new BorderLayout());
 +      ((JPanel) pane).setOpaque(true);
 +      pane.setBackground(Color.WHITE);
 +      ((JPanel) pane).add(textLabel, BorderLayout.NORTH);
 +    }
 +    else
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
 +    {
 +      JEditorPane editPane = new JEditorPane("text/html", "");
 +      editPane.setEditable(false);
 +      editPane.setText(content);
 +      pane = editPane;
 +    }
 +
      JInternalFrame frame = new JInternalFrame();
 -    frame.getContentPane().add(new JScrollPane(editPane));
 +
 +    frame.getContentPane().add(new JScrollPane(pane));
  
      Desktop.addInternalFrame(frame, MessageManager
              .formatMessage("label.alignment_properties", new Object[]
                {
                  overview.dispose();
                  alignPanel.setOverviewPanel(null);
 -              };
 +              }
              });
 +    if (getKeyListeners().length > 0)
 +    {
 +      frame.addKeyListener(getKeyListeners()[0]);
 +    }
  
      alignPanel.setOverviewPanel(overview);
    }
       * otherwise set the chosen colour scheme (or null for 'None')
       */
      ColourSchemeI cs = ColourSchemes.getInstance().getColourScheme(name,
 -            viewport.getAlignment(), viewport.getHiddenRepSequences());
 +            viewport, viewport.getAlignment(),
 +            viewport.getHiddenRepSequences());
      changeColour(cs);
    }
  
  
      frameTitle += " from ";
  
 -    if (viewport.viewName != null)
 +    if (viewport.getViewName() != null)
      {
 -      frameTitle += viewport.viewName + " of ";
 +      frameTitle += viewport.getViewName() + " of ";
      }
  
      frameTitle += this.title;
      {
        sortByAnnotScore.removeAll();
        // almost certainly a quicker way to do this - but we keep it simple
 -      Hashtable scoreSorts = new Hashtable();
 +      Hashtable<String, String> scoreSorts = new Hashtable<>();
        AlignmentAnnotation aann[];
        for (SequenceI sqa : viewport.getAlignment().getSequences())
        {
            }
          }
        }
 -      Enumeration labels = scoreSorts.keys();
 +      Enumeration<String> labels = scoreSorts.keys();
        while (labels.hasMoreElements())
        {
 -        addSortByAnnotScoreMenuItem(sortByAnnotScore,
 -                (String) labels.nextElement());
 +        addSortByAnnotScoreMenuItem(sortByAnnotScore, labels.nextElement());
        }
        sortByAnnotScore.setVisible(scoreSorts.size() > 0);
        scoreSorts.clear();
      }
    }
  
    /**
     * Maintain the Order by->Displayed Tree menu. Creates a new menu item for a
     * TreePanel with an appropriate <code>jalview.analysis.AlignmentSorter</code>
      List<Component> comps = PaintRefresher.components
              .get(viewport.getSequenceSetId());
      List<TreePanel> treePanels = new ArrayList<>();
+     Map<TreeFrameI, TreeViewerBindingI> aptxFrames = TreeViewerUtils
+             .getActiveTreeViews();
      for (Component comp : comps)
      {
+       // old treepanels
        if (comp instanceof TreePanel)
        {
          treePanels.add((TreePanel) comp);
        }
      }
  
-     if (treePanels.size() < 1)
+     if (treePanels.isEmpty() && aptxFrames.isEmpty())
      {
        sortByTreeMenu.setVisible(false);
        return;
  
      sortByTreeMenu.setVisible(true);
  
+     for (Entry<TreeFrameI, TreeViewerBindingI> aptxFrameWithBinding : aptxFrames
+             .entrySet())
+     {
+       TreeFrameI aptxFrame = aptxFrameWithBinding.getKey();
+       TreeViewerBindingI binding = aptxFrameWithBinding.getValue();
+       // future support for multiple tabs
+       // for (org.forester.archaeopteryx.TreePanel aptxTree : aptxFrame
+       // .getMainPanel().getTreePanels())
+       {
+         final JMenuItem item = new JMenuItem(
+                 aptxFrame.getTree().getTreeName());
+           item.addActionListener(new ActionListener()
+           {
+             @Override
+             public void actionPerformed(ActionEvent e)
+             {
+             binding.sortByTree_actionPerformed(); // redundant here??
+             addHistoryItem(binding.sortAlignmentIn(alignPanel));
+             }
+           });
+         sortByTreeMenu.add(item);
+       }
+       }
+        
+            
+     // old treepanels
      for (final TreePanel tp : treePanels)
      {
        final JMenuItem item = new JMenuItem(tp.getTitle());
          @Override
          public void actionPerformed(ActionEvent e)
          {
-           // adapt to Aptx
            tp.sortByTree_actionPerformed();
            addHistoryItem(tp.sortAlignmentIn(alignPanel));
  
    @Override
    protected void loadTreeBaseStudy_actionPerformed(ActionEvent e)
    {
-     chooseTreeDb();
+     chooseTreeDb(0, null);
  
    }
  
    @Override
-   protected void loadTreeOfLife_actionPerformed(ActionEvent e)
+   protected void loadTreeBase_actionPerformed(ActionEvent e)
    {
-     chooseTreeDb();
+     chooseTreeDb(1, null);
  
    }
    @Override
-   protected void loadTreeFam_actionPerformed(ActionEvent e)
+   protected void loadTreePfam_actionPerformed(ActionEvent e)
    {
-     chooseTreeDb();
  
-   }
+     // only DBRefs of first sequence are checked for matching DB for now,
+     // iterating through them all seems excessive
+     SequenceI seq = viewport.getAlignment().getSequenceAt(0);
+     String dbId = null;
+     for (DBRefEntry pfamRef : DBRefUtils
+             .searchRefsForSource(seq.getDBRefs(), "pfam"))
+     {
+       if (pfamRef.getAccessionId().startsWith("PF"))
+       {
+         dbId = pfamRef.getAccessionId().replaceAll("[A-Za-z]", "");
+       }
+     }
+     chooseTreeDb(2, dbId);
  
+   }
    @Override
-   protected void loadTreePfam_actionPerformed(ActionEvent e)
+   protected void loadTreeFam_actionPerformed(ActionEvent e)
    {
-     chooseTreeDb();
+     chooseTreeDb(3, null);
  
    }
  
    @Override
-   protected void loadTreeBase_actionPerformed(ActionEvent e)
+   protected void loadTreeOfLife_actionPerformed(ActionEvent e)
    {
-     chooseTreeDb();
+     chooseTreeDb(4, null);
  
    }
  
  
    public void chooseTreeFile()
    {
      // Pick the tree file
      JalviewFileChooser chooser = new JalviewFileChooser(
 -            jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
 +            Cache.getProperty("LAST_DIRECTORY"));
      chooser.setFileView(new JalviewFileView());
      chooser.setDialogTitle(
              MessageManager.getString("label.select_tree_file")); // modify
      {
        chooser.setFileFilter(treeFormat.getTreeFilter());
      }
--
 -    int value = chooser.showOpenDialog(null);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
++    final AlignFrame us=this;
 +    chooser.setResponseHandler(0, new Runnable()
      {
 +      @Override
 +      public void run()
 +      {
-         String filePath = chooser.getSelectedFile().getPath();
-         Cache.setProperty("LAST_DIRECTORY", filePath);
-       TreeParser treeParser = null;
-       try {
-         treeParser = new TreeParser(filePath);
-         treeParser.loadTree(viewport);        
-         } catch (Exception ex)
-         {
-           JvOptionPane.showMessageDialog(Desktop.desktop, ex.getMessage(),
-                   MessageManager
-                           .getString("label.problem_reading_tree_file"),
-                   JvOptionPane.WARNING_MESSAGE);
-           ex.printStackTrace();
-         }
- // TODO: handle any other warnings from treeParser ?
-       //        if (treeParser != null && treeParser.fin.hasWarningMessage())
- //        {
- //          JvOptionPane.showMessageDialog(Desktop.desktop,
- //                  fin.getWarningMessage(),
- //                  MessageManager.getString(
- //                          "label.possible_problem_with_tree_file"),
- //                  JvOptionPane.WARNING_MESSAGE);
- //        }
+       String filePath = chooser.getSelectedFile().getPath();
+       Cache.setProperty("LAST_DIRECTORY", filePath);
+       
+       
+       NewickFile fin = null; // old tree
+       try
+       {
+         AptxInit.createInstancesFromFile(filePath, viewport);
+         // fin = new NewickFile(filePath, DataSourceType.FILE);
+         // viewport.setCurrentTree(viewport.getAlignPanel().alignFrame
+         // .showNewickTree(fin, filePath).getTree());
+       } catch (Exception ex)
+       {
 -        JvOptionPane.showMessageDialog(this, ex.getMessage(),
++        JvOptionPane.showMessageDialog(us, ex.getMessage(),
+                 MessageManager.getString("label.problem_reading_tree_file"),
+                 JvOptionPane.WARNING_MESSAGE);
+         ex.printStackTrace();
+       }
 -
 -    }
 +      }
 +    });
 +    chooser.showOpenDialog(this);
    }
  
    /**
          format = new IdentifyFile().identify(urlString, DataSourceType.URL);
          // add actual use for the format identification (jalview .jar files)
          treeUrl = new URL(urlString);
-         AptxInit.createInstanceFromUrl(treeUrl, viewport);
+         AptxInit.createInstancesFromUrl(treeUrl, viewport);
  
        } catch (IOException | RuntimeException e)
        {
      }
    }
  
-   public void chooseTreeDb()
+   /**
+    * Disgustingly hardcoded atm.
+    * 
+    * @param databaseIndex
+    */
+   public void chooseTreeDb(int databaseIndex, String defaultIdentifier)
    {
+     final WebservicesManager webservices_manager = WebservicesManager
+             .getInstance();
+     final PhylogeniesWebserviceClient client = webservices_manager
+             .getAvailablePhylogeniesWebserviceClient(databaseIndex);
+     String identifier = JvOptionPane
+             .showInternalInputDialog(Desktop.desktop,
+                     client.getInstructions() + "\n(Reference: "
+                             + client.getReference() + ")",
+                     client.getDescription(), JvOptionPane.QUESTION_MESSAGE,
+                     null, null, defaultIdentifier)
+             .toString();
+     AptxInit.createInstancesFromDb(client, identifier, viewport);
    }
  
    public TreePanel showNewickTree(NewickFile nf, String treeTitle)
  
    private boolean buildingMenu = false;
  
+   public void BuildTreeDbMenu()
+   {
+   }
    /**
     * Generates menu items and listener event actions for web service clients
     * 
              // No MSAWS used any more:
              // Vector msaws = null; // (Vector)
              // Discoverer.services.get("MsaWS");
 -            Vector secstrpr = (Vector) Discoverer.services
 +            Vector<ServiceHandle> secstrpr = Discoverer.services
                      .get("SecStrPred");
              if (secstrpr != null)
              {
                // Add any secondary structure prediction services
                for (int i = 0, j = secstrpr.size(); i < j; i++)
                {
 -                final ext.vamsas.ServiceHandle sh = (ext.vamsas.ServiceHandle) secstrpr
 -                        .get(i);
 +                final ext.vamsas.ServiceHandle sh = secstrpr.get(i);
                  jalview.ws.WSMenuEntryProviderI impl = jalview.ws.jws1.Discoverer
                          .getServiceClient(sh);
                  int p = secstrmenu.getItemCount();
                    webService.add(me.webServiceNoServices);
                  }
                  // TODO: move into separate menu builder class.
 -                boolean new_sspred = false;
 -                if (Cache.getDefault("SHOW_JWS2_SERVICES", true))
                  {
 +                  // logic for 2.11.1.4 is
 +                  // always look to see if there is a discover. if there isn't
 +                  // we can't show any Jws2 services
 +                  // if there are services available, show them - regardless of
 +                  // the 'show JWS2 preference'
 +                  // if the discoverer is running then say so
 +                  // otherwise offer to trigger discovery if 'show JWS2' is not
 +                  // enabled
                    Jws2Discoverer jws2servs = Jws2Discoverer.getDiscoverer();
                    if (jws2servs != null)
                    {
                        jws2servs.attachWSMenuEntry(webService, me);
                        for (Jws2Instance sv : jws2servs.getServices())
                        {
 -                        if (sv.description.toLowerCase().contains("jpred"))
 +                        if (sv.description.toLowerCase(Locale.ROOT)
 +                                .contains("jpred"))
                          {
                            for (JMenuItem jmi : legacyItems)
                            {
                            }
                          }
                        }
 -
                      }
 +
                      if (jws2servs.isRunning())
                      {
                        JMenuItem tm = new JMenuItem(
                        tm.setEnabled(false);
                        webService.add(tm);
                      }
 +                    else if (!Cache.getDefault("SHOW_JWS2_SERVICES", true))
 +                    {
 +                      JMenuItem enableJws2 = new JMenuItem(
 +                              "Discover Web Services");
 +                      enableJws2.setToolTipText(
 +                              "Select to start JABA Web Service discovery (or enable option in Web Service preferences)");
 +                      enableJws2.setEnabled(true);
 +                      enableJws2.addActionListener(new ActionListener()
 +                      {
 +
 +                        @Override
 +                        public void actionPerformed(ActionEvent e)
 +                        {
 +                          // start service discoverer, but ignore preference
 +                          Desktop.instance.startServiceDiscovery(false,
 +                                  true);
 +                        }
 +                      });
 +                      webService.add(enableJws2);
 +                    }
                    }
                  }
                  build_urlServiceMenu(me.webService);
                  }
                } catch (Exception e)
                {
 -                Cache.log.debug(
 +                Console.debug(
                          "Exception during web service menu building process.",
                          e);
                }
          }
          buildingMenu = false;
        }
-     }, "BuildWebServiceThread").start();
+     }, "BuildWebService").start();
  
    }
  
     * 
     * @param webService
     */
 -  private void build_urlServiceMenu(JMenu webService)
 +  protected void build_urlServiceMenu(JMenu webService)
    {
      // TODO: remove this code when 2.7 is released
      // DEBUG - alignmentView
        showProducts.setEnabled(showp);
      } catch (Exception e)
      {
 -      Cache.log.warn(
 +      Console.warn(
                "canShowProducts threw an exception - please report to help@jalview.org",
                e);
        return false;
    protected void showProductsFor(final SequenceI[] sel, final boolean _odna,
            final String source)
    {
 -    new Thread(CrossRefAction.showProductsFor(sel, _odna, source, this),
 +    new Thread(CrossRefAction.getHandlerFor(sel, _odna, source, this),
-             "CrossReferencesThread")
+             "CrossReferences")
              .start();
    }
  
     * frame's DNA sequences to their aligned protein (amino acid) equivalents.
     */
    @Override
 -  public void showTranslation_actionPerformed(ActionEvent e)
 +  public void showTranslation_actionPerformed(GeneticCodeI codeTable)
    {
      AlignmentI al = null;
      try
      {
        Dna dna = new Dna(viewport, viewport.getViewAsVisibleContigs(true));
  
 -      al = dna.translateCdna();
 +      al = dna.translateCdna(codeTable);
      } catch (Exception ex)
      {
 -      jalview.bin.Cache.log.error(
 -              "Exception during translation. Please report this !", ex);
 +      Console.error("Exception during translation. Please report this !",
 +              ex);
        final String msg = MessageManager.getString(
                "label.error_when_translating_sequences_submit_bug_report");
        final String errorTitle = MessageManager
        af.setFileFormat(this.currentFileFormat);
        final String newTitle = MessageManager
                .formatMessage("label.translation_of_params", new Object[]
 -              { this.getTitle() });
 +              { this.getTitle(), codeTable.getId() });
        af.setTitle(newTitle);
        if (Cache.getDefault(Preferences.ENABLE_SPLIT_FRAME, true))
        {
     * Try to load a features file onto the alignment.
     * 
     * @param file
 -   *          contents or path to retrieve file
 +   *          contents or path to retrieve file or a File object
     * @param sourceType
     *          access mode of file (see jalview.io.AlignFile)
     * @return true if features file was parsed correctly.
     */
 -  public boolean parseFeaturesFile(String file, DataSourceType sourceType)
 +  public boolean parseFeaturesFile(Object file, DataSourceType sourceType)
    {
 +    // BH 2018
      return avc.parseFeaturesFile(file, sourceType,
              Cache.getDefault("RELAXEDSEQIDMATCHING", false));
  
      // Java's Transferable for native dnd
      evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
      Transferable t = evt.getTransferable();
 +
      final AlignFrame thisaf = this;
 -    final List<String> files = new ArrayList<>();
 +    final List<Object> files = new ArrayList<>();
      List<DataSourceType> protocols = new ArrayList<>();
  
      try
               * Object[] { String,SequenceI}
               */
              ArrayList<Object[]> filesmatched = new ArrayList<>();
 -            ArrayList<String> filesnotmatched = new ArrayList<>();
 +            ArrayList<Object> filesnotmatched = new ArrayList<>();
              for (int i = 0; i < files.size(); i++)
              {
 -              String file = files.get(i).toString();
 +              // BH 2018
 +              Object file = files.get(i);
 +              String fileName = file.toString();
                String pdbfn = "";
 -              DataSourceType protocol = FormatAdapter.checkProtocol(file);
 +              DataSourceType protocol = (file instanceof File
 +                      ? DataSourceType.FILE
 +                      : FormatAdapter.checkProtocol(fileName));
                if (protocol == DataSourceType.FILE)
                {
 -                File fl = new File(file);
 +                File fl;
 +                if (file instanceof File)
 +                {
 +                  fl = (File) file;
 +                  Platform.cacheFileData(fl);
 +                }
 +                else
 +                {
 +                  fl = new File(fileName);
 +                }
                  pdbfn = fl.getName();
                }
                else if (protocol == DataSourceType.URL)
                {
 -                URL url = new URL(file);
 +                URL url = new URL(fileName);
                  pdbfn = url.getFile();
                }
                if (pdbfn.length() > 0)
                  }
                  if (mtch != null)
                  {
 -                  FileFormatI type = null;
 +                  FileFormatI type;
                    try
                    {
                      type = new IdentifyFile().identify(file, protocol);
              int assocfiles = 0;
              if (filesmatched.size() > 0)
              {
 -              if (Cache.getDefault("AUTOASSOCIATE_PDBANDSEQS", false)
 -                      || JvOptionPane.showConfirmDialog(thisaf,
 -                              MessageManager.formatMessage(
 -                                      "label.automatically_associate_structure_files_with_sequences_same_name",
 -                                      new Object[]
 -                                      { Integer.valueOf(filesmatched.size())
 -                                              .toString() }),
 -                              MessageManager.getString(
 -                                      "label.automatically_associate_structure_files_by_name"),
 -                              JvOptionPane.YES_NO_OPTION) == JvOptionPane.YES_OPTION)
 -
 +              boolean autoAssociate = Cache
 +                      .getDefault("AUTOASSOCIATE_PDBANDSEQS", false);
 +              if (!autoAssociate)
 +              {
 +                String msg = MessageManager.formatMessage(
 +                        "label.automatically_associate_structure_files_with_sequences_same_name",
 +                        new Object[]
 +                        { Integer.valueOf(filesmatched.size())
 +                                .toString() });
 +                String ttl = MessageManager.getString(
 +                        "label.automatically_associate_structure_files_by_name");
 +                int choice = JvOptionPane.showConfirmDialog(thisaf, msg,
 +                        ttl, JvOptionPane.YES_NO_OPTION);
 +                autoAssociate = choice == JvOptionPane.YES_OPTION;
 +              }
 +              if (autoAssociate)
                {
                  for (Object[] fm : filesmatched)
                  {
                    for (SequenceI toassoc : (SequenceI[]) fm[2])
                    {
                      PDBEntry pe = new AssociatePdbFileWithSeq()
 -                            .associatePdbWithSeq((String) fm[0],
 +                            .associatePdbWithSeq(fm[0].toString(),
                                      (DataSourceType) fm[1], toassoc, false,
                                      Desktop.instance);
                      if (pe != null)
                      {
                        System.err.println("Associated file : "
 -                              + ((String) fm[0]) + " with "
 +                              + (fm[0].toString()) + " with "
                                + toassoc.getDisplayId(true));
                        assocfiles++;
                      }
                    alignPanel.paintAlignment(true, false);
                  }
                }
 +              else
 +              {
 +                /*
 +                 * add declined structures as sequences
 +                 */
 +                for (Object[] o : filesmatched)
 +                {
 +                  filesnotmatched.add(o[0]);
 +                }
 +              }
              }
              if (filesnotmatched.size() > 0)
              {
                {
                  return;
                }
 -              for (String fn : filesnotmatched)
 +              for (Object fn : filesnotmatched)
                {
                  loadJalviewDataFile(fn, null, null, null);
                }
              ex.printStackTrace();
            }
          }
-       }, "DropFileThread").start();
+       }, "DropFile").start();
      }
    }
  
     * @param file
     *          either a filename or a URL string.
     */
 -  public void loadJalviewDataFile(String file, DataSourceType sourceType,
 +  public void loadJalviewDataFile(Object file, DataSourceType sourceType,
            FileFormatI format, SequenceI assocSeq)
    {
 +    // BH 2018 was String file
      try
      {
        if (sourceType == null)
                changeColour(
                        new TCoffeeColourScheme(viewport.getAlignment()));
                isAnnotation = true;
 -              statusBar.setText(MessageManager.getString(
 +              setStatus(MessageManager.getString(
                        "label.successfully_pasted_tcoffee_scores_to_alignment"));
              }
              else
            }
          } catch (Exception x)
          {
 -          Cache.log.debug(
 +          Console.debug(
                    "Exception when processing data source as T-COFFEE score file",
                    x);
            tcf = null;
                      new FileParse(file, sourceType));
              sm.parse();
              // todo: i18n this message
 -            statusBar.setText(MessageManager.formatMessage(
 +            setStatus(MessageManager.formatMessage(
                      "label.successfully_loaded_matrix",
                      sm.getMatrixName()));
            }
              new JnetAnnotationMaker();
              JnetAnnotationMaker.add_annotation(predictions,
                      viewport.getAlignment(), 0, false);
 -            SequenceI repseq = viewport.getAlignment().getSequenceAt(0);
 -            viewport.getAlignment().setSeqrep(repseq);
 -            HiddenColumns cs = new HiddenColumns();
 -            cs.hideInsertionsFor(repseq);
 -            viewport.getAlignment().setHiddenColumns(cs);
 +            viewport.getAlignment().setupJPredAlignment();
              isAnnotation = true;
            }
            // else if (IdentifyFile.FeaturesFile.equals(format))
            {
              if (parseFeaturesFile(file, sourceType))
              {
 -              alignPanel.paintAlignment(true, true);
 +              SplitFrame splitFrame = (SplitFrame) getSplitViewContainer();
 +              if (splitFrame != null)
 +              {
 +                splitFrame.repaint();
 +              }
 +              else
 +              {
 +                alignPanel.paintAlignment(true, true);
 +              }
              }
            }
            else
        viewport = alignPanel.av;
        avc.setViewportAndAlignmentPanel(viewport, alignPanel);
        setMenusFromViewport(viewport);
 +      if (featureSettings != null && featureSettings.isOpen()
 +              && featureSettings.fr.getViewport() != viewport)
 +      {
 +        if (viewport.isShowSequenceFeatures())
 +        {
 +          // refresh the featureSettings to reflect UI change
 +          showFeatureSettingsUI();
 +        }
 +        else
 +        {
 +          // close feature settings for this view.
 +          featureSettings.close();
 +        }
 +      }
 +
      }
  
      /*
      if (e.isPopupTrigger())
      {
        String msg = MessageManager.getString("label.enter_view_name");
 -      String reply = JvOptionPane.showInternalInputDialog(this, msg, msg,
 -              JvOptionPane.QUESTION_MESSAGE);
 +      String ttl = tabbedPane.getTitleAt(tabbedPane.getSelectedIndex());
 +      String reply = JvOptionPane.showInputDialog(msg, ttl);
  
        if (reply != null)
        {
 -        viewport.viewName = reply;
 +        viewport.setViewName(reply);
          // TODO warn if reply is in getExistingViewNames()?
          tabbedPane.setTitleAt(tabbedPane.getSelectedIndex(), reply);
        }
              MessageManager.getString("option.trim_retrieved_seqs"));
      trimrs.setToolTipText(
              MessageManager.getString("label.trim_retrieved_sequences"));
 -    trimrs.setSelected(Cache.getDefault("TRIM_FETCHED_DATASET_SEQS", true));
 +    trimrs.setSelected(
 +            Cache.getDefault(DBRefFetcher.TRIM_RETRIEVED_SEQUENCES, true));
      trimrs.addActionListener(new ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent e)
        {
          trimrs.setSelected(trimrs.isSelected());
 -        Cache.setProperty("TRIM_FETCHED_DATASET_SEQS",
 +        Cache.setProperty(DBRefFetcher.TRIM_RETRIEVED_SEQUENCES,
                  Boolean.valueOf(trimrs.isSelected()).toString());
 -      };
 +      }
      });
      rfetch.add(trimrs);
      JMenuItem fetchr = new JMenuItem(
                @Override
                public void finished()
                {
 +
 +                for (FeatureSettingsModelI srcSettings : dbRefFetcher
 +                        .getFeatureSettingsModels())
 +                {
 +
 +                  alignPanel.av.mergeFeaturesStyle(srcSettings);
 +                }
                  AlignFrame.this.setMenusForViewport();
                }
              });
              dbRefFetcher.fetchDBRefs(false);
            }
-         }, "BuildFetchDBMenuThread").start();
+         }, "BuildFetchDBMenu").start();
  
        }
  
      });
      rfetch.add(fetchr);
 -    final AlignFrame me = this;
      new Thread(new Runnable()
      {
        @Override
        public void run()
        {
          final jalview.ws.SequenceFetcher sf = jalview.gui.SequenceFetcher
 -                .getSequenceFetcherSingleton(me);
 +                .getSequenceFetcherSingleton();
          javax.swing.SwingUtilities.invokeLater(new Runnable()
          {
            @Override
            public void run()
            {
 -            String[] dbclasses = sf.getOrderedSupportedSources();
 -            // sf.getDbInstances(jalview.ws.dbsources.DasSequenceSource.class);
 -            // jalview.util.QuickSort.sort(otherdb, otherdb);
 +            String[] dbclasses = sf.getNonAlignmentSources();
              List<DbSourceProxy> otherdb;
              JMenu dfetch = new JMenu();
              JMenu ifetch = new JMenu();
                {
                  continue;
                }
 -              // List<DbSourceProxy> dbs=otherdb;
 -              // otherdb=new ArrayList<DbSourceProxy>();
 -              // for (DbSourceProxy db:dbs)
 -              // {
 -              // if (!db.isA(DBRefSource.ALIGNMENTDB)
 -              // }
                if (mname == null)
                {
                  mname = "From " + dbclass;
                                    @Override
                                    public void finished()
                                    {
 +                                    FeatureSettingsModelI srcSettings = dassource[0]
 +                                            .getFeatureColourScheme();
 +                                    alignPanel.av.mergeFeaturesStyle(
 +                                            srcSettings);
                                      AlignFrame.this.setMenusForViewport();
                                    }
                                  });
      {
        PaintRefresher.Refresh(this, viewport.getSequenceSetId());
        alignPanel.updateAnnotation();
 -      alignPanel.paintAlignment(true, true);
 +      alignPanel.paintAlignment(true,
 +              viewport.needToUpdateStructureViews());
      }
    }
  
    {
      if (avc.createGroup())
      {
 +      if (applyAutoAnnotationSettings.isSelected())
 +      {
 +        alignPanel.updateAnnotation(true, false);
 +      }
        alignPanel.alignmentChanged();
      }
    }
     */
    public List<? extends AlignmentViewPanel> getAlignPanels()
    {
 -    return alignPanels == null ? Arrays.asList(alignPanel) : alignPanels;
 +    // alignPanels is never null
 +    // return alignPanels == null ? Arrays.asList(alignPanel) : alignPanels;
 +    return alignPanels;
    }
  
    /**
      colourMenu.add(textColour);
      colourMenu.addSeparator();
  
 -    ColourMenuHelper.addMenuItems(colourMenu, this, viewport.getAlignment(),
 -            false);
 +    ButtonGroup bg = ColourMenuHelper.addMenuItems(colourMenu, this,
 +            viewport.getAlignment(), false);
  
 +    colourMenu.add(annotationColour);
 +    bg.add(annotationColour);
      colourMenu.addSeparator();
      colourMenu.add(conservationMenuItem);
      colourMenu.add(modifyConservation);
      colourMenu.add(abovePIDThreshold);
      colourMenu.add(modifyPID);
 -    colourMenu.add(annotationColour);
  
      ColourSchemeI colourScheme = viewport.getGlobalColourScheme();
      ColourMenuHelper.setColourSelected(colourMenu, colourScheme);
        new CalculationChooser(AlignFrame.this);
      }
    }
 +
 +  @Override
 +  protected void loadVcf_actionPerformed()
 +  {
 +    JalviewFileChooser chooser = new JalviewFileChooser(
 +            Cache.getProperty("LAST_DIRECTORY"));
 +    chooser.setFileView(new JalviewFileView());
 +    chooser.setDialogTitle(MessageManager.getString("label.load_vcf_file"));
 +    chooser.setToolTipText(MessageManager.getString("label.load_vcf_file"));
 +    final AlignFrame us = this;
 +    chooser.setResponseHandler(0, new Runnable()
 +    {
 +      @Override
 +      public void run()
 +      {
 +        String choice = chooser.getSelectedFile().getPath();
 +        Cache.setProperty("LAST_DIRECTORY", choice);
 +        SequenceI[] seqs = viewport.getAlignment().getSequencesArray();
 +        new VCFLoader(choice).loadVCF(seqs, us);
 +      }
 +    });
 +    chooser.showOpenDialog(null);
 +
 +  }
 +
 +  private Rectangle lastFeatureSettingsBounds = null;
 +
 +  @Override
 +  public void setFeatureSettingsGeometry(Rectangle bounds)
 +  {
 +    lastFeatureSettingsBounds = bounds;
 +  }
 +
 +  @Override
 +  public Rectangle getFeatureSettingsGeometry()
 +  {
 +    return lastFeatureSettingsBounds;
 +  }
  }
  
  class PrintThread extends Thread
@@@ -29,7 -29,6 +29,7 @@@ import jalview.api.FeatureSettingsModel
  import jalview.api.FeaturesDisplayedI;
  import jalview.api.ViewStyleI;
  import jalview.bin.Cache;
 +import jalview.bin.Console;
  import jalview.commands.CommandI;
  import jalview.datamodel.AlignedCodonFrame;
  import jalview.datamodel.Alignment;
@@@ -48,7 -47,6 +48,7 @@@ import jalview.schemes.UserColourScheme
  import jalview.structure.SelectionSource;
  import jalview.structure.StructureSelectionManager;
  import jalview.structure.VamsasSource;
 +import jalview.util.ColorUtils;
  import jalview.util.MessageManager;
  import jalview.viewmodel.AlignmentViewport;
  import jalview.ws.params.AutoCalcSetting;
@@@ -58,7 -56,6 +58,7 @@@ import java.awt.Dimension
  import java.awt.Font;
  import java.awt.FontMetrics;
  import java.awt.Rectangle;
 +import java.util.ArrayList;
  import java.util.Hashtable;
  import java.util.List;
  
@@@ -79,9 -76,9 +79,9 @@@ public class AlignViewport extends Alig
  
    boolean antiAlias = false;
  
 -  private Rectangle explodedGeometry;
 +  private Rectangle explodedGeometry = null;
  
 -  String viewName;
 +  private String viewName = null;
  
    /*
     * Flag set true on the view that should 'gather' multiple views of the same
      sequenceSetID = seqsetid;
      viewId = viewid;
      // TODO remove these once 2.4.VAMSAS release finished
 -    if (Cache.log != null && Cache.log.isDebugEnabled() && seqsetid != null)
 +    if (seqsetid != null)
      {
 -      Cache.log.debug(
 +      Console.debug(
                "Setting viewport's sequence set id : " + sequenceSetID);
      }
 -    if (Cache.log != null && Cache.log.isDebugEnabled() && viewId != null)
 +    if (viewId != null)
      {
 -      Cache.log.debug("Setting viewport's view id : " + viewId);
 +      Console.debug("Setting viewport's view id : " + viewId);
      }
      init();
  
      sequenceSetID = seqsetid;
      viewId = viewid;
      // TODO remove these once 2.4.VAMSAS release finished
 -    if (Cache.log != null && Cache.log.isDebugEnabled() && seqsetid != null)
 +    if (seqsetid != null)
      {
 -      Cache.log.debug(
 +      Console.debug(
                "Setting viewport's sequence set id : " + sequenceSetID);
      }
 -    if (Cache.log != null && Cache.log.isDebugEnabled() && viewId != null)
 +    if (viewId != null)
      {
 -      Cache.log.debug("Setting viewport's view id : " + viewId);
 +      Console.debug("Setting viewport's view id : " + viewId);
      }
  
      if (hiddenColumns != null)
     */
    private void applyViewProperties()
    {
 -    antiAlias = Cache.getDefault("ANTI_ALIAS", false);
 +    antiAlias = Cache.getDefault("ANTI_ALIAS", true);
  
      viewStyle.setShowJVSuffix(Cache.getDefault("SHOW_JVSUFFIX", true));
      setShowAnnotation(Cache.getDefault("SHOW_ANNOTATIONS", true));
        schemeName = Cache.getDefault(Preferences.DEFAULT_COLOUR,
                ResidueColourScheme.NONE);
      }
 -    ColourSchemeI colourScheme = ColourSchemeProperty
 -            .getColourScheme(alignment, schemeName);
 +    ColourSchemeI colourScheme = ColourSchemeProperty.getColourScheme(this,
 +            alignment, schemeName);
      residueShading = new ResidueShader(colourScheme);
  
      if (colourScheme instanceof UserColourScheme)
      {
        residueShading.setConsensus(hconsensus);
      }
 +    setColourAppliesToAllGroups(true);
    }
  
    boolean validCharWidth;
    }
  
    /**
 -   * returns the visible column regions of the alignment
 -   * 
 -   * @param selectedRegionOnly
 -   *          true to just return the contigs intersecting with the selected
 -   *          area
 -   * @return
 -   */
 -  public int[] getViewAsVisibleContigs(boolean selectedRegionOnly)
 -  {
 -    int[] viscontigs = null;
 -    int start = 0, end = 0;
 -    if (selectedRegionOnly && selectionGroup != null)
 -    {
 -      start = selectionGroup.getStartRes();
 -      end = selectionGroup.getEndRes() + 1;
 -    }
 -    else
 -    {
 -      end = alignment.getWidth();
 -    }
 -    viscontigs = alignment.getHiddenColumns().getVisibleContigs(start, end);
 -    return viscontigs;
 -  }
 -
 -  /**
     * get hash of undo and redo list for the alignment
     * 
     * @return long[] { historyList.hashCode, redoList.hashCode };
     * return the alignPanel containing the given viewport. Use this to get the
     * components currently handling the given viewport.
     * 
-    * @param av
     * @return null or an alignPanel guaranteed to have non-null alignFrame
     *         reference
     */
      // calculator.getRegisteredWorkersOfClass(settings.getWorkerClass())
      if (needsUpdate)
      {
 -      Cache.log.debug("trigger update for " + calcId);
 +      Console.debug("trigger update for " + calcId);
      }
    }
  
      {
        if (AlignmentUtils.isMappable(toAdd, getAlignment()))
        {
 -        if (openLinkedAlignment(toAdd, title))
 -        {
 -          return;
 -        }
 +        openLinkedAlignment(toAdd, title);
 +        return;
        }
      }
 +    addDataToAlignment(toAdd);
 +  }
  
 -    /*
 -     * No mappings, or offer declined - add sequences to this alignment
 -     */
 +  /**
 +   * adds sequences to this alignment
 +   * 
 +   * @param toAdd
 +   */
 +  void addDataToAlignment(AlignmentI toAdd)
 +  {
      // TODO: JAL-407 regardless of above - identical sequences (based on ID and
      // provenance) should share the same dataset sequence
  
        }
      }
  
 -    ranges.setEndSeq(getAlignment().getHeight());
 +    ranges.setEndSeq(getAlignment().getHeight() - 1); // BH 2019.04.18
      firePropertyChange("alignment", null, getAlignment().getSequences());
    }
  
     * @param al
     * @param title
     */
 -  protected boolean openLinkedAlignment(AlignmentI al, String title)
 +  protected void openLinkedAlignment(AlignmentI al, String title)
    {
      String[] options = new String[] { MessageManager.getString("action.no"),
          MessageManager.getString("label.split_window"),
          MessageManager.getString("label.new_window"), };
      final String question = JvSwingUtils.wrapTooltip(true,
              MessageManager.getString("label.open_split_window?"));
 -    int response = JvOptionPane.showOptionDialog(Desktop.desktop, question,
 +    final AlignViewport us = this;
 +
 +    /*
 +     * options No, Split Window, New Window correspond to
 +     * dialog responses 0, 1, 2 (even though JOptionPane shows them
 +     * in reverse order)
 +     */
 +    JvOptionPane dialog = JvOptionPane.newOptionDialog(Desktop.desktop)
 +            .setResponseHandler(0, new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                addDataToAlignment(al);
 +              }
 +            }).setResponseHandler(1, new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                us.openLinkedAlignmentAs(al, title, true);
 +              }
 +            }).setResponseHandler(2, new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                us.openLinkedAlignmentAs(al, title, false);
 +              }
 +            });
 +    dialog.showDialog(question,
              MessageManager.getString("label.open_split_window"),
              JvOptionPane.DEFAULT_OPTION, JvOptionPane.PLAIN_MESSAGE, null,
              options, options[0]);
 +  }
  
 -    if (response != 1 && response != 2)
 -    {
 -      return false;
 -    }
 -    final boolean openSplitPane = (response == 1);
 -    final boolean openInNewWindow = (response == 2);
 -
 +  protected void openLinkedAlignmentAs(AlignmentI al, String title,
 +          boolean newWindowOrSplitPane)
 +  {
      /*
       * Identify protein and dna alignments. Make a copy of this one if opening
       * in a new split pane.
       */
 -    AlignmentI thisAlignment = openSplitPane ? new Alignment(getAlignment())
 +    AlignmentI thisAlignment = newWindowOrSplitPane
 +            ? new Alignment(getAlignment())
              : getAlignment();
      AlignmentI protein = al.isNucleotide() ? thisAlignment : al;
      final AlignmentI cdna = al.isNucleotide() ? al : thisAlignment;
      AlignFrame newAlignFrame = new AlignFrame(al, AlignFrame.DEFAULT_WIDTH,
              AlignFrame.DEFAULT_HEIGHT);
      newAlignFrame.setTitle(title);
 -    newAlignFrame.statusBar.setText(MessageManager
 +    newAlignFrame.setStatus(MessageManager
              .formatMessage("label.successfully_loaded_file", new Object[]
              { title }));
  
      // alignFrame.setFileName(file, format);
      // }
  
 -    if (openInNewWindow)
 +    if (!newWindowOrSplitPane)
      {
        Desktop.addInternalFrame(newAlignFrame, title,
                AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
  
      try
      {
 -      newAlignFrame.setMaximum(
 -              jalview.bin.Cache.getDefault("SHOW_FULLSCREEN", false));
 +      newAlignFrame.setMaximum(Cache.getDefault("SHOW_FULLSCREEN", false));
      } catch (java.beans.PropertyVetoException ex)
      {
      }
  
 -    if (openSplitPane)
 +    if (newWindowOrSplitPane)
      {
        al.alignAs(thisAlignment);
        protein = openSplitFrame(newAlignFrame, thisAlignment);
      }
 -
 -    return true;
    }
  
    /**
      if (ap != null)
      {
        // modify GUI elements to reflect geometry change
 -      Dimension idw = getAlignPanel().getIdPanel().getIdCanvas()
 -              .getPreferredSize();
 +      Dimension idw = ap.getIdPanel().getIdCanvas().getPreferredSize();
        idw.width = i;
 -      getAlignPanel().getIdPanel().getIdCanvas().setPreferredSize(idw);
 +      ap.getIdPanel().getIdCanvas().setPreferredSize(idw);
      }
    }
  
    @Override
    public void applyFeaturesStyle(FeatureSettingsModelI featureSettings)
    {
 +    transferFeaturesStyles(featureSettings, false);
 +  }
 +
 +  /**
 +   * Applies the supplied feature settings descriptor to currently known
 +   * features. This supports an 'initial configuration' of feature colouring
 +   * based on a preset or user favourite. This may then be modified in the usual
 +   * way using the Feature Settings dialogue.
 +   * 
 +   * @param featureSettings
 +   */
 +  @Override
 +  public void mergeFeaturesStyle(FeatureSettingsModelI featureSettings)
 +  {
 +    transferFeaturesStyles(featureSettings, true);
 +  }
 +
 +  /**
 +   * when mergeOnly is set, then group and feature visibility or feature colours
 +   * are not modified for features and groups already known to the feature
 +   * renderer. Feature ordering is always adjusted, and transparency is always
 +   * set regardless.
 +   * 
 +   * @param featureSettings
 +   * @param mergeOnly
 +   */
 +  private void transferFeaturesStyles(FeatureSettingsModelI featureSettings,
 +          boolean mergeOnly)
 +  {
      if (featureSettings == null)
      {
        return;
  
      FeatureRenderer fr = getAlignPanel().getSeqPanel().seqCanvas
              .getFeatureRenderer();
 +    List<String> origRenderOrder = new ArrayList<>();
 +    List<String> origGroups = new ArrayList<>();
 +    // preserve original render order - allows differentiation between user
 +    // configured colours and autogenerated ones
 +    origRenderOrder.addAll(fr.getRenderOrder());
 +    origGroups.addAll(fr.getFeatureGroups());
 +
      fr.findAllFeatures(true);
      List<String> renderOrder = fr.getRenderOrder();
      FeaturesDisplayedI displayed = fr.getFeaturesDisplayed();
 -    displayed.clear();
 +    if (!mergeOnly)
 +    {
 +      // only clear displayed features if we are mergeing
 +      // displayed.clear();
 +    }
      // TODO this clears displayed.featuresRegistered - do we care?
 -
 +    //
 +    // JAL-3330 - JBP - yes we do - calling applyFeatureStyle to a view where
 +    // feature visibility has already been configured is not very friendly
      /*
       * set feature colour if specified by feature settings
       * set visibility of all features
      {
        FeatureColourI preferredColour = featureSettings
                .getFeatureColour(type);
 -      if (preferredColour != null)
 -      {
 -        fr.setColour(type, preferredColour);
 -      }
 -      if (featureSettings.isFeatureDisplayed(type))
 +      FeatureColourI origColour = fr.getFeatureStyle(type);
 +      if (!mergeOnly || (!origRenderOrder.contains(type)
 +              || origColour == null
 +              || (!origColour.isGraduatedColour()
 +                      && origColour.getColour() != null
 +                      && origColour.getColour().equals(
 +                              ColorUtils.createColourFromName(type)))))
        {
 -        displayed.setVisible(type);
 +        // if we are merging, only update if there wasn't already a colour
 +        // defined for
 +        // this type
 +        if (preferredColour != null)
 +        {
 +          fr.setColour(type, preferredColour);
 +        }
 +        if (featureSettings.isFeatureDisplayed(type))
 +        {
 +          displayed.setVisible(type);
 +        }
 +        else if (featureSettings.isFeatureHidden(type))
 +        {
 +          displayed.setHidden(type);
 +        }
        }
      }
  
       */
      for (String group : fr.getFeatureGroups())
      {
 -      fr.setGroupVisibility(group, featureSettings.isGroupDisplayed(group));
 +      if (!mergeOnly || !origGroups.contains(group))
 +      {
 +        // when merging, display groups only if the aren't already marked as not
 +        // visible
 +        fr.setGroupVisibility(group,
 +                featureSettings.isGroupDisplayed(group));
 +      }
      }
  
      /*
        fr.orderFeatures(featureSettings);
      }
      fr.setTransparency(featureSettings.getTransparency());
 +
 +    fr.notifyFeaturesChanged();
 +  }
 +
 +  public String getViewName()
 +  {
 +    return viewName;
 +  }
 +
 +  public void setViewName(String viewName)
 +  {
 +    this.viewName = viewName;
    }
  }
   */
  package jalview.gui;
  
 -import jalview.bin.Cache;
 -import jalview.datamodel.AlignmentI;
 -import jalview.datamodel.PDBEntry;
 -import jalview.datamodel.SequenceI;
 -import jalview.gui.StructureViewer.ViewerType;
 -import jalview.structures.models.AAStructureBindingModel;
 -import jalview.util.BrowserLauncher;
 -import jalview.util.MessageManager;
 -import jalview.util.Platform;
 -import jalview.ws.dbsources.Pdb;
 +import java.util.Locale;
  
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Dimension;
  import java.awt.Font;
  import java.awt.Graphics;
 -import java.awt.Rectangle;
 -import java.awt.event.ActionEvent;
  import java.io.File;
 -import java.util.ArrayList;
  import java.util.List;
 -import java.util.Vector;
 +import java.util.Map;
  
 -import javax.swing.JCheckBoxMenuItem;
 -import javax.swing.JInternalFrame;
  import javax.swing.JPanel;
  import javax.swing.JSplitPane;
  import javax.swing.SwingUtilities;
  import javax.swing.event.InternalFrameAdapter;
  import javax.swing.event.InternalFrameEvent;
  
 +import jalview.api.AlignmentViewPanel;
 +import jalview.bin.Console;
 +import jalview.datamodel.PDBEntry;
 +import jalview.datamodel.SequenceI;
 +import jalview.datamodel.StructureViewerModel;
 +import jalview.datamodel.StructureViewerModel.StructureData;
 +import jalview.fts.service.alphafold.AlphafoldRestClient;
 +import jalview.gui.ImageExporter.ImageWriterI;
 +import jalview.gui.StructureViewer.ViewerType;
 +import jalview.structure.StructureCommand;
 +import jalview.structures.models.AAStructureBindingModel;
 +import jalview.util.BrowserLauncher;
 +import jalview.util.ImageMaker;
 +import jalview.util.MessageManager;
 +import jalview.util.Platform;
 +
  public class AppJmol extends StructureViewerBase
  {
    // ms to wait for Jmol to load files
@@@ -60,7 -58,7 +60,7 @@@
  
    private static final String SPACE = " ";
  
 -  private static final String BACKSLASH = "\"";
 +  private static final String QUOTE = "\"";
  
    AppJmolBinding jmb;
  
     * @param bounds
     * @param viewid
     */
 -  public AppJmol(String[] files, String[] ids, SequenceI[][] seqs,
 -          AlignmentPanel ap, boolean usetoColour, boolean useToAlign,
 -          boolean leaveColouringToJmol, String loadStatus, Rectangle bounds,
 -          String viewid)
 +  public AppJmol(StructureViewerModel viewerModel, AlignmentPanel ap,
 +          String sessionFile, String viewid)
    {
 -    PDBEntry[] pdbentrys = new PDBEntry[files.length];
 -    for (int i = 0; i < pdbentrys.length; i++)
 -    {
 -      // PDBEntry pdbentry = new PDBEntry(files[i], ids[i]);
 -      PDBEntry pdbentry = new PDBEntry(ids[i], null, PDBEntry.Type.PDB,
 -              files[i]);
 +    Map<File, StructureData> pdbData = viewerModel.getFileData();
 +    PDBEntry[] pdbentrys = new PDBEntry[pdbData.size()];
 +    SequenceI[][] seqs = new SequenceI[pdbData.size()][];
 +    int i = 0;
 +    for (StructureData data : pdbData.values())
 +    {
 +      PDBEntry pdbentry = new PDBEntry(data.getPdbId(), null,
 +              PDBEntry.Type.PDB, data.getFilePath());
        pdbentrys[i] = pdbentry;
 +      List<SequenceI> sequencesForPdb = data.getSeqList();
 +      seqs[i] = sequencesForPdb
 +              .toArray(new SequenceI[sequencesForPdb.size()]);
 +      i++;
      }
 -    // / TODO: check if protocol is needed to be set, and if chains are
 +
 +    // TODO: check if protocol is needed to be set, and if chains are
      // autodiscovered.
      jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
              pdbentrys, seqs, null);
  
      jmb.setLoadingFromArchive(true);
      addAlignmentPanel(ap);
 -    if (useToAlign)
 +    if (viewerModel.isAlignWithPanel())
      {
        useAlignmentPanelForSuperposition(ap);
      }
      initMenus();
 -    if (leaveColouringToJmol || !usetoColour)
 +    boolean useToColour = viewerModel.isColourWithAlignPanel();
 +    boolean leaveColouringToJmol = viewerModel.isColourByViewer();
 +    if (leaveColouringToJmol || !useToColour)
      {
        jmb.setColourBySequence(false);
        seqColour.setSelected(false);
        viewerColour.setSelected(true);
      }
 -    else if (usetoColour)
 +    else if (useToColour)
      {
        useAlignmentPanelForColourbyseq(ap);
        jmb.setColourBySequence(true);
        seqColour.setSelected(true);
        viewerColour.setSelected(false);
      }
 -    this.setBounds(bounds);
 +
 +    this.setBounds(viewerModel.getX(), viewerModel.getY(),
 +            viewerModel.getWidth(), viewerModel.getHeight());
      setViewId(viewid);
 -    // jalview.gui.Desktop.addInternalFrame(this, "Loading File",
 -    // bounds.width,bounds.height);
  
      this.addInternalFrameListener(new InternalFrameAdapter()
      {
          closeViewer(false);
        }
      });
 -    initJmol(loadStatus); // pdbentry, seq, JBPCHECK!
 +    StringBuilder cmd = new StringBuilder();
 +    cmd.append("load FILES ").append(QUOTE)
 +            .append(Platform.escapeBackslashes(sessionFile)).append(QUOTE);
 +    initJmol(cmd.toString());
    }
  
    @Override
    {
      super.initMenus();
  
 -    viewerActionMenu.setText(MessageManager.getString("label.jmol"));
 -
      viewerColour
              .setText(MessageManager.getString("label.colour_with_jmol"));
      viewerColour.setToolTipText(MessageManager
              .getString("label.let_jmol_manage_structure_colours"));
    }
  
 -  IProgressIndicator progressBar = null;
 -
 -  @Override
 -  protected IProgressIndicator getIProgressIndicator()
 -  {
 -    return progressBar;
 -  }
    /**
 -   * add a single PDB structure to a new or existing Jmol view
 +   * display a single PDB structure in a new Jmol view
     * 
     * @param pdbentry
     * @param seq
    public AppJmol(PDBEntry pdbentry, SequenceI[] seq, String[] chains,
            final AlignmentPanel ap)
    {
 -    progressBar = ap.alignFrame;
 -    String pdbId = pdbentry.getId();
 -
 -    /*
 -     * If the PDB file is already loaded, the user may just choose to add to an
 -     * existing viewer (or cancel)
 -     */
 -    if (addAlreadyLoadedFile(seq, chains, ap, pdbId))
 -    {
 -      return;
 -    }
 -
 -    /*
 -     * Check if there are other Jmol views involving this alignment and prompt
 -     * user about adding this molecule to one of them
 -     */
 -    if (addToExistingViewer(pdbentry, seq, chains, ap, pdbId))
 -    {
 -      return;
 -    }
 +    setProgressIndicator(ap.alignFrame);
  
 -    /*
 -     * If the options above are declined or do not apply, open a new viewer
 -     */
 -    openNewJmol(ap, new PDBEntry[] { pdbentry }, new SequenceI[][] { seq });
 +    openNewJmol(ap, alignAddedStructures, new PDBEntry[] { pdbentry },
 +            new SequenceI[][]
 +            { seq });
    }
  
 -  private void openNewJmol(AlignmentPanel ap, PDBEntry[] pdbentrys,
 -          SequenceI[][] seqs)
 +  private void openNewJmol(AlignmentPanel ap, boolean alignAdded,
 +          PDBEntry[] pdbentrys, SequenceI[][] seqs)
    {
 -    progressBar = ap.alignFrame;
 +    setProgressIndicator(ap.alignFrame);
      jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
              pdbentrys, seqs, null);
      addAlignmentPanel(ap);
      useAlignmentPanelForColourbyseq(ap);
  
 +    alignAddedStructures = alignAdded;
      if (pdbentrys.length > 1)
      {
 -      alignAddedStructures = true;
        useAlignmentPanelForSuperposition(ap);
      }
 +
      jmb.setColourBySequence(true);
      setSize(400, 400); // probably should be a configurable/dynamic default here
      initMenus();
      addingStructures = false;
-     worker = new Thread(this, "OpenJmolThread");
+     worker = new Thread(this, "OpenJmol");
      worker.start();
  
      this.addInternalFrameListener(new InternalFrameAdapter()
    }
  
    /**
 -   * create a new Jmol containing several structures superimposed using the
 -   * given alignPanel.
 +   * create a new Jmol containing several structures optionally superimposed
 +   * using the given alignPanel.
     * 
     * @param ap
 +   * @param alignAdded
 +   *          - true to superimpose
     * @param pe
     * @param seqs
     */
 -  public AppJmol(AlignmentPanel ap, PDBEntry[] pe, SequenceI[][] seqs)
 -  {
 -    openNewJmol(ap, pe, seqs);
 -  }
 -
 -  /**
 -   * Returns a list of any Jmol viewers. The list is restricted to those linked
 -   * to the given alignment panel if it is not null.
 -   */
 -  @Override
 -  protected List<StructureViewerBase> getViewersFor(AlignmentPanel apanel)
 +  public AppJmol(AlignmentPanel ap, boolean alignAdded, PDBEntry[] pe,
 +          SequenceI[][] seqs)
    {
 -    List<StructureViewerBase> result = new ArrayList<>();
 -    JInternalFrame[] frames = Desktop.instance.getAllFrames();
 -
 -    for (JInternalFrame frame : frames)
 -    {
 -      if (frame instanceof AppJmol)
 -      {
 -        if (apanel == null
 -                || ((StructureViewerBase) frame).isLinkedWith(apanel))
 -        {
 -          result.add((StructureViewerBase) frame);
 -        }
 -      }
 -    }
 -    return result;
 +    openNewJmol(ap, alignAdded, pe, seqs);
    }
  
    void initJmol(String command)
      {
        command = "";
      }
 -    jmb.evalStateCommand(command);
 -    jmb.evalStateCommand("set hoverDelay=0.1");
 +    jmb.executeCommand(new StructureCommand(command), false);
 +    jmb.executeCommand(new StructureCommand("set hoverDelay=0.1"), false);
      jmb.setFinishedInit(true);
    }
  
 -  boolean allChainsSelected = false;
 -
 -  @Override
 -  void showSelectedChains()
 -  {
 -    Vector<String> toshow = new Vector<>();
 -    for (int i = 0; i < chainMenu.getItemCount(); i++)
 -    {
 -      if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
 -      {
 -        JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
 -        if (item.isSelected())
 -        {
 -          toshow.addElement(item.getText());
 -        }
 -      }
 -    }
 -    jmb.centerViewer(toshow);
 -  }
 -
 -  @Override
 -  public void closeViewer(boolean closeExternalViewer)
 -  {
 -    // Jmol does not use an external viewer
 -    if (jmb != null)
 -    {
 -      jmb.closeViewer();
 -    }
 -    setAlignmentPanel(null);
 -    _aps.clear();
 -    _alignwith.clear();
 -    _colourwith.clear();
 -    // TODO: check for memory leaks where instance isn't finalised because jmb
 -    // holds a reference to the window
 -    jmb = null;
 -  }
 -
    @Override
    public void run()
    {
      _started = true;
      try
      {
 -      List<String> files = fetchPdbFiles();
 +      List<String> files = jmb.fetchPdbFiles(this);
        if (files.size() > 0)
        {
          showFilesInViewer(files);
      StringBuilder fileList = new StringBuilder();
      for (String s : files)
      {
 -      fileList.append(SPACE).append(BACKSLASH)
 -              .append(Platform.escapeString(s)).append(BACKSLASH);
 +      fileList.append(SPACE).append(QUOTE)
 +              .append(Platform.escapeBackslashes(s)).append(QUOTE);
      }
      String filesString = fileList.toString();
  
        } catch (OutOfMemoryError oomerror)
        {
          new OOMWarning("When trying to open the Jmol viewer!", oomerror);
 -        Cache.log.debug("File locations are " + filesString);
 +        Console.debug("File locations are " + filesString);
        } catch (Exception ex)
        {
 -        Cache.log.error("Couldn't open Jmol viewer!", ex);
 +        Console.error("Couldn't open Jmol viewer!", ex);
 +        ex.printStackTrace();
 +        return;
        }
      }
      else
        cmd.append("loadingJalviewdata=true\nload APPEND ");
        cmd.append(filesString);
        cmd.append("\nloadingJalviewdata=null");
 -      final String command = cmd.toString();
 +      final StructureCommand command = new StructureCommand(cmd.toString());
        lastnotify = jmb.getLoadNotifiesHandled();
  
        try
        {
 -        jmb.evalStateCommand(command);
 +        jmb.executeCommand(command, false);
        } catch (OutOfMemoryError oomerror)
        {
          new OOMWarning("When trying to add structures to the Jmol viewer!",
                  oomerror);
 -        Cache.log.debug("File locations are " + filesString);
 +        Console.debug("File locations are " + filesString);
 +        return;
        } catch (Exception ex)
        {
 -        Cache.log.error("Couldn't add files to Jmol viewer!", ex);
 +        Console.error("Couldn't add files to Jmol viewer!", ex);
 +        ex.printStackTrace();
 +        return;
        }
      }
  
      {
        try
        {
 -        Cache.log.debug("Waiting around for jmb notify.");
 -        Thread.sleep(waitFor);
 +        Console.debug("Waiting around for jmb notify.");
          waitTotal += waitFor;
 +
 +        // Thread.sleep() throws an exception in JS
 +        Thread.sleep(waitFor);
        } catch (Exception e)
        {
        }
      }
  
      // refresh the sequence colours for the new structure(s)
 -    for (AlignmentPanel ap : _colourwith)
 +    for (AlignmentViewPanel ap : _colourwith)
      {
        jmb.updateColours(ap);
      }
      // do superposition if asked to
 -    if (Cache.getDefault("AUTOSUPERIMPOSE", true) && alignAddedStructures)
 +    if (alignAddedStructures)
      {
        alignAddedStructures();
      }
        @Override
        public void run()
        {
 -        if (jmb.viewer.isScriptExecuting())
 +        if (jmb.jmolViewer.isScriptExecuting())
          {
            SwingUtilities.invokeLater(this);
            try
          }
          else
          {
 -          alignStructs_withAllAlignPanels();
 +          alignStructsWithAllAlignPanels();
          }
        }
      });
 -    alignAddedStructures = false;
 +
    }
  
    /**
 -   * Retrieves and saves as file any modelled PDB entries for which we do not
 -   * already have a file saved. Returns a list of absolute paths to structure
 -   * files which were either retrieved, or already stored but not modelled in
 -   * the structure viewer (i.e. files to add to the viewer display).
 +   * Outputs the Jmol viewer image as an image file, after prompting the user to
 +   * choose a file and (for EPS) choice of Text or Lineart character rendering
 +   * (unless a preference for this is set)
     * 
 -   * @return
 +   * @param type
     */
 -  List<String> fetchPdbFiles()
 -  {
 -    // todo - record which pdbids were successfully imported.
 -    StringBuilder errormsgs = new StringBuilder();
 -
 -    List<String> files = new ArrayList<>();
 -    String pdbid = "";
 -    try
 -    {
 -      String[] filesInViewer = jmb.getStructureFiles();
 -      // TODO: replace with reference fetching/transfer code (validate PDBentry
 -      // as a DBRef?)
 -      Pdb pdbclient = new Pdb();
 -      for (int pi = 0; pi < jmb.getPdbCount(); pi++)
 -      {
 -        String file = jmb.getPdbEntry(pi).getFile();
 -        if (file == null)
 -        {
 -          // retrieve the pdb and store it locally
 -          AlignmentI pdbseq = null;
 -          pdbid = jmb.getPdbEntry(pi).getId();
 -          long hdl = pdbid.hashCode() - System.currentTimeMillis();
 -          if (progressBar != null)
 -          {
 -            progressBar.setProgressBar(MessageManager
 -                    .formatMessage("status.fetching_pdb", new String[]
 -                    { pdbid }), hdl);
 -          }
 -          try
 -          {
 -            pdbseq = pdbclient.getSequenceRecords(pdbid);
 -          } catch (OutOfMemoryError oomerror)
 -          {
 -            new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
 -          } catch (Exception ex)
 -          {
 -            ex.printStackTrace();
 -            errormsgs.append("'").append(pdbid).append("'");
 -          } finally
 -          {
 -            if (progressBar != null)
 -            {
 -              progressBar.setProgressBar(
 -                      MessageManager.getString("label.state_completed"),
 -                      hdl);
 -            }
 -          }
 -          if (pdbseq != null)
 -          {
 -            // just transfer the file name from the first sequence's first
 -            // PDBEntry
 -            file = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
 -                    .elementAt(0).getFile()).getAbsolutePath();
 -            jmb.getPdbEntry(pi).setFile(file);
 -            files.add(file);
 -          }
 -          else
 -          {
 -            errormsgs.append("'").append(pdbid).append("' ");
 -          }
 -        }
 -        else
 -        {
 -          if (filesInViewer != null && filesInViewer.length > 0)
 -          {
 -            addingStructures = true; // already files loaded.
 -            for (int c = 0; c < filesInViewer.length; c++)
 -            {
 -              if (filesInViewer[c].equals(file))
 -              {
 -                file = null;
 -                break;
 -              }
 -            }
 -          }
 -          if (file != null)
 -          {
 -            files.add(file);
 -          }
 -        }
 -      }
 -    } catch (OutOfMemoryError oomerror)
 -    {
 -      new OOMWarning("Retrieving PDB files: " + pdbid, oomerror);
 -    } catch (Exception ex)
 -    {
 -      ex.printStackTrace();
 -      errormsgs.append("When retrieving pdbfiles : current was: '")
 -              .append(pdbid).append("'");
 -    }
 -    if (errormsgs.length() > 0)
 -    {
 -      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 -              MessageManager.formatMessage(
 -                      "label.pdb_entries_couldnt_be_retrieved", new String[]
 -                      { errormsgs.toString() }),
 -              MessageManager.getString("label.couldnt_load_file"),
 -              JvOptionPane.ERROR_MESSAGE);
 -    }
 -    return files;
 -  }
 -
 -  @Override
 -  public void eps_actionPerformed(ActionEvent e)
 -  {
 -    makePDBImage(jalview.util.ImageMaker.TYPE.EPS);
 -  }
 -
    @Override
 -  public void png_actionPerformed(ActionEvent e)
 -  {
 -    makePDBImage(jalview.util.ImageMaker.TYPE.PNG);
 -  }
 -
 -  void makePDBImage(jalview.util.ImageMaker.TYPE type)
 +  public void makePDBImage(ImageMaker.TYPE type)
    {
      int width = getWidth();
      int height = getHeight();
 -
 -    jalview.util.ImageMaker im;
 -
 -    if (type == jalview.util.ImageMaker.TYPE.PNG)
 -    {
 -      im = new jalview.util.ImageMaker(this,
 -              jalview.util.ImageMaker.TYPE.PNG, "Make PNG image from view",
 -              width, height, null, null, null, 0, false);
 -    }
 -    else if (type == jalview.util.ImageMaker.TYPE.EPS)
 -    {
 -      im = new jalview.util.ImageMaker(this,
 -              jalview.util.ImageMaker.TYPE.EPS, "Make EPS file from view",
 -              width, height, null, this.getTitle(), null, 0, false);
 -    }
 -    else
 -    {
 -
 -      im = new jalview.util.ImageMaker(this,
 -              jalview.util.ImageMaker.TYPE.SVG, "Make SVG file from PCA",
 -              width, height, null, this.getTitle(), null, 0, false);
 -    }
 -
 -    if (im.getGraphics() != null)
 +    ImageWriterI writer = new ImageWriterI()
      {
 -      jmb.viewer.renderScreenImage(im.getGraphics(), width, height);
 -      im.writeImage();
 -    }
 +      @Override
 +      public void exportImage(Graphics g) throws Exception
 +      {
 +        jmb.jmolViewer.renderScreenImage(g, width, height);
 +      }
 +    };
 +    String view = MessageManager.getString("action.view")
 +            .toLowerCase(Locale.ROOT);
 +    ImageExporter exporter = new ImageExporter(writer,
 +            getProgressIndicator(), type, getTitle());
 +    exporter.doExport(null, this, width, height, view);
    }
  
    @Override
 -  public void showHelp_actionPerformed(ActionEvent actionEvent)
 +  public void showHelp_actionPerformed()
    {
      try
      {
 -      BrowserLauncher
 -              .openURL("http://jmol.sourceforge.net/docs/JmolUserGuide/");
 +      BrowserLauncher // BH 2018
 +              .openURL("http://wiki.jmol.org");// http://jmol.sourceforge.net/docs/JmolUserGuide/");
      } catch (Exception ex)
      {
 +      System.err.println("Show Jmol help failed with: " + ex.getMessage());
      }
    }
  
 +  @Override
    public void showConsole(boolean showConsole)
    {
 -
      if (showConsole)
      {
        if (splitPane == null)
            }
          }
        }
 -      else if (jmb == null || jmb.viewer == null || !jmb.isFinishedInit())
 +      else if (jmb == null || jmb.jmolViewer == null
 +              || !jmb.isFinishedInit())
        {
          g.setColor(Color.black);
          g.fillRect(0, 0, currentSize.width, currentSize.height);
        }
        else
        {
 -        jmb.viewer.renderScreenImage(g, currentSize.width,
 +        jmb.jmolViewer.renderScreenImage(g, currentSize.width,
                  currentSize.height);
        }
      }
    }
  
    @Override
 -  public String getStateInfo()
 -  {
 -    return jmb == null ? null : jmb.viewer.getStateInfo();
 -  }
 -
 -  @Override
    public ViewerType getViewerType()
    {
      return ViewerType.JMOL;
@@@ -27,7 -27,6 +27,7 @@@ import jalview.analysis.scoremodels.Sco
  import jalview.analysis.scoremodels.SimilarityParams;
  import jalview.api.analysis.ScoreModelI;
  import jalview.api.analysis.SimilarityParamsI;
 +import jalview.bin.Cache;
  import jalview.datamodel.SequenceGroup;
  import jalview.ext.archaeopteryx.AptxInit;
  import jalview.util.MessageManager;
@@@ -47,6 -46,7 +47,7 @@@ import java.awt.event.FocusListener
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
  import java.beans.PropertyVetoException;
+ import java.io.IOException;
  import java.util.ArrayList;
  import java.util.List;
  
@@@ -159,15 -159,12 +160,15 @@@ public class CalculationChooser extend
      pca = new JRadioButton(
              MessageManager.getString("label.principal_component_analysis"));
      pca.setOpaque(false);
 +
      neighbourJoining = new JRadioButton(
              MessageManager.getString("label.tree_calc_nj"));
      neighbourJoining.setSelected(true);
 +    neighbourJoining.setOpaque(false);
 +
      averageDistance = new JRadioButton(
              MessageManager.getString("label.tree_calc_av"));
 -    neighbourJoining.setOpaque(false);
 +    averageDistance.setOpaque(false);
  
      JPanel calcChoicePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
      calcChoicePanel.setOpaque(false);
      JPanel treePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
      treePanel.setOpaque(false);
  
 -    treePanel.setBorder(BorderFactory
 -            .createTitledBorder(MessageManager.getString("label.tree")));
 +    JvSwingUtils.createTitledBorder(treePanel,
 +            MessageManager.getString("label.tree"), true);
  
      // then copy the inset dimensions for the border-less PCA panel
      JPanel pcaBorderless = new JPanel(new FlowLayout(FlowLayout.LEFT));
  
      setMinimumSize(new Dimension(325, height - 10));
      String title = MessageManager.getString("label.choose_calculation");
 -    if (af.getViewport().viewName != null)
 +    if (af.getViewport().getViewName() != null)
      {
 -      title = title + " (" + af.getViewport().viewName + ")";
 +      title = title + " (" + af.getViewport().getViewName() + ")";
      }
  
      Desktop.addInternalFrame(frame, title, width, height, false);
        };
      });
  
 +    validateCalcTypes();
      frame.setLayer(JLayeredPane.PALETTE_LAYER);
    }
  
      Object curSel = comboBox.getSelectedItem();
      toolTips.clear();
      DefaultComboBoxModel<String> model = new DefaultComboBoxModel<>();
 +    /*
 +     * select the score models applicable to the alignment type
 +     */
 +    boolean nucleotide = af.getViewport().getAlignment().isNucleotide();
 +    List<ScoreModelI> models = getApplicableScoreModels(nucleotide,
 +            pca.isSelected());
  
      /*
       * now we can actually add entries to the combobox,
       * remembering their descriptions for tooltips
       */
 -    ScoreModels scoreModels = ScoreModels.getInstance();
      boolean selectedIsPresent = false;
 -    for (ScoreModelI sm : scoreModels.getModels())
 +    for (ScoreModelI sm : models)
      {
 -      boolean nucleotide = af.getViewport().getAlignment().isNucleotide();
 -      if (sm.isDNA() && nucleotide || sm.isProtein() && !nucleotide)
 +      if (curSel != null && sm.getName().equals(curSel))
 +      {
 +        selectedIsPresent = true;
 +        curSel = sm.getName();
 +      }
 +      model.addElement(sm.getName());
 +
 +      /*
 +       * tooltip is description if provided, else text lookup with
 +       * fallback on the model name
 +       */
 +      String tooltip = sm.getDescription();
 +      if (tooltip == null)
        {
 -        if (curSel != null && sm.getName().equals(curSel))
 -        {
 -          selectedIsPresent = true;
 -          curSel = sm.getName();
 -        }
 -        model.addElement(sm.getName());
 -
 -        /*
 -         * tooltip is description if provided, else text lookup with
 -         * fallback on the model name
 -         */
 -        String tooltip = sm.getDescription();
 -        if (tooltip == null)
 -        {
 -          tooltip = MessageManager.getStringOrReturn("label.score_model_",
 -                  sm.getName());
 -        }
 -        toolTips.add(tooltip);
 +        tooltip = MessageManager.getStringOrReturn("label.score_model_",
 +                sm.getName());
        }
 +      toolTips.add(tooltip);
      }
 +
      if (selectedIsPresent)
      {
        model.setSelectedItem(curSel);
    }
  
    /**
 +   * Builds a list of score models which are applicable for the alignment and
 +   * calculation type (peptide or generic models for protein, nucleotide or
 +   * generic models for nucleotide).
 +   * <p>
 +   * As a special case, includes BLOSUM62 as an extra option for nucleotide PCA.
 +   * This is for backwards compatibility with Jalview prior to 2.8 when BLOSUM62
 +   * was the only score matrix supported. This is included if property
 +   * BLOSUM62_PCA_FOR_NUCLEOTIDE is set to true in the Jalview properties file.
 +   * 
 +   * @param nucleotide
 +   * @param forPca
 +   * @return
 +   */
 +  protected static List<ScoreModelI> getApplicableScoreModels(
 +          boolean nucleotide, boolean forPca)
 +  {
 +    List<ScoreModelI> filtered = new ArrayList<>();
 +
 +    ScoreModels scoreModels = ScoreModels.getInstance();
 +    for (ScoreModelI sm : scoreModels.getModels())
 +    {
 +      if (!nucleotide && sm.isProtein() || nucleotide && sm.isDNA())
 +      {
 +        filtered.add(sm);
 +      }
 +    }
 +
 +    /*
 +     * special case: add BLOSUM62 as last option for nucleotide PCA, 
 +     * for backwards compatibility with Jalview < 2.8 (JAL-2962)
 +     */
 +    if (nucleotide && forPca
 +            && Cache.getDefault("BLOSUM62_PCA_FOR_NUCLEOTIDE", false))
 +    {
 +      filtered.add(scoreModels.getBlosum62());
 +    }
 +
 +    return filtered;
 +  }
 +
 +  /**
     * Open and calculate the selected tree or PCA on 'OK'
+    * 
+    * @throws IOException
     */
    protected void calculate_actionPerformed()
    {
      }
      else
      {
-       createTree(substitutionMatrix, params);
+       try
+       {
+         createTree(substitutionMatrix, params);
+       } catch (IOException e)
+       {
+         // TODO Auto-generated catch block
+         e.printStackTrace();
+       }
  
  
  
    }
  
    protected void createTree(String substitutionMatrix,
-           SimilarityParamsI params)
+           SimilarityParamsI params) throws IOException
    {
      String treeAlgo = determineTreeAlgo();
      TreeCalculator treeCalculator = new TreeCalculator(treeAlgo,
              substitutionMatrix, params);
      TreeBuilder calculatedTree = treeCalculator.makeTree(af.getViewport());
  
-     AptxInit.createInstance(calculatedTree);
+     // AptxInit.createInstanceFromCalculation(calculatedTree);
  
      TreeModel tree = new TreeModel(calculatedTree);
-     openTreePanel(tree, treeAlgo, substitutionMatrix);
+     jalview.io.NewickFile newick = new jalview.io.NewickFile(
+             tree.getTopNode());
+     String output = newick.print(tree.hasBootstrap(), tree.hasDistances(),
+             tree.hasRootDistance());
+     AptxInit.createInstanceFromNhx(af.getTitle(), output, 
+             af.getViewport());
+     // openTreePanel(tree, treeAlgo, substitutionMatrix);
    }
  
  
                JvOptionPane.WARNING_MESSAGE);
        return;
      }
 +
 +    /*
 +     * construct the panel and kick off its calculation thread
 +     */
      pcaPanel = new PCAPanel(af.alignPanel, modelName, params);
 +    new Thread(pcaPanel).start();
 +
    }
  
    /**
   */
  package jalview.gui;
  
 -import jalview.api.FeatureRenderer;
 -import jalview.bin.Cache;
 -import jalview.datamodel.AlignmentI;
 -import jalview.datamodel.PDBEntry;
 -import jalview.datamodel.SequenceI;
 -import jalview.ext.rbvi.chimera.ChimeraCommands;
 -import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
 -import jalview.gui.StructureViewer.ViewerType;
 -import jalview.io.DataSourceType;
 -import jalview.io.StructureFile;
 -import jalview.structures.models.AAStructureBindingModel;
 -import jalview.util.BrowserLauncher;
 -import jalview.util.MessageManager;
 -import jalview.util.Platform;
 -import jalview.ws.dbsources.Pdb;
 -
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
  import java.io.File;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
 -import java.util.Random;
 +import java.util.Map;
  
 -import javax.swing.JCheckBoxMenuItem;
  import javax.swing.JInternalFrame;
  import javax.swing.JMenu;
  import javax.swing.JMenuItem;
  import javax.swing.event.InternalFrameAdapter;
  import javax.swing.event.InternalFrameEvent;
  
 +import jalview.api.AlignmentViewPanel;
 +import jalview.api.FeatureRenderer;
 +import jalview.bin.Console;
 +import jalview.datamodel.PDBEntry;
 +import jalview.datamodel.SequenceI;
 +import jalview.datamodel.StructureViewerModel;
 +import jalview.datamodel.StructureViewerModel.StructureData;
 +import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
 +import jalview.gui.StructureViewer.ViewerType;
 +import jalview.io.DataSourceType;
 +import jalview.io.StructureFile;
 +import jalview.structures.models.AAStructureBindingModel;
 +import jalview.util.ImageMaker.TYPE;
 +import jalview.util.MessageManager;
 +import jalview.util.Platform;
 +
  /**
   * GUI elements for handling an external chimera display
   * 
@@@ -62,6 -66,8 +62,6 @@@ public class ChimeraViewFrame extends S
  {
    private JalviewChimeraBinding jmb;
  
 -  private IProgressIndicator progressBar = null;
 -
    /*
     * Path to Chimera session file. This is set when an open Jalview/Chimera
     * session is saved, or on restore from a Jalview project (if it holds the
     */
    private String chimeraSessionFile = null;
  
 -  private Random random = new Random();
 -
    private int myWidth = 500;
  
    private int myHeight = 150;
  
 +  private JMenuItem writeFeatures = null;
 +
 +  private JMenu fetchAttributes = null;
 +
    /**
     * Initialise menu options.
     */
    {
      super.initMenus();
  
      savemenu.setVisible(false); // not yet implemented
      viewMenu.add(fitToWindow);
  
 -    /*
 -     * exchange of Jalview features and Chimera attributes is for now
 -     * an optionally enabled experimental feature
 -     */
 -    if (Desktop.instance.showExperimental())
 +    writeFeatures = new JMenuItem(
 +            MessageManager.getString("label.create_viewer_attributes"));
 +    writeFeatures.setToolTipText(
 +            MessageManager.getString("label.create_viewer_attributes_tip"));
 +    writeFeatures.addActionListener(new ActionListener()
      {
 -      JMenuItem writeFeatures = new JMenuItem(
 -              MessageManager.getString("label.create_chimera_attributes"));
 -      writeFeatures.setToolTipText(MessageManager
 -              .getString("label.create_chimera_attributes_tip"));
 -      writeFeatures.addActionListener(new ActionListener()
 +      @Override
 +      public void actionPerformed(ActionEvent e)
        {
 -        @Override
 -        public void actionPerformed(ActionEvent e)
 -        {
 -          sendFeaturesToChimera();
 -        }
 -      });
 -      viewerActionMenu.add(writeFeatures);
 +        sendFeaturesToChimera();
 +      }
 +    });
 +    viewerActionMenu.add(writeFeatures);
  
 -      final JMenu fetchAttributes = new JMenu(
 -              MessageManager.getString("label.fetch_chimera_attributes"));
 -      fetchAttributes.setToolTipText(MessageManager
 -              .getString("label.fetch_chimera_attributes_tip"));
 -      fetchAttributes.addMouseListener(new MouseAdapter()
 -      {
 +    fetchAttributes = new JMenu(MessageManager.formatMessage(
 +            "label.fetch_viewer_attributes", getViewerName()));
 +    fetchAttributes.setToolTipText(MessageManager.formatMessage(
 +            "label.fetch_viewer_attributes_tip", getViewerName()));
 +    fetchAttributes.addMouseListener(new MouseAdapter()
 +    {
  
 -        @Override
 -        public void mouseEntered(MouseEvent e)
 -        {
 -          buildAttributesMenu(fetchAttributes);
 -        }
 -      });
 -      viewerActionMenu.add(fetchAttributes);
 -    }
 +      @Override
 +      public void mouseEntered(MouseEvent e)
 +      {
 +        buildAttributesMenu(fetchAttributes);
 +      }
 +    });
 +    viewerActionMenu.add(fetchAttributes);
    }
  
 +  @Override
 +  protected void buildActionMenu()
 +  {
 +    super.buildActionMenu();
 +    // add these back in after menu is refreshed
 +    viewerActionMenu.add(writeFeatures);
 +    viewerActionMenu.add(fetchAttributes);
 +
 +  };
 +
    /**
 -   * Query Chimera for its residue attribute names and add them as items off the
 -   * attributes menu
 +   * Query the structure viewer for its residue attribute names and add them as
 +   * items off the attributes menu
     * 
     * @param attributesMenu
     */
    protected void buildAttributesMenu(JMenu attributesMenu)
    {
 -    List<String> atts = jmb.sendChimeraCommand("list resattr", true);
 -    if (atts == null)
 -    {
 -      return;
 -    }
 +    List<String> atts = jmb.getChimeraAttributes();
      attributesMenu.removeAll();
      Collections.sort(atts);
 -    for (String att : atts)
 +    for (String attName : atts)
      {
 -      final String attName = att.split(" ")[1];
 -
 -      /*
 -       * ignore 'jv_*' attributes, as these are Jalview features that have
 -       * been transferred to residue attributes in Chimera!
 -       */
 -      if (!attName.startsWith(ChimeraCommands.NAMESPACE_PREFIX))
 +      JMenuItem menuItem = new JMenuItem(attName);
 +      menuItem.addActionListener(new ActionListener()
        {
 -        JMenuItem menuItem = new JMenuItem(attName);
 -        menuItem.addActionListener(new ActionListener()
 +        @Override
 +        public void actionPerformed(ActionEvent e)
          {
 -          @Override
 -          public void actionPerformed(ActionEvent e)
 +          if (getBinding().copyStructureAttributesToFeatures(attName,
 +                  getAlignmentPanel()) > 0)
            {
 -            getChimeraAttributes(attName);
 +            getAlignmentPanel().getFeatureRenderer().featuresAdded();
            }
 -        });
 -        attributesMenu.add(menuItem);
 -      }
 +        }
 +      });
 +      attributesMenu.add(menuItem);
      }
    }
  
    /**
 -   * Read residues in Chimera with the given attribute name, and set as features
 -   * on the corresponding sequence positions (if any)
 -   * 
 -   * @param attName
 -   */
 -  protected void getChimeraAttributes(String attName)
 -  {
 -    jmb.copyStructureAttributesToFeatures(attName, getAlignmentPanel());
 -  }
 -
 -  /**
 -   * Send a command to Chimera to create residue attributes for Jalview features
 -   * <p>
 -   * The syntax is: setattr r <attName> <attValue> <atomSpec>
 -   * <p>
 -   * For example: setattr r jv:chain "Ferredoxin-1, Chloroplastic" #0:94.A
 +   * Sends command(s) to the structure viewer to create residue attributes for
 +   * visible Jalview features
     */
    protected void sendFeaturesToChimera()
    {
 +    // todo pull up?
      int count = jmb.sendFeaturesToViewer(getAlignmentPanel());
 -    statusBar.setText(
 -            MessageManager.formatMessage("label.attributes_set", count));
 +    statusBar.setText(MessageManager.formatMessage("label.attributes_set",
 +            count, getViewerName()));
    }
  
    /**
 -   * add a single PDB structure to a new or existing Chimera view
 +   * open a single PDB structure in a new Chimera view
     * 
     * @param pdbentry
     * @param seq
            String[] chains, final AlignmentPanel ap)
    {
      this();
 -    String pdbId = pdbentry.getId();
  
 -    /*
 -     * If the PDB file is already loaded, the user may just choose to add to an
 -     * existing viewer (or cancel)
 -     */
 -    if (addAlreadyLoadedFile(seq, chains, ap, pdbId))
 -    {
 -      return;
 -    }
 -
 -    /*
 -     * Check if there are other Chimera views involving this alignment and give
 -     * user the option to add and align this molecule to one of them (or cancel)
 -     */
 -    if (addToExistingViewer(pdbentry, seq, chains, ap, pdbId))
 -    {
 -      return;
 -    }
 -
 -    /*
 -     * If the options above are declined or do not apply, show the structure in
 -     * a new viewer
 -     */
      openNewChimera(ap, new PDBEntry[] { pdbentry },
              new SequenceI[][]
              { seq });
     */
    protected void createProgressBar()
    {
 -    if (progressBar == null)
 +    if (getProgressIndicator() == null)
      {
 -      progressBar = new ProgressBar(statusPanel, statusBar);
 +      setProgressIndicator(new ProgressBar(statusPanel, statusBar));
      }
    }
  
            SequenceI[][] seqs)
    {
      createProgressBar();
 -    jmb = new JalviewChimeraBindingModel(this,
 -            ap.getStructureSelectionManager(), pdbentrys, seqs, null);
 +    jmb = newBindingModel(ap, pdbentrys, seqs);
      addAlignmentPanel(ap);
      useAlignmentPanelForColourbyseq(ap);
  
      if (pdbentrys.length > 1)
      {
 -      alignAddedStructures = true;
        useAlignmentPanelForSuperposition(ap);
      }
      jmb.setColourBySequence(true);
      initMenus();
  
      addingStructures = false;
-     worker = new Thread(this, "OpenChimeraThread");
+     worker = new Thread(this, "OpenChimera");
      worker.start();
  
      this.addInternalFrameListener(new InternalFrameAdapter()
  
    }
  
 +  protected JalviewChimeraBindingModel newBindingModel(AlignmentPanel ap,
 +          PDBEntry[] pdbentrys, SequenceI[][] seqs)
 +  {
 +    return new JalviewChimeraBindingModel(this,
 +            ap.getStructureSelectionManager(), pdbentrys, seqs, null);
 +  }
 +
    /**
     * Create a new viewer from saved session state data including Chimera session
     * file
     * @param colourBySequence
     * @param newViewId
     */
 -  public ChimeraViewFrame(String chimeraSessionFile,
 -          AlignmentPanel alignPanel, PDBEntry[] pdbArray,
 -          SequenceI[][] seqsArray, boolean colourByChimera,
 -          boolean colourBySequence, String newViewId)
 +  public ChimeraViewFrame(StructureViewerModel viewerData,
 +          AlignmentPanel alignPanel, String sessionFile, String vid)
    {
      this();
 -    setViewId(newViewId);
 -    this.chimeraSessionFile = chimeraSessionFile;
 +    setViewId(vid);
 +    this.chimeraSessionFile = sessionFile;
 +    Map<File, StructureData> pdbData = viewerData.getFileData();
 +    PDBEntry[] pdbArray = new PDBEntry[pdbData.size()];
 +    SequenceI[][] seqsArray = new SequenceI[pdbData.size()][];
 +    int i = 0;
 +    for (StructureData data : pdbData.values())
 +    {
 +      PDBEntry pdbentry = new PDBEntry(data.getPdbId(), null,
 +              PDBEntry.Type.PDB, data.getFilePath());
 +      pdbArray[i] = pdbentry;
 +      List<SequenceI> sequencesForPdb = data.getSeqList();
 +      seqsArray[i] = sequencesForPdb
 +              .toArray(new SequenceI[sequencesForPdb.size()]);
 +      i++;
 +    }
      openNewChimera(alignPanel, pdbArray, seqsArray);
 -    if (colourByChimera)
 +    if (viewerData.isColourByViewer())
      {
        jmb.setColourBySequence(false);
        seqColour.setSelected(false);
        viewerColour.setSelected(true);
      }
 -    else if (colourBySequence)
 +    else if (viewerData.isColourWithAlignPanel())
      {
        jmb.setColourBySequence(true);
        seqColour.setSelected(true);
    }
  
    /**
 -   * create a new viewer containing several structures superimposed using the
 -   * given alignPanel.
 +   * create a new viewer containing several structures, optionally superimposed
 +   * using the given alignPanel.
     * 
     * @param pe
     * @param seqs
     * @param ap
     */
 -  public ChimeraViewFrame(PDBEntry[] pe, SequenceI[][] seqs,
 -          AlignmentPanel ap)
 +  public ChimeraViewFrame(PDBEntry[] pe, boolean alignAdded,
 +          SequenceI[][] seqs, AlignmentPanel ap)
    {
      this();
 +    setAlignAddedStructures(alignAdded);
      openNewChimera(ap, pe, seqs);
    }
  
    }
  
    /**
 -   * Returns a list of any Chimera viewers in the desktop. The list is
 -   * restricted to those linked to the given alignment panel if it is not null.
 -   */
 -  @Override
 -  protected List<StructureViewerBase> getViewersFor(AlignmentPanel ap)
 -  {
 -    List<StructureViewerBase> result = new ArrayList<>();
 -    JInternalFrame[] frames = Desktop.instance.getAllFrames();
 -
 -    for (JInternalFrame frame : frames)
 -    {
 -      if (frame instanceof ChimeraViewFrame)
 -      {
 -        if (ap == null || ((StructureViewerBase) frame).isLinkedWith(ap))
 -        {
 -          result.add((StructureViewerBase) frame);
 -        }
 -      }
 -    }
 -    return result;
 -  }
 -
 -  /**
     * Launch Chimera. If we have a chimera session file name, send Chimera the
     * command to open its saved session file.
     */
      if (!jmb.launchChimera())
      {
        JvOptionPane.showMessageDialog(Desktop.desktop,
 -              MessageManager.getString("label.chimera_failed"),
 +              MessageManager.formatMessage("label.open_viewer_failed",
 +                      getViewerName()),
                MessageManager.getString("label.error_loading_file"),
                JvOptionPane.ERROR_MESSAGE);
 +      jmb.closeViewer(true);
        this.dispose();
        return;
      }
    }
  
    /**
 -   * Show only the selected chain(s) in the viewer
 -   */
 -  @Override
 -  void showSelectedChains()
 -  {
 -    List<String> toshow = new ArrayList<>();
 -    for (int i = 0; i < chainMenu.getItemCount(); i++)
 -    {
 -      if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
 -      {
 -        JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
 -        if (item.isSelected())
 -        {
 -          toshow.add(item.getText());
 -        }
 -      }
 -    }
 -    jmb.showChains(toshow);
 -  }
 -
 -  /**
 -   * Close down this instance of Jalview's Chimera viewer, giving the user the
 -   * option to close the associated Chimera window (process). They may wish to
 -   * keep it open until they have had an opportunity to save any work.
 -   * 
 -   * @param closeChimera
 -   *          if true, close any linked Chimera process; if false, prompt first
 -   */
 -  @Override
 -  public void closeViewer(boolean closeChimera)
 -  {
 -    if (jmb != null && jmb.isChimeraRunning())
 -    {
 -      if (!closeChimera)
 -      {
 -        String prompt = MessageManager
 -                .formatMessage("label.confirm_close_chimera", new Object[]
 -                { jmb.getViewerTitle(getViewerName(), false) });
 -        prompt = JvSwingUtils.wrapTooltip(true, prompt);
 -        int confirm = JvOptionPane.showConfirmDialog(this, prompt,
 -                MessageManager.getString("label.close_viewer"),
 -                JvOptionPane.YES_NO_CANCEL_OPTION);
 -        /*
 -         * abort closure if user hits escape or Cancel
 -         */
 -        if (confirm == JvOptionPane.CANCEL_OPTION
 -                || confirm == JvOptionPane.CLOSED_OPTION)
 -        {
 -          return;
 -        }
 -        closeChimera = confirm == JvOptionPane.YES_OPTION;
 -      }
 -      jmb.closeViewer(closeChimera);
 -    }
 -    setAlignmentPanel(null);
 -    _aps.clear();
 -    _alignwith.clear();
 -    _colourwith.clear();
 -    // TODO: check for memory leaks where instance isn't finalised because jmb
 -    // holds a reference to the window
 -    jmb = null;
 -    dispose();
 -  }
 -
 -  /**
     * Open any newly added PDB structures in Chimera, having first fetched data
     * from PDB (if not already saved).
     */
          {
            filePDB.add(thePdbEntry);
            filePDBpos.add(Integer.valueOf(pi));
 -          files.append(" \"" + Platform.escapeString(file) + "\"");
 +          files.append(" \"" + Platform.escapeBackslashes(file) + "\"");
          }
        }
      } catch (OutOfMemoryError oomerror)
            initChimera();
          } catch (Exception ex)
          {
 -          Cache.log.error("Couldn't open Chimera viewer!", ex);
 +          Console.error("Couldn't open Chimera viewer!", ex);
          }
        }
 +      if (!jmb.isViewerRunning())
 +      {
 +        // nothing to do
 +        // TODO: ensure we tidy up JAL-3619
 +        return;
 +      }
        int num = -1;
        for (PDBEntry pe : filePDB)
        {
  
              pdb = jmb.getSsm().setMapping(jmb.getSequence()[pos],
                      jmb.getChains()[pos], pe.getFile(), protocol,
 -                    progressBar);
 -            stashFoundChains(pdb, pe.getFile());
 +                    getProgressIndicator());
 +            jmb.stashFoundChains(pdb, pe.getFile());
  
            } catch (OutOfMemoryError oomerror)
            {
                      oomerror);
            } catch (Exception ex)
            {
 -            Cache.log.error(
 +            Console.error(
                      "Couldn't open " + pe.getFile() + " in Chimera viewer!",
                      ex);
            } finally
            {
 -            Cache.log.debug("File locations are " + files);
 +            Console.debug("File locations are " + files);
            }
          }
        }
  
        /*
         * ensure that any newly discovered features (e.g. RESNUM)
 -       * are added to any open feature settings dialog
 +       * are notified to the FeatureRenderer (and added to any 
 +       * open feature settings dialog)
         */
        FeatureRenderer fr = getBinding().getFeatureRenderer(null);
        if (fr != null)
        }
  
        // refresh the sequence colours for the new structure(s)
 -      for (AlignmentPanel ap : _colourwith)
 +      for (AlignmentViewPanel ap : _colourwith)
        {
          jmb.updateColours(ap);
        }
        // do superposition if asked to
 -      if (Cache.getDefault("AUTOSUPERIMPOSE", true) && alignAddedStructures)
 +      if (alignAddedStructures)
        {
          new Thread(new Runnable()
          {
            @Override
            public void run()
            {
 -            alignStructs_withAllAlignPanels();
 +            alignStructsWithAllAlignPanels();
            }
          }).start();
 -        alignAddedStructures = false;
        }
        addingStructures = false;
      }
      worker = null;
    }
  
 -  /**
 -   * Fetch PDB data and save to a local file. Returns the full path to the file,
 -   * or null if fetch fails. TODO: refactor to common with Jmol ? duplication
 -   * 
 -   * @param processingEntry
 -   * @return
 -   * @throws Exception
 -   */
 -
 -  private void stashFoundChains(StructureFile pdb, String file)
 -  {
 -    for (int i = 0; i < pdb.getChains().size(); i++)
 -    {
 -      String chid = new String(
 -              pdb.getId() + ":" + pdb.getChains().elementAt(i).id);
 -      jmb.getChainNames().add(chid);
 -      jmb.getChainFile().put(chid, file);
 -    }
 -  }
 -
 -  private String fetchPdbFile(PDBEntry processingEntry) throws Exception
 -  {
 -    // FIXME: this is duplicated code with Jmol frame ?
 -    String filePath = null;
 -    Pdb pdbclient = new Pdb();
 -    AlignmentI pdbseq = null;
 -    String pdbid = processingEntry.getId();
 -    long handle = System.currentTimeMillis()
 -            + Thread.currentThread().hashCode();
 -
 -    /*
 -     * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
 -     */
 -    String msg = MessageManager.formatMessage("status.fetching_pdb",
 -            new Object[]
 -            { pdbid });
 -    getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
 -    // long hdl = startProgressBar(MessageManager.formatMessage(
 -    // "status.fetching_pdb", new Object[]
 -    // { pdbid }));
 -    try
 -    {
 -      pdbseq = pdbclient.getSequenceRecords(pdbid);
 -    } catch (OutOfMemoryError oomerror)
 -    {
 -      new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
 -    } finally
 -    {
 -      msg = pdbid + " " + MessageManager.getString("label.state_completed");
 -      getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
 -      // stopProgressBar(msg, hdl);
 -    }
 -    /*
 -     * If PDB data were saved and are not invalid (empty alignment), return the
 -     * file path.
 -     */
 -    if (pdbseq != null && pdbseq.getHeight() > 0)
 -    {
 -      // just use the file name from the first sequence's first PDBEntry
 -      filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
 -              .elementAt(0).getFile()).getAbsolutePath();
 -      processingEntry.setFile(filePath);
 -    }
 -    return filePath;
 -  }
 -
 -  /**
 -   * Convenience method to update the progress bar if there is one. Be sure to
 -   * call stopProgressBar with the returned handle to remove the message.
 -   * 
 -   * @param msg
 -   * @param handle
 -   */
 -  public long startProgressBar(String msg)
 -  {
 -    // TODO would rather have startProgress/stopProgress as the
 -    // IProgressIndicator interface
 -    long tm = random.nextLong();
 -    if (progressBar != null)
 -    {
 -      progressBar.setProgressBar(msg, tm);
 -    }
 -    return tm;
 -  }
 -
 -  /**
 -   * End the progress bar with the specified handle, leaving a message (if not
 -   * null) on the status bar
 -   * 
 -   * @param msg
 -   * @param handle
 -   */
 -  public void stopProgressBar(String msg, long handle)
 -  {
 -    if (progressBar != null)
 -    {
 -      progressBar.setProgressBar(msg, handle);
 -    }
 -  }
 -
 -  @Override
 -  public void eps_actionPerformed(ActionEvent e)
 -  {
 -    throw new Error(MessageManager
 -            .getString("error.eps_generation_not_implemented"));
 -  }
 -
    @Override
 -  public void png_actionPerformed(ActionEvent e)
 +  public void makePDBImage(TYPE imageType)
    {
 -    throw new Error(MessageManager
 -            .getString("error.png_generation_not_implemented"));
 -  }
 -
 -  @Override
 -  public void showHelp_actionPerformed(ActionEvent actionEvent)
 -  {
 -    try
 -    {
 -      BrowserLauncher
 -              .openURL("https://www.cgl.ucsf.edu/chimera/docs/UsersGuide");
 -    } catch (IOException ex)
 -    {
 -    }
 +    throw new UnsupportedOperationException(
 +            "Image export for Chimera is not implemented");
    }
  
    @Override
      return jmb;
    }
  
 -  /**
 -   * Ask Chimera to save its session to the designated file path, or to a
 -   * temporary file if the path is null. Returns the file path if successful,
 -   * else null.
 -   * 
 -   * @param filepath
 -   * @see getStateInfo
 -   */
 -  protected String saveSession(String filepath)
 -  {
 -    String pathUsed = filepath;
 -    try
 -    {
 -      if (pathUsed == null)
 -      {
 -        File tempFile = File.createTempFile("chimera", ".py");
 -        tempFile.deleteOnExit();
 -        pathUsed = tempFile.getPath();
 -      }
 -      boolean result = jmb.saveSession(pathUsed);
 -      if (result)
 -      {
 -        this.chimeraSessionFile = pathUsed;
 -        return pathUsed;
 -      }
 -    } catch (IOException e)
 -    {
 -    }
 -    return null;
 -  }
 -
 -  /**
 -   * Returns a string representing the state of the Chimera session. This is
 -   * done by requesting Chimera to save its session to a temporary file, then
 -   * reading the file contents. Returns an empty string on any error.
 -   */
 -  @Override
 -  public String getStateInfo()
 -  {
 -    String sessionFile = saveSession(null);
 -    if (sessionFile == null)
 -    {
 -      return "";
 -    }
 -    InputStream is = null;
 -    try
 -    {
 -      File f = new File(sessionFile);
 -      byte[] bytes = new byte[(int) f.length()];
 -      is = new FileInputStream(sessionFile);
 -      is.read(bytes);
 -      return new String(bytes);
 -    } catch (IOException e)
 -    {
 -      return "";
 -    } finally
 -    {
 -      if (is != null)
 -      {
 -        try
 -        {
 -          is.close();
 -        } catch (IOException e)
 -        {
 -          // ignore
 -        }
 -      }
 -    }
 -  }
 -
 -  @Override
 -  protected void fitToWindow_actionPerformed()
 -  {
 -    jmb.focusView();
 -  }
 -
    @Override
    public ViewerType getViewerType()
    {
    {
      return "Chimera";
    }
 -
 -  /**
 -   * Sends commands to align structures according to associated alignment(s).
 -   * 
 -   * @return
 -   */
 -  @Override
 -  protected String alignStructs_withAllAlignPanels()
 -  {
 -    String reply = super.alignStructs_withAllAlignPanels();
 -    if (reply != null)
 -    {
 -      statusBar.setText("Superposition failed: " + reply);
 -    }
 -    return reply;
 -  }
 -
 -  @Override
 -  protected IProgressIndicator getIProgressIndicator()
 -  {
 -    return progressBar;
 -  }
  }
   */
  package jalview.gui;
  
 -import jalview.util.MessageManager;
 -
  import java.awt.BorderLayout;
 +import java.awt.Color;
  import java.awt.Dimension;
  import java.awt.GraphicsEnvironment;
 +import java.awt.GridBagConstraints;
 +import java.awt.GridBagLayout;
  import java.awt.Rectangle;
  import java.awt.Toolkit;
 +import java.awt.datatransfer.Clipboard;
 +import java.awt.datatransfer.StringSelection;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
 +import java.awt.event.MouseAdapter;
 +import java.awt.event.MouseEvent;
  import java.awt.event.WindowAdapter;
  import java.awt.event.WindowEvent;
  import java.awt.event.WindowListener;
@@@ -42,23 -37,12 +42,23 @@@ import java.io.PipedInputStream
  import java.io.PipedOutputStream;
  import java.io.PrintStream;
  
 +import javax.swing.BorderFactory;
  import javax.swing.JButton;
 +import javax.swing.JComboBox;
  import javax.swing.JFrame;
 +import javax.swing.JLabel;
 +import javax.swing.JPanel;
  import javax.swing.JScrollPane;
  import javax.swing.JTextArea;
 +import javax.swing.border.Border;
 +import javax.swing.text.DefaultCaret;
  
 -import org.apache.log4j.SimpleLayout;
 +import jalview.log.JLoggerI.LogLevel;
 +import jalview.log.JLoggerLog4j;
 +import jalview.log.JalviewAppender;
 +import jalview.util.ChannelProperties;
 +import jalview.util.MessageManager;
 +import jalview.util.Platform;
  
  /**
   * Simple Jalview Java Console. Version 1 - allows viewing of console output
@@@ -104,10 -88,6 +104,10 @@@ public class Console extends WindowAdap
  
    private int MIN_HEIGHT = 250;
  
 +  private JComboBox<LogLevel> logLevelCombo = new JComboBox<LogLevel>();
 +
 +  protected LogLevel startingLogLevel = LogLevel.INFO;
 +
    public Console()
    {
      // create all components and add them
      // textArea = cpt.getTextArea();
      textArea = new JTextArea();
      textArea.setEditable(false);
 -    JButton button = new JButton(MessageManager.getString("action.clear"));
 +    // autoscroll
 +    DefaultCaret caret = (DefaultCaret) textArea.getCaret();
 +    caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
 +    // toggle autoscroll by clicking on the text area
 +    Border pausedBorder = BorderFactory.createMatteBorder(2, 2, 2, 2,
 +            textArea.getForeground());
 +    Border noBorder = BorderFactory.createEmptyBorder(2, 2, 2, 2);
 +    JScrollPane scrollPane = new JScrollPane(textArea);
 +    scrollPane.setBorder(noBorder);
 +    textArea.addMouseListener(new MouseAdapter()
 +    {
 +      public void mouseClicked(MouseEvent e)
 +      {
 +        if (e.getButton() == MouseEvent.BUTTON1)
 +        {
 +          if (caret.getUpdatePolicy() == DefaultCaret.ALWAYS_UPDATE)
 +          {
 +            caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
 +            scrollPane.setBorder(pausedBorder);
 +          }
 +          else
 +          {
 +            caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
 +            textArea.setCaretPosition(textArea.getDocument().getLength());
 +            scrollPane.setBorder(noBorder);
 +          }
 +        }
 +      }
 +    });
 +
 +    JButton clearButton = new JButton(
 +            MessageManager.getString("action.clear"));
 +    JButton copyToClipboardButton = new JButton(
 +            MessageManager.getString("label.copy_to_clipboard"));
 +    copyToClipboardButton.addActionListener(new ActionListener()
 +    {
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        copyConsoleTextToClipboard();
 +      }
 +    });
 +    copyToClipboardButton.addMouseListener(new MouseAdapter()
 +    {
 +      private Color bg = textArea.getBackground();
 +
 +      private Color fg = textArea.getForeground();
 +
 +      public void mousePressed(MouseEvent e)
 +      {
 +        textArea.setBackground(textArea.getSelectionColor());
 +        textArea.setForeground(textArea.getSelectedTextColor());
 +      }
 +
 +      public void mouseReleased(MouseEvent e)
 +      {
 +        textArea.setBackground(bg);
 +        textArea.setForeground(fg);
 +      }
 +
 +    });
 +    copyToClipboardButton.setToolTipText(
 +            MessageManager.getString("label.copy_to_clipboard_tooltip"));
 +
 +    JLabel logLevelLabel = new JLabel(
 +            MessageManager.getString("label.log_level") + ":");
 +
 +    // logLevelCombo.addItem(LogLevel.ALL);
 +    logLevelCombo.addItem(LogLevel.TRACE);
 +    logLevelCombo.addItem(LogLevel.DEBUG);
 +    logLevelCombo.addItem(LogLevel.INFO);
 +    logLevelCombo.addItem(LogLevel.WARN);
 +    // logLevelCombo.addItem(LogLevel.ERROR);
 +    // logLevelCombo.addItem(LogLevel.FATAL);
 +    // logLevelCombo.addItem(LogLevel.ERROR);
 +    // logLevelCombo.addItem(LogLevel.OFF);
 +    // set startingLogLevel
 +    startingLogLevel = jalview.bin.Console.log == null ? LogLevel.INFO
 +            : jalview.bin.Console.log.getLevel();
 +    setChosenLogLevelCombo();
 +    logLevelCombo.addActionListener(new ActionListener()
 +    {
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        if (jalview.bin.Console.log != null)
 +        {
 +          jalview.bin.Console.log
 +                  .setLevel((LogLevel) logLevelCombo.getSelectedItem());
 +        }
 +      }
 +
 +    });
  
      // frame = cpt;
      frame.getContentPane().setLayout(new BorderLayout());
 -    frame.getContentPane().add(new JScrollPane(textArea),
 -            BorderLayout.CENTER);
 -    frame.getContentPane().add(button, BorderLayout.SOUTH);
 +    frame.getContentPane().add(scrollPane, BorderLayout.CENTER);
 +    JPanel southPanel = new JPanel();
 +    southPanel.setLayout(new GridBagLayout());
 +
 +    JPanel logLevelPanel = new JPanel();
 +    logLevelPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
 +    logLevelPanel.add(logLevelLabel);
 +    logLevelPanel.add(logLevelCombo);
 +    String logLevelTooltip = MessageManager.formatMessage(
 +            "label.log_level_tooltip", startingLogLevel.toString());
 +    logLevelLabel.setToolTipText(logLevelTooltip);
 +    logLevelCombo.setToolTipText(logLevelTooltip);
 +
 +    GridBagConstraints gbc = new GridBagConstraints();
 +    gbc.gridx = 0;
 +    gbc.gridy = 0;
 +    gbc.gridwidth = 1;
 +    gbc.gridheight = 1;
 +    gbc.weightx = 0.1;
 +    southPanel.add(logLevelPanel, gbc);
 +
 +    gbc.gridx++;
 +    gbc.weightx = 0.8;
 +    gbc.fill = GridBagConstraints.HORIZONTAL;
 +    southPanel.add(clearButton, gbc);
 +
 +    gbc.gridx++;
 +    gbc.weightx = 0.1;
 +    gbc.fill = GridBagConstraints.NONE;
 +    southPanel.add(copyToClipboardButton, gbc);
 +
 +    southPanel.setVisible(true);
 +    frame.getContentPane().add(southPanel, BorderLayout.SOUTH);
      frame.setVisible(visible);
      updateConsole = visible;
      frame.addWindowListener(this);
 -    button.addActionListener(this);
 +    clearButton.addActionListener(this);
 +
      if (redirect)
      {
        redirectStreams();
  
      // Starting two seperate threads to read from the PipedInputStreams
      //
-     reader = new Thread(this, "ConsoleReader1Thread");
+     reader = new Thread(this, "ConsoleReader1");
      reader.setDaemon(true);
      reader.start();
      //
-     reader2 = new Thread(this, "ConsoleReader2Thread");
+     reader2 = new Thread(this, "ConsoleReader2");
      reader2.setDaemon(true);
      reader2.start();
      // and a thread to append text to the textarea
-     textAppender = new Thread(this, "ConsoleTextAppendThread");
+     textAppender = new Thread(this, "ConsoleTextAppend");
      textAppender.setDaemon(true);
      textAppender.start();
 +
 +    // set icons
 +    frame.setIconImages(ChannelProperties.getIconList());
 +  }
 +
 +  private void setChosenLogLevelCombo()
 +  {
 +    setChosenLogLevelCombo(startingLogLevel);
 +  }
 +
 +  private void setChosenLogLevelCombo(LogLevel setLogLevel)
 +  {
 +    logLevelCombo.setSelectedItem(setLogLevel);
 +    if (!logLevelCombo.getSelectedItem().equals(setLogLevel))
 +    {
 +      // setLogLevel not (yet) in list
 +      if (setLogLevel != null && setLogLevel instanceof LogLevel)
 +      {
 +        // add new item to list (might be set via .jalview_properties)
 +        boolean added = false;
 +        for (int i = 0; i < logLevelCombo.getItemCount(); i++)
 +        {
 +          LogLevel l = (LogLevel) logLevelCombo.getItemAt(i);
 +          if (l.compareTo(setLogLevel) >= 0)
 +          {
 +            logLevelCombo.insertItemAt(setLogLevel, i);
 +            added = true;
 +            break;
 +          }
 +        }
 +        if (!added) // lower priority than others or some confusion -- add to
 +                    // end of list
 +        {
 +          logLevelCombo.addItem(setLogLevel);
 +        }
 +        logLevelCombo.setSelectedItem(setLogLevel);
 +      }
 +      else
 +      {
 +        logLevelCombo.setSelectedItem(LogLevel.INFO);
 +      }
 +    }
 +  }
 +
 +  private void copyConsoleTextToClipboard()
 +  {
 +    String consoleText = textArea.getText();
 +    StringSelection consoleTextSelection = new StringSelection(consoleText);
 +    Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
 +    cb.setContents(consoleTextSelection, null);
    }
  
    PipedOutputStream pout = null, perr = null;
      // We do it with a seperate Thread becasue we don't wan't to break a Thread
      // used by the Console.
      System.out.println("\nLets throw an error on this console");
-     errorThrower = new Thread(this, "ConsoleErrorLogThread");
+     errorThrower = new Thread(this, "ConsoleErrorLog");
      errorThrower.setDaemon(true);
      errorThrower.start();
    }
      Rectangle bounds = desktop.getLastKnownDimensions("JAVA_CONSOLE_");
      if (bounds == null)
      {
 -      frame = initFrame("Jalview Java Console", desktop.getWidth() / 2,
 -              desktop.getHeight() / 4, desktop.getX(), desktop.getY());
 +      frame = initFrame(
 +              ChannelProperties.getProperty("app_name") + " Java Console",
 +              desktop.getWidth() / 2, desktop.getHeight() / 4,
 +              desktop.getX(), desktop.getY());
      }
      else
      {
 -      frame = initFrame("Jalview Java Console", bounds.width, bounds.height,
 -              bounds.x, bounds.y);
 +      frame = initFrame(
 +              ChannelProperties.getProperty("app_name") + " Java Console",
 +              bounds.width, bounds.height, bounds.x, bounds.y);
      }
      frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
      // desktop.add(frame);
      initConsole(false);
 -    JalviewAppender jappender = new JalviewAppender();
 -    jappender.setLayout(new SimpleLayout());
 -    JalviewAppender.setTextArea(textArea);
 -    org.apache.log4j.Logger.getRootLogger().addAppender(jappender);
 +    LogLevel level = (LogLevel) logLevelCombo.getSelectedItem();
 +    if (!Platform.isJS())
 +    {
 +      JalviewAppender jappender = new JalviewAppender(level);
 +      JalviewAppender.setTextArea(textArea);
 +      jappender.start();
 +      if (jalview.bin.Console.log != null
 +              && jalview.bin.Console.log instanceof JLoggerLog4j)
 +      {
 +        JLoggerLog4j.addAppender(jalview.bin.Console.log, jappender);
 +      }
 +    }
    }
  
    public synchronized void stopConsole()
              } catch (InterruptedException e)
              {
              }
 -            ;
            }
          }
          else
      return input;
    }
  
 +  /**
 +   * @j2sIgnore
 +   * @param arg
 +   */
    public static void main(String[] arg)
    {
      new Console().test(); // create console with not reference
      frame.setVisible(selected);
      if (selected == true)
      {
 +      setChosenLogLevelCombo();
        redirectStreams();
        updateConsole = true;
        frame.toFront();
      }
      else
      {
 +      // reset log level to what it was before
 +      if (jalview.bin.Console.log != null)
 +      {
 +        jalview.bin.Console.log.setLevel(startingLogLevel);
 +      }
 +
        unredirectStreams();
        updateConsole = false;
      }
   */
  package jalview.gui;
  
 -import static jalview.util.UrlConstants.SEQUENCE_ID;
 -
 -import jalview.api.AlignViewportI;
 -import jalview.api.AlignmentViewPanel;
 -import jalview.bin.Cache;
 -import jalview.bin.Jalview;
 -import jalview.io.DataSourceType;
 -import jalview.io.FileFormat;
 -import jalview.io.FileFormatException;
 -import jalview.io.FileFormatI;
 -import jalview.io.FileFormats;
 -import jalview.io.FileLoader;
 -import jalview.io.IdentifyFile;
 -import jalview.io.JalviewFileChooser;
 -import jalview.io.JalviewFileView;
 -import jalview.jbgui.GSplitFrame;
 -import jalview.jbgui.GStructureViewer;
 -import jalview.structure.StructureSelectionManager;
 -import jalview.urls.IdOrgSettings;
 -import jalview.util.ImageMaker;
 -import jalview.util.MessageManager;
 -import jalview.util.Platform;
 -import jalview.util.UrlConstants;
 -import jalview.viewmodel.AlignmentViewport;
 -import jalview.ws.params.ParamManager;
 -import jalview.ws.utils.UrlDownloadClient;
 +import java.util.Locale;
  
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Dimension;
  import java.awt.FontMetrics;
  import java.awt.Graphics;
 +import java.awt.Graphics2D;
  import java.awt.GridLayout;
  import java.awt.Point;
  import java.awt.Rectangle;
@@@ -44,91 -68,53 +44,91 @@@ import java.awt.dnd.DropTargetEvent
  import java.awt.dnd.DropTargetListener;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
 +import java.awt.event.InputEvent;
  import java.awt.event.KeyEvent;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
  import java.awt.event.WindowAdapter;
  import java.awt.event.WindowEvent;
 +import java.awt.geom.AffineTransform;
  import java.beans.PropertyChangeEvent;
  import java.beans.PropertyChangeListener;
 -import java.io.BufferedInputStream;
  import java.io.File;
 -import java.io.FileOutputStream;
 +import java.io.FileWriter;
  import java.io.IOException;
 +import java.lang.reflect.Field;
  import java.net.URL;
  import java.util.ArrayList;
 +import java.util.Arrays;
 +import java.util.HashMap;
  import java.util.Hashtable;
  import java.util.List;
  import java.util.ListIterator;
 -import java.util.StringTokenizer;
  import java.util.Vector;
  import java.util.concurrent.ExecutorService;
  import java.util.concurrent.Executors;
  import java.util.concurrent.Semaphore;
  
  import javax.swing.AbstractAction;
 +import javax.swing.Action;
 +import javax.swing.ActionMap;
  import javax.swing.Box;
  import javax.swing.BoxLayout;
  import javax.swing.DefaultDesktopManager;
  import javax.swing.DesktopManager;
 +import javax.swing.InputMap;
  import javax.swing.JButton;
  import javax.swing.JCheckBox;
  import javax.swing.JComboBox;
  import javax.swing.JComponent;
  import javax.swing.JDesktopPane;
 -import javax.swing.JFrame;
  import javax.swing.JInternalFrame;
  import javax.swing.JLabel;
  import javax.swing.JMenuItem;
  import javax.swing.JPanel;
  import javax.swing.JPopupMenu;
  import javax.swing.JProgressBar;
 +import javax.swing.JTextField;
  import javax.swing.KeyStroke;
  import javax.swing.SwingUtilities;
  import javax.swing.event.HyperlinkEvent;
  import javax.swing.event.HyperlinkEvent.EventType;
  import javax.swing.event.InternalFrameAdapter;
  import javax.swing.event.InternalFrameEvent;
 -import javax.swing.event.MenuEvent;
 -import javax.swing.event.MenuListener;
 +
 +import org.stackoverflowusers.file.WindowsShortcut;
 +
 +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;
 +import jalview.io.FileFormatI;
 +import jalview.io.FileFormats;
 +import jalview.io.FileLoader;
 +import jalview.io.FormatAdapter;
 +import jalview.io.IdentifyFile;
 +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.BrowserLauncher;
 +import jalview.util.ChannelProperties;
 +import jalview.util.ImageMaker.TYPE;
 +import jalview.util.MessageManager;
 +import jalview.util.Platform;
 +import jalview.util.ShortcutKeyMaskExWrapper;
 +import jalview.util.UrlConstants;
 +import jalview.viewmodel.AlignmentViewport;
 +import jalview.ws.params.ParamManager;
 +import jalview.ws.utils.UrlDownloadClient;
  
  /**
   * Jalview Desktop
@@@ -141,38 -127,6 +141,38 @@@ public class Desktop extends jalview.jb
          implements DropTargetListener, ClipboardOwner, IProgressIndicator,
          jalview.api.StructureSelectionManagerProvider
  {
 +  private static final String CITATION;
 +  static
 +  {
 +    URL bg_logo_url = ChannelProperties.getImageURL(
 +            "bg_logo." + String.valueOf(SplashScreen.logoSize));
 +    URL uod_logo_url = ChannelProperties.getImageURL(
 +            "uod_banner." + String.valueOf(SplashScreen.logoSize));
 +    boolean logo = (bg_logo_url != null || uod_logo_url != null);
 +    StringBuilder sb = new StringBuilder();
 +    sb.append(
 +            "<br><br>Jalview is free software released under GPLv3.<br><br>Development is managed by The Barton Group, University of Dundee, Scotland, UK.");
 +    if (logo)
 +    {
 +      sb.append("<br>");
 +    }
 +    sb.append(bg_logo_url == null ? ""
 +            : "<img alt=\"Barton Group logo\" src=\""
 +                    + bg_logo_url.toString() + "\">");
 +    sb.append(uod_logo_url == null ? ""
 +            : "&nbsp;<img alt=\"University of Dundee shield\" src=\""
 +                    + uod_logo_url.toString() + "\">");
 +    sb.append(
 +            "<br><br>For help, see <a href=\"https://www.jalview.org/faq\">www.jalview.org/faq</a> and join <a href=\"https://discourse.jalview.org\">discourse.jalview.org</a>");
 +    sb.append("<br><br>If  you use Jalview, please cite:"
 +            + "<br>Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)"
 +            + "<br>Jalview Version 2 - a multiple sequence alignment editor and analysis workbench"
 +            + "<br>Bioinformatics <a href=\"https://doi.org/10.1093/bioinformatics/btp033\">doi: 10.1093/bioinformatics/btp033</a>");
 +    CITATION = sb.toString();
 +  }
 +
 +  private static final String DEFAULT_AUTHORS = "The Jalview Authors (See AUTHORS file for current list)";
 +
    private static int DEFAULT_MIN_WIDTH = 300;
  
    private static int DEFAULT_MIN_HEIGHT = 250;
  
    private static final String EXPERIMENTAL_FEATURES = "EXPERIMENTAL_FEATURES";
  
 +  protected static final String CONFIRM_KEYBOARD_QUIT = "CONFIRM_KEYBOARD_QUIT";
 +
 +  public static HashMap<String, FileWriter> savingFiles = new HashMap<String, FileWriter>();
 +
    private JalviewChangeSupport changeSupport = new JalviewChangeSupport();
  
 +  public static boolean nosplash = false;
 +
    /**
     * news reader - null if it was never started.
     */
  
    public static MyDesktopPane desktop;
  
 +  public static MyDesktopPane getDesktop()
 +  {
 +    // BH 2018 could use currentThread() here as a reference to a
 +    // Hashtable<Thread, MyDesktopPane> in JavaScript
 +    return desktop;
 +  }
 +
    static int openFrameCount = 0;
  
    static final int xOffset = 30;
     */
    public Desktop()
    {
 +    super();
      /**
       * A note to implementors. It is ESSENTIAL that any activities that might
       * block are spawned off as threads rather than waited for during this
       * constructor.
       */
      instance = this;
 -    doVamsasClientCheck();
  
      doConfigureStructurePrefs();
 -    setTitle("Jalview " + jalview.bin.Cache.getProperty("VERSION"));
 -    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 -    boolean selmemusage = jalview.bin.Cache.getDefault("SHOW_MEMUSAGE",
 -            false);
 -    boolean showjconsole = jalview.bin.Cache.getDefault("SHOW_JAVA_CONSOLE",
 -            false);
 +    setTitle(ChannelProperties.getProperty("app_name") + " "
 +            + Cache.getProperty("VERSION"));
 +
 +    /**
 +     * Set taskbar "grouped windows" name for linux desktops (works in GNOME and
 +     * KDE). This uses sun.awt.X11.XToolkit.awtAppClassName which is not
 +     * officially documented or guaranteed to exist, so we access it via
 +     * reflection. There appear to be unfathomable criteria about what this
 +     * string can contain, and it if doesn't meet those criteria then "java"
 +     * (KDE) or "jalview-bin-Jalview" (GNOME) is used. "Jalview", "Jalview
 +     * Develop" and "Jalview Test" seem okay, but "Jalview non-release" does
 +     * not. The reflection access may generate a warning: WARNING: An illegal
 +     * reflective access operation has occurred WARNING: Illegal reflective
 +     * access by jalview.gui.Desktop () to field
 +     * sun.awt.X11.XToolkit.awtAppClassName which I don't think can be avoided.
 +     */
 +    if (Platform.isLinux())
 +    {
 +      try
 +      {
 +        Toolkit xToolkit = Toolkit.getDefaultToolkit();
 +        Field[] declaredFields = xToolkit.getClass().getDeclaredFields();
 +        Field awtAppClassNameField = null;
 +
 +        if (Arrays.stream(declaredFields)
 +                .anyMatch(f -> f.getName().equals("awtAppClassName")))
 +        {
 +          awtAppClassNameField = xToolkit.getClass()
 +                  .getDeclaredField("awtAppClassName");
 +        }
 +
 +        String title = ChannelProperties.getProperty("app_name");
 +        if (awtAppClassNameField != null)
 +        {
 +          awtAppClassNameField.setAccessible(true);
 +          awtAppClassNameField.set(xToolkit, title);
 +        }
 +        else
 +        {
 +          jalview.bin.Console.debug("XToolkit: awtAppClassName not found");
 +        }
 +      } catch (Exception e)
 +      {
 +        jalview.bin.Console.debug("Error setting awtAppClassName");
 +        jalview.bin.Console.trace(Cache.getStackTraceString(e));
 +      }
 +    }
 +
 +    /**
 +     * APQHandlers sets handlers for About, Preferences and Quit actions
 +     * peculiar to macOS's application menu. APQHandlers will check to see if a
 +     * handler is supported before setting it.
 +     */
 +    try
 +    {
 +      APQHandlers.setAPQHandlers(this);
 +    } catch (Exception e)
 +    {
 +      System.out.println("Cannot set APQHandlers");
 +      // e.printStackTrace();
 +    } catch (Throwable t)
 +    {
 +      jalview.bin.Console
 +              .warn("Error setting APQHandlers: " + t.toString());
 +      jalview.bin.Console.trace(Cache.getStackTraceString(t));
 +    }
 +    setIconImages(ChannelProperties.getIconList());
 +
 +    addWindowListener(new WindowAdapter()
 +    {
 +
 +      @Override
 +      public void windowClosing(WindowEvent ev)
 +      {
 +        quit();
 +      }
 +    });
 +
 +    boolean selmemusage = Cache.getDefault("SHOW_MEMUSAGE", false);
 +
 +    boolean showjconsole = Cache.getDefault("SHOW_JAVA_CONSOLE", false);
      desktop = new MyDesktopPane(selmemusage);
 +
      showMemusage.setSelected(selmemusage);
      desktop.setBackground(Color.white);
 +
      getContentPane().setLayout(new BorderLayout());
      // alternate config - have scrollbars - see notes in JAL-153
      // JScrollPane sp = new JScrollPane();
      // sp.getViewport().setView(desktop);
      // getContentPane().add(sp, BorderLayout.CENTER);
 +
 +    // BH 2018 - just an experiment to try unclipped JInternalFrames.
 +    if (Platform.isJS())
 +    {
 +      getRootPane().putClientProperty("swingjs.overflow.hidden", "false");
 +    }
 +
      getContentPane().add(desktop, BorderLayout.CENTER);
      desktop.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
  
      // This line prevents Windows Look&Feel resizing all new windows to maximum
      // if previous window was maximised
 -    desktop.setDesktopManager(
 -            new MyDesktopManager(
 -                    (Platform.isWindows() ? new DefaultDesktopManager()
 -                            : Platform.isAMac()
 -                                    ? new AquaInternalFrameManager(
 -                                            desktop.getDesktopManager())
 -                                    : desktop.getDesktopManager())));
 +    desktop.setDesktopManager(new MyDesktopManager(
 +            (Platform.isWindowsAndNotJS() ? new DefaultDesktopManager()
 +                    : Platform.isAMacAndNotJS()
 +                            ? new AquaInternalFrameManager(
 +                                    desktop.getDesktopManager())
 +                            : desktop.getDesktopManager())));
  
      Rectangle dims = getLastKnownDimensions("");
      if (dims != null)
      else
      {
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
 -      setBounds((screenSize.width - 900) / 2, (screenSize.height - 650) / 2,
 -              900, 650);
 +      int xPos = Math.max(5, (screenSize.width - 900) / 2);
 +      int yPos = Math.max(5, (screenSize.height - 650) / 2);
 +      setBounds(xPos, yPos, 900, 650);
      }
 -    jconsole = new Console(this, showjconsole);
 -    // add essential build information
 -    jconsole.setHeader(
 -            "Jalview Version: " + jalview.bin.Cache.getProperty("VERSION")
 -                    + "\n" + "Jalview Installation: "
 -                    + jalview.bin.Cache.getDefault("INSTALLATION",
 -                            "unknown")
 -                    + "\n" + "Build Date: "
 -                    + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown")
 -                    + "\n" + "Java version: "
 -                    + System.getProperty("java.version") + "\n"
 -                    + System.getProperty("os.arch") + " "
 -                    + System.getProperty("os.name") + " "
 -                    + System.getProperty("os.version"));
  
 -    showConsole(showjconsole);
 +    if (!Platform.isJS())
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
 +    {
 +      jconsole = new Console(this, showjconsole);
 +      jconsole.setHeader(Cache.getVersionDetailsForConsole());
 +      showConsole(showjconsole);
 +
 +      showNews.setVisible(false);
  
 -    showNews.setVisible(false);
 +      experimentalFeatures.setSelected(showExperimental());
 +
 +      getIdentifiersOrgData();
 +
 +      checkURLLinks();
 +
 +      // Spawn a thread that shows the splashscreen
 +      if (!nosplash)
 +      {
 +        SwingUtilities.invokeLater(new Runnable()
 +        {
 +          @Override
 +          public void run()
 +          {
 +            new SplashScreen(true);
 +          }
 +        });
 +      }
  
 -    experimentalFeatures.setSelected(showExperimental());
 +      // Thread off a new instance of the file chooser - this reduces the time
 +      // it
 +      // takes to open it later on.
 +      new Thread(new Runnable()
 +      {
 +        @Override
 +        public void run()
 +        {
 +          jalview.bin.Console.debug("Filechooser init thread started.");
 +          String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
 +          JalviewFileChooser.forRead(Cache.getProperty("LAST_DIRECTORY"),
 +                  fileFormat);
 +          jalview.bin.Console.debug("Filechooser init thread finished.");
 +        }
 +      }).start();
 +      // Add the service change listener
 +      changeSupport.addJalviewPropertyChangeListener("services",
 +              new PropertyChangeListener()
 +              {
  
 -    getIdentifiersOrgData();
 +                @Override
 +                public void propertyChange(PropertyChangeEvent evt)
 +                {
 +                  jalview.bin.Console
 +                          .debug("Firing service changed event for "
 +                                  + evt.getNewValue());
 +                  JalviewServicesChanged(evt);
 +                }
 +              });
 +    }
  
 -    checkURLLinks();
 +    this.setDropTarget(new java.awt.dnd.DropTarget(desktop, this));
  
      this.addWindowListener(new WindowAdapter()
      {
        }
      });
      desktop.addMouseListener(ma);
 -
 -    this.setDropTarget(new java.awt.dnd.DropTarget(desktop, this));
 -    // Spawn a thread that shows the splashscreen
 -    SwingUtilities.invokeLater(new Runnable()
 -    {
 -      @Override
 -      public void run()
 -      {
 -        new SplashScreen();
 -      }
 -    });
 -
 -    // Thread off a new instance of the file chooser - this reduces the time it
 -    // takes to open it later on.
 -    new Thread(new Runnable()
 -    {
 -      @Override
 -      public void run()
 -      {
 -        Cache.log.debug("Filechooser init thread started.");
 -        String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
 -        JalviewFileChooser.forRead(Cache.getProperty("LAST_DIRECTORY"),
 -                fileFormat);
 -        Cache.log.debug("Filechooser init thread finished.");
 -      }
 -    }, "InitFileChooser").start();
 -    // Add the service change listener
 -    changeSupport.addJalviewPropertyChangeListener("services",
 -            new PropertyChangeListener()
 -            {
 -
 -              @Override
 -              public void propertyChange(PropertyChangeEvent evt)
 -              {
 -                Cache.log.debug("Firing service changed event for "
 -                        + evt.getNewValue());
 -                JalviewServicesChanged(evt);
 -              }
 -
 -            });
    }
  
    /**
      // configure services
      StructureSelectionManager ssm = StructureSelectionManager
              .getStructureSelectionManager(this);
 -    if (jalview.bin.Cache.getDefault(Preferences.ADD_SS_ANN, true))
 +    if (Cache.getDefault(Preferences.ADD_SS_ANN, true))
      {
 -      ssm.setAddTempFacAnnot(jalview.bin.Cache
 -              .getDefault(Preferences.ADD_TEMPFACT_ANN, true));
 -      ssm.setProcessSecondaryStructure(jalview.bin.Cache
 -              .getDefault(Preferences.STRUCT_FROM_PDB, true));
 +      ssm.setAddTempFacAnnot(
 +              Cache.getDefault(Preferences.ADD_TEMPFACT_ANN, true));
 +      ssm.setProcessSecondaryStructure(
 +              Cache.getDefault(Preferences.STRUCT_FROM_PDB, true));
 +      // JAL-3915 - RNAView is no longer an option so this has no effect
        ssm.setSecStructServices(
 -              jalview.bin.Cache.getDefault(Preferences.USE_RNAVIEW, true));
 +              Cache.getDefault(Preferences.USE_RNAVIEW, false));
      }
      else
      {
    {
      final Desktop me = this;
      // Thread off the news reader, in case there are connection problems.
 -    addDialogThread(new Runnable()
 +    new Thread(new Runnable()
      {
        @Override
        public void run()
        {
 -        Cache.log.debug("Starting news thread.");
 -
 +        jalview.bin.Console.debug("Starting news thread.");
          jvnews = new BlogReader(me);
          showNews.setVisible(true);
 -        Cache.log.debug("Completed news thread.");
 +        jalview.bin.Console.debug("Completed news thread.");
        }
 -    });
 +    }).start();
    }
  
    public void getIdentifiersOrgData()
    {
 -    // Thread off the identifiers fetcher
 -    addDialogThread(new Runnable()
 -    {
 -      @Override
 -      public void run()
 +    if (Cache.getProperty("NOIDENTIFIERSSERVICE") == null)
 +    {// Thread off the identifiers fetcher
 +      new Thread(new Runnable()
        {
 -        Cache.log.debug("Downloading data from identifiers.org");
 -        UrlDownloadClient client = new UrlDownloadClient();
 -        try
 -        {
 -          client.download(IdOrgSettings.getUrl(),
 -                  IdOrgSettings.getDownloadLocation());
 -        } catch (IOException e)
 +        @Override
 +        public void run()
          {
 -          Cache.log.debug("Exception downloading identifiers.org data"
 -                  + e.getMessage());
 +          jalview.bin.Console
 +                  .debug("Downloading data from identifiers.org");
 +          try
 +          {
 +            UrlDownloadClient.download(IdOrgSettings.getUrl(),
 +                    IdOrgSettings.getDownloadLocation());
 +          } catch (IOException e)
 +          {
 +            jalview.bin.Console
 +                    .debug("Exception downloading identifiers.org data"
 +                            + e.getMessage());
 +          }
          }
 -      }
 -    });
 +      }).start();
 +      ;
 +    }
    }
  
    @Override
  
    void showNews(boolean visible)
    {
 +    jalview.bin.Console.debug((visible ? "Showing" : "Hiding") + " news.");
 +    showNews.setSelected(visible);
 +    if (visible && !jvnews.isVisible())
      {
 -      Cache.log.debug((visible ? "Showing" : "Hiding") + " news.");
 -      showNews.setSelected(visible);
 -      if (visible && !jvnews.isVisible())
 +      new Thread(new Runnable()
        {
 -        new Thread(new Runnable()
 +        @Override
 +        public void run()
          {
 -          @Override
 -          public void run()
 -          {
 -            long now = System.currentTimeMillis();
 -            Desktop.instance.setProgressBar(
 -                    MessageManager.getString("status.refreshing_news"),
 -                    now);
 -            jvnews.refreshNews();
 -            Desktop.instance.setProgressBar(null, now);
 -            jvnews.showNews();
 -          }
 -        }, "ShowNewsWindow").start();
 -      }
 +          long now = System.currentTimeMillis();
 +          Desktop.instance.setProgressBar(
 +                  MessageManager.getString("status.refreshing_news"), now);
 +          jvnews.refreshNews();
 +          Desktop.instance.setProgressBar(null, now);
 +          jvnews.showNews();
 +        }
 +        }, "ShowNewsWindowThread").start();
      }
    }
  
    {
      // TODO: lock aspect ratio for scaling desktop Bug #0058199
      Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
 -    String x = jalview.bin.Cache.getProperty(windowName + "SCREEN_X");
 -    String y = jalview.bin.Cache.getProperty(windowName + "SCREEN_Y");
 -    String width = jalview.bin.Cache
 -            .getProperty(windowName + "SCREEN_WIDTH");
 -    String height = jalview.bin.Cache
 -            .getProperty(windowName + "SCREEN_HEIGHT");
 +    String x = Cache.getProperty(windowName + "SCREEN_X");
 +    String y = Cache.getProperty(windowName + "SCREEN_Y");
 +    String width = Cache.getProperty(windowName + "SCREEN_WIDTH");
 +    String height = Cache.getProperty(windowName + "SCREEN_HEIGHT");
      if ((x != null) && (y != null) && (width != null) && (height != null))
      {
        int ix = Integer.parseInt(x), iy = Integer.parseInt(y),
                iw = Integer.parseInt(width), ih = Integer.parseInt(height);
 -      if (jalview.bin.Cache.getProperty("SCREENGEOMETRY_WIDTH") != null)
 +      if (Cache.getProperty("SCREENGEOMETRY_WIDTH") != null)
        {
          // attempt #1 - try to cope with change in screen geometry - this
          // version doesn't preserve original jv aspect ratio.
          // take ratio of current screen size vs original screen size.
 -        double sw = ((1f * screenSize.width) / (1f * Integer.parseInt(
 -                jalview.bin.Cache.getProperty("SCREENGEOMETRY_WIDTH"))));
 -        double sh = ((1f * screenSize.height) / (1f * Integer.parseInt(
 -                jalview.bin.Cache.getProperty("SCREENGEOMETRY_HEIGHT"))));
 +        double sw = ((1f * screenSize.width) / (1f * Integer
 +                .parseInt(Cache.getProperty("SCREENGEOMETRY_WIDTH"))));
 +        double sh = ((1f * screenSize.height) / (1f * Integer
 +                .parseInt(Cache.getProperty("SCREENGEOMETRY_HEIGHT"))));
          // rescale the bounds depending upon the current screen geometry.
          ix = (int) (ix * sw);
          iw = (int) (iw * sw);
          ih = (int) (ih * sh);
          while (ix >= screenSize.width)
          {
 -          jalview.bin.Cache.log.debug(
 +          jalview.bin.Console.debug(
                    "Window geometry location recall error: shifting horizontal to within screenbounds.");
            ix -= screenSize.width;
          }
          while (iy >= screenSize.height)
          {
 -          jalview.bin.Cache.log.debug(
 +          jalview.bin.Console.debug(
                    "Window geometry location recall error: shifting vertical to within screenbounds.");
            iy -= screenSize.height;
          }
 -        jalview.bin.Cache.log.debug(
 +        jalview.bin.Console.debug(
                  "Got last known dimensions for " + windowName + ": x:" + ix
                          + " y:" + iy + " width:" + iw + " height:" + ih);
        }
      return null;
    }
  
 -  private void doVamsasClientCheck()
 -  {
 -    if (jalview.bin.Cache.vamsasJarsPresent())
 -    {
 -      setupVamsasDisconnectedGui();
 -      VamsasMenu.setVisible(true);
 -      final Desktop us = this;
 -      VamsasMenu.addMenuListener(new MenuListener()
 -      {
 -        // this listener remembers when the menu was first selected, and
 -        // doesn't rebuild the session list until it has been cleared and
 -        // reselected again.
 -        boolean refresh = true;
 -
 -        @Override
 -        public void menuCanceled(MenuEvent e)
 -        {
 -          refresh = true;
 -        }
 -
 -        @Override
 -        public void menuDeselected(MenuEvent e)
 -        {
 -          refresh = true;
 -        }
 -
 -        @Override
 -        public void menuSelected(MenuEvent e)
 -        {
 -          if (refresh)
 -          {
 -            us.buildVamsasStMenu();
 -            refresh = false;
 -          }
 -        }
 -      });
 -      vamsasStart.setVisible(true);
 -    }
 -  }
 -
    void showPasteMenu(int x, int y)
    {
      JPopupMenu popup = new JPopupMenu();
      frame.setResizable(resizable);
      frame.setMaximizable(resizable);
      frame.setIconifiable(resizable);
 +    frame.setOpaque(Platform.isJS());
  
      if (frame.getX() < 1 && frame.getY() < 1)
      {
      }
  
      /*
 -     * add an entry for the new frame in the Window menu 
 -     * (and remove it when the frame is closed)
 +     * add an entry for the new frame in the Window menu (and remove it when the
 +     * frame is closed)
       */
      final JMenuItem menuItem = new JMenuItem(title);
      frame.addInternalFrameListener(new InternalFrameAdapter()
          PaintRefresher.RemoveComponent(frame);
  
          /*
 -         * defensive check to prevent frames being
 -         * added half off the window
 +         * defensive check to prevent frames being added half off the window
           */
          if (openFrameCount > 0)
          {
            menuItem.removeActionListener(menuItem.getActionListeners()[0]);
          }
          windowMenu.remove(menuItem);
 -
 -        System.gc();
 -      };
 +      }
      });
  
      menuItem.addActionListener(new ActionListener()
        }
      });
  
 +    setKeyBindings(frame);
 +
      desktop.add(frame);
  
      windowMenu.add(menuItem);
      {
      } catch (java.lang.ClassCastException cex)
      {
 -      Cache.log.warn(
 -              "Squashed a possible GUI implementation error. If you can recreate this, please look at http://issues.jalview.org/browse/JAL-869",
 +      jalview.bin.Console.warn(
 +              "Squashed a possible GUI implementation error. If you can recreate this, please look at https://issues.jalview.org/browse/JAL-869",
                cex);
      }
    }
  
 +  /**
 +   * Add key bindings to a JInternalFrame so that Ctrl-W and Cmd-W will close
 +   * the window
 +   * 
 +   * @param frame
 +   */
 +  private static void setKeyBindings(JInternalFrame frame)
 +  {
 +    @SuppressWarnings("serial")
 +    final Action closeAction = new AbstractAction()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        frame.dispose();
 +      }
 +    };
 +
 +    /*
 +     * set up key bindings for Ctrl-W and Cmd-W, with the same (Close) action
 +     */
 +    KeyStroke ctrlWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
 +            InputEvent.CTRL_DOWN_MASK);
 +    KeyStroke cmdWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
 +            ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx());
 +
 +    InputMap inputMap = frame
 +            .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
 +    String ctrlW = ctrlWKey.toString();
 +    inputMap.put(ctrlWKey, ctrlW);
 +    inputMap.put(cmdWKey, ctrlW);
 +
 +    ActionMap actionMap = frame.getActionMap();
 +    actionMap.put(ctrlW, closeAction);
 +  }
 +
    @Override
    public void lostOwnership(Clipboard clipboard, Transferable contents)
    {
      // Java's Transferable for native dnd
      evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
      Transferable t = evt.getTransferable();
 -    List<String> files = new ArrayList<>();
 +    List<Object> files = new ArrayList<>();
      List<DataSourceType> protocols = new ArrayList<>();
  
      try
        {
          for (int i = 0; i < files.size(); i++)
          {
 -          String file = files.get(i).toString();
 +          // BH 2018 File or String
 +          Object file = files.get(i);
 +          String fileName = file.toString();
            DataSourceType protocol = (protocols == null)
                    ? DataSourceType.FILE
                    : protocols.get(i);
            FileFormatI format = null;
  
 -          if (file.endsWith(".jar"))
 +          if (fileName.endsWith(".jar"))
            {
              format = FileFormat.Jalview;
  
            {
              format = new IdentifyFile().identify(file, protocol);
            }
 -
 -          new FileLoader().LoadFile(file, protocol, format);
 +          if (file instanceof File)
 +          {
 +            Platform.cacheFileData((File) file);
 +          }
 +          new FileLoader().LoadFile(null, file, protocol, format);
  
          }
        } catch (Exception ex)
    public void inputLocalFileMenuItem_actionPerformed(AlignViewport viewport)
    {
      String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
 -    JalviewFileChooser chooser = JalviewFileChooser
 -            .forRead(Cache.getProperty("LAST_DIRECTORY"), fileFormat);
 +    JalviewFileChooser chooser = JalviewFileChooser.forRead(
 +            Cache.getProperty("LAST_DIRECTORY"), fileFormat,
 +            BackupFiles.getEnabled());
  
      chooser.setFileView(new JalviewFileView());
      chooser.setDialogTitle(
              MessageManager.getString("label.open_local_file"));
      chooser.setToolTipText(MessageManager.getString("action.open"));
  
 -    int value = chooser.showOpenDialog(this);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    chooser.setResponseHandler(0, new Runnable()
      {
 -      String choice = chooser.getSelectedFile().getPath();
 -      Cache.setProperty("LAST_DIRECTORY",
 -              chooser.getSelectedFile().getParent());
 +      @Override
 +      public void run()
 +      {
 +        File selectedFile = chooser.getSelectedFile();
 +        Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
  
 -      FileFormatI format = chooser.getSelectedFormat();
 +        FileFormatI format = chooser.getSelectedFormat();
  
 -      /*
 -       * Call IdentifyFile to verify the file contains what its extension implies.
 -       * Skip this step for dynamically added file formats, because
 -       * IdentifyFile does not know how to recognise them.
 -       */
 -      if (FileFormats.getInstance().isIdentifiable(format))
 -      {
 -        try
 -        {
 -          format = new IdentifyFile().identify(choice, DataSourceType.FILE);
 -        } catch (FileFormatException e)
 +        /*
 +         * Call IdentifyFile to verify the file contains what its extension implies.
 +         * Skip this step for dynamically added file formats, because IdentifyFile does
 +         * not know how to recognise them.
 +         */
 +        if (FileFormats.getInstance().isIdentifiable(format))
          {
 -          // format = null; //??
 +          try
 +          {
 +            format = new IdentifyFile().identify(selectedFile,
 +                    DataSourceType.FILE);
 +          } catch (FileFormatException e)
 +          {
 +            // format = null; //??
 +          }
          }
 -      }
  
 -      if (viewport != null)
 -      {
 -        new FileLoader().LoadFile(viewport, choice, DataSourceType.FILE,
 -                format);
 -      }
 -      else
 -      {
 -        new FileLoader().LoadFile(choice, DataSourceType.FILE, format);
 +        new FileLoader().LoadFile(viewport, selectedFile,
 +                DataSourceType.FILE, format);
        }
 -    }
 +    });
 +    chooser.showOpenDialog(this);
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Shows a dialog for input of a URL at which to retrieve alignment data
     * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * @param viewport
     */
    @Override
    public void inputURLMenuItem_actionPerformed(AlignViewport viewport)
      // for viewing
      JLabel label = new JLabel(
              MessageManager.getString("label.input_file_url"));
  
      JPanel panel = new JPanel(new GridLayout(2, 1));
      panel.add(label);
 -    panel.add(history);
 -    history.setPreferredSize(new Dimension(400, 20));
 -    history.setEditable(true);
 -    history.addItem("http://www.");
 -
 -    String historyItems = jalview.bin.Cache.getProperty("RECENT_URL");
  
 -    StringTokenizer st;
 -
 -    if (historyItems != null)
 +    /*
 +     * the URL to fetch is input in Java: an editable combobox with history JS:
 +     * (pending JAL-3038) a plain text field
 +     */
 +    JComponent history;
 +    String urlBase = "https://www.";
 +    if (Platform.isJS())
      {
 -      st = new StringTokenizer(historyItems, "\t");
 -
 -      while (st.hasMoreTokens())
 -      {
 -        history.addItem(st.nextElement());
 -      }
 -    }
 -
 -    int reply = JvOptionPane.showInternalConfirmDialog(desktop, panel,
 -            MessageManager.getString("label.input_alignment_from_url"),
 -            JvOptionPane.OK_CANCEL_OPTION);
 -
 -    if (reply != JvOptionPane.OK_OPTION)
 -    {
 -      return;
 +      history = new JTextField(urlBase, 35);
      }
 -
 -    String url = history.getSelectedItem().toString();
 -
 -    if (url.toLowerCase().endsWith(".jar"))
 +    else
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
      {
 -      if (viewport != null)
 +      JComboBox<String> asCombo = new JComboBox<>();
 +      asCombo.setPreferredSize(new Dimension(400, 20));
 +      asCombo.setEditable(true);
 +      asCombo.addItem(urlBase);
 +      String historyItems = Cache.getProperty("RECENT_URL");
 +      if (historyItems != null)
        {
 -        new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
 -                FileFormat.Jalview);
 -      }
 -      else
 -      {
 -        new FileLoader().LoadFile(url, DataSourceType.URL,
 -                FileFormat.Jalview);
 +        for (String token : historyItems.split("\\t"))
 +        {
 +          asCombo.addItem(token);
 +        }
        }
 +      history = asCombo;
      }
 -    else
 +    panel.add(history);
 +
 +    Object[] options = new Object[] { MessageManager.getString("action.ok"),
 +        MessageManager.getString("action.cancel") };
 +    Runnable action = new Runnable()
      {
 -      FileFormatI format = null;
 -      try
 -      {
 -        format = new IdentifyFile().identify(url, DataSourceType.URL);
 -      } catch (FileFormatException e)
 +      @Override
 +      public void run()
        {
 -        // TODO revise error handling, distinguish between
 -        // URL not found and response not valid
 -      }
 +        @SuppressWarnings("unchecked")
 +        String url = (history instanceof JTextField
 +                ? ((JTextField) history).getText()
 +                : ((JComboBox<String>) history).getEditor().getItem()
 +                        .toString().trim());
  
 -      if (format == null)
 -      {
 -        JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 -                MessageManager.formatMessage(
 -                        "label.couldnt_locate",
 -                        new String[]
 -                        { url }),
 -                MessageManager.getString("label.url_not_found"),
 -                JvOptionPane.WARNING_MESSAGE);
 +        if (url.toLowerCase(Locale.ROOT).endsWith(".jar"))
 +        {
 +          if (viewport != null)
 +          {
 +            new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
 +                    FileFormat.Jalview);
 +          }
 +          else
 +          {
 +            new FileLoader().LoadFile(url, DataSourceType.URL,
 +                    FileFormat.Jalview);
 +          }
 +        }
 +        else
 +        {
 +          FileFormatI format = null;
 +          try
 +          {
 +            format = new IdentifyFile().identify(url, DataSourceType.URL);
 +          } catch (FileFormatException e)
 +          {
 +            // TODO revise error handling, distinguish between
 +            // URL not found and response not valid
 +          }
  
 -        return;
 -      }
 +          if (format == null)
 +          {
 +            String msg = MessageManager
 +                    .formatMessage("label.couldnt_locate", url);
 +            JvOptionPane.showInternalMessageDialog(Desktop.desktop, msg,
 +                    MessageManager.getString("label.url_not_found"),
 +                    JvOptionPane.WARNING_MESSAGE);
  
 -      if (viewport != null)
 -      {
 -        new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
 -                format);
 -      }
 -      else
 -      {
 -        new FileLoader().LoadFile(url, DataSourceType.URL, format);
 +            return;
 +          }
 +
 +          if (viewport != null)
 +          {
 +            new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
 +                    format);
 +          }
 +          else
 +          {
 +            new FileLoader().LoadFile(url, DataSourceType.URL, format);
 +          }
 +        }
        }
 -    }
 +    };
 +    String dialogOption = MessageManager
 +            .getString("label.input_alignment_from_url");
 +    JvOptionPane.newOptionDialog(desktop).setResponseHandler(0, action)
 +            .showInternalDialog(panel, dialogOption,
 +                    JvOptionPane.YES_NO_CANCEL_OPTION,
 +                    JvOptionPane.PLAIN_MESSAGE, null, options,
 +                    MessageManager.getString("action.ok"));
    }
  
    /**
    public void quit()
    {
      Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
 -    jalview.bin.Cache.setProperty("SCREENGEOMETRY_WIDTH",
 -            screen.width + "");
 -    jalview.bin.Cache.setProperty("SCREENGEOMETRY_HEIGHT",
 -            screen.height + "");
 +    Cache.setProperty("SCREENGEOMETRY_WIDTH", screen.width + "");
 +    Cache.setProperty("SCREENGEOMETRY_HEIGHT", screen.height + "");
      storeLastKnownDimensions("", new Rectangle(getBounds().x, getBounds().y,
              getWidth(), getHeight()));
  
  
    private void storeLastKnownDimensions(String string, Rectangle jc)
    {
 -    jalview.bin.Cache.log.debug("Storing last known dimensions for "
 -            + string + ": x:" + jc.x + " y:" + jc.y + " width:" + jc.width
 +    jalview.bin.Console.debug("Storing last known dimensions for " + string
 +            + ": x:" + jc.x + " y:" + jc.y + " width:" + jc.width
              + " height:" + jc.height);
  
 -    jalview.bin.Cache.setProperty(string + "SCREEN_X", jc.x + "");
 -    jalview.bin.Cache.setProperty(string + "SCREEN_Y", jc.y + "");
 -    jalview.bin.Cache.setProperty(string + "SCREEN_WIDTH", jc.width + "");
 -    jalview.bin.Cache.setProperty(string + "SCREEN_HEIGHT", jc.height + "");
 +    Cache.setProperty(string + "SCREEN_X", jc.x + "");
 +    Cache.setProperty(string + "SCREEN_Y", jc.y + "");
 +    Cache.setProperty(string + "SCREEN_WIDTH", jc.width + "");
 +    Cache.setProperty(string + "SCREEN_HEIGHT", jc.height + "");
    }
  
    /**
    @Override
    public void aboutMenuItem_actionPerformed(ActionEvent e)
    {
 -    // StringBuffer message = getAboutMessage(false);
 -    // JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 -    //
 -    // message.toString(), "About Jalview", JvOptionPane.INFORMATION_MESSAGE);
      new Thread(new Runnable()
      {
        @Override
        public void run()
        {
 -        new SplashScreen(true);
 +        new SplashScreen(false);
        }
-     }, "ShowAboutMenuThread").start();
+     }, "ShowAboutMenu").start();
    }
  
 -  public StringBuffer getAboutMessage(boolean shortv)
 +  /**
 +   * Returns the html text for the About screen, including any available version
 +   * number, build details, author details and citation reference, but without
 +   * the enclosing {@code html} tags
 +   * 
 +   * @return
 +   */
 +  public String getAboutMessage()
    {
 -    StringBuffer message = new StringBuffer();
 -    message.append("<html>");
 -    if (shortv)
 -    {
 -      message.append("<h1><strong>Version: "
 -              + jalview.bin.Cache.getProperty("VERSION")
 -              + "</strong></h1>");
 -      message.append("<strong>Last Updated: <em>"
 -              + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown")
 -              + "</em></strong>");
 -
 -    }
 -    else
 -    {
 -
 -      message.append("<strong>Version "
 -              + jalview.bin.Cache.getProperty("VERSION")
 -              + "; last updated: "
 -              + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown"));
 -    }
 +    StringBuilder message = new StringBuilder(1024);
 +    message.append("<div style=\"font-family: sans-serif;\">")
 +            .append("<h1><strong>Version: ")
 +            .append(Cache.getProperty("VERSION")).append("</strong></h1>")
 +            .append("<strong>Built: <em>")
 +            .append(Cache.getDefault("BUILD_DATE", "unknown"))
 +            .append("</em> from ").append(Cache.getBuildDetailsForSplash())
 +            .append("</strong>");
  
 -    if (jalview.bin.Cache.getDefault("LATEST_VERSION", "Checking")
 -            .equals("Checking"))
 +    String latestVersion = Cache.getDefault("LATEST_VERSION", "Checking");
 +    if (latestVersion.equals("Checking"))
      {
 -      message.append("<br>...Checking latest version...</br>");
 +      // JBP removed this message for 2.11: May be reinstated in future version
 +      // message.append("<br>...Checking latest version...</br>");
      }
 -    else if (!jalview.bin.Cache.getDefault("LATEST_VERSION", "Checking")
 -            .equals(jalview.bin.Cache.getProperty("VERSION")))
 +    else if (!latestVersion.equals(Cache.getProperty("VERSION")))
      {
        boolean red = false;
 -      if (jalview.bin.Cache.getProperty("VERSION").toLowerCase()
 +      if (Cache.getProperty("VERSION").toLowerCase(Locale.ROOT)
                .indexOf("automated build") == -1)
        {
          red = true;
          message.append("<div style=\"color: #FF0000;font-style: bold;\">");
        }
  
 -      message.append("<br>!! Version "
 -              + jalview.bin.Cache.getDefault("LATEST_VERSION",
 -                      "..Checking..")
 -              + " is available for download from "
 -              + jalview.bin.Cache.getDefault("www.jalview.org",
 -                      "http://www.jalview.org")
 -              + " !!");
 +      message.append("<br>!! Version ")
 +              .append(Cache.getDefault("LATEST_VERSION", "..Checking.."))
 +              .append(" is available for download from ")
 +              .append(Cache.getDefault("www.jalview.org",
 +                      "https://www.jalview.org"))
 +              .append(" !!");
        if (red)
        {
          message.append("</div>");
        }
      }
 -    message.append("<br>Authors:  " + jalview.bin.Cache.getDefault(
 -            "AUTHORFNAMES",
 -            "The Jalview Authors (See AUTHORS file for current list)")
 -            + "<br><br>Development managed by The Barton Group, University of Dundee, Scotland, UK.<br>"
 -            + "<br><br>For help, see the FAQ at <a href=\"http://www.jalview.org/faq\">www.jalview.org/faq</a> and/or join the jalview-discuss@jalview.org mailing list"
 -            + "<br><br>If  you use Jalview, please cite:"
 -            + "<br>Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)"
 -            + "<br>Jalview Version 2 - a multiple sequence alignment editor and analysis workbench"
 -            + "<br>Bioinformatics doi: 10.1093/bioinformatics/btp033"
 -            + "</html>");
 -    return message;
 +    message.append("<br>Authors:  ");
 +    message.append(Cache.getDefault("AUTHORFNAMES", DEFAULT_AUTHORS));
 +    message.append(CITATION);
 +
 +    message.append("</div>");
 +
 +    return message.toString();
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Action on requesting Help documentation
     */
    @Override
 -  public void documentationMenuItem_actionPerformed(ActionEvent e)
 +  public void documentationMenuItem_actionPerformed()
    {
      try
      {
 -      Help.showHelpWindow();
 +      if (Platform.isJS())
 +      {
 +        BrowserLauncher.openURL("https://www.jalview.org/help.html");
 +      }
 +      else
 +      /**
 +       * Java only
 +       * 
 +       * @j2sIgnore
 +       */
 +      {
 +        Help.showHelpWindow();
 +      }
      } catch (Exception ex)
      {
 +      System.err.println("Error opening help: " + ex.getMessage());
      }
    }
  
      }
      Jalview.setCurrentAlignFrame(null);
      System.out.println("ALL CLOSED");
 -    if (v_client != null)
 -    {
 -      // TODO clear binding to vamsas document objects on close_all
 -    }
  
      /*
       * reset state of singleton objects as appropriate (clear down session state
      {
        ssm.resetAll();
      }
 -    System.gc();
    }
  
    @Override
    protected void garbageCollect_actionPerformed(ActionEvent e)
    {
      // We simply collect the garbage
 -    jalview.bin.Cache.log.debug("Collecting garbage...");
 +    jalview.bin.Console.debug("Collecting garbage...");
      System.gc();
 -    jalview.bin.Cache.log.debug("Finished garbage collection.");
 +    jalview.bin.Console.debug("Finished garbage collection.");
    }
  
    /*
     * (non-Javadoc)
     * 
 -   * @see
 -   * jalview.jbgui.GDesktop#showMemusage_actionPerformed(java.awt.event.ActionEvent
 -   * )
 +   * @see jalview.jbgui.GDesktop#showMemusage_actionPerformed(java.awt.event.
 +   * ActionEvent )
     */
    @Override
    protected void showMemusage_actionPerformed(ActionEvent e)
     */
    void showConsole(boolean selected)
    {
 -    showConsole.setSelected(selected);
      // TODO: decide if we should update properties file
 -    Cache.setProperty("SHOW_JAVA_CONSOLE",
 -            Boolean.valueOf(selected).toString());
 -    jconsole.setVisible(selected);
 +    if (jconsole != null) // BH 2018
 +    {
 +      showConsole.setSelected(selected);
 +      Cache.setProperty("SHOW_JAVA_CONSOLE",
 +              Boolean.valueOf(selected).toString());
 +      jconsole.setVisible(selected);
 +    }
    }
  
    void reorderAssociatedWindows(boolean minimize, boolean close)
    @Override
    protected void preferences_actionPerformed(ActionEvent e)
    {
 -    new Preferences();
 +    Preferences.openPreferences();
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Prompts the user to choose a file and then saves the Jalview state as a
 +   * Jalview project file
     */
    @Override
 -  public void saveState_actionPerformed(ActionEvent e)
 +  public void saveState_actionPerformed()
    {
 -    JalviewFileChooser chooser = new JalviewFileChooser("jvp",
 -            "Jalview Project");
 +    saveState_actionPerformed(false);
 +  }
  
 -    chooser.setFileView(new JalviewFileView());
 -    chooser.setDialogTitle(MessageManager.getString("label.save_state"));
 +  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();
  
 -    int value = chooser.showSaveDialog(this);
 +    // System.out.println("autoSave="+autoSave+", projectFile='"+projectFile+"',
 +    // saveAs="+saveAs+", Backups
 +    // "+(BackupFiles.getEnabled()?"enabled":"disabled"));
  
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    boolean approveSave = false;
 +    if (!autoSave)
      {
 -      final Desktop me = this;
 -      final java.io.File choice = chooser.getSelectedFile();
 -      setProjectFile(choice);
 +      JalviewFileChooser chooser = new JalviewFileChooser("jvp",
 +              "Jalview Project");
  
 +      chooser.setFileView(new JalviewFileView());
 +      chooser.setDialogTitle(MessageManager.getString("label.save_state"));
 +
 +      int value = chooser.showSaveDialog(this);
 +
 +      if (value == JalviewFileChooser.APPROVE_OPTION)
 +      {
 +        projectFile = chooser.getSelectedFile();
 +        setProjectFile(projectFile);
 +        approveSave = true;
 +      }
 +    }
 +
 +    if (approveSave || autoSave)
 +    {
 +      final Desktop me = this;
 +      final java.io.File chosenFile = projectFile;
        new Thread(new Runnable()
        {
          @Override
            // TODO: refactor to Jalview desktop session controller action.
            setProgressBar(MessageManager.formatMessage(
                    "label.saving_jalview_project", new Object[]
 -                  { choice.getName() }), choice.hashCode());
 -          jalview.bin.Cache.setProperty("LAST_DIRECTORY",
 -                  choice.getParent());
 +                  { chosenFile.getName() }), chosenFile.hashCode());
 +          Cache.setProperty("LAST_DIRECTORY", chosenFile.getParent());
            // TODO catch and handle errors for savestate
            // TODO prevent user from messing with the Desktop whilst we're saving
            try
            {
 -            new Jalview2XML().saveState(choice);
 +            boolean doBackup = BackupFiles.getEnabled();
 +            BackupFiles backupfiles = doBackup ? new BackupFiles(chosenFile)
 +                    : null;
 +
 +            new Jalview2XML().saveState(
 +                    doBackup ? backupfiles.getTempFile() : chosenFile);
 +
 +            if (doBackup)
 +            {
 +              backupfiles.setWriteSuccess(true);
 +              backupfiles.rollBackupsAndRenameTempFile();
 +            }
            } catch (OutOfMemoryError oom)
            {
 -            new OOMWarning(
 -                    "Whilst saving current state to " + choice.getName(),
 -                    oom);
 +            new OOMWarning("Whilst saving current state to "
 +                    + chosenFile.getName(), oom);
            } catch (Exception ex)
            {
 -            Cache.log.error(
 -                    "Problems whilst trying to save to " + choice.getName(),
 -                    ex);
 +            jalview.bin.Console.error("Problems whilst trying to save to "
 +                    + chosenFile.getName(), ex);
              JvOptionPane.showMessageDialog(me,
                      MessageManager.formatMessage(
                              "label.error_whilst_saving_current_state_to",
                              new Object[]
 -                            { choice.getName() }),
 +                            { chosenFile.getName() }),
                      MessageManager.getString("label.couldnt_save_project"),
                      JvOptionPane.WARNING_MESSAGE);
            }
 -          setProgressBar(null, choice.hashCode());
 +          setProgressBar(null, chosenFile.hashCode());
          }
-       }, "SaveJalviewProjectThread").start();
+       }, "SaveJalviewProject").start();
      }
    }
  
 +  @Override
 +  public void saveAsState_actionPerformed(ActionEvent e)
 +  {
 +    saveState_actionPerformed(true);
 +  }
 +
    private void setProjectFile(File choice)
    {
      this.projectFile = choice;
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Shows a file chooser dialog and tries to read in the selected file as a
 +   * Jalview project
     */
    @Override
 -  public void loadState_actionPerformed(ActionEvent e)
 +  public void loadState_actionPerformed()
    {
 +    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"), new String[]
 -            { "jvp", "jar" },
 -            new String[]
 -            { "Jalview Project", "Jalview Project (old)" },
 -            "Jalview Project");
 +            Cache.getProperty("LAST_DIRECTORY"), suffix, desc,
 +            "Jalview Project", true, BackupFiles.getEnabled()); // 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();
++        final 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()
            {
 -            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(MessageManager.formatMessage(
++                "label.loading_jalview_project", new Object[]
++                 { choice }), choice.hashCode());
++
 +            try
 +            {
 +              new Jalview2XML().loadJalviewAlign(selectedFile);
 +            } catch (OutOfMemoryError oom)
 +            {
 +              new OOMWarning("Whilst loading project from " + choice, oom);
 +            } catch (Exception ex)
 +            {
 +              jalview.bin.Console.error(
 +                      "Problems whilst loading project from " + choice, ex);
 +              JvOptionPane.showMessageDialog(Desktop.desktop,
 +                      MessageManager.formatMessage(
 +                              "label.error_whilst_loading_project_from",
 +                              new Object[]
 +                              { choice }),
 +                      MessageManager
 +                              .getString("label.couldnt_load_project"),
 +                      JvOptionPane.WARNING_MESSAGE);
 +            }
+           setProgressBar(null, choice.hashCode());
 -        }
 -      }, "LoadJalviewProject").start();
 -    }
 +          }
 +        }, "Project Loader").start();
 +      }
 +    });
 +
 +    chooser.showOpenDialog(this);
    }
  
    @Override
  
    ArrayList<JPanel> fileLoadingPanels = new ArrayList<>();
  
 -  public void startLoading(final String fileName)
 +  public void startLoading(final Object fileName)
    {
      if (fileLoadingCount == 0)
      {
      if (desktop != null)
      {
        AlignFrame[] frames = Desktop.getAlignFrames();
 -
 -      for (AlignFrame afr : frames)
 -      {
 -        if (sequenceSetId == null || afr.getViewport().getSequenceSetId()
 -                .equals(sequenceSetId))
 -        {
 -          if (afr.alignPanels != null)
 -          {
 -            for (AlignmentPanel ap : afr.alignPanels)
 -            {
 -              if (sequenceSetId == null
 -                      || sequenceSetId.equals(ap.av.getSequenceSetId()))
 -              {
 -                viewp.add(ap.av);
 -              }
 -            }
 -          }
 -          else
 -          {
 -            viewp.add(afr.getViewport());
 -          }
 -        }
 -      }
 -      if (viewp.size() > 0)
 -      {
 -        return viewp.toArray(new AlignmentViewport[viewp.size()]);
 -      }
 -    }
 -    return null;
 -  }
 -
 -  /**
 -   * Explode the views in the given frame into separate AlignFrame
 -   * 
 -   * @param af
 -   */
 -  public static void explodeViews(AlignFrame af)
 -  {
 -    int size = af.alignPanels.size();
 -    if (size < 2)
 -    {
 -      return;
 -    }
 -
 -    for (int i = 0; i < size; i++)
 -    {
 -      AlignmentPanel ap = af.alignPanels.get(i);
 -      AlignFrame newaf = new AlignFrame(ap);
 -
 -      /*
 -       * Restore the view's last exploded frame geometry if known. Multiple
 -       * views from one exploded frame share and restore the same (frame)
 -       * position and size.
 -       */
 -      Rectangle geometry = ap.av.getExplodedGeometry();
 -      if (geometry != null)
 -      {
 -        newaf.setBounds(geometry);
 -      }
 -
 -      ap.av.setGatherViewsHere(false);
 -
 -      addInternalFrame(newaf, af.getTitle(), AlignFrame.DEFAULT_WIDTH,
 -              AlignFrame.DEFAULT_HEIGHT);
 -    }
 -
 -    af.alignPanels.clear();
 -    af.closeMenuItem_actionPerformed(true);
 -
 -  }
 -
 -  /**
 -   * Gather expanded views (separate AlignFrame's) with the same sequence set
 -   * identifier back in to this frame as additional views, and close the
 -   * expanded views. Note the expanded frames may themselves have multiple
 -   * views. We take the lot.
 -   * 
 -   * @param source
 -   */
 -  public void gatherViews(AlignFrame source)
 -  {
 -    source.viewport.setGatherViewsHere(true);
 -    source.viewport.setExplodedGeometry(source.getBounds());
 -    JInternalFrame[] frames = desktop.getAllFrames();
 -    String viewId = source.viewport.getSequenceSetId();
 -
 -    for (int t = 0; t < frames.length; t++)
 -    {
 -      if (frames[t] instanceof AlignFrame && frames[t] != source)
 -      {
 -        AlignFrame af = (AlignFrame) frames[t];
 -        boolean gatherThis = false;
 -        for (int a = 0; a < af.alignPanels.size(); a++)
 -        {
 -          AlignmentPanel ap = af.alignPanels.get(a);
 -          if (viewId.equals(ap.av.getSequenceSetId()))
 -          {
 -            gatherThis = true;
 -            ap.av.setGatherViewsHere(false);
 -            ap.av.setExplodedGeometry(af.getBounds());
 -            source.addAlignmentPanel(ap, false);
 -          }
 -        }
 -
 -        if (gatherThis)
 -        {
 -          af.alignPanels.clear();
 -          af.closeMenuItem_actionPerformed(true);
 -        }
 -      }
 -    }
 -
 -  }
 -
 -  jalview.gui.VamsasApplication v_client = null;
 -
 -  @Override
 -  public void vamsasImport_actionPerformed(ActionEvent e)
 -  {
 -    if (v_client == null)
 -    {
 -      // Load and try to start a session.
 -      JalviewFileChooser chooser = new JalviewFileChooser(
 -              jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
 -
 -      chooser.setFileView(new JalviewFileView());
 -      chooser.setDialogTitle(
 -              MessageManager.getString("label.open_saved_vamsas_session"));
 -      chooser.setToolTipText(MessageManager.getString(
 -              "label.select_vamsas_session_opened_as_new_vamsas_session"));
 -
 -      int value = chooser.showOpenDialog(this);
 -
 -      if (value == JalviewFileChooser.APPROVE_OPTION)
 -      {
 -        String fle = chooser.getSelectedFile().toString();
 -        if (!vamsasImport(chooser.getSelectedFile()))
 -        {
 -          JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 -                  MessageManager.formatMessage(
 -                          "label.couldnt_import_as_vamsas_session",
 -                          new Object[]
 -                          { fle }),
 -                  MessageManager
 -                          .getString("label.vamsas_document_import_failed"),
 -                  JvOptionPane.ERROR_MESSAGE);
 -        }
 -      }
 -    }
 -    else
 -    {
 -      jalview.bin.Cache.log.error(
 -              "Implementation error - load session from a running session is not supported.");
 -    }
 -  }
 -
 -  /**
 -   * import file into a new vamsas session (uses jalview.gui.VamsasApplication)
 -   * 
 -   * @param file
 -   * @return true if import was a success and a session was started.
 -   */
 -  public boolean vamsasImport(URL url)
 -  {
 -    // TODO: create progress bar
 -    if (v_client != null)
 -    {
 -
 -      jalview.bin.Cache.log.error(
 -              "Implementation error - load session from a running session is not supported.");
 -      return false;
 -    }
 -
 -    try
 -    {
 -      // copy the URL content to a temporary local file
 -      // TODO: be a bit cleverer here with nio (?!)
 -      File file = File.createTempFile("vdocfromurl", ".vdj");
 -      FileOutputStream fos = new FileOutputStream(file);
 -      BufferedInputStream bis = new BufferedInputStream(url.openStream());
 -      byte[] buffer = new byte[2048];
 -      int ln;
 -      while ((ln = bis.read(buffer)) > -1)
 -      {
 -        fos.write(buffer, 0, ln);
 -      }
 -      bis.close();
 -      fos.close();
 -      v_client = new jalview.gui.VamsasApplication(this, file,
 -              url.toExternalForm());
 -    } catch (Exception ex)
 -    {
 -      jalview.bin.Cache.log.error(
 -              "Failed to create new vamsas session from contents of URL "
 -                      + url,
 -              ex);
 -      return false;
 -    }
 -    setupVamsasConnectedGui();
 -    v_client.initial_update(); // TODO: thread ?
 -    return v_client.inSession();
 -  }
 -
 -  /**
 -   * import file into a new vamsas session (uses jalview.gui.VamsasApplication)
 -   * 
 -   * @param file
 -   * @return true if import was a success and a session was started.
 -   */
 -  public boolean vamsasImport(File file)
 -  {
 -    if (v_client != null)
 -    {
 -
 -      jalview.bin.Cache.log.error(
 -              "Implementation error - load session from a running session is not supported.");
 -      return false;
 -    }
 -
 -    setProgressBar(MessageManager.formatMessage(
 -            "status.importing_vamsas_session_from", new Object[]
 -            { file.getName() }), file.hashCode());
 -    try
 -    {
 -      v_client = new jalview.gui.VamsasApplication(this, file, null);
 -    } catch (Exception ex)
 -    {
 -      setProgressBar(MessageManager.formatMessage(
 -              "status.importing_vamsas_session_from", new Object[]
 -              { file.getName() }), file.hashCode());
 -      jalview.bin.Cache.log.error(
 -              "New vamsas session from existing session file failed:", ex);
 -      return false;
 -    }
 -    setupVamsasConnectedGui();
 -    v_client.initial_update(); // TODO: thread ?
 -    setProgressBar(MessageManager.formatMessage(
 -            "status.importing_vamsas_session_from", new Object[]
 -            { file.getName() }), file.hashCode());
 -    return v_client.inSession();
 -  }
 -
 -  public boolean joinVamsasSession(String mysesid)
 -  {
 -    if (v_client != null)
 -    {
 -      throw new Error(MessageManager
 -              .getString("error.try_join_vamsas_session_another"));
 -    }
 -    if (mysesid == null)
 -    {
 -      throw new Error(
 -              MessageManager.getString("error.invalid_vamsas_session_id"));
 -    }
 -    v_client = new VamsasApplication(this, mysesid);
 -    setupVamsasConnectedGui();
 -    v_client.initial_update();
 -    return (v_client.inSession());
 -  }
 -
 -  @Override
 -  public void vamsasStart_actionPerformed(ActionEvent e)
 -  {
 -    if (v_client == null)
 -    {
 -      // Start a session.
 -      // we just start a default session for moment.
 -      /*
 -       * JalviewFileChooser chooser = new JalviewFileChooser(jalview.bin.Cache.
 -       * getProperty("LAST_DIRECTORY"));
 -       * 
 -       * chooser.setFileView(new JalviewFileView());
 -       * chooser.setDialogTitle("Load Vamsas file");
 -       * chooser.setToolTipText("Import");
 -       * 
 -       * int value = chooser.showOpenDialog(this);
 -       * 
 -       * if (value == JalviewFileChooser.APPROVE_OPTION) { v_client = new
 -       * jalview.gui.VamsasApplication(this, chooser.getSelectedFile());
 -       */
 -      v_client = new VamsasApplication(this);
 -      setupVamsasConnectedGui();
 -      v_client.initial_update(); // TODO: thread ?
 -    }
 -    else
 -    {
 -      // store current data in session.
 -      v_client.push_update(); // TODO: thread
 -    }
 -  }
 -
 -  protected void setupVamsasConnectedGui()
 -  {
 -    vamsasStart.setText(MessageManager.getString("label.session_update"));
 -    vamsasSave.setVisible(true);
 -    vamsasStop.setVisible(true);
 -    vamsasImport.setVisible(false); // Document import to existing session is
 -    // not possible for vamsas-client-1.0.
 -  }
 -
 -  protected void setupVamsasDisconnectedGui()
 -  {
 -    vamsasSave.setVisible(false);
 -    vamsasStop.setVisible(false);
 -    vamsasImport.setVisible(true);
 -    vamsasStart
 -            .setText(MessageManager.getString("label.new_vamsas_session"));
 -  }
 -
 -  @Override
 -  public void vamsasStop_actionPerformed(ActionEvent e)
 -  {
 -    if (v_client != null)
 -    {
 -      v_client.end_session();
 -      v_client = null;
 -      setupVamsasDisconnectedGui();
 -    }
 -  }
 -
 -  protected void buildVamsasStMenu()
 -  {
 -    if (v_client == null)
 -    {
 -      String[] sess = null;
 -      try
 -      {
 -        sess = VamsasApplication.getSessionList();
 -      } catch (Exception e)
 +
 +      for (AlignFrame afr : frames)
        {
 -        jalview.bin.Cache.log.warn("Problem getting current sessions list.",
 -                e);
 -        sess = null;
 -      }
 -      if (sess != null)
 -      {
 -        jalview.bin.Cache.log.debug(
 -                "Got current sessions list: " + sess.length + " entries.");
 -        VamsasStMenu.removeAll();
 -        for (int i = 0; i < sess.length; i++)
 -        {
 -          JMenuItem sessit = new JMenuItem();
 -          sessit.setText(sess[i]);
 -          sessit.setToolTipText(MessageManager
 -                  .formatMessage("label.connect_to_session", new Object[]
 -                  { sess[i] }));
 -          final Desktop dsktp = this;
 -          final String mysesid = sess[i];
 -          sessit.addActionListener(new ActionListener()
 +        if (sequenceSetId == null || afr.getViewport().getSequenceSetId()
 +                .equals(sequenceSetId))
 +        {
 +          if (afr.alignPanels != null)
            {
 -
 -            @Override
 -            public void actionPerformed(ActionEvent e)
 +            for (AlignmentPanel ap : afr.alignPanels)
              {
 -              if (dsktp.v_client == null)
 +              if (sequenceSetId == null
 +                      || sequenceSetId.equals(ap.av.getSequenceSetId()))
                {
 -                Thread rthr = new Thread(new Runnable()
 -                {
 -
 -                  @Override
 -                  public void run()
 -                  {
 -                    dsktp.v_client = new VamsasApplication(dsktp, mysesid);
 -                    dsktp.setupVamsasConnectedGui();
 -                    dsktp.v_client.initial_update();
 -                  }
 -
 -                }, "VamsasSession");
 -                rthr.start();
 +                viewp.add(ap.av);
                }
 -            };
 -          });
 -          VamsasStMenu.add(sessit);
 +            }
 +          }
 +          else
 +          {
 +            viewp.add(afr.getViewport());
 +          }
          }
 -        // don't show an empty menu.
 -        VamsasStMenu.setVisible(sess.length > 0);
 -
        }
 -      else
 +      if (viewp.size() > 0)
        {
 -        jalview.bin.Cache.log.debug("No current vamsas sessions.");
 -        VamsasStMenu.removeAll();
 -        VamsasStMenu.setVisible(false);
 +        return viewp.toArray(new AlignmentViewport[viewp.size()]);
        }
      }
 -    else
 -    {
 -      // Not interested in the content. Just hide ourselves.
 -      VamsasStMenu.setVisible(false);
 -    }
 +    return null;
    }
  
 -  @Override
 -  public void vamsasSave_actionPerformed(ActionEvent e)
 +  /**
 +   * Explode the views in the given frame into separate AlignFrame
 +   * 
 +   * @param af
 +   */
 +  public static void explodeViews(AlignFrame af)
    {
 -    if (v_client != null)
 +    int size = af.alignPanels.size();
 +    if (size < 2)
      {
 -      // TODO: VAMSAS DOCUMENT EXTENSION is VDJ
 -      JalviewFileChooser chooser = new JalviewFileChooser("vdj",
 -              "Vamsas Document");
 +      return;
 +    }
  
 -      chooser.setFileView(new JalviewFileView());
 -      chooser.setDialogTitle(MessageManager
 -              .getString("label.save_vamsas_document_archive"));
 +    // FIXME: ideally should use UI interface API
 +    FeatureSettings viewFeatureSettings = (af.featureSettings != null
 +            && af.featureSettings.isOpen()) ? af.featureSettings : null;
 +    Rectangle fsBounds = af.getFeatureSettingsGeometry();
 +    for (int i = 0; i < size; i++)
 +    {
 +      AlignmentPanel ap = af.alignPanels.get(i);
  
 -      int value = chooser.showSaveDialog(this);
 +      AlignFrame newaf = new AlignFrame(ap);
  
 -      if (value == JalviewFileChooser.APPROVE_OPTION)
 +      // transfer reference for existing feature settings to new alignFrame
 +      if (ap == af.alignPanel)
        {
 -        java.io.File choice = chooser.getSelectedFile();
 -        JPanel progpanel = addProgressPanel(MessageManager
 -                .formatMessage("label.saving_vamsas_doc", new Object[]
 -                { choice.getName() }));
 -        Cache.setProperty("LAST_DIRECTORY", choice.getParent());
 -        String warnmsg = null;
 -        String warnttl = null;
 -        try
 +        if (viewFeatureSettings != null && viewFeatureSettings.fr.ap == ap)
          {
 -          v_client.vclient.storeDocument(choice);
 -        } catch (Error ex)
 -        {
 -          warnttl = "Serious Problem saving Vamsas Document";
 -          warnmsg = ex.toString();
 -          jalview.bin.Cache.log
 -                  .error("Error Whilst saving document to " + choice, ex);
 +          newaf.featureSettings = viewFeatureSettings;
 +        }
 +        newaf.setFeatureSettingsGeometry(fsBounds);
 +      }
  
 -        } catch (Exception ex)
 -        {
 -          warnttl = "Problem saving Vamsas Document.";
 -          warnmsg = ex.toString();
 -          jalview.bin.Cache.log.warn(
 -                  "Exception Whilst saving document to " + choice, ex);
 +      /*
 +       * Restore the view's last exploded frame geometry if known. Multiple views from
 +       * one exploded frame share and restore the same (frame) position and size.
 +       */
 +      Rectangle geometry = ap.av.getExplodedGeometry();
 +      if (geometry != null)
 +      {
 +        newaf.setBounds(geometry);
 +      }
  
 -        }
 -        removeProgressPanel(progpanel);
 -        if (warnmsg != null)
 -        {
 -          JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 +      ap.av.setGatherViewsHere(false);
  
 -                  warnmsg, warnttl, JvOptionPane.ERROR_MESSAGE);
 -        }
 +      addInternalFrame(newaf, af.getTitle(), AlignFrame.DEFAULT_WIDTH,
 +              AlignFrame.DEFAULT_HEIGHT);
 +      // and materialise a new feature settings dialog instance for the new
 +      // alignframe
 +      // (closes the old as if 'OK' was pressed)
 +      if (ap == af.alignPanel && newaf.featureSettings != null
 +              && newaf.featureSettings.isOpen()
 +              && af.alignPanel.getAlignViewport().isShowSequenceFeatures())
 +      {
 +        newaf.showFeatureSettingsUI();
        }
      }
 -  }
  
 -  JPanel vamUpdate = null;
 +    af.featureSettings = null;
 +    af.alignPanels.clear();
 +    af.closeMenuItem_actionPerformed(true);
 +
 +  }
  
    /**
 -   * hide vamsas user gui bits when a vamsas document event is being handled.
 +   * Gather expanded views (separate AlignFrame's) with the same sequence set
 +   * identifier back in to this frame as additional views, and close the
 +   * expanded views. Note the expanded frames may themselves have multiple
 +   * views. We take the lot.
     * 
 -   * @param b
 -   *          true to hide gui, false to reveal gui
 +   * @param source
     */
 -  public void setVamsasUpdate(boolean b)
 +  public void gatherViews(AlignFrame source)
    {
 -    Cache.log.debug("Setting gui for Vamsas update "
 -            + (b ? "in progress" : "finished"));
 -
 -    if (vamUpdate != null)
 +    source.viewport.setGatherViewsHere(true);
 +    source.viewport.setExplodedGeometry(source.getBounds());
 +    JInternalFrame[] frames = desktop.getAllFrames();
 +    String viewId = source.viewport.getSequenceSetId();
 +    for (int t = 0; t < frames.length; t++)
      {
 -      this.removeProgressPanel(vamUpdate);
 +      if (frames[t] instanceof AlignFrame && frames[t] != source)
 +      {
 +        AlignFrame af = (AlignFrame) frames[t];
 +        boolean gatherThis = false;
 +        for (int a = 0; a < af.alignPanels.size(); a++)
 +        {
 +          AlignmentPanel ap = af.alignPanels.get(a);
 +          if (viewId.equals(ap.av.getSequenceSetId()))
 +          {
 +            gatherThis = true;
 +            ap.av.setGatherViewsHere(false);
 +            ap.av.setExplodedGeometry(af.getBounds());
 +            source.addAlignmentPanel(ap, false);
 +          }
 +        }
 +
 +        if (gatherThis)
 +        {
 +          if (af.featureSettings != null && af.featureSettings.isOpen())
 +          {
 +            if (source.featureSettings == null)
 +            {
 +              // preserve the feature settings geometry for this frame
 +              source.featureSettings = af.featureSettings;
 +              source.setFeatureSettingsGeometry(
 +                      af.getFeatureSettingsGeometry());
 +            }
 +            else
 +            {
 +              // close it and forget
 +              af.featureSettings.close();
 +            }
 +          }
 +          af.alignPanels.clear();
 +          af.closeMenuItem_actionPerformed(true);
 +        }
 +      }
      }
 -    if (b)
 +
 +    // refresh the feature setting UI for the source frame if it exists
 +    if (source.featureSettings != null && source.featureSettings.isOpen())
      {
 -      vamUpdate = this.addProgressPanel(
 -              MessageManager.getString("label.updating_vamsas_session"));
 +      source.showFeatureSettingsUI();
      }
 -    vamsasStart.setVisible(!b);
 -    vamsasStop.setVisible(!b);
 -    vamsasSave.setVisible(!b);
    }
  
    public JInternalFrame[] getAllFrames()
    {
      UserQuestionnaireCheck jvq = new UserQuestionnaireCheck(url);
      // javax.swing.SwingUtilities.invokeLater(jvq);
-     new Thread(jvq, "CheckQuestionnaireThread").start();
+     new Thread(jvq, "CheckQuestionnaire").start();
    }
  
    public void checkURLLinks()
            while (li.hasNext())
            {
              String link = li.next();
 -            if (link.contains(SEQUENCE_ID)
 +            if (link.contains(jalview.util.UrlConstants.SEQUENCE_ID)
                      && !UrlConstants.isDefaultString(link))
              {
                check = true;
     */
    public class MyDesktopPane extends JDesktopPane implements Runnable
    {
 -
      private static final float ONE_MB = 1048576f;
  
      boolean showMemoryUsage = false;
        this.showMemoryUsage = showMemory;
        if (showMemory)
        {
-         Thread worker = new Thread(this, "ShowMemoryUsageThread");
+         Thread worker = new Thread(this, "ShowMemoryUsage");
          worker.start();
        }
        repaint();
                    10, getHeight() - fm.getHeight());
          }
        }
 +
 +      // output debug scale message. Important for jalview.bin.HiDPISettingTest2
 +      Desktop.debugScaleMessage(Desktop.getDesktop().getGraphics());
      }
    }
  
 -
    /**
     * Accessor method to quickly get all the AlignmentFrames loaded.
     * 
        openGroovyConsole();
      } catch (Exception ex)
      {
 -      jalview.bin.Cache.log.error("Groovy Shell Creation failed.", ex);
 +      jalview.bin.Console.error("Groovy Shell Creation failed.", ex);
        JvOptionPane.showInternalMessageDialog(Desktop.desktop,
  
                MessageManager.getString("label.couldnt_create_groovy_shell"),
  
        /*
         * We allow only one console at a time, so that AlignFrame menu option
 -       * 'Calculate | Run Groovy script' is unambiguous.
 -       * Disable 'Groovy Console', and enable 'Run script', when the console is 
 -       * opened, and the reverse when it is closed
 +       * 'Calculate | Run Groovy script' is unambiguous. Disable 'Groovy Console', and
 +       * enable 'Run script', when the console is opened, and the reverse when it is
 +       * closed
         */
        Window window = (Window) groovyConsole.getFrame();
        window.addWindowListener(new WindowAdapter()
      ((Window) groovyConsole.getFrame()).setVisible(true);
  
      /*
 -     * if we got this far, enable 'Run Groovy' in AlignFrame menus
 -     * and disable opening a second console
 +     * if we got this far, enable 'Run Groovy' in AlignFrame menus and disable
 +     * opening a second console
       */
      enableExecuteGroovy(true);
    }
     */
    protected void addQuitHandler()
    {
 -    getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
 -            .put(KeyStroke.getKeyStroke(KeyEvent.VK_Q,
 -                    Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()),
 +    getRootPane()
 +            .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
 +                    KeyStroke
 +                            .getKeyStroke(KeyEvent.VK_Q,
 +                                    jalview.util.ShortcutKeyMaskExWrapper
 +                                            .getMenuShortcutKeyMaskEx()),
                      "Quit");
      getRootPane().getActionMap().put("Quit", new AbstractAction()
      {
    public void enableExecuteGroovy(boolean enabled)
    {
      /*
 -     * disable opening a second Groovy console
 -     * (or re-enable when the console is closed)
 +     * disable opening a second Groovy console (or re-enable when the console is
 +     * closed)
       */
      groovyShell.setEnabled(!enabled);
  
        progressBarHandlers = new Hashtable<>();
      }
  
 -    if (progressBars.get(new Long(id)) != null)
 +    if (progressBars.get(Long.valueOf(id)) != null)
      {
 -      JPanel panel = progressBars.remove(new Long(id));
 -      if (progressBarHandlers.contains(new Long(id)))
 +      JPanel panel = progressBars.remove(Long.valueOf(id));
 +      if (progressBarHandlers.contains(Long.valueOf(id)))
        {
 -        progressBarHandlers.remove(new Long(id));
 +        progressBarHandlers.remove(Long.valueOf(id));
        }
        removeProgressPanel(panel);
      }
      else
      {
 -      progressBars.put(new Long(id), addProgressPanel(message));
 +      progressBars.put(Long.valueOf(id), addProgressPanel(message));
      }
    }
  
            final IProgressIndicatorHandler handler)
    {
      if (progressBarHandlers == null
 -            || !progressBars.containsKey(new Long(id)))
 +            || !progressBars.containsKey(Long.valueOf(id)))
      {
        throw new Error(MessageManager.getString(
                "error.call_setprogressbar_before_registering_handler"));
      }
 -    progressBarHandlers.put(new Long(id), handler);
 -    final JPanel progressPanel = progressBars.get(new Long(id));
 +    progressBarHandlers.put(Long.valueOf(id), handler);
 +    final JPanel progressPanel = progressBars.get(Long.valueOf(id));
      if (handler.canCancel())
      {
        JButton cancel = new JButton(
  
    public VamsasApplication getVamsasApplication()
    {
 -    return v_client;
 +    // TODO: JAL-3311 remove remaining code from Jalview relating to VAMSAS
 +    return null;
  
    }
  
      this.inBatchMode = inBatchMode;
    }
  
 +  /**
 +   * start service discovery and wait till it is done
 +   */
    public void startServiceDiscovery()
    {
      startServiceDiscovery(false);
    }
  
 +  /**
 +   * start service discovery threads - blocking or non-blocking
 +   * 
 +   * @param blocking
 +   */
    public void startServiceDiscovery(boolean blocking)
    {
 +    startServiceDiscovery(blocking, false);
 +  }
 +
 +  /**
 +   * start service discovery threads
 +   * 
 +   * @param blocking
 +   *          - false means call returns immediately
 +   * @param ignore_SHOW_JWS2_SERVICES_preference
 +   *          - when true JABA services are discovered regardless of user's JWS2
 +   *          discovery preference setting
 +   */
 +  public void startServiceDiscovery(boolean blocking,
 +          boolean ignore_SHOW_JWS2_SERVICES_preference)
 +  {
      boolean alive = true;
      Thread t0 = null, t1 = null, t2 = null;
      // JAL-940 - JALVIEW 1 services are now being EOLed as of JABA 2.1 release
        }
        // JAL-940 - disabled JWS1 service configuration - always start discoverer
        // until we phase out completely
-       (t0 = new Thread(discoverer, "DiscovererThread")).start();
+       (t0 = new Thread(discoverer, "Discoverer")).start();
      }
  
 -    if (Cache.getDefault("SHOW_JWS2_SERVICES", true))
 +    if (ignore_SHOW_JWS2_SERVICES_preference
 +            || Cache.getDefault("SHOW_JWS2_SERVICES", true))
      {
        t2 = jalview.ws.jws2.Jws2Discoverer.getDiscoverer()
                .startDiscoverer(changeSupport);
                  /*
                   * JalviewDialog jd =new JalviewDialog() {
                   * 
 -                 * @Override protected void cancelPressed() { // TODO
 -                 * Auto-generated method stub
 +                 * @Override protected void cancelPressed() { // TODO Auto-generated method stub
                   * 
 -                 * }@Override protected void okPressed() { // TODO
 -                 * Auto-generated method stub
 +                 * }@Override protected void okPressed() { // TODO Auto-generated method stub
                   * 
 -                 * }@Override protected void raiseClosed() { // TODO
 -                 * Auto-generated method stub
 +                 * }@Override protected void raiseClosed() { // TODO Auto-generated method stub
                   * 
 -                 * } }; jd.initDialogFrame(new
 -                 * JLabel("<html><table width=\"450\"><tr><td>" + ermsg +
 +                 * } }; jd.initDialogFrame(new JLabel("<html><table width=\"450\"><tr><td>" +
 +                 * ermsg +
                   * "<br/>It may be that you have invalid JABA URLs in your web service preferences,"
                   * + " or mis-configured HTTP proxy settings.<br/>" +
 -                 * "Check the <em>Connections</em> and <em>Web services</em> tab of the"
 -                 * +
 -                 * " Tools->Preferences dialog box to change them.</td></tr></table></html>"
 -                 * ), true, true, "Web Service Configuration Problem", 450,
 -                 * 400);
 +                 * "Check the <em>Connections</em> and <em>Web services</em> tab of the" +
 +                 * " Tools->Preferences dialog box to change them.</td></tr></table></html>" ),
 +                 * true, true, "Web Service Configuration Problem", 450, 400);
                   * 
                   * jd.waitForInput();
                   */
          }
          else
          {
 -          Cache.log.error(
 +          jalview.bin.Console.error(
                    "Errors reported by JABA discovery service. Check web services preferences.\n"
                            + ermsg);
          }
            progress.setProgressBar(null, this.hashCode());
          }
        }
-     }, "OpenURLThread").start();
+     }, "OpenURL").start();
    }
  
    public static WsParamSetManager wsparamManager = null;
        {
          if (url != null)
          {
 -          if (Cache.log != null)
 -          {
 -            Cache.log.error("Couldn't handle string " + url + " as a URL.");
 -          }
 -          else
 -          {
 -            System.err.println(
 -                    "Couldn't handle string " + url + " as a URL.");
 -          }
 +          jalview.bin.Console
 +                  .error("Couldn't handle string " + url + " as a URL.");
          }
          // ignore any exceptions due to dud links.
        }
            } catch (InterruptedException x)
            {
            }
 -          ;
          }
          if (instance == null)
          {
            SwingUtilities.invokeAndWait(prompter);
          } catch (Exception q)
          {
 -          Cache.log.warn("Unexpected Exception in dialog thread.", q);
 +          jalview.bin.Console.warn("Unexpected Exception in dialog thread.",
 +                  q);
          }
        }
      });
      block.release();
    }
  
 +  /**
 +   * Outputs an image of the desktop to file in EPS format, after prompting the
 +   * user for choice of Text or Lineart character rendering (unless a preference
 +   * has been set). The file name is generated as
 +   * 
 +   * <pre>
 +   * Jalview_snapshot_nnnnn.eps where nnnnn is the current timestamp in milliseconds
 +   * </pre>
 +   */
    @Override
    protected void snapShotWindow_actionPerformed(ActionEvent e)
    {
 +    // currently the menu option to do this is not shown
      invalidate();
 -    File of;
 -    ImageMaker im = new jalview.util.ImageMaker(
 -            this, ImageMaker.TYPE.EPS, "View of Desktop", getWidth(),
 -            getHeight(), of = new File("Jalview_snapshot"
 -                    + System.currentTimeMillis() + ".eps"),
 -            "View of desktop", null, 0, false);
 -    try
 -    {
 -      paintAll(im.getGraphics());
 -      im.writeImage();
 -    } catch (Exception q)
 +
 +    int width = getWidth();
 +    int height = getHeight();
 +    File of = new File(
 +            "Jalview_snapshot_" + System.currentTimeMillis() + ".eps");
 +    ImageWriterI writer = new ImageWriterI()
      {
 -      Cache.log.error("Couldn't write snapshot to " + of.getAbsolutePath(),
 -              q);
 -      return;
 -    }
 -    Cache.log.info("Successfully written snapshot to file "
 -            + of.getAbsolutePath());
 +      @Override
 +      public void exportImage(Graphics g) throws Exception
 +      {
 +        paintAll(g);
 +        jalview.bin.Console.info("Successfully written snapshot to file "
 +                + of.getAbsolutePath());
 +      }
 +    };
 +    String title = "View of desktop";
 +    ImageExporter exporter = new ImageExporter(writer, null, TYPE.EPS,
 +            title);
 +    exporter.doExport(of, this, width, height, title);
    }
  
    /**
      }
  
      /*
 -     * Processing in reverse order works, forwards order leaves the first panels
 -     * not visible. I don't know why!
 +     * Processing in reverse order works, forwards order leaves the first panels not
 +     * visible. I don't know why!
       */
      for (int i = viewCount - 1; i >= 0; i--)
      {
        /*
 -       * Make new top and bottom frames. These take over the respective
 -       * AlignmentPanel objects, including their AlignmentViewports, so the
 -       * cdna/protein relationships between the viewports is carried over to the
 -       * new split frames.
 +       * Make new top and bottom frames. These take over the respective AlignmentPanel
 +       * objects, including their AlignmentViewports, so the cdna/protein
 +       * relationships between the viewports is carried over to the new split frames.
         * 
         * explodedGeometry holds the (x, y) position of the previously exploded
         * SplitFrame, and the (width, height) of the AlignFrame component
      }
  
      /*
 -     * Clear references to the panels (now relocated in the new SplitFrames)
 -     * before closing the old SplitFrame.
 +     * Clear references to the panels (now relocated in the new SplitFrames) before
 +     * closing the old SplitFrame.
       */
      topPanels.clear();
      bottomPanels.clear();
      return groovyConsole;
    }
  
 -  public static void transferFromDropTarget(List<String> files,
 +  /**
 +   * handles the payload of a drag and drop event.
 +   * 
 +   * TODO refactor to desktop utilities class
 +   * 
 +   * @param files
 +   *          - Data source strings extracted from the drop event
 +   * @param protocols
 +   *          - protocol for each data source extracted from the drop event
 +   * @param evt
 +   *          - the drop event
 +   * @param t
 +   *          - the payload from the drop event
 +   * @throws Exception
 +   */
 +  public static void transferFromDropTarget(List<Object> files,
            List<DataSourceType> protocols, DropTargetDropEvent evt,
            Transferable t) throws Exception
    {
  
      DataFlavor uriListFlavor = new DataFlavor(
 -            "text/uri-list;class=java.lang.String");
 +            "text/uri-list;class=java.lang.String"), urlFlavour = null;
 +    try
 +    {
 +      urlFlavour = new DataFlavor(
 +              "application/x-java-url; class=java.net.URL");
 +    } catch (ClassNotFoundException cfe)
 +    {
 +      jalview.bin.Console.debug("Couldn't instantiate the URL dataflavor.",
 +              cfe);
 +    }
 +
 +    if (urlFlavour != null && t.isDataFlavorSupported(urlFlavour))
 +    {
 +
 +      try
 +      {
 +        java.net.URL url = (URL) t.getTransferData(urlFlavour);
 +        // nb: java 8 osx bug https://bugs.openjdk.java.net/browse/JDK-8156099
 +        // means url may be null.
 +        if (url != null)
 +        {
 +          protocols.add(DataSourceType.URL);
 +          files.add(url.toString());
 +          jalview.bin.Console.debug("Drop handled as URL dataflavor "
 +                  + files.get(files.size() - 1));
 +          return;
 +        }
 +        else
 +        {
 +          if (Platform.isAMacAndNotJS())
 +          {
 +            System.err.println(
 +                    "Please ignore plist error - occurs due to problem with java 8 on OSX");
 +          }
 +        }
 +      } catch (Throwable ex)
 +      {
 +        jalview.bin.Console.debug("URL drop handler failed.", ex);
 +      }
 +    }
      if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor))
      {
        // Works on Windows and MacOSX
 -      Cache.log.debug("Drop handled as javaFileListFlavor");
 +      jalview.bin.Console.debug("Drop handled as javaFileListFlavor");
        for (Object file : (List) t
                .getTransferData(DataFlavor.javaFileListFlavor))
        {
 -        files.add(((File) file).toString());
 +        files.add(file);
          protocols.add(DataSourceType.FILE);
        }
      }
        String data = null;
        if (t.isDataFlavorSupported(uriListFlavor))
        {
 -        Cache.log.debug("Drop handled as uriListFlavor");
 +        jalview.bin.Console.debug("Drop handled as uriListFlavor");
          // This is used by Unix drag system
          data = (String) t.getTransferData(uriListFlavor);
        }
        if (data == null)
        {
          // fallback to text: workaround - on OSX where there's a JVM bug
 -        Cache.log.debug("standard URIListFlavor failed. Trying text");
 +        jalview.bin.Console
 +                .debug("standard URIListFlavor failed. Trying text");
          // try text fallback
 -        data = (String) t.getTransferData(
 -                new DataFlavor("text/plain;class=java.lang.String"));
 -        if (Cache.log.isDebugEnabled())
 +        DataFlavor textDf = new DataFlavor(
 +                "text/plain;class=java.lang.String");
 +        if (t.isDataFlavorSupported(textDf))
          {
 -          Cache.log.debug("fallback returned " + data);
 +          data = (String) t.getTransferData(textDf);
          }
 +
 +        jalview.bin.Console.debug("Plain text drop content returned "
 +                + (data == null ? "Null - failed" : data));
 +
        }
 -      while (protocols.size() < files.size())
 -      {
 -        Cache.log.debug("Adding missing FILE protocol for "
 -                + files.get(protocols.size()));
 -        protocols.add(DataSourceType.FILE);
 -      }
 -      for (java.util.StringTokenizer st = new java.util.StringTokenizer(
 -              data, "\r\n"); st.hasMoreTokens();)
 +      if (data != null)
        {
 -        added = true;
 -        String s = st.nextToken();
 -        if (s.startsWith("#"))
 -        {
 -          // the line is a comment (as per the RFC 2483)
 -          continue;
 -        }
 -        java.net.URI uri = new java.net.URI(s);
 -        if (uri.getScheme().toLowerCase().startsWith("http"))
 +        while (protocols.size() < files.size())
          {
 -          protocols.add(DataSourceType.URL);
 -          files.add(uri.toString());
 +          jalview.bin.Console.debug("Adding missing FILE protocol for "
 +                  + files.get(protocols.size()));
 +          protocols.add(DataSourceType.FILE);
          }
 -        else
 +        for (java.util.StringTokenizer st = new java.util.StringTokenizer(
 +                data, "\r\n"); st.hasMoreTokens();)
          {
 -          // otherwise preserve old behaviour: catch all for file objects
 -          java.io.File file = new java.io.File(uri);
 -          protocols.add(DataSourceType.FILE);
 -          files.add(file.toString());
 +          added = true;
 +          String s = st.nextToken();
 +          if (s.startsWith("#"))
 +          {
 +            // the line is a comment (as per the RFC 2483)
 +            continue;
 +          }
 +          java.net.URI uri = new java.net.URI(s);
 +          if (uri.getScheme().toLowerCase(Locale.ROOT).startsWith("http"))
 +          {
 +            protocols.add(DataSourceType.URL);
 +            files.add(uri.toString());
 +          }
 +          else
 +          {
 +            // otherwise preserve old behaviour: catch all for file objects
 +            java.io.File file = new java.io.File(uri);
 +            protocols.add(DataSourceType.FILE);
 +            files.add(file.toString());
 +          }
          }
        }
 -      if (Cache.log.isDebugEnabled())
 +
 +      if (jalview.bin.Console.isDebugEnabled())
        {
          if (data == null || !added)
          {
 -          Cache.log.debug(
 -                  "Couldn't resolve drop data. Here are the supported flavors:");
 -          for (DataFlavor fl : t.getTransferDataFlavors())
 +
 +          if (t.getTransferDataFlavors() != null
 +                  && t.getTransferDataFlavors().length > 0)
            {
 -            Cache.log.debug(
 -                    "Supported transfer dataflavor: " + fl.toString());
 -            Object df = t.getTransferData(fl);
 -            if (df != null)
 -            {
 -              Cache.log.debug("Retrieves: " + df);
 -            }
 -            else
 +            jalview.bin.Console.debug(
 +                    "Couldn't resolve drop data. Here are the supported flavors:");
 +            for (DataFlavor fl : t.getTransferDataFlavors())
              {
 -              Cache.log.debug("Retrieved nothing");
 +              jalview.bin.Console.debug(
 +                      "Supported transfer dataflavor: " + fl.toString());
 +              Object df = t.getTransferData(fl);
 +              if (df != null)
 +              {
 +                jalview.bin.Console.debug("Retrieves: " + df);
 +              }
 +              else
 +              {
 +                jalview.bin.Console.debug("Retrieved nothing");
 +              }
              }
            }
 +          else
 +          {
 +            jalview.bin.Console
 +                    .debug("Couldn't resolve dataflavor for drop: "
 +                            + t.toString());
 +          }
 +        }
 +      }
 +    }
 +    if (Platform.isWindowsAndNotJS())
 +    {
 +      jalview.bin.Console
 +              .debug("Scanning dropped content for Windows Link Files");
 +
 +      // resolve any .lnk files in the file drop
 +      for (int f = 0; f < files.size(); f++)
 +      {
 +        String source = files.get(f).toString().toLowerCase(Locale.ROOT);
 +        if (protocols.get(f).equals(DataSourceType.FILE)
 +                && (source.endsWith(".lnk") || source.endsWith(".url")
 +                        || source.endsWith(".site")))
 +        {
 +          try
 +          {
 +            Object obj = files.get(f);
 +            File lf = (obj instanceof File ? (File) obj
 +                    : new File((String) obj));
 +            // process link file to get a URL
 +            jalview.bin.Console.debug("Found potential link file: " + lf);
 +            WindowsShortcut wscfile = new WindowsShortcut(lf);
 +            String fullname = wscfile.getRealFilename();
 +            protocols.set(f, FormatAdapter.checkProtocol(fullname));
 +            files.set(f, fullname);
 +            jalview.bin.Console.debug("Parsed real filename " + fullname
 +                    + " to extract protocol: " + protocols.get(f));
 +          } catch (Exception ex)
 +          {
 +            jalview.bin.Console.error(
 +                    "Couldn't parse " + files.get(f) + " as a link file.",
 +                    ex);
 +          }
          }
        }
      }
    {
      Cache.setProperty(EXPERIMENTAL_FEATURES, Boolean.toString(selected));
    }
 +
 +  /**
 +   * Answers a (possibly empty) list of any structure viewer frames (currently
 +   * for either Jmol or Chimera) which are currently open. This may optionally
 +   * be restricted to viewers of a specified class, or viewers linked to a
 +   * specified alignment panel.
 +   * 
 +   * @param apanel
 +   *          if not null, only return viewers linked to this panel
 +   * @param structureViewerClass
 +   *          if not null, only return viewers of this class
 +   * @return
 +   */
 +  public List<StructureViewerBase> getStructureViewers(
 +          AlignmentPanel apanel,
 +          Class<? extends StructureViewerBase> structureViewerClass)
 +  {
 +    List<StructureViewerBase> result = new ArrayList<>();
 +    JInternalFrame[] frames = Desktop.instance.getAllFrames();
 +
 +    for (JInternalFrame frame : frames)
 +    {
 +      if (frame instanceof StructureViewerBase)
 +      {
 +        if (structureViewerClass == null
 +                || structureViewerClass.isInstance(frame))
 +        {
 +          if (apanel == null
 +                  || ((StructureViewerBase) frame).isLinkedWith(apanel))
 +          {
 +            result.add((StructureViewerBase) frame);
 +          }
 +        }
 +      }
 +    }
 +    return result;
 +  }
 +
 +  public static final String debugScaleMessage = "Desktop graphics transform scale=";
 +
 +  private static boolean debugScaleMessageDone = false;
 +
 +  public static void debugScaleMessage(Graphics g)
 +  {
 +    if (debugScaleMessageDone)
 +    {
 +      return;
 +    }
 +    // output used by tests to check HiDPI scaling settings in action
 +    try
 +    {
 +      Graphics2D gg = (Graphics2D) g;
 +      if (gg != null)
 +      {
 +        AffineTransform t = gg.getTransform();
 +        double scaleX = t.getScaleX();
 +        double scaleY = t.getScaleY();
 +        jalview.bin.Console.debug(debugScaleMessage + scaleX + " (X)");
 +        jalview.bin.Console.debug(debugScaleMessage + scaleY + " (Y)");
 +        debugScaleMessageDone = true;
 +      }
 +      else
 +      {
 +        jalview.bin.Console.debug("Desktop graphics null");
 +      }
 +    } catch (Exception e)
 +    {
 +      jalview.bin.Console.debug(Cache.getStackTraceString(e));
 +    }
 +  }
  }
@@@ -27,8 -27,8 +27,8 @@@ import java.awt.Dimension
  import java.awt.Rectangle;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
 +import java.awt.event.WindowAdapter;
  import java.awt.event.WindowEvent;
 -import java.awt.event.WindowListener;
  
  import javax.swing.JButton;
  import javax.swing.JDialog;
@@@ -66,7 -66,7 +66,7 @@@ public abstract class JalviewDialog ext
            frame.setVisible(true);
          }
  
-       }, "UnblockedDialogThread").start();
+       }, "UnblockedDialog").start();
      }
      else
      {
          closeDialog();
        }
      });
 -    frame.addWindowListener(new WindowListener()
 +    frame.addWindowListener(new WindowAdapter()
      {
 -
 -      @Override
 -      public void windowOpened(WindowEvent e)
 -      {
 -        // TODO Auto-generated method stub
 -
 -      }
 -
 -      @Override
 -      public void windowIconified(WindowEvent e)
 -      {
 -        // TODO Auto-generated method stub
 -
 -      }
 -
 -      @Override
 -      public void windowDeiconified(WindowEvent e)
 -      {
 -        // TODO Auto-generated method stub
 -
 -      }
 -
 -      @Override
 -      public void windowDeactivated(WindowEvent e)
 -      {
 -        // TODO Auto-generated method stub
 -
 -      }
 -
        @Override
        public void windowClosing(WindowEvent e)
        {
          // user has cancelled the dialog
          closeDialog();
        }
 -
 -      @Override
 -      public void windowClosed(WindowEvent e)
 -      {
 -      }
 -
 -      @Override
 -      public void windowActivated(WindowEvent e)
 -      {
 -        // TODO Auto-generated method stub
 -
 -      }
      });
    }
  
    {
      try
      {
 -      frame.dispose();
        raiseClosed();
 +      frame.dispose();
      } catch (Exception ex)
      {
      }
@@@ -55,25 -55,24 +55,25 @@@ import javax.swing.SwingUtilities
   * @author $author$
   * @version $Revision$
   */
 +@SuppressWarnings("serial")
  public class OverviewPanel extends JPanel
          implements Runnable, ViewportListenerI
  {
 -  private OverviewDimensions od;
 +  protected OverviewDimensions od;
  
    private OverviewCanvas oviewCanvas;
  
 -  private AlignViewport av;
 +  protected AlignViewport av;
  
    private AlignmentPanel ap;
  
 -  private JCheckBoxMenuItem displayToggle;
 +  protected JCheckBoxMenuItem displayToggle;
  
 -  private boolean showHidden = true;
 +  protected boolean showHidden = true;
  
 -  private boolean draggingBox = false;
 +  protected boolean draggingBox = false;
  
 -  private ProgressPanel progressPanel;
 +  protected ProgressPanel progressPanel;
  
    /**
     * Creates a new OverviewPanel object.
      this.ap = alPanel;
  
      showHidden = Cache.getDefault(Preferences.SHOW_OV_HIDDEN_AT_START,
 -            true);
 +            false);
      if (showHidden)
      {
        od = new OverviewDimensionsShowHidden(av.getRanges(),
 -            (av.isShowAnnotation()
 -                    && av.getAlignmentConservationAnnotation() != null));
 +              (av.isShowAnnotation()
 +                      && av.getAlignmentConservationAnnotation() != null));
      }
      else
      {
        {
          if (od.isPositionInBox(evt.getX(), evt.getY()))
          {
 -          // display drag cursor at mouse position
 -          setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
 +          /*
 +           * using HAND_CURSOR rather than DRAG_CURSOR 
 +           * as the latter is not supported on Mac
 +           */
 +          getParent().setCursor(
 +                  Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
          }
          else
          {
            // reset cursor
 -          setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
 +          getParent().setCursor(
 +                  Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
          }
        }
 +
      });
  
      addMouseListener(new MouseAdapter()
        @Override
        public void mousePressed(MouseEvent evt)
        {
 +
 +        if (Platform.isWinRightButton(evt))
 +        {
 +          showPopupMenu(evt);
 +          return;
 +        }
          if (SwingUtilities.isRightMouseButton(evt))
          {
 -          if (!Platform.isAMac())
 -          {
 -            showPopupMenu(evt);
 -          }
 +          return;
 +        }
 +        // don't do anything if the mouse press is in the overview's box
 +        // (wait to see if it's a drag instead)
 +        // otherwise update the viewport
 +        if (!od.isPositionInBox(evt.getX(), evt.getY()))
 +        {
 +          draggingBox = false;
 +
 +          // display drag cursor at mouse position
 +          setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
 +
 +          od.updateViewportFromMouse(evt.getX(), evt.getY(),
 +                  av.getAlignment().getHiddenSequences(),
 +                  av.getAlignment().getHiddenColumns());
 +          getParent().setCursor(
 +                  Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
          }
          else
          {
 -          // don't do anything if the mouse press is in the overview's box
 -          // (wait to see if it's a drag instead)
 -          // otherwise update the viewport
 -          if (!od.isPositionInBox(evt.getX(), evt.getY()))
 -          {
 -            draggingBox = false;
 -            od.updateViewportFromMouse(evt.getX(), evt.getY(),
 -                    av.getAlignment().getHiddenSequences(),
 -                    av.getAlignment().getHiddenColumns());
 -          }
 -          else
 -          {
 -            draggingBox = true;
 -            od.setDragPoint(evt.getX(), evt.getY(),
 -                    av.getAlignment().getHiddenSequences(),
 -                    av.getAlignment().getHiddenColumns());
 -          }
 +          draggingBox = true;
 +          od.setDragPoint(evt.getX(), evt.getY(),
 +                  av.getAlignment().getHiddenSequences(),
 +                  av.getAlignment().getHiddenColumns());
          }
        }
  
            showPopupMenu(evt);
          }
        }
 +
 +      @Override
 +      public void mouseReleased(MouseEvent evt)
 +      {
 +        draggingBox = false;
 +      }
 +
      });
 +
 +    /*
 +     * Javascript does not call componentResized on initial display,
 +     * so do the update here
 +     */
 +    if (Platform.isJS())
 +    {
 +      updateOverviewImage();
 +    }
    }
  
    /*
     * Displays the popup menu and acts on user input
     */
 -  private void showPopupMenu(MouseEvent e)
 +  protected void showPopupMenu(MouseEvent e)
    {
      JPopupMenu popup = new JPopupMenu();
      ActionListener menuListener = new ActionListener()
    /*
     * Toggle overview display between showing hidden columns and hiding hidden columns
     */
 -  private void toggleHiddenColumns()
 +  protected void toggleHiddenColumns()
    {
      if (showHidden)
      {
        od.setWidth(getWidth());
        od.setHeight(getHeight() - progressPanel.getHeight());
      }
 -    
 +
      setPreferredSize(new Dimension(od.getWidth(),
              od.getHeight() + progressPanel.getHeight()));
  
        return;
      }
  
-     Thread thread = new Thread(this, "UpdateOverviewThread");
+     Thread thread = new Thread(this, "UpdateOverview");
      thread.start();
      repaint();
  
 -    
    }
  
    @Override
         * close the parent frame (which also removes it from the
         * Desktop Windows menu)
         */
 -      ((JInternalFrame) SwingUtilities.getAncestorOfClass(
 -              JInternalFrame.class, (this))).setClosed(true);
 +      ((JInternalFrame) SwingUtilities
 +              .getAncestorOfClass(JInternalFrame.class, (this)))
 +                      .setClosed(true);
      } catch (PropertyVetoException e)
      {
        // ignore
@@@ -96,7 -96,7 +96,7 @@@ public class RedundancyPanel extends GS
      slider.setMaximum(100);
      slider.setValue(100);
  
-     Thread worker = new Thread(this, "CreateRedundancyPanelThread");
+     Thread worker = new Thread(this, "CreateRedundancyPanel");
      worker.start();
  
      frame = new JInternalFrame();
      Desktop.addInternalFrame(frame,
              MessageManager
                      .getString("label.redundancy_threshold_selection"),
 -            400, 100, false);
 +            true, FRAME_WIDTH, FRAME_HEIGHT, false, true);
      frame.addInternalFrameListener(new InternalFrameAdapter()
      {
        @Override
    @Override
    public void applyButton_actionPerformed(ActionEvent e)
    {
 -    Vector del = new Vector();
 +    List<SequenceI> del = new ArrayList<>();
  
      undoButton.setEnabled(true);
  
      {
        if (value <= redundancy[i])
        {
 -        del.addElement(originalSequences[i]);
 +        del.add(originalSequences[i]);
        }
      }
  
      // This has to be done before the restoreHistoryItem method of alignFrame
 -    // will
 -    // actually restore these sequences.
 +    // will actually restore these sequences.
      if (del.size() > 0)
      {
        SequenceI[] deleted = new SequenceI[del.size()];
        int width = 0;
        for (int i = 0; i < del.size(); i++)
        {
 -        deleted[i] = (SequenceI) del.elementAt(i);
 +        deleted[i] = del.get(i);
          if (deleted[i].getLength() > width)
          {
            width = deleted[i].getLength();
   */
  package jalview.gui;
  
 -import jalview.bin.Cache;
 -import jalview.datamodel.Alignment;
 -import jalview.datamodel.AlignmentI;
 -import jalview.datamodel.HiddenColumns;
 -import jalview.datamodel.PDBEntry;
 -import jalview.datamodel.SequenceI;
 -import jalview.gui.StructureViewer.ViewerType;
 -import jalview.gui.ViewSelectionMenu.ViewSetProvider;
 -import jalview.io.DataSourceType;
 -import jalview.io.JalviewFileChooser;
 -import jalview.io.JalviewFileView;
 -import jalview.jbgui.GStructureViewer;
 -import jalview.schemes.ColourSchemeI;
 -import jalview.schemes.ColourSchemes;
 -import jalview.structures.models.AAStructureBindingModel;
 -import jalview.util.MessageManager;
 -
  import java.awt.Color;
  import java.awt.Component;
  import java.awt.event.ActionEvent;
@@@ -34,40 -51,17 +34,40 @@@ import java.io.IOException
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.List;
 +import java.util.Random;
  import java.util.Vector;
  
  import javax.swing.ButtonGroup;
  import javax.swing.JCheckBoxMenuItem;
 -import javax.swing.JColorChooser;
  import javax.swing.JMenu;
  import javax.swing.JMenuItem;
  import javax.swing.JRadioButtonMenuItem;
  import javax.swing.event.MenuEvent;
  import javax.swing.event.MenuListener;
  
 +import jalview.api.AlignmentViewPanel;
 +import jalview.bin.Cache;
 +import jalview.bin.Console;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.PDBEntry;
 +import jalview.datamodel.SequenceI;
 +import jalview.gui.JalviewColourChooser.ColourChooserListener;
 +import jalview.gui.StructureViewer.ViewerType;
 +import jalview.gui.ViewSelectionMenu.ViewSetProvider;
 +import jalview.io.DataSourceType;
 +import jalview.io.JalviewFileChooser;
 +import jalview.io.JalviewFileView;
 +import jalview.jbgui.GStructureViewer;
 +import jalview.schemes.ColourSchemeI;
 +import jalview.schemes.ColourSchemes;
 +import jalview.structure.StructureMapping;
 +import jalview.structures.models.AAStructureBindingModel;
 +import jalview.util.BrowserLauncher;
 +import jalview.util.MessageManager;
 +import jalview.ws.dbsources.EBIAlfaFold;
 +import jalview.ws.dbsources.Pdb;
 +import jalview.ws.utils.UrlDownloadClient;
 +
  /**
   * Base class with common functionality for JMol, Chimera or other structure
   * viewers.
@@@ -94,13 -88,13 +94,13 @@@ public abstract class StructureViewerBa
    /**
     * list of alignment panels to use for superposition
     */
 -  protected Vector<AlignmentPanel> _alignwith = new Vector<>();
 +  protected Vector<AlignmentViewPanel> _alignwith = new Vector<>();
  
    /**
     * list of alignment panels that are used for colouring structures by aligned
     * sequences
     */
 -  protected Vector<AlignmentPanel> _colourwith = new Vector<>();
 +  protected Vector<AlignmentViewPanel> _colourwith = new Vector<>();
  
    private String viewId = null;
  
  
    protected boolean alignAddedStructures = false;
  
 -  protected boolean _started = false;
 +  protected volatile boolean _started = false;
  
 -  protected boolean addingStructures = false;
 +  protected volatile boolean addingStructures = false;
  
    protected Thread worker = null;
  
    protected JMenu viewSelectionMenu;
  
    /**
 +   * set after sequence colouring has been applied for this structure viewer.
 +   * used to determine if the final sequence/structure mapping has been
 +   * determined
 +   */
 +  protected volatile boolean seqColoursApplied = false;
 +
 +  private IProgressIndicator progressBar = null;
 +
 +  private Random random = new Random();
 +
 +  /**
     * Default constructor
     */
    public StructureViewerBase()
    }
  
    /**
 +   * @return true if added structures should be aligned to existing one(s)
 +   */
 +  @Override
 +  public boolean isAlignAddedStructures()
 +  {
 +    return alignAddedStructures;
 +  }
 +
 +  /**
 +   * 
 +   * @param true
 +   *          if added structures should be aligned to existing one(s)
 +   */
 +  @Override
 +  public void setAlignAddedStructures(boolean alignAdded)
 +  {
 +    alignAddedStructures = alignAdded;
 +  }
 +
 +  /**
 +   * called by the binding model to indicate when adding structures is happening
 +   * or has been completed
 +   * 
 +   * @param addingStructures
 +   */
 +  public synchronized void setAddingStructures(boolean addingStructures)
 +  {
 +    this.addingStructures = addingStructures;
 +  }
 +
 +  /**
     * 
     * @param ap2
     * @return true if this Jmol instance is linked with the given alignPanel
      return _aps.contains(ap2.av.getSequenceSetId());
    }
  
 -  public boolean isUsedforaligment(AlignmentPanel ap2)
 +  public boolean isUsedforaligment(AlignmentViewPanel ap2)
    {
  
      return (_alignwith != null) && _alignwith.contains(ap2);
    }
  
 -  public boolean isUsedforcolourby(AlignmentPanel ap2)
 +  @Override
 +  public boolean isUsedForColourBy(AlignmentViewPanel ap2)
    {
      return (_colourwith != null) && _colourwith.contains(ap2);
    }
      this.viewId = viewId;
    }
  
 -  public abstract String getStateInfo();
 -
    protected void buildActionMenu()
    {
      if (_alignwith == null)
        _alignwith.add(ap);
      }
      ;
 +    // TODO: refactor to allow concrete classes to register buttons for adding
 +    // here
 +    // currently have to override to add buttons back in after they are cleared
 +    // in this loop
      for (Component c : viewerActionMenu.getMenuComponents())
      {
        if (c != alignStructs)
      }
    }
  
 +  @Override
    public AlignmentPanel getAlignmentPanel()
    {
      return ap;
     * 
     * @param nap
     */
 -  public void removeAlignmentPanel(AlignmentPanel nap)
 +  @Override
 +  public void removeAlignmentPanel(AlignmentViewPanel nap)
    {
      try
      {
  
    public abstract ViewerType getViewerType();
  
 -  protected abstract IProgressIndicator getIProgressIndicator();
 -
    /**
     * add a new structure (with associated sequences and chains) to this viewer,
     * retrieving it if necessary first.
     */
    protected void addStructure(final PDBEntry pdbentry,
            final SequenceI[] seqs, final String[] chains,
 -          final boolean align, final IProgressIndicator alignFrame)
 +          final IProgressIndicator alignFrame)
    {
      if (pdbentry.getFile() == null)
      {
                }
              }
              // and call ourselves again.
 -            addStructure(pdbentry, seqs, chains, align, alignFrame);
 +            addStructure(pdbentry, seqs, chains, alignFrame);
            }
-         }, "Adding3DStructureQueueThread").start();
+         }, "Adding3DStructureQueue").start();
          return;
        }
      }
              { seqs }, new String[][] { chains });
      addingStructures = true;
      _started = false;
-     worker = new Thread(this, "Adding3DStructureThread");
 -    alignAddedStructures = align;
+     worker = new Thread(this, "Adding3DStructure");
      worker.start();
      return;
    }
  
 -  /**
 -   * Presents a dialog with the option to add an align a structure to an
 -   * existing structure view
 -   * 
 -   * @param pdbId
 -   * @param view
 -   * @return YES, NO or CANCEL JvOptionPane code
 -   */
 -  protected int chooseAlignStructureToViewer(String pdbId,
 -          StructureViewerBase view)
 -  {
 -    int option = JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
 -            MessageManager.formatMessage("label.add_pdbentry_to_view",
 -                    new Object[]
 -                    { pdbId, view.getTitle() }),
 -            MessageManager
 -                    .getString("label.align_to_existing_structure_view"),
 -            JvOptionPane.YES_NO_CANCEL_OPTION);
 -    return option;
 -  }
 -
    protected boolean hasPdbId(String pdbId)
    {
      return getBinding().hasPdbId(pdbId);
    }
  
 -  protected abstract List<StructureViewerBase> getViewersFor(
 -          AlignmentPanel alp);
 -
    /**
 -   * Check for any existing views involving this alignment and give user the
 -   * option to add and align this molecule to one of them
 -   * 
 -   * @param pdbentry
 -   * @param seq
 -   * @param chains
 -   * @param apanel
 -   * @param pdbId
 -   * @return true if user adds to a view, or cancels entirely, else false
 +   * Returns a list of any viewer of the instantiated type. The list is
 +   * restricted to those linked to the given alignment panel if it is not null.
     */
 -  protected boolean addToExistingViewer(PDBEntry pdbentry, SequenceI[] seq,
 -          String[] chains, final AlignmentPanel apanel, String pdbId)
 +  protected List<StructureViewerBase> getViewersFor(AlignmentPanel alp)
    {
 -    for (StructureViewerBase view : getViewersFor(apanel))
 -    {
 -      // TODO: highlight the view somehow
 -      /*
 -       * JAL-1742 exclude view with this structure already mapped (don't offer
 -       * to align chain B to chain A of the same structure)
 -       */
 -      if (view.hasPdbId(pdbId))
 -      {
 -        continue;
 -      }
 -      int option = chooseAlignStructureToViewer(pdbId, view);
 -      if (option == JvOptionPane.CANCEL_OPTION)
 -      {
 -        return true;
 -      }
 -      else if (option == JvOptionPane.YES_OPTION)
 -      {
 -        view.useAlignmentPanelForSuperposition(apanel);
 -        view.addStructure(pdbentry, seq, chains, true, apanel.alignFrame);
 -        return true;
 -      }
 -      else
 -      {
 -        // NO_OPTION - offer the next viewer if any
 -      }
 -    }
 +    return Desktop.instance.getStructureViewers(alp, this.getClass());
 +  }
  
 +  @Override
 +  public void addToExistingViewer(PDBEntry pdbentry, SequenceI[] seq,
 +          String[] chains, final AlignmentViewPanel apanel, String pdbId)
 +  {
      /*
 -     * nothing offered and selected
 +     * JAL-1742 exclude view with this structure already mapped (don't offer
 +     * to align chain B to chain A of the same structure); code may defend
 +     * against this possibility before we reach here
       */
 -    return false;
 +    if (hasPdbId(pdbId))
 +    {
 +      return;
 +    }
 +    AlignmentPanel alignPanel = (AlignmentPanel) apanel; // Implementation error
 +                                                         // if this
 +    // cast fails
 +    useAlignmentPanelForSuperposition(alignPanel);
 +    addStructure(pdbentry, seq, chains, alignPanel.alignFrame);
    }
  
    /**
     * @param apanel
     * @param pdbFilename
     */
 -  protected void addSequenceMappingsToStructure(SequenceI[] seq,
 -          String[] chains, final AlignmentPanel apanel, String pdbFilename)
 +  public void addSequenceMappingsToStructure(SequenceI[] seq,
 +          String[] chains, final AlignmentViewPanel alpanel,
 +          String pdbFilename)
    {
 +    AlignmentPanel apanel = (AlignmentPanel) alpanel;
 +
      // TODO : Fix multiple seq to one chain issue here.
      /*
       * create the mappings
       */
      apanel.getStructureSelectionManager().setMapping(seq, chains,
 -            pdbFilename, DataSourceType.FILE, getIProgressIndicator());
 +            pdbFilename, DataSourceType.FILE, getProgressIndicator());
  
      /*
       * alert the FeatureRenderer to show new (PDB RESNUM) features
      }
    }
  
 -  /**
 -   * Check if the PDB file is already loaded, if so offer to add it to the
 -   * existing viewer
 -   * 
 -   * @param seq
 -   * @param chains
 -   * @param apanel
 -   * @param pdbId
 -   * @return true if the user chooses to add to a viewer, or to cancel entirely
 -   */
 -  protected boolean addAlreadyLoadedFile(SequenceI[] seq, String[] chains,
 -          final AlignmentPanel apanel, String pdbId)
 +  @Override
 +  public boolean addAlreadyLoadedFile(SequenceI[] seq, String[] chains,
 +          final AlignmentViewPanel apanel, String pdbId)
    {
      String alreadyMapped = apanel.getStructureSelectionManager()
              .alreadyMappedToFile(pdbId);
  
 -    if (alreadyMapped != null)
 -    {
 -      /*
 -       * the PDB file is already loaded
 -       */
 -      int option = JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
 -              MessageManager.formatMessage(
 -                      "label.pdb_entry_is_already_displayed", new Object[]
 -                      { pdbId }),
 -              MessageManager.formatMessage(
 -                      "label.map_sequences_to_visible_window", new Object[]
 -                      { pdbId }),
 -              JvOptionPane.YES_NO_CANCEL_OPTION);
 -      if (option == JvOptionPane.CANCEL_OPTION)
 -      {
 -        finished = true;
 -      }
 -      else if (option == JvOptionPane.YES_OPTION)
 -      {
 -        addSequenceMappingsToStructure(seq, chains, apanel, alreadyMapped);
 -        finished = true;
 -      }
 +    if (alreadyMapped == null)
 +    {
 +      return false;
      }
 -    return finished;
 +
 +    addSequenceMappingsToStructure(seq, chains, apanel, alreadyMapped);
 +    return true;
    }
  
    void setChainMenuItems(List<String> chainNames)
      }
    }
  
 -  abstract void showSelectedChains();
 -
    /**
     * Action on selecting one of Jalview's registered colour schemes
     */
    public void changeColour_actionPerformed(String colourSchemeName)
    {
      AlignmentI al = getAlignmentPanel().av.getAlignment();
 -    ColourSchemeI cs = ColourSchemes.getInstance()
 -            .getColourScheme(colourSchemeName, al, null);
 -    getBinding().setJalviewColourScheme(cs);
 +    ColourSchemeI cs = ColourSchemes.getInstance().getColourScheme(
 +            colourSchemeName, getAlignmentPanel().av, al, null);
 +    getBinding().colourByJalviewColourScheme(cs);
    }
  
    /**
        @Override
        public void actionPerformed(ActionEvent actionEvent)
        {
 -        viewerColour_actionPerformed(actionEvent);
 +        viewerColour_actionPerformed();
        }
      });
      colourMenu.add(viewerColour);
        @Override
        public void actionPerformed(ActionEvent actionEvent)
        {
 -        background_actionPerformed(actionEvent);
 +        background_actionPerformed();
        }
      });
      colourMenu.add(backGround);
        @Override
        public void actionPerformed(ActionEvent actionEvent)
        {
 -        seqColour_actionPerformed(actionEvent);
 +        seqColour_actionPerformed();
        }
      });
  
        @Override
        public void actionPerformed(ActionEvent actionEvent)
        {
 -        chainColour_actionPerformed(actionEvent);
 +        chainColour_actionPerformed();
        }
      });
  
        @Override
        public void actionPerformed(ActionEvent actionEvent)
        {
 -        chargeColour_actionPerformed(actionEvent);
 +        chargeColour_actionPerformed();
        }
      });
  
      viewerColour = new JRadioButtonMenuItem();
 -    // text is set in overrides of this method
 +    viewerColour
 +            .setText(MessageManager.getString("label.colour_with_viewer"));
 +    viewerColour.setToolTipText(MessageManager
 +            .getString("label.let_viewer_manage_structure_colours"));
      viewerColour.setName(ViewerColour.ByViewer.name());
      viewerColour.setSelected(!binding.isColourBySequence());
  
                  }
                  else
                  {
 -                  // update the Chimera display now.
 -                  seqColour_actionPerformed(null);
 +                  // update the viewer display now.
 +                  seqColour_actionPerformed();
                  }
                }
              });
        @Override
        public void itemStateChanged(ItemEvent e)
        {
 -        alignStructs.setEnabled(!_alignwith.isEmpty());
 -        alignStructs.setToolTipText(MessageManager.formatMessage(
 -                "label.align_structures_using_linked_alignment_views",
 -                _alignwith.size()));
 +        if (_alignwith.isEmpty())
 +        {
 +          alignStructs.setEnabled(false);
 +          alignStructs.setToolTipText(null);
 +        }
 +        else
 +        {
 +          alignStructs.setEnabled(true);
 +          alignStructs.setToolTipText(MessageManager.formatMessage(
 +                  "label.align_structures_using_linked_alignment_views",
 +                  _alignwith.size()));
 +        }
        }
      };
      viewSelectionMenu = new ViewSelectionMenu(
        }
      });
  
 -    buildColourMenu();
 -  }
 +    viewerActionMenu.setText(getViewerName());
 +    helpItem.setText(MessageManager.formatMessage("label.viewer_help",
 +            getViewerName()));
  
 -  @Override
 -  public void setJalviewColourScheme(ColourSchemeI cs)
 -  {
 -    getBinding().setJalviewColourScheme(cs);
 +    buildColourMenu();
    }
  
    /**
     * the operation.
     */
    @Override
 -  protected String alignStructs_actionPerformed(ActionEvent actionEvent)
 -  {
 -    return alignStructs_withAllAlignPanels();
 -  }
 -
 -  protected String alignStructs_withAllAlignPanels()
 +  protected String alignStructsWithAllAlignPanels()
    {
      if (getAlignmentPanel() == null)
      {
      String reply = null;
      try
      {
 -      AlignmentI[] als = new Alignment[_alignwith.size()];
 -      HiddenColumns[] alc = new HiddenColumns[_alignwith.size()];
 -      int[] alm = new int[_alignwith.size()];
 -      int a = 0;
 -
 -      for (AlignmentPanel ap : _alignwith)
 -      {
 -        als[a] = ap.av.getAlignment();
 -        alm[a] = -1;
 -        alc[a++] = ap.av.getAlignment().getHiddenColumns();
 -      }
 -      reply = getBinding().superposeStructures(als, alm, alc);
 -      if (reply != null)
 +      reply = getBinding().superposeStructures(_alignwith);
 +      if (reply != null && !reply.isEmpty())
        {
          String text = MessageManager
                  .formatMessage("error.superposition_failed", reply);
      } catch (Exception e)
      {
        StringBuffer sp = new StringBuffer();
 -      for (AlignmentPanel ap : _alignwith)
 +      for (AlignmentViewPanel alignPanel : _alignwith)
        {
 -        sp.append("'" + ap.alignFrame.getTitle() + "' ");
 +        sp.append("'" + alignPanel.getViewName() + "' ");
        }
 -      Cache.log.info("Couldn't align structures with the " + sp.toString()
 +      Console.info("Couldn't align structures with the " + sp.toString()
                + "associated alignment panels.", e);
      }
      return reply;
    }
  
 +  /**
 +   * Opens a colour chooser dialog, and applies the chosen colour to the
 +   * background of the structure viewer
 +   */
    @Override
 -  public void background_actionPerformed(ActionEvent actionEvent)
 +  public void background_actionPerformed()
    {
 -    Color col = JColorChooser.showDialog(this,
 -            MessageManager.getString("label.select_background_colour"),
 -            null);
 -    if (col != null)
 +    String ttl = MessageManager.getString("label.select_background_colour");
 +    ColourChooserListener listener = new ColourChooserListener()
      {
 -      getBinding().setBackgroundColour(col);
 -    }
 +      @Override
 +      public void colourSelected(Color c)
 +      {
 +        getBinding().setBackgroundColour(c);
 +      }
 +    };
 +    JalviewColourChooser.showColourChooser(this, ttl, null, listener);
    }
  
    @Override
 -  public void viewerColour_actionPerformed(ActionEvent actionEvent)
 +  public void viewerColour_actionPerformed()
    {
      if (viewerColour.isSelected())
      {
    }
  
    @Override
 -  public void chainColour_actionPerformed(ActionEvent actionEvent)
 +  public void chainColour_actionPerformed()
    {
      chainColour.setSelected(true);
      getBinding().colourByChain();
    }
  
    @Override
 -  public void chargeColour_actionPerformed(ActionEvent actionEvent)
 +  public void chargeColour_actionPerformed()
    {
      chargeColour.setSelected(true);
      getBinding().colourByCharge();
    }
  
    @Override
 -  public void seqColour_actionPerformed(ActionEvent actionEvent)
 +  public void seqColour_actionPerformed()
    {
      AAStructureBindingModel binding = getBinding();
      binding.setColourBySequence(seqColour.isSelected());
          }
        }
        // Set the colour using the current view for the associated alignframe
 -      for (AlignmentPanel ap : _colourwith)
 +      for (AlignmentViewPanel alignPanel : _colourwith)
        {
 -        binding.colourBySequence(ap);
 +        binding.colourBySequence(alignPanel);
        }
 +      seqColoursApplied = true;
      }
    }
  
    @Override
 -  public void pdbFile_actionPerformed(ActionEvent actionEvent)
 +  public void pdbFile_actionPerformed()
    {
 +    // TODO: JAL-3048 not needed for Jalview-JS - save PDB file
      JalviewFileChooser chooser = new JalviewFileChooser(
              Cache.getProperty("LAST_DIRECTORY"));
  
    }
  
    @Override
 -  public void viewMapping_actionPerformed(ActionEvent actionEvent)
 +  public void viewMapping_actionPerformed()
    {
      CutAndPasteTransfer cap = new CutAndPasteTransfer();
      try
    /**
     * Configures the title and menu items of the viewer panel.
     */
 +  @Override
    public void updateTitleAndMenus()
    {
      AAStructureBindingModel binding = getBinding();
       * enable 'Superpose with' if more than one mapped structure
       */
      viewSelectionMenu.setEnabled(false);
 -    if (getBinding().getStructureFiles().length > 1
 +    if (getBinding().getMappedStructureCount() > 1
              && getBinding().getSequence().length > 1)
      {
        viewSelectionMenu.setEnabled(true);
  
      if (!binding.isLoadingFromArchive())
      {
 -      seqColour_actionPerformed(null);
 +      seqColour_actionPerformed();
 +    }
 +  }
 +
 +  @Override
 +  public String toString()
 +  {
 +    return getTitle();
 +  }
 +
 +  @Override
 +  public boolean hasMapping()
 +  {
 +    if (worker != null && (addingStructures || _started))
 +    {
 +      return false;
 +    }
 +    if (getBinding() == null)
 +    {
 +      if (_aps == null || _aps.size() == 0)
 +      {
 +        // viewer has been closed, but we did at some point run.
 +        return true;
 +      }
 +      return false;
 +    }
 +    String[] pdbids = getBinding().getStructureFiles();
 +    if (pdbids == null)
 +    {
 +      return false;
 +    }
 +    int p = 0;
 +    for (String pdbid : pdbids)
 +    {
 +      StructureMapping sm[] = getBinding().getSsm().getMapping(pdbid);
 +      if (sm != null && sm.length > 0 && sm[0] != null)
 +      {
 +        p++;
 +      }
 +    }
 +    // only return true if there is a mapping for every structure file we have
 +    // loaded
 +    if (p == 0 || p != pdbids.length)
 +    {
 +      return false;
 +    }
 +    // and that coloring has been applied
 +    return seqColoursApplied;
 +  }
 +
 +  @Override
 +  public void raiseViewer()
 +  {
 +    toFront();
 +  }
 +
 +  @Override
 +  public long startProgressBar(String msg)
 +  {
 +    // TODO would rather have startProgress/stopProgress as the
 +    // IProgressIndicator interface
 +    long tm = random.nextLong();
 +    if (progressBar != null)
 +    {
 +      progressBar.setProgressBar(msg, tm);
 +    }
 +    return tm;
 +  }
 +
 +  @Override
 +  public void stopProgressBar(String msg, long handle)
 +  {
 +    if (progressBar != null)
 +    {
 +      progressBar.setProgressBar(msg, handle);
 +    }
 +  }
 +
 +  protected IProgressIndicator getProgressIndicator()
 +  {
 +    return progressBar;
 +  }
 +
 +  protected void setProgressIndicator(IProgressIndicator pi)
 +  {
 +    progressBar = pi;
 +  }
 +
 +  public void setProgressMessage(String message, long id)
 +  {
 +    if (progressBar != null)
 +    {
 +      progressBar.setProgressBar(message, id);
 +    }
 +  }
 +
 +  @Override
 +  public void showConsole(boolean show)
 +  {
 +    // default does nothing
 +  }
 +
 +  /**
 +   * Show only the selected chain(s) in the viewer
 +   */
 +  protected void showSelectedChains()
 +  {
 +    List<String> toshow = new ArrayList<>();
 +    for (int i = 0; i < chainMenu.getItemCount(); i++)
 +    {
 +      if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
 +      {
 +        JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
 +        if (item.isSelected())
 +        {
 +          toshow.add(item.getText());
 +        }
 +      }
      }
 +    getBinding().showChains(toshow);
 +  }
 +
 +  /**
 +   * Tries to fetch a PDB file and save to a temporary local file. Returns the
 +   * saved file path if successful, or null if not.
 +   * 
 +   * @param processingEntry
 +   * @return
 +   */
 +  protected String fetchPdbFile(PDBEntry processingEntry)
 +  {
 +    String filePath = null;
 +    Pdb pdbclient = new Pdb();
 +    EBIAlfaFold afclient = new EBIAlfaFold();
 +    AlignmentI pdbseq = null;
 +    String pdbid = processingEntry.getId();
 +    long handle = System.currentTimeMillis()
 +            + Thread.currentThread().hashCode();
 +
 +    /*
 +     * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
 +     */
 +    String msg = MessageManager.formatMessage("status.fetching_pdb",
 +            new Object[]
 +            { pdbid });
 +    getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
 +    // long hdl = startProgressBar(MessageManager.formatMessage(
 +    // "status.fetching_pdb", new Object[]
 +    // { pdbid }));
 +    try
 +    {
 +      if (afclient.isValidReference(pdbid))
 +      {
 +        pdbseq = afclient.getSequenceRecords(pdbid,
 +                processingEntry.getRetrievalUrl());
 +      }
 +      else
 +      {
 +        if (processingEntry.hasRetrievalUrl())
 +        {
 +          String safePDBId = java.net.URLEncoder.encode(pdbid, "UTF-8")
 +                  .replace("%", "__");
 +
 +          // retrieve from URL to new local tmpfile
 +          File tmpFile = File.createTempFile(safePDBId,
 +                  "." + (PDBEntry.Type.MMCIF.toString().equals(
 +                          processingEntry.getType().toString()) ? "cif"
 +                                  : "pdb"));
 +          String fromUrl = processingEntry.getRetrievalUrl();
 +          UrlDownloadClient.download(fromUrl, tmpFile);
 +
 +          // may not need this check ?
 +          String file = tmpFile.getAbsolutePath();
 +          if (file != null)
 +          {
 +            pdbseq = EBIAlfaFold.importDownloadedStructureFromUrl(fromUrl,
 +                    tmpFile, pdbid, null, null, null);
 +          }
 +        }
 +        else
 +        {
 +          pdbseq = pdbclient.getSequenceRecords(pdbid);
 +        }
 +      }
 +    } catch (Exception e)
 +    {
 +      System.err.println(
 +              "Error retrieving PDB id " + pdbid + ": " + e.getMessage());
 +    } finally
 +    {
 +      msg = pdbid + " " + MessageManager.getString("label.state_completed");
 +      getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
 +      // stopProgressBar(msg, hdl);
 +    }
 +    /*
 +     * If PDB data were saved and are not invalid (empty alignment), return the
 +     * file path.
 +     */
 +    if (pdbseq != null && pdbseq.getHeight() > 0)
 +    {
 +      // just use the file name from the first sequence's first PDBEntry
 +      filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
 +              .elementAt(0).getFile()).getAbsolutePath();
 +      processingEntry.setFile(filePath);
 +    }
 +    return filePath;
 +  }
 +
 +  /**
 +   * If supported, saves the state of the structure viewer to a temporary file
 +   * and returns the file, else returns null
 +   * 
 +   * @return
 +   */
 +  public File saveSession()
 +  {
 +    if (getBinding() == null)
 +    {
 +      return null;
 +    }
 +    File session = getBinding().saveSession();
 +    long l = session.length();
 +    int wait = 50;
 +    do
 +    {
 +      try
 +      {
 +        Thread.sleep(5);
 +      } catch (InterruptedException e)
 +      {
 +      }
 +      long nextl = session.length();
 +      if (nextl != l)
 +      {
 +        wait = 50;
 +        l = nextl;
 +      }
 +    } while (--wait > 0);
 +    return session;
 +  }
 +
 +  /**
 +   * Close down this instance of Jalview's Chimera viewer, giving the user the
 +   * option to close the associated Chimera window (process). They may wish to
 +   * keep it open until they have had an opportunity to save any work.
 +   * 
 +   * @param forceClose
 +   *          if true, close any linked Chimera process; if false, prompt first
 +   */
 +  @Override
 +  public void closeViewer(boolean forceClose)
 +  {
 +    AAStructureBindingModel binding = getBinding();
 +    if (binding != null && binding.isViewerRunning())
 +    {
 +      if (!forceClose)
 +      {
 +        String viewerName = getViewerName();
 +        String prompt = MessageManager
 +                .formatMessage("label.confirm_close_viewer", new Object[]
 +                { binding.getViewerTitle(viewerName, false), viewerName });
 +        prompt = JvSwingUtils.wrapTooltip(true, prompt);
 +        int confirm = JvOptionPane.showConfirmDialog(this, prompt,
 +                MessageManager.getString("label.close_viewer"),
 +                JvOptionPane.YES_NO_CANCEL_OPTION);
 +        /*
 +         * abort closure if user hits escape or Cancel
 +         */
 +        if (confirm == JvOptionPane.CANCEL_OPTION
 +                || confirm == JvOptionPane.CLOSED_OPTION)
 +        {
 +          return;
 +        }
 +        forceClose = confirm == JvOptionPane.YES_OPTION;
 +      }
 +    }
 +    if (binding != null)
 +    {
 +      binding.closeViewer(forceClose);
 +    }
 +    setAlignmentPanel(null);
 +    _aps.clear();
 +    _alignwith.clear();
 +    _colourwith.clear();
 +    // TODO: check for memory leaks where instance isn't finalised because jmb
 +    // holds a reference to the window
 +    // jmb = null;
 +    dispose();
 +  }
 +
 +  @Override
 +  public void showHelp_actionPerformed()
 +  {
 +    /*
 +    try
 +    {
 +    */
 +    String url = getBinding().getHelpURL();
 +    if (url != null)
 +    {
 +      BrowserLauncher.openURL(url);
 +    }
 +    /* 
 +    }
 +    catch (IOException ex)
 +    {
 +      System.err
 +              .println("Show " + getViewerName() + " failed with: "
 +                      + ex.getMessage());
 +    }
 +    */
 +  }
 +
 +  @Override
 +  public boolean hasViewerActionsMenu()
 +  {
 +    return viewerActionMenu != null && viewerActionMenu.isEnabled()
 +            && viewerActionMenu.getItemCount() > 0
 +            && viewerActionMenu.isVisible();
    }
  }
   */
  package jalview.gui;
  
 -import jalview.analysis.Conservation;
 -import jalview.analysis.TreeModel;
 -import jalview.api.AlignViewportI;
 -import jalview.datamodel.Sequence;
 -import jalview.datamodel.SequenceGroup;
 -import jalview.datamodel.SequenceI;
 -import jalview.datamodel.SequenceNode;
 -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;
  import java.awt.Dimension;
  import java.awt.Font;
@@@ -36,30 -51,17 +36,30 @@@ import java.awt.print.PageFormat
  import java.awt.print.Printable;
  import java.awt.print.PrinterException;
  import java.awt.print.PrinterJob;
 -import java.util.Enumeration;
  import java.util.Hashtable;
  import java.util.List;
 +import java.util.Map;
 +import java.util.Map.Entry;
  import java.util.Vector;
  
 -import javax.swing.JColorChooser;
  import javax.swing.JPanel;
  import javax.swing.JScrollPane;
  import javax.swing.SwingUtilities;
  import javax.swing.ToolTipManager;
  
 +import jalview.analysis.Conservation;
 +import jalview.analysis.TreeModel;
 +import jalview.api.AlignViewportI;
 +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.structure.SelectionSource;
 +import jalview.util.Format;
 +import jalview.util.MessageManager;
 +
  /**
   * DOCUMENT ME!
   * 
@@@ -78,9 -80,9 +78,9 @@@ public class TreeCanvas extends JPanel 
  
    TreePanel tp;
  
 -  AlignViewport av;
 +  private AlignViewport av;
  
 -  AlignmentPanel ap;
 +  private AlignmentPanel ap;
  
    Font font;
  
  
    int offy;
  
 -  float threshold;
 +  private float threshold;
  
    String longestName;
  
    int labelLength = -1;
  
 -  Hashtable nameHash = new Hashtable();
 +  Map<Object, Rectangle> nameHash = new Hashtable<>();
  
 -  Hashtable nodeHash = new Hashtable();
 +  Map<SequenceNode, Rectangle> nodeHash = new Hashtable<>();
  
    SequenceNode highlightNode;
  
    {
      this.tp = tp;
      this.av = ap.av;
 -    this.ap = ap;
 +    this.setAssociatedPanel(ap);
      font = av.getFont();
      scrollPane = scroller;
      addMouseListener(this);
     */
    public Object findElement(int x, int y)
    {
 -    Enumeration keys = nameHash.keys();
 -
 -    while (keys.hasMoreElements())
 +    for (Entry<Object, Rectangle> entry : nameHash.entrySet())
      {
 -      Object ob = keys.nextElement();
 -      Rectangle rect = (Rectangle) nameHash.get(ob);
 +      Rectangle rect = entry.getValue();
  
        if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
                && (y <= (rect.y + rect.height)))
        {
 -        return ob;
 +        return entry.getKey();
        }
      }
  
 -    keys = nodeHash.keys();
 -
 -    while (keys.hasMoreElements())
 +    for (Entry<SequenceNode, Rectangle> entry : nodeHash.entrySet())
      {
 -      Object ob = keys.nextElement();
 -      Rectangle rect = (Rectangle) nodeHash.get(ob);
 +      Rectangle rect = entry.getValue();
  
        if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
                && (y <= (rect.y + rect.height)))
        {
 -        return ob;
 +        return entry.getKey();
        }
      }
  
      if ((node.left() == null) && (node.right() == null))
      {
        double height = node.height;
 -      double dist = node.dist;
 -
 -      int xstart = (int) ((height - dist) * wscale) + offx;
 +      // double dist = node.dist;
 +      // int xstart = (int) ((height - dist) * wscale) + offx;
        int xend = (int) (height * wscale) + offx;
  
        int ypos = (int) (node.ycount * chunk) + offy;
        return;
      }
  
 -    if ((node.left() == null) && (node.right() == null)) // TODO: internal node
 +    node.color = c;
 +    if (node.element() instanceof SequenceI)
      {
 -      node.color = c;
 -
 -      if (node.element() instanceof SequenceI)
 +      final SequenceI seq = (SequenceI) node.element();
 +      AlignmentPanel[] aps = getAssociatedPanels();
 +      if (aps != null)
        {
 -        AlignmentPanel[] aps = getAssociatedPanels();
 -        if (aps != null)
 +        for (int a = 0; a < aps.length; a++)
          {
 -          for (int a = 0; a < aps.length; a++)
 -          {
 -            final SequenceI seq = (SequenceI) node.element();
 -            aps[a].av.setSequenceColour(seq, c);
 -          }
 +          aps[a].av.setSequenceColour(seq, c);
          }
        }
      }
 -    else
 -    {
 -      node.color = c;
 -      setColor((SequenceNode) node.left(), c);
 -      setColor((SequenceNode) node.right(), c);
 -    }
 +    setColor((SequenceNode) node.left(), c);
 +    setColor((SequenceNode) node.right(), c);
    }
  
    /**
     */
    void startPrinting()
    {
-     Thread thread = new Thread(this, "PrintTreeCanvasThread");
+     Thread thread = new Thread(this, "PrintTreeCanvas");
      thread.start();
    }
  
      {
        fm = g.getFontMetrics(font);
  
 -      if (nameHash.size() == 0)
 +      int nameCount = nameHash.size();
 +      if (nameCount == 0)
        {
          repaint();
        }
  
        if (fitToWindow || (!fitToWindow && (scrollPane
 -              .getHeight() > ((fm.getHeight() * nameHash.size()) + offy))))
 +              .getHeight() > ((fm.getHeight() * nameCount) + offy))))
        {
          draw(g, scrollPane.getWidth(), scrollPane.getHeight());
          setPreferredSize(null);
        else
        {
          setPreferredSize(new Dimension(scrollPane.getWidth(),
 -                fm.getHeight() * nameHash.size()));
 -        draw(g, scrollPane.getWidth(), fm.getHeight() * nameHash.size());
 +                fm.getHeight() * nameCount));
 +        draw(g, scrollPane.getWidth(), fm.getHeight() * nameCount);
        }
  
        scrollPane.revalidate();
     */
    void chooseSubtreeColour()
    {
 -    Color col = JColorChooser.showDialog(this,
 -            MessageManager.getString("label.select_subtree_colour"),
 -            highlightNode.color);
 -    if (col != null)
 +    String ttl = MessageManager.getString("label.select_subtree_colour");
 +    ColourChooserListener listener = new ColourChooserListener()
      {
 -      setColor(highlightNode, col);
 -      PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
 -      repaint();
 -    }
 +      @Override
 +      public void colourSelected(Color c)
 +      {
 +        setColor(highlightNode, c);
 +        PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
 +        repaint();
 +      }
 +    };
 +    JalviewColourChooser.showColourChooser(this, ttl, highlightNode.color,
 +            listener);
    }
  
    @Override
      if (ob instanceof SequenceI)
      {
        treeSelectionChanged((Sequence) ob);
 -      PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
 +      PaintRefresher.Refresh(tp,
 +              getAssociatedPanel().av.getSequenceSetId());
        repaint();
        av.sendSelection();
        return;
                      .deleteAllGroups();
              aps[a].av.getCodingComplement().clearSequenceColours();
            }
 +          aps[a].av.setUpdateStructures(true);
          }
          colourGroups(groups);
 +
 +        /*
 +         * clear partition (don't show vertical line) if
 +         * it is to the right of all nodes
 +         */
 +        if (groups.isEmpty())
 +        {
 +          threshold = 0f;
 +        }
        }
  
 -      PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
 +      PaintRefresher.Refresh(tp,
 +              getAssociatedPanel().av.getSequenceSetId());
        repaint();
      }
  
        }
  
        ColourSchemeI cs = null;
 -      SequenceGroup sg = new SequenceGroup(sequences, null, cs, true, true,
 +      SequenceGroup _sg = new SequenceGroup(sequences, null, cs, true, true,
                false, 0, av.getAlignment().getWidth() - 1);
  
 -      if (av.getGlobalColourScheme() != null)
 -      {
 -        if (av.getGlobalColourScheme() instanceof UserColourScheme)
 -        {
 -          cs = new UserColourScheme(
 -                  ((UserColourScheme) av.getGlobalColourScheme())
 -                          .getColours());
 -
 -        }
 -        else
 -        {
 -          cs = ColourSchemeProperty.getColourScheme(sg, ColourSchemeProperty
 -                  .getColourName(av.getGlobalColourScheme()));
 -        }
 -        // cs is null if shading is an annotationColourGradient
 -        // if (cs != null)
 -        // {
 -        // cs.setThreshold(av.getViewportColourScheme().getThreshold(),
 -        // av.isIgnoreGapsConsensus());
 -        // }
 -      }
 -      sg.setColourScheme(cs);
 -      sg.getGroupColourScheme().setThreshold(
 -              av.getResidueShading().getThreshold(),
 -              av.isIgnoreGapsConsensus());
 -      // sg.recalcConservation();
 -      sg.setName("JTreeGroup:" + sg.hashCode());
 -      sg.setIdColour(col);
 +      _sg.setName("JTreeGroup:" + _sg.hashCode());
 +      _sg.setIdColour(col);
  
        for (int a = 0; a < aps.length; a++)
        {
 -        if (aps[a].av.getGlobalColourScheme() != null
 -                && aps[a].av.getResidueShading().conservationApplied())
 -        {
 -          Conservation c = new Conservation("Group", sg.getSequences(null),
 -                  sg.getStartRes(), sg.getEndRes());
 -          c.calculate();
 -          c.verdict(false, aps[a].av.getConsPercGaps());
 -          sg.cs.setConservation(c);
 -        }
 +        SequenceGroup sg = new SequenceGroup(_sg);
 +        AlignViewport viewport = aps[a].av;
  
 -        aps[a].av.getAlignment().addGroup(new SequenceGroup(sg));
 -        // TODO can we push all of the below into AlignViewportI?
 -        final AlignViewportI codingComplement = aps[a].av
 -                .getCodingComplement();
 -        if (codingComplement != null)
 +        // Propagate group colours in each view
 +        if (viewport.getGlobalColourScheme() != null)
          {
 -          SequenceGroup mappedGroup = MappingUtils.mapSequenceGroup(sg, av,
 -                  codingComplement);
 -          if (mappedGroup.getSequences().size() > 0)
 +          cs = viewport.getGlobalColourScheme().getInstance(viewport, sg);
 +          sg.setColourScheme(cs);
 +          sg.getGroupColourScheme().setThreshold(
 +                  viewport.getResidueShading().getThreshold(),
 +                  viewport.isIgnoreGapsConsensus());
 +
 +          if (viewport.getResidueShading().conservationApplied())
            {
 -            codingComplement.getAlignment().addGroup(mappedGroup);
 -            for (SequenceI seq : mappedGroup.getSequences())
 -            {
 -              codingComplement.setSequenceColour(seq, col.brighter());
 -            }
 +            Conservation c = new Conservation("Group",
 +                    sg.getSequences(null), sg.getStartRes(),
 +                    sg.getEndRes());
 +            c.calculate();
 +            c.verdict(false, viewport.getConsPercGaps());
 +            sg.cs.setConservation(c);
            }
          }
 +        // indicate that associated structure views will need an update
 +        viewport.setUpdateStructures(true);
 +        // propagate structure view update and sequence group to complement view
 +        viewport.addSequenceGroup(sg);
        }
      }
  
 -    // notify the panel(s) to redo any group specific stuff.
 +    // notify the panel(s) to redo any group specific stuff
 +    // also updates structure views if necessary
      for (int a = 0; a < aps.length; a++)
      {
        aps[a].updateAnnotation();
 -      // TODO: JAL-868 - need to ensure view colour change message is broadcast
 -      // to any Jmols listening in
        final AlignViewportI codingComplement = aps[a].av
                .getCodingComplement();
        if (codingComplement != null)
      }
      else
      {
 -      return new AlignmentPanel[] { ap };
 +      return new AlignmentPanel[] { getAssociatedPanel() };
      }
    }
 +
 +  public AlignmentPanel getAssociatedPanel()
 +  {
 +    return ap;
 +  }
 +
 +  public void setAssociatedPanel(AlignmentPanel ap)
 +  {
 +    this.ap = ap;
 +  }
 +
 +  public AlignViewport getViewport()
 +  {
 +    return av;
 +  }
 +
 +  public void setViewport(AlignViewport av)
 +  {
 +    this.av = av;
 +  }
 +
 +  public float getThreshold()
 +  {
 +    return threshold;
 +  }
 +
 +  public void setThreshold(float threshold)
 +  {
 +    this.threshold = threshold;
 +  }
 +
 +  public boolean isApplyToAllViews()
 +  {
 +    return this.applyToAllViews;
 +  }
 +
 +  public void setApplyToAllViews(boolean applyToAllViews)
 +  {
 +    this.applyToAllViews = applyToAllViews;
 +  }
  }
@@@ -21,7 -21,6 +21,7 @@@
  package jalview.gui;
  
  import jalview.bin.Cache;
 +import jalview.bin.Console;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.HiddenColumns;
@@@ -202,7 -201,8 +202,7 @@@ public class VamsasApplication implemen
  
      } catch (Exception e)
      {
 -      jalview.bin.Cache.log.error("Couldn't instantiate vamsas client !",
 -              e);
 +      Console.error("Couldn't instantiate vamsas client !", e);
        return false;
      }
      return true;
        }
      } catch (Error e)
      {
 -      Cache.log.warn(
 +      Console.warn(
                "Probable SERIOUS VAMSAS client incompatibility - carrying on regardless",
                e);
      } catch (Exception e)
      {
 -      Cache.log.warn(
 +      Console.warn(
                "Probable VAMSAS client incompatibility - carrying on regardless",
                e);
      }
    private ClientHandle getJalviewHandle()
    {
      return new ClientHandle("jalview.bin.Jalview",
 -            jalview.bin.Cache.getProperty("VERSION"));
 +            Cache.getProperty("VERSION"));
    }
  
    /**
    {
      if (!inSession())
      {
 -      throw new Error(MessageManager.getString(
 -              "error.implementation_error_vamsas_operation_not_init"));
 +      throw new Error(
 +              "Implementation error! Vamsas Operations when client not initialised and connected");
      }
      addDocumentUpdateHandler();
      addStoreDocumentHandler();
      startSession();
      inInitialUpdate = true;
 -    Cache.log.debug(
 -            "Jalview loading the Vamsas Session for the first time.");
 +    Console.debug("Jalview loading the Vamsas Session for the first time.");
      dealWithDocumentUpdate(false); // we don't push an update out to the
      inInitialUpdate = false;
      // document yet.
 -    Cache.log.debug("... finished update for the first time.");
 +    Console.debug("... finished update for the first time.");
    }
  
    /**
        }
      } catch (Exception e)
      {
 -      Cache.log.warn(
 +      Console.warn(
                "Exception whilst refreshing jalview windows after a vamsas document update.",
                e);
      }
        @Override
        public void run()
        {
 -        Cache.log.info("Jalview updating to the Vamsas Session.");
 +        Console.info("Jalview updating to the Vamsas Session.");
  
          dealWithDocumentUpdate(true);
 -        Cache.log.info("Jalview finished updating to the Vamsas Session.");
 +        Console.info("Jalview finished updating to the Vamsas Session.");
        }
  
-     }, "UpdateVamsasThread");
+     }, "UpdateVamsas");
      udthread.start();
    }
  
    {
      if (!inSession())
      {
 -      throw new Error(MessageManager
 -              .getString("error.jalview_no_connected_vamsas_session"));
 +      throw new Error("Jalview not connected to Vamsas session");
      }
 -    Cache.log.info("Jalview disconnecting from the Vamsas Session.");
 +    Console.info("Jalview disconnecting from the Vamsas Session.");
      try
      {
        if (joinedSession)
          boolean ourprompt = this.promptUser;
          this.promptUser = promptUser;
          vclient.finalizeClient();
 -        Cache.log.info("Jalview has left the session.");
 +        Console.info("Jalview has left the session.");
          this.promptUser = ourprompt; // restore default value
        }
        else
        {
 -        Cache.log.warn(
 +        Console.warn(
                  "JV Client leaving a session that's its not joined yet.");
        }
        joinedSession = false;
        vobj2jv = null;
      } catch (Exception e)
      {
 -      Cache.log.error("Vamsas Session finalization threw exceptions!", e);
 +      Console.error("Vamsas Session finalization threw exceptions!", e);
      }
    }
  
    public void updateJalview(IClientDocument cdoc)
    {
 -    Cache.log.debug("Jalview updating from sesion document ..");
 +    Console.debug("Jalview updating from sesion document ..");
      ensureJvVamsas();
      VamsasAppDatastore vds = new VamsasAppDatastore(cdoc, vobj2jv, jv2vobj,
              baseProvEntry(), alRedoState);
        vds.updateToJalview();
      } catch (Exception e)
      {
 -      Cache.log.error("Failed to update Jalview from vamsas document.", e);
 +      Console.error("Failed to update Jalview from vamsas document.", e);
      }
      try
      {
        }
      } catch (Exception e)
      {
 -      Cache.log.error(
 +      Console.error(
                "Exception when updating Jalview settings from Appdata.", e);
      }
 -    Cache.log.debug(".. finished updating from sesion document.");
 +    Console.debug(".. finished updating from sesion document.");
  
    }
  
              } catch (Exception e)
              {
                errorsDuringUpdate = true;
 -              Cache.log.error("Exception synchronizing " + af.getTitle()
 -                      + " "
 -                      + (af.getViewport().viewName == null ? ""
 -                              : " view " + af.getViewport().viewName)
 +              Console.error("Exception synchronizing " + af.getTitle() + " "
 +                      + (af.getViewport().getViewName() == null ? ""
 +                              : " view " + af.getViewport().getViewName())
                        + " to document.", e);
                stored = false;
              }
        }
      } catch (Exception e)
      {
 -      Cache.log.error("Exception synchronizing Views to Document :", e);
 +      Console.error("Exception synchronizing Views to Document :", e);
        errorsDuringUpdate = true;
      }
  
        }
      } catch (Exception e)
      {
 -      Cache.log.error("Client Appdata Write exception", e);
 +      Console.error("Client Appdata Write exception", e);
        errorsDuringAppUpdate = true;
      }
      vds.clearSkipList();
    {
      int storedviews = 0;
      // called by update handler for document update.
 -    Cache.log.debug("Updating jalview from changed vamsas document.");
 +    Console.debug("Updating jalview from changed vamsas document.");
      disableGui(true);
      try
      {
        long time = System.currentTimeMillis();
        IClientDocument cdoc = vclient.getClientDocument();
 -      if (Cache.log.isDebugEnabled())
 +      if (Console.isDebugEnabled())
        {
 -        Cache.log.debug("Time taken to get ClientDocument = "
 +        Console.debug("Time taken to get ClientDocument = "
                  + (System.currentTimeMillis() - time));
          time = System.currentTimeMillis();
        }
        if (fromJalview)
        {
          storedviews += updateVamsasDocument(cdoc);
 -        if (Cache.log.isDebugEnabled())
 +        if (Console.isDebugEnabled())
          {
 -          Cache.log.debug(
 +          Console.debug(
                    "Time taken to update Vamsas Document from jalview\t= "
                            + (System.currentTimeMillis() - time));
            time = System.currentTimeMillis();
          }
          cdoc.setVamsasRoots(cdoc.getVamsasRoots());
 -        if (Cache.log.isDebugEnabled())
 +        if (Console.isDebugEnabled())
          {
 -          Cache.log.debug("Time taken to set Document Roots\t\t= "
 +          Console.debug("Time taken to set Document Roots\t\t= "
                    + (System.currentTimeMillis() - time));
            time = System.currentTimeMillis();
          }
        else
        {
          updateJalview(cdoc);
 -        if (Cache.log.isDebugEnabled())
 +        if (Console.isDebugEnabled())
          {
 -          Cache.log.debug(
 +          Console.debug(
                    "Time taken to update Jalview from vamsas document Roots\t= "
                            + (System.currentTimeMillis() - time));
            time = System.currentTimeMillis();
  
        }
        vclient.updateDocument(cdoc);
 -      if (Cache.log.isDebugEnabled())
 +      if (Console.isDebugEnabled())
        {
 -        Cache.log.debug("Time taken to update Session Document\t= "
 +        Console.debug("Time taken to update Session Document\t= "
                  + (System.currentTimeMillis() - time));
          time = System.currentTimeMillis();
        }
        recover_objectMappingBackup();
        storedviews = 0;
      }
 -    Cache.log.debug("Finished updating from document change.");
 +    Console.debug("Finished updating from document change.");
      disableGui(false);
      return storedviews;
    }
        @Override
        public void propertyChange(PropertyChangeEvent evt)
        {
 -        Cache.log.debug("Dealing with document update event.");
 +        Console.debug("Dealing with document update event.");
          client.dealWithDocumentUpdate(false);
 -        Cache.log.debug("finished dealing with event.");
 +        Console.debug("finished dealing with event.");
        }
      });
 -    Cache.log.debug("Added Jalview handler for vamsas document updates.");
 +    Console.debug("Added Jalview handler for vamsas document updates.");
    }
  
    private void addStoreDocumentHandler()
                {
                  if (client.promptUser)
                  {
 -                  Cache.log.debug(
 +                  Console.debug(
                            "Asking user if the vamsas session should be stored.");
                    int reply = JvOptionPane.showInternalConfirmDialog(
                            Desktop.desktop,
  
                    if (reply == JvOptionPane.YES_OPTION)
                    {
 -                    Cache.log.debug("Prompting for vamsas store filename.");
 +                    Console.debug("Prompting for vamsas store filename.");
                      Desktop.instance.vamsasSave_actionPerformed(null);
 -                    Cache.log
 -                            .debug("Finished attempt at storing document.");
 +                    Console.debug("Finished attempt at storing document.");
                    }
 -                  Cache.log.debug(
 +                  Console.debug(
                            "finished dealing with REQUESTTOCLOSE event.");
                  }
                  else
                  {
 -                  Cache.log.debug(
 +                  Console.debug(
                            "Ignoring store document request (promptUser==false)");
                  }
                }
              });
 -    Cache.log.debug("Added Jalview handler for vamsas document updates.");
 +    Console.debug("Added Jalview handler for vamsas document updates.");
    }
  
    public void disableGui(boolean b)
    {
 -    Desktop.instance.setVamsasUpdate(b);
 +    // JAL-3311 TODO: remove this class!
 +    // Desktop.instance.setVamsasUpdate(b);
    }
  
    Hashtable _backup_vobj2jv;
          return;
        }
  
 -      throw new Error(MessageManager.getString(
 -              "error.implementation_error_cannot_recover_vamsas_object_mappings"));
 +      throw new Error(
 +              "IMPLEMENTATION ERROR: Cannot recover vamsas object mappings - no backup was made");
      }
      jv2vobj.clear();
      Iterator el = _backup_jv2vobj.entrySet().iterator();
        } catch (Exception e)
        {
          // Complain to GUI
 -        Cache.log.error("Failed to join vamsas session.", e);
 +        Console.error("Failed to join vamsas session.", e);
          vclient = null;
        }
        try
                {
                  return;
                }
 -              // if (Cache.log.isDebugEnabled())
 +              // if (Cache.isDebugEnabled())
                // {
 -              // Cache.log.debug("Received MouseOverMessage "+mm.getVorbaID()+"
 +              // Cache.debug("Received MouseOverMessage "+mm.getVorbaID()+"
                // "+mm.getPosition());
                // }
                Object jvobj = vobj2jv.get(mm.getVorbaID());
                if (jvobj != null && jvobj instanceof SequenceI)
                {
                  last = mstring;
 -                // Cache.log.debug("Handling Mouse over "+mm.getVorbaID()+"
 +                // Cache.debug("Handling Mouse over "+mm.getVorbaID()+"
                  // bound to "+jvobj+" at "+mm.getPosition());
                  // position is character position in aligned sequence
                  ssm.mouseOverVamsasSequence((SequenceI) jvobj,
                if (v != null)
                {
                  // this should really be a trace message.
 -                // Cache.log.debug("Mouse over " + v.getId() + " bound to "
 +                // Cache.debug("Mouse over " + v.getId() + " bound to "
                  // + seq + " at " + index);
                  last = seq;
                  i = index;
            {
              if (vobj2jv == null)
              {
 -              Cache.log.warn(
 +              Console.warn(
                        "Selection listener still active for dead session.");
                // not in a session.
                return;
                    }
                    else
                    {
 -                    // int[] intervals = colsel.getVisibleContigs(
 -                    // seqsel.getStartRes(), seqsel.getEndRes() + 1);
 -                    int[] intervals = hidden.getVisibleContigs(
 -                            seqsel.getStartRes(), seqsel.getEndRes() + 1);
 -                    for (int iv = 0; iv < intervals.length; iv += 2)
 +                    Iterator<int[]> intervals = hidden
 +                            .getVisContigsIterator(seqsel.getStartRes(),
 +                                    seqsel.getEndRes() + 1, false);
 +                    while (intervals.hasNext())
                      {
 +                      int[] region = intervals.next();
                        Seg s = new Seg();
 -                      s.setStart(intervals[iv] + 1); // vamsas indices begin at
 -                      // 1, not zero.
 -                      s.setEnd(intervals[iv + 1] + 1);
 +                      s.setStart(region[0] + 1); // vamsas indices begin at 1,
 +                                                 // not zero.
 +                      s.setEnd(region[1] + 1);
                        s.setInclusive(true);
                        range.addSeg(s);
                      }
                if (sm != null)
                {
                  sm.validate(); // debug
 -                Cache.log.debug("Selection Message\n" + sm.getRawMessage());
 +                Console.debug("Selection Message\n" + sm.getRawMessage());
                  pm.sendMessage(sm);
                }
              }
          ssm.addSelectionListener(selecter);
        } catch (Exception e)
        {
 -        Cache.log.error("Failed to init Vamsas Picking", e);
 +        Console.error("Failed to init Vamsas Picking", e);
        }
      }
    }
   */
  package jalview.gui;
  
 -import jalview.jbgui.GWebserviceInfo;
 -import jalview.util.MessageManager;
 -import jalview.ws.WSClientI;
 +import java.util.Locale;
  
  import java.awt.BorderLayout;
  import java.awt.Color;
 -import java.awt.Font;
 +import java.awt.Dimension;
  import java.awt.Graphics;
  import java.awt.Graphics2D;
  import java.awt.GridLayout;
  import java.awt.Image;
  import java.awt.MediaTracker;
 +import java.awt.RenderingHints;
  import java.awt.event.ActionEvent;
  import java.awt.image.BufferedImage;
  import java.util.Vector;
@@@ -44,16 -45,9 +44,16 @@@ import javax.swing.JTabbedPane
  import javax.swing.JTextArea;
  import javax.swing.event.HyperlinkEvent;
  import javax.swing.event.HyperlinkListener;
 +import javax.swing.event.InternalFrameAdapter;
 +import javax.swing.event.InternalFrameEvent;
  import javax.swing.text.html.HTMLEditorKit;
  import javax.swing.text.html.StyleSheet;
  
 +import jalview.jbgui.GWebserviceInfo;
 +import jalview.util.ChannelProperties;
 +import jalview.util.MessageManager;
 +import jalview.ws.WSClientI;
 +
  /**
   * Base class for web service client thread and gui TODO: create StAX parser to
   * extract html body content reliably when preparing html formatted job statuses
@@@ -87,7 -81,7 +87,7 @@@ public class WebserviceInfo extends GWe
  
    Image image;
  
 -  int angle = 0;
 +  float angle = 0f;
  
    String title = "";
  
    {
      super.setVisible(aFlag);
      frame.setVisible(aFlag);
 -  };
 +  }
  
    JTabbedPane subjobs = null;
  
      this.title = title;
      setInfoText(info);
  
 -    java.net.URL url = getClass()
 -            .getResource("/images/Jalview_Logo_small.png");
 -    image = java.awt.Toolkit.getDefaultToolkit().createImage(url);
 +    image = ChannelProperties.getImage("rotatable_logo.48");
  
      MediaTracker mt = new MediaTracker(this);
      mt.addImage(image, 0);
      }
  
      AnimatedPanel ap = new AnimatedPanel();
 -    titlePanel.add(ap, BorderLayout.CENTER);
 +    ap.setPreferredSize(new Dimension(60, 60));
 +    titlePanel.add(ap, BorderLayout.WEST);
 +    titlePanel.add(titleText, BorderLayout.CENTER);
 +    setStatus(currentStatus);
  
-     Thread thread = new Thread(ap, "AnimatedPanelThread");
+     Thread thread = new Thread(ap, "AnimatedPanel");
      thread.start();
      final WebserviceInfo thisinfo = this;
 -    frame.addInternalFrameListener(
 -            new javax.swing.event.InternalFrameAdapter()
 -            {
 -              @Override
 -              public void internalFrameClosed(
 -                      javax.swing.event.InternalFrameEvent evt)
 -              {
 -                // System.out.println("Shutting down webservice client");
 -                WSClientI service = thisinfo.getthisService();
 -                if (service != null && service.isCancellable())
 -                {
 -                  service.cancelJob();
 -                }
 -              };
 -            });
 +    frame.addInternalFrameListener(new InternalFrameAdapter()
 +    {
 +      @Override
 +      public void internalFrameClosed(InternalFrameEvent evt)
 +      {
 +        // System.out.println("Shutting down webservice client");
 +        WSClientI service = thisinfo.getthisService();
 +        if (service != null && service.isCancellable())
 +        {
 +          service.cancelJob();
 +        }
 +      }
 +    });
      frame.validate();
  
    }
    public void setStatus(int status)
    {
      currentStatus = status;
 +
 +    String message = null;
 +    switch (currentStatus)
 +    {
 +    case STATE_QUEUING:
 +      message = MessageManager.getString("label.state_queueing");
 +      break;
 +
 +    case STATE_RUNNING:
 +      message = MessageManager.getString("label.state_running");
 +      break;
 +
 +    case STATE_STOPPED_OK:
 +      message = MessageManager.getString("label.state_completed");
 +      break;
 +
 +    case STATE_CANCELLED_OK:
 +      message = MessageManager.getString("label.state_job_cancelled");
 +      break;
 +
 +    case STATE_STOPPED_ERROR:
 +      message = MessageManager.getString("label.state_job_error");
 +      break;
 +
 +    case STATE_STOPPED_SERVERERROR:
 +      message = MessageManager.getString("label.server_error_try_later");
 +      break;
 +    }
 +    titleText.setText(title + (message == null ? "" : " - " + message));
 +    titleText.repaint();
    }
  
    /**
      {
        return null;
      }
 -    String lowertxt = text.toLowerCase();
 +    String lowertxt = text.toLowerCase(Locale.ROOT);
      int htmlpos = leaveFirst ? -1 : lowertxt.indexOf("<body");
  
      int htmlend = leaveLast ? -1 : lowertxt.indexOf("</body");
      {
        return "";
      }
 -    String lowertxt = text.toLowerCase();
 +    String lowertxt = text.toLowerCase(Locale.ROOT);
      int htmlpos = lowertxt.indexOf("<body");
      int htmlend = lowertxt.indexOf("</body");
      int doctype = lowertxt.indexOf("<!doctype");
      {
        startTime = System.currentTimeMillis();
  
 +      float invSpeed = 15f;
 +      float factor = 1f;
        while (currentStatus < STATE_STOPPED_OK)
        {
 +        if (currentStatus == STATE_QUEUING)
 +        {
 +          invSpeed = 25f;
 +          factor = 1f;
 +        }
 +        else if (currentStatus == STATE_RUNNING)
 +        {
 +          invSpeed = 10f;
 +          factor = (float) (0.5 + 1.5
 +                  * (0.5 - (0.5 * Math.sin(3.14159 / 180 * (angle + 45)))));
 +        }
          try
          {
            Thread.sleep(50);
  
 -          int units = (int) ((System.currentTimeMillis() - startTime)
 -                  / 10f);
 -          angle += units;
 +          float delta = (System.currentTimeMillis() - startTime) / invSpeed;
 +          angle += delta * factor;
            angle %= 360;
            startTime = System.currentTimeMillis();
  
            if (currentStatus >= STATE_STOPPED_OK)
            {
 +            park();
              angle = 0;
            }
  
        cancel.setEnabled(false);
      }
  
 +    public void park()
 +    {
 +      startTime = System.currentTimeMillis();
 +
 +      while (angle < 360)
 +      {
 +        float invSpeed = 5f;
 +        float factor = 1f;
 +        try
 +        {
 +          Thread.sleep(25);
 +
 +          float delta = (System.currentTimeMillis() - startTime) / invSpeed;
 +          angle += delta * factor;
 +          startTime = System.currentTimeMillis();
 +
 +          if (angle >= 360)
 +          {
 +            angle = 360;
 +          }
 +
 +          repaint();
 +        } catch (Exception ex)
 +        {
 +        }
 +      }
 +
 +    }
 +
      void drawPanel()
      {
        if (offscreen == null || offscreen.getWidth(this) != getWidth()
                || offscreen.getHeight(this) != getHeight())
        {
          offscreen = new BufferedImage(getWidth(), getHeight(),
 -                BufferedImage.TYPE_INT_ARGB);
 +                BufferedImage.TYPE_INT_RGB);
        }
  
        Graphics2D g = (Graphics2D) offscreen.getGraphics();
  
 +      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
 +              RenderingHints.VALUE_ANTIALIAS_ON);
 +      g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
 +              RenderingHints.VALUE_INTERPOLATION_BICUBIC);
 +      g.setRenderingHint(RenderingHints.KEY_RENDERING,
 +              RenderingHints.VALUE_RENDER_QUALITY);
 +
        g.setColor(Color.white);
        g.fillRect(0, 0, getWidth(), getHeight());
  
        if (image != null)
        {
          int x = image.getWidth(this) / 2, y = image.getHeight(this) / 2;
 -        g.rotate(Math.toRadians(angle), 10 + x, 10 + y);
 -        g.drawImage(image, 10, 10, this);
 -        g.rotate(-Math.toRadians(angle), 10 + x, 10 + y);
 +        g.rotate(3.14159 / 180 * (angle), x, y);
 +        g.drawImage(image, 0, 0, this);
 +        g.rotate(-3.14159 / 180 * (angle), x, y);
        }
      }
  
   */
  package jalview.gui;
  
 -import jalview.bin.Cache;
 -import jalview.jbgui.GWsPreferences;
 -import jalview.util.MessageManager;
 -import jalview.ws.jws2.Jws2Discoverer;
 -import jalview.ws.rest.RestServiceDescription;
 -
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Component;
@@@ -37,12 -43,6 +37,12 @@@ import javax.swing.JTextField
  import javax.swing.table.AbstractTableModel;
  import javax.swing.table.TableCellRenderer;
  
 +import jalview.bin.Cache;
 +import jalview.jbgui.GWsPreferences;
 +import jalview.util.MessageManager;
 +import jalview.ws.jws2.Jws2Discoverer;
 +import jalview.ws.rest.RestServiceDescription;
 +
  public class WsPreferences extends GWsPreferences
  {
  
      for (String url : wsUrls)
      {
        int status = Jws2Discoverer.getDiscoverer().getServerStatusFor(url);
 -      tdat[r][1] = new Integer(status);
 +      tdat[r][1] = Integer.valueOf(status);
        tdat[r++][0] = url;
      }
  
          updateWsMenuConfig(false);
          refreshWsMenu(true);
        }
-     }, "RefreshWebServicesThread").start();
+     }, "RefreshWebServices").start();
  
    }
  
            progressBar.setVisible(false);
            validate();
          }
-       }, "RefreshWebServicesMenuProgressBarThread").start();
+       }, "RefreshWebServicesMenuProgressBar").start();
  
      }
      else
            Desktop.instance.setProgressBar(null, ct);
          }
  
-       }, "RefreshWebServicesMenuThread").start();
+       }, "RefreshWebServicesMenu").start();
      }
    }
  
    /**
     * state counters for ensuring that updates only happen if config has changed.
     */
 -  private long update = 0, lastrefresh = 0;
 +  protected long update = 0;
 +
 +  private long lastrefresh = 0;
  
    /*
     * (non-Javadoc)
          updateWsMenuConfig(false);
          refreshWsMenu(showProgressInDialog);
        }
-     }, "UpdateAndRefreshWebServicesMenuThread").start();
+     }, "UpdateAndRefreshWebServicesMenu").start();
  
    }
  }
   */
  package jalview.io;
  
 +import java.io.IOException;
 +
  import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.DBRefSource;
  import jalview.datamodel.PDBEntry;
+ import jalview.ext.forester.io.PhyloXmlFile;
  import jalview.ext.jmol.JmolParser;
  import jalview.structure.StructureImportSettings;
  
 -import java.io.IOException;
 -
  public enum FileFormat implements FileFormatI
  {
    Fasta("Fasta", "fa, fasta, mfa, fastq", true, true)
        return new PhylipFile();
      }
    },
 +  GenBank("GenBank Flatfile", "gb, gbk", true, false)
 +  {
 +    @Override
 +    public AlignmentFileReaderI getReader(FileParse source)
 +            throws IOException
 +    {
 +      return new GenBankFile(source, "GenBank");
 +    }
 +
 +    @Override
 +    public AlignmentFileWriterI getWriter(AlignmentI al)
 +    {
 +      return null;
 +    }
 +  },
 +  Embl("ENA Flatfile", "txt", true, false)
 +  {
 +    @Override
 +    public AlignmentFileReaderI getReader(FileParse source)
 +            throws IOException
 +    {
 +      // Always assume we import from EMBL for now
 +      return new EmblFlatFile(source, DBRefSource.EMBL);
 +    }
 +
 +    @Override
 +    public AlignmentFileWriterI getWriter(AlignmentI al)
 +    {
 +      return null;
 +    }
 +  },
    Jnet("JnetFile", "", false, false)
    {
      @Override
        else
        {
          StructureImportSettings.setShowSeqFeatures(true);
 -        return new MCview.PDBfile(
 +        return new mc_view.PDBfile(
                  StructureImportSettings.isVisibleChainAnnotation(),
                  StructureImportSettings.isProcessSecondaryStructure(),
                  StructureImportSettings.isExternalSecondaryStructure(),
        return true;
      }
    },
 -  Jalview("Jalview", "jar,jvp", true, true)
 +  Jalview("Jalview", "jvp, jar", true, true)
    {
      @Override
      public AlignmentFileReaderI getReader(FileParse source)
      @Override
      public boolean isIdentifiable()
      {
 -      return false;
 +      return true;
      }
-   };
+   },
+   // Nexus("Nexus", "nex,nexus,nx,tre", true, true)
+   // {
+   //
+   // @Override
+   // public AlignmentFileReaderI getReader(FileParse source)
+   // throws IOException
+   // {
+   // return new NexusFile(source);
+   // }
+   //
+   // @Override
+   // public AlignmentFileWriterI getWriter(AlignmentI al)
+   // {
+   // // handle within Aptx?
+   // return null;
+   // }
+   //
+   // @Override
+   // public boolean isTextFormat()
+   // {
+   // return true;
+   // }
+   //
+   // @Override
+   // public boolean isTreeFile()
+   // {
+   // return true;
+   // }
+   //
+   // },
+   PhyloXML("PhyloXML", "phyloxml,phylo.xml,pxml", true, true)
+   {
+     @Override
+     public AlignmentFileReaderI getReader(FileParse source)
+             throws IOException
+     {
+       return new PhyloXmlFile(source);
+     }
+     @Override
+     public AlignmentFileWriterI getWriter(AlignmentI al)
+     {
+       // handle within Aptx?
+       return null;
+     }
+     @Override
+     public boolean isTextFormat()
+     {
+       return true;
+     }
+     @Override
+     public boolean isTreeFile()
+     {
+       return true;
+     }
  
+   };
    private boolean writable;
  
    private boolean readable;
     * @param extensions
     *          comma-separated list of file extensions associated with the format
     * @param isReadable
 +   *          - can be recognised by IdentifyFile and imported with the given
 +   *          reader
     * @param isWritable
 +   *          - can be exported with the returned writer
     */
    private FileFormat(String shortName, String extensions,
            boolean isReadable, boolean isWritable)
      return false;
    }
  
+   @Override
+   public boolean isTreeFile()
+   {
+     return false;
+   }
    /**
     * By default, answers true, indicating the format is one that can be
     * identified by IdentifyFile. Formats that cannot be identified should
   */
  package jalview.io;
  
 +import java.io.File;
 +import java.io.IOException;
 +import java.util.StringTokenizer;
 +import java.util.Vector;
 +
 +import javax.swing.SwingUtilities;
 +
  import jalview.api.ComplexAlignFile;
  import jalview.api.FeatureSettingsModelI;
  import jalview.api.FeaturesDisplayedI;
@@@ -37,17 -30,25 +37,18 @@@ import jalview.datamodel.AlignmentI
  import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.SequenceI;
+ import jalview.ext.archaeopteryx.AptxInit;
  import jalview.gui.AlignFrame;
  import jalview.gui.AlignViewport;
  import jalview.gui.Desktop;
 -import jalview.gui.Jalview2XML;
  import jalview.gui.JvOptionPane;
  import jalview.json.binding.biojson.v1.ColourSchemeMapper;
 +import jalview.project.Jalview2XML;
  import jalview.schemes.ColourSchemeI;
  import jalview.structure.StructureSelectionManager;
  import jalview.util.MessageManager;
  import jalview.ws.utils.UrlDownloadClient;
  
 -import java.io.File;
 -import java.io.IOException;
 -import java.util.StringTokenizer;
 -import java.util.Vector;
 -
 -import javax.swing.SwingUtilities;
 -
  public class FileLoader implements Runnable
  {
    String file;
@@@ -71,8 -72,6 +72,8 @@@
  
    boolean raiseGUI = true;
  
 +  private File selectedFile;
 +
    /**
     * default constructor always raised errors in GUI dialog boxes
     */
      this.raiseGUI = raiseGUI;
    }
  
 -  public void LoadFile(AlignViewport viewport, String file,
 +  public void LoadFile(AlignViewport viewport, Object file,
            DataSourceType protocol, FileFormatI format)
    {
      this.viewport = viewport;
 -    LoadFile(file, protocol, format);
 +    if (file instanceof File)
 +    {
 +      this.selectedFile = (File) file;
 +      file = selectedFile.getPath();
 +    }
 +    LoadFile(file.toString(), protocol, format);
    }
  
    public void LoadFile(String file, DataSourceType protocol,
      this.protocol = protocol;
      this.format = format;
  
-     final Thread loader = new Thread(this, "LoadFileThread");
+     final Thread loader = new Thread(this, "LoadFile");
  
      SwingUtilities.invokeLater(new Runnable()
      {
    }
  
    /**
 +   * Load alignment from (file, protocol) of type format and wait till loaded
 +   * 
 +   * @param file
 +   * @param sourceType
 +   * @param format
 +   * @return alignFrame constructed from file contents
 +   */
 +  public AlignFrame LoadFileWaitTillLoaded(File file,
 +          DataSourceType sourceType, FileFormatI format)
 +  {
 +    this.selectedFile = file;
 +    this.file = file.getPath();
 +    this.protocol = sourceType;
 +    this.format = format;
 +    return _LoadFileWaitTillLoaded();
 +  }
 +
 +  /**
     * Load alignment from FileParse source of type format and wait till loaded
     * 
     * @param source
    }
  
    /**
 -   * start thread and wait until finished, then return the alignFrame that's
 -   * (hopefully) been read.
 +   * runs the 'run' method (in this thread), then return the alignFrame that's
 +   * (hopefully) been read
     * 
     * @return
     */
    protected AlignFrame _LoadFileWaitTillLoaded()
    {
 -    Thread loader = new Thread(this, "LoadFileWithWaiting");
 -    loader.start();
 -
 -    while (loader.isAlive())
 -    {
 -      try
 -      {
 -        Thread.sleep(500);
 -      } catch (Exception ex)
 -      {
 -      }
 -    }
 -
 +    this.run();
      return alignFrame;
    }
  
+   // add support for recently opened Aptx trees
    public void updateRecentlyOpened()
    {
 -    Vector recent = new Vector();
 +    Vector<String> recent = new Vector<>();
      if (protocol == DataSourceType.PASTE)
      {
        // do nothing if the file was pasted in as text... there is no filename to
      String type = protocol == DataSourceType.FILE ? "RECENT_FILE"
              : "RECENT_URL";
  
 -    String historyItems = jalview.bin.Cache.getProperty(type);
 +    String historyItems = Cache.getProperty(type);
  
      StringTokenizer st;
  
  
        while (st.hasMoreTokens())
        {
 -        recent.addElement(st.nextElement().toString().trim());
 +        recent.addElement(st.nextToken().trim());
        }
      }
  
            format = new IdentifyFile().identify(source, false);
            // identify stream and rewind rather than close
          }
 +        else if (selectedFile != null)
 +        {
 +          format = new IdentifyFile().identify(selectedFile, protocol);
 +        }
          else
          {
            format = new IdentifyFile().identify(file, protocol);
                    "IMPLEMENTATION ERROR: Cannot read consecutive Jalview XML projects from a stream.");
            // We read the data anyway - it might make sense.
          }
 -        alignFrame = new Jalview2XML(raiseGUI).loadJalviewAlign(file);
 +        // BH 2018 switch to File object here instead of filename
 +        alignFrame = new Jalview2XML(raiseGUI).loadJalviewAlign(
 +                selectedFile == null ? file : selectedFile);
        }
        else
        {
                        file.lastIndexOf("."));
                String tempStructureFileStr = createNamedJvTempFile(
                        urlLeafName, structExt);
 -              UrlDownloadClient.download(file, tempStructureFileStr);
 -              al = fa.readFile(tempStructureFileStr, DataSourceType.FILE,
 -                      format);
 +
 +              // BH - switching to File object here so as to hold
 +              // ._bytes array directly
 +              File tempFile = new File(tempStructureFileStr);
 +              UrlDownloadClient.download(file, tempFile);
 +
 +              al = fa.readFile(tempFile, DataSourceType.FILE, format);
                source = fa.getAlignFile();
              }
              else
              {
 -              al = fa.readFile(file, protocol, format);
 +              if (selectedFile == null)
 +              {
 +                al = fa.readFile(file, protocol, format);
 +
 +              }
 +              else
 +              {
 +                al = fa.readFile(selectedFile, protocol, format);
 +              }
                source = fa.getAlignFile(); // keep reference for later if
 -                                          // necessary.
 +
 +              // necessary.
              }
            }
          } catch (java.io.IOException ex)
                    .getFeatureColourScheme();
            if (viewport != null)
            {
 -            if (proxyColourScheme != null)
 -            {
 -              viewport.applyFeaturesStyle(proxyColourScheme);
 -            }
              // append to existing alignment
              viewport.addAlignment(al, title);
 +            viewport.applyFeaturesStyle(proxyColourScheme);
            }
            else
            {
              if (!(protocol == DataSourceType.PASTE))
              {
                alignFrame.setFileName(file, format);
 +              alignFrame.setFileObject(selectedFile); // BH 2018 SwingJS
              }
              if (proxyColourScheme != null)
              {
                alignFrame.getViewport()
                        .applyFeaturesStyle(proxyColourScheme);
              }
+             if (format.isTreeFile())
+             {
+               // make generic instead of Aptx specific?
+               AptxInit.createInstancesFromFile(file,
+                       alignFrame.getViewport());
+             }
 -            alignFrame.statusBar.setText(MessageManager.formatMessage(
 +            alignFrame.setStatus(MessageManager.formatMessage(
                      "label.successfully_loaded_file", new String[]
                      { title }));
  
                        AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
              }
  
              try
              {
 -              alignFrame.setMaximum(jalview.bin.Cache
 -                      .getDefault("SHOW_FULLSCREEN", false));
 +              alignFrame.setMaximum(
 +                      Cache.getDefault("SHOW_FULLSCREEN", false));
              } catch (java.beans.PropertyVetoException ex)
              {
              }
@@@ -20,9 -20,6 +20,9 @@@
   */
  package jalview.io;
  
 +import java.util.Locale;
 +
 +import java.io.File;
  import java.io.IOException;
  
  /**
   */
  public class IdentifyFile
  {
 +
 +  public FileFormatI identify(Object file, DataSourceType protocol)
 +          throws FileFormatException
 +  {
 +    // BH 2018
 +    return (file instanceof File ? identify((File) file, protocol)
 +            : identify((String) file, protocol));
 +
 +  }
 +
 +  public FileFormatI identify(File file, DataSourceType sourceType)
 +          throws FileFormatException
 +  {
 +    // BH 2018
 +    String emessage = "UNIDENTIFIED FILE PARSING ERROR";
 +    FileParse parser = null;
 +    try
 +    {
 +      parser = new FileParse(file, sourceType);
 +      if (parser.isValid())
 +      {
 +        return identify(parser);
 +      }
 +    } catch (Exception e)
 +    {
 +      System.err.println("Error whilst identifying " + file);
 +      e.printStackTrace(System.err);
 +      emessage = e.getMessage();
 +    }
 +    if (parser != null)
 +    {
 +      throw new FileFormatException(parser.errormessage);
 +    }
 +    throw new FileFormatException(emessage);
 +  }
 +
    /**
     * Identify a datasource's file content.
     *
@@@ -94,7 -55,7 +94,7 @@@
        }
      } catch (Exception e)
      {
 -      System.err.println("Error whilst identifying");
 +      System.err.println("Error whilst identifying " + file);
        e.printStackTrace(System.err);
        emessage = e.getMessage();
      }
            if (source.inFile != null)
            {
              String fileStr = source.inFile.getName();
 -            // possibly a Jalview archive.
 -            if (fileStr.lastIndexOf(".jar") > -1
 -                    || fileStr.lastIndexOf(".zip") > -1)
 +            if (fileStr.contains(".jar") || fileStr.contains(".zip")
 +                    || fileStr.contains(".jvp"))
              {
 +              // possibly a Jalview archive (but check further)
                reply = FileFormat.Jalview;
              }
            }
            if (!lineswereskipped && data.startsWith("PK"))
            {
 -            reply = FileFormat.Jalview; // archive.
 +            reply = FileFormat.Jalview; // archive
              break;
            }
          }
 -        data = data.toUpperCase();
 +        data = data.toUpperCase(Locale.ROOT);
  
          if (data.startsWith(ScoreMatrixFile.SCOREMATRIX))
          {
            reply = FileFormat.ScoreMatrix;
            break;
          }
 +        if (data.startsWith("LOCUS"))
 +        {
 +          reply = FileFormat.GenBank;
 +          break;
 +        }
 +        if (data.startsWith("ID "))
 +        {
 +          if (data.substring(2).trim().split(";").length == 7)
 +          {
 +            reply = FileFormat.Embl;
 +            break;
 +          }
 +        }
          if (data.startsWith("H ") && !aaIndexHeaderRead)
          {
            aaIndexHeaderRead = true;
          if ((lessThan > -1)) // possible Markup Language data i.e HTML,
                               // RNAML, XML
          {
 -          String upper = data.toUpperCase();
 +          String upper = data.toUpperCase(Locale.ROOT);
            if (upper.substring(lessThan).startsWith("<HTML"))
            {
              reply = FileFormat.Html;
            reply = FileFormat.Phylip;
            break;
          }
-         else
+         else if (!lineswereskipped && looksLikeJnetData(data))
          {
-           if (!lineswereskipped && looksLikeJnetData(data))
-           {
              reply = FileFormat.Jnet;
              break;
-           }
          }
+         else // phylogenetic file
+         {
+           String identifier = data.toLowerCase();
+           String secondLine = source.nextLine().toLowerCase();
+           if (identifier.startsWith("<phyloxml")
+                   || secondLine.startsWith("<phyloxml"))
+           {
+             reply = FileFormat.PhyloXML;
+             break;
+           }
+           // commented out, nexus parser is troublesome
  
+           // else if (((identifier.startsWith("nexus"))
+           // || (identifier.startsWith("#nexus"))
+           // || (identifier.startsWith("# nexus"))
+           // || (identifier.startsWith("begin"))))
+           // {
+           // reply = FileFormat.Nexus;
+           // break;
+           // }
+         }
          lineswereskipped = true; // this means there was some junk before any
          // key file signature
        }
      return true;
    }
  
 +  /**
 +   * 
 +   * @param args
 +   * @j2sIgnore
 +   */
    public static void main(String[] args)
    {
      for (int i = 0; args != null && i < args.length; i++)
        System.err.println("Usage: <Filename> [<Filename> ...]");
      }
    }
 +
  }
  package jalview.jbgui;
  
  import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
 +import jalview.analysis.GeneticCodeI;
 +import jalview.analysis.GeneticCodes;
  import jalview.api.SplitContainerI;
  import jalview.bin.Cache;
  import jalview.gui.JvSwingUtils;
  import jalview.gui.Preferences;
  import jalview.io.FileFormats;
 +import jalview.schemes.ResidueColourScheme;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
  
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.GridLayout;
 -import java.awt.Toolkit;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.FocusAdapter;
@@@ -61,18 -59,15 +61,18 @@@ import javax.swing.event.ChangeEvent
  import javax.swing.event.MenuEvent;
  import javax.swing.event.MenuListener;
  
 +@SuppressWarnings("serial")
  public class GAlignFrame extends JInternalFrame
  {
    protected JMenuBar alignFrameMenuBar = new JMenuBar();
  
    protected JMenuItem closeMenuItem = new JMenuItem();
  
 -  protected JMenu webService = new JMenu();
 +  public JMenu webService = new JMenu();// BH 2019 was protected, but not
 +                                        // sufficient for AlignFrame thread run
  
 -  protected JMenuItem webServiceNoServices;
 +  public JMenuItem webServiceNoServices;// BH 2019 was protected, but not
 +                                        // sufficient for AlignFrame thread run
  
    protected JCheckBoxMenuItem viewBoxesMenuItem = new JCheckBoxMenuItem();
  
@@@ -80,9 -75,7 +80,9 @@@
  
    protected JMenu sortByAnnotScore = new JMenu();
  
 -  public JLabel statusBar = new JLabel();
 +  public JLabel statusBar = new JLabel(); // BH 2019 was protected, but not
 +                                          // sufficient for
 +                                          // AlignFrame.printWriter
  
    protected JMenu outputTextboxMenu = new JMenu();
  
  
    protected JMenuItem modifyPID;
  
 -  protected JMenuItem annotationColour;
 +  protected JRadioButtonMenuItem annotationColour;
  
    protected JMenu sortByTreeMenu = new JMenu();
  
  
    protected JCheckBoxMenuItem showDbRefsMenuitem = new JCheckBoxMenuItem();
  
 -  protected JMenuItem showTranslation = new JMenuItem();
 +  protected JMenu showTranslation = new JMenu();
  
    protected JMenuItem showReverse = new JMenuItem();
  
  
    protected JMenuItem runGroovy = new JMenuItem();
  
 +  protected JMenuItem loadVcf;
 +
    protected JCheckBoxMenuItem autoCalculate = new JCheckBoxMenuItem();
  
    protected JCheckBoxMenuItem sortByTree = new JCheckBoxMenuItem();
  
    protected JCheckBoxMenuItem applyAutoAnnotationSettings = new JCheckBoxMenuItem();
  
 +  protected JMenuItem openFeatureSettings;
 +
    private SequenceAnnotationOrder annotationSortOrder;
  
    private boolean showAutoCalculatedAbove = false;
    {
      try
      {
 +
 +      // for Web-page embedding using id=align-frame-div
 +      setName("jalview-alignment");
 +
        jbInit();
        setJMenuBar(alignFrameMenuBar);
  
            @Override
            public void actionPerformed(ActionEvent e)
            {
 -            outputText_actionPerformed(e);
 +            outputText_actionPerformed(e.getActionCommand());
            }
          });
  
        System.err.println(e.toString());
      }
  
 -    if (!Platform.isAMac())
 +    if (Platform.allowMnemonics()) // was "not mac and not JS"
      {
        closeMenuItem.setMnemonic('C');
        outputTextboxMenu.setMnemonic('T');
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        saveAs_actionPerformed(e);
 +        saveAs_actionPerformed();
        }
      };
  
      // FIXME getDefaultToolkit throws an exception in Headless mode
      KeyStroke keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_S,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()
 -                    | KeyEvent.SHIFT_MASK,
 +            jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx()
 +                    | jalview.util.ShortcutKeyMaskExWrapper.SHIFT_DOWN_MASK,
              false);
      addMenuActionAndAccelerator(keyStroke, saveAs, al);
  
      closeMenuItem.setText(MessageManager.getString("action.close"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_W,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      JMenuItem selectAllSequenceMenuItem = new JMenuItem(
              MessageManager.getString("action.select_all"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_A,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      JMenuItem invertSequenceMenuItem = new JMenuItem(
              MessageManager.getString("action.invert_sequence_selection"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_I,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      JMenuItem remove2LeftMenuItem = new JMenuItem(
              MessageManager.getString("action.remove_left"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_L,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      JMenuItem remove2RightMenuItem = new JMenuItem(
              MessageManager.getString("action.remove_right"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_R,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      JMenuItem removeGappedColumnMenuItem = new JMenuItem(
              MessageManager.getString("action.remove_empty_columns"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_E,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      JMenuItem removeAllGapsMenuItem = new JMenuItem(
              MessageManager.getString("action.remove_all_gaps"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_E,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()
 -                    | KeyEvent.SHIFT_MASK,
 +            jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx()
 +                    | jalview.util.ShortcutKeyMaskExWrapper.SHIFT_DOWN_MASK,
              false);
      al = new ActionListener()
      {
      JMenuItem removeRedundancyMenuItem = new JMenuItem(
              MessageManager.getString("action.remove_redundancy"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_D,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      undoMenuItem.setEnabled(false);
      undoMenuItem.setText(MessageManager.getString("action.undo"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_Z,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      redoMenuItem.setEnabled(false);
      redoMenuItem.setText(MessageManager.getString("action.redo"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_Y,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      JMenuItem printMenuItem = new JMenuItem(
              MessageManager.getString("action.print"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_P,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      JMenuItem findMenuItem = new JMenuItem(
              MessageManager.getString("action.find"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_F,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      findMenuItem.setToolTipText(JvSwingUtils.wrapTooltip(true,
              MessageManager.getString("label.find_tip")));
      al = new ActionListener()
      JMenuItem deleteGroups = new JMenuItem(
              MessageManager.getString("action.undefine_groups"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_U,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      JMenuItem createGroup = new JMenuItem(
              MessageManager.getString("action.create_group"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_G,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      JMenuItem unGroup = new JMenuItem(
              MessageManager.getString("action.remove_group"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_G,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()
 -                    | KeyEvent.SHIFT_MASK,
 +            jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx()
 +                    | jalview.util.ShortcutKeyMaskExWrapper.SHIFT_DOWN_MASK,
              false);
      al = new ActionListener()
      {
  
      copy.setText(MessageManager.getString("action.copy"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_C,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
  
      al = new ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        copy_actionPerformed(e);
 +        copy_actionPerformed();
        }
      };
      addMenuActionAndAccelerator(keyStroke, copy, al);
  
      cut.setText(MessageManager.getString("action.cut"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_X,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        cut_actionPerformed(e);
 +        cut_actionPerformed();
        }
      };
      addMenuActionAndAccelerator(keyStroke, cut, al);
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        delete_actionPerformed(e);
 +        delete_actionPerformed();
        }
      });
  
      JMenuItem pasteNew = new JMenuItem(
              MessageManager.getString("label.to_new_alignment"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_V,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()
 -                    | KeyEvent.SHIFT_MASK,
 +            jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx()
 +                    | jalview.util.ShortcutKeyMaskExWrapper.SHIFT_DOWN_MASK,
              false);
      al = new ActionListener()
      {
      JMenuItem pasteThis = new JMenuItem(
              MessageManager.getString("label.to_this_alignment"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_V,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      });
      seqLimits.setText(
              MessageManager.getString("label.show_sequence_limits"));
 -    seqLimits.setState(jalview.bin.Cache.getDefault("SHOW_JVSUFFIX", true));
 +    seqLimits.setState(Cache.getDefault("SHOW_JVSUFFIX", true));
      seqLimits.addActionListener(new ActionListener()
      {
        @Override
  
  
      JMenuItem loadTreeBaseStudy = new JMenuItem(
-             MessageManager.getString("treebase study"));
+             MessageManager.getString("label.treebase_study"));
      loadTreeBaseStudy.addActionListener(new ActionListener()
      {
  
  
  
      JMenuItem loadTreeBase = new JMenuItem(
-             MessageManager.getString("treebase"));
+             MessageManager.getString("label.treebase"));
      loadTreeBase.addActionListener(new ActionListener()
      {
  
  
      });
      JMenuItem loadTreePfam = new JMenuItem(
-             MessageManager.getString("pfam"));
+             MessageManager.getString("label.pfam"));
      loadTreePfam.addActionListener(new ActionListener()
      {
  
  
      });
      JMenuItem loadTreeFam = new JMenuItem(
-             MessageManager.getString("treefam"));
+             MessageManager.getString("label.treefam"));
      loadTreeFam.addActionListener(new ActionListener()
      {
  
      });
  
      JMenuItem loadTreeOfLife = new JMenuItem(
-             MessageManager.getString("tree_of_life"));
+             MessageManager.getString("label.tree_of_life"));
      loadTreeOfLife.addActionListener(new ActionListener()
      {
  
              .setText(MessageManager.getString("action.calculate_tree_pca"));
  
      padGapsMenuitem.setText(MessageManager.getString("label.pad_gaps"));
 -    padGapsMenuitem
 -            .setState(jalview.bin.Cache.getDefault("PAD_GAPS", false));
 +    padGapsMenuitem.setState(Cache.getDefault("PAD_GAPS", false));
      padGapsMenuitem.addActionListener(new ActionListener()
      {
        @Override
          vamsasStore_actionPerformed(e);
        }
      });
 +
 +    /*
 +     * Translate as cDNA with sub-menu of translation tables
 +     */
      showTranslation
              .setText(MessageManager.getString("label.translate_cDNA"));
 -    showTranslation.addActionListener(new ActionListener()
 +    boolean first = true;
 +    for (final GeneticCodeI table : GeneticCodes.getInstance()
 +            .getCodeTables())
      {
 -      @Override
 -      public void actionPerformed(ActionEvent e)
 +      JMenuItem item = new JMenuItem(table.getId() + " " + table.getName());
 +      showTranslation.add(item);
 +      item.addActionListener(new ActionListener()
 +      {
 +        @Override
 +        public void actionPerformed(ActionEvent e)
 +        {
 +          showTranslation_actionPerformed(table);
 +        }
 +      });
 +      if (first)
        {
 -        showTranslation_actionPerformed(e);
 +        showTranslation.addSeparator();
        }
 -    });
 +      first = false;
 +    }
 +
      showReverse.setText(MessageManager.getString("label.reverse"));
      showReverse.addActionListener(new ActionListener()
      {
        }
      });
  
 -    JMenuItem openFeatureSettings = new JMenuItem(
 +    openFeatureSettings = new JMenuItem(
              MessageManager.getString("action.feature_settings"));
      openFeatureSettings.addActionListener(new ActionListener()
      {
          featureSettings_actionPerformed(e);
        }
      });
 +
 +    /*
 +     * add sub-menu of database we can fetch from
 +     */
      JMenuItem fetchSequence = new JMenuItem(
              MessageManager.getString("label.fetch_sequences"));
      fetchSequence.addActionListener(new ActionListener()
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        fetchSequence_actionPerformed(e);
 +        fetchSequence_actionPerformed();
        }
      });
  
          associatedData_actionPerformed(e);
        }
      });
 +    loadVcf = new JMenuItem(
 +            MessageManager.getString("label.load_vcf_file"));
 +    loadVcf.setToolTipText(MessageManager.getString("label.load_vcf"));
 +    loadVcf.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        loadVcf_actionPerformed();
 +      }
 +    });
      autoCalculate.setText(
              MessageManager.getString("label.autocalculate_consensus"));
 -    autoCalculate.setState(
 -            jalview.bin.Cache.getDefault("AUTO_CALC_CONSENSUS", true));
 +    autoCalculate.setState(Cache.getDefault("AUTO_CALC_CONSENSUS", true));
      autoCalculate.addActionListener(new ActionListener()
      {
        @Override
              MessageManager.getString("label.sort_alignment_new_tree"));
      sortByTree.setToolTipText("<html>" + MessageManager.getString(
              "label.enable_automatically_sort_alignment_when_open_new_tree"));
 -    sortByTree
 -            .setState(jalview.bin.Cache.getDefault("SORT_BY_TREE", false));
 +    sortByTree.setState(Cache.getDefault("SORT_BY_TREE", false));
      sortByTree.addActionListener(new ActionListener()
      {
        @Override
      JMenuItem invertColSel = new JMenuItem(
              MessageManager.getString("action.invert_column_selection"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_I,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()
 -                    | KeyEvent.ALT_MASK,
 +            jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx()
 +                    | jalview.util.ShortcutKeyMaskExWrapper.ALT_DOWN_MASK,
              false);
      al = new ActionListener()
      {
  
      JMenuItem save = new JMenuItem(MessageManager.getString("action.save"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_S,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      JMenuItem newView = new JMenuItem(
              MessageManager.getString("action.new_view"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_T,
 -            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
 +            jalview.util.ShortcutKeyMaskExWrapper
 +                    .getMenuShortcutKeyMaskEx(),
 +            false);
      al = new ActionListener()
      {
        @Override
      alignFrameMenuBar.add(formatMenu);
      alignFrameMenuBar.add(colourMenu);
      alignFrameMenuBar.add(calculateMenu);
 -    alignFrameMenuBar.add(webService);
 +    if (!Platform.isJS())
 +    {
 +      alignFrameMenuBar.add(webService);
 +    }
  
      fileMenu.add(fetchSequence);
      fileMenu.add(addSequenceMenu);
      fileMenu.add(exportAnnotations);
      fileMenu.add(loadTreeMenu);
      fileMenu.add(associatedData);
 +    if (!Platform.isJS())
 +    {
 +      fileMenu.add(loadVcf);
 +    }
      fileMenu.addSeparator();
      fileMenu.add(closeMenuItem);
  
      calculateMenu.addSeparator();
      calculateMenu.add(expandAlignment);
      calculateMenu.add(extractScores);
 -    calculateMenu.addSeparator();
 -    calculateMenu.add(runGroovy);
 +    if (!Platform.isJS())
 +    {
 +      calculateMenu.addSeparator();
 +      calculateMenu.add(runGroovy);
 +    }
  
      webServiceNoServices = new JMenuItem(
              MessageManager.getString("label.no_services"));
      webService.add(webServiceNoServices);
 -    exportImageMenu.add(htmlMenuItem);
 +    if (!Platform.isJS())
 +    {
 +      exportImageMenu.add(htmlMenuItem);
 +    }
      exportImageMenu.add(epsFile);
      exportImageMenu.add(createPNG);
 -    exportImageMenu.add(createBioJS);
 -    exportImageMenu.add(createSVG);
 +    if (!Platform.isJS())
 +    {
 +      exportImageMenu.add(createBioJS);
 +      exportImageMenu.add(createSVG);
 +    }
      addSequenceMenu.add(addFromFile);
      addSequenceMenu.add(addFromText);
      addSequenceMenu.add(addFromURL);
  
    }
  
 +  protected void loadVcf_actionPerformed()
 +  {
 +  }
 +
    /**
     * Constructs the entries on the Colour menu (but does not add them to the
     * menu).
        }
      });
  
 -    annotationColour = new JMenuItem(
 +    annotationColour = new JRadioButtonMenuItem(
              MessageManager.getString("action.by_annotation"));
 +    annotationColour.setName(ResidueColourScheme.ANNOTATION_COLOUR);
      annotationColour.addActionListener(new ActionListener()
      {
        @Override
    {
    }
  
 -  protected void outputText_actionPerformed(ActionEvent e)
 +  protected void outputText_actionPerformed(String formatName)
    {
    }
  
    {
    }
  
 -  protected void copy_actionPerformed(ActionEvent e)
 +  protected void copy_actionPerformed()
    {
    }
  
 -  protected void cut_actionPerformed(ActionEvent e)
 +  protected void cut_actionPerformed()
    {
    }
  
 -  protected void delete_actionPerformed(ActionEvent e)
 +  protected void delete_actionPerformed()
    {
    }
  
    {
    }
  
 -  protected void saveAs_actionPerformed(ActionEvent e)
 +  protected void saveAs_actionPerformed()
    {
    }
  
  
    }
  
 -  public void showTranslation_actionPerformed(ActionEvent e)
 +  public void showTranslation_actionPerformed(GeneticCodeI codeTable)
    {
  
    }
  
    }
  
 -  public void fetchSequence_actionPerformed(ActionEvent e)
 +  public void fetchSequence_actionPerformed()
    {
  
    }
index d4b2c04,0000000..1a48a16
mode 100644,000000..100644
--- /dev/null
@@@ -1,6540 -1,0 +1,6596 @@@
 +/*
 + * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
 + * Copyright (C) $$Year-Rel$$ The Jalview Authors
 + * 
 + * This file is part of Jalview.
 + * 
 + * Jalview is free software: you can redistribute it and/or
 + * modify it under the terms of the GNU General Public License 
 + * as published by the Free Software Foundation, either version 3
 + * of the License, or (at your option) any later version.
 + *  
 + * Jalview is distributed in the hope that it will be useful, but 
 + * WITHOUT ANY WARRANTY; without even the implied warranty 
 + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
 + * PURPOSE.  See the GNU General Public License for more details.
 + * 
 + * You should have received a copy of the GNU General Public License
 + * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
 + * The Jalview Authors are detailed in the 'AUTHORS' file.
 + */
 +package jalview.project;
 +
 +import static jalview.math.RotatableMatrix.Axis.X;
 +import static jalview.math.RotatableMatrix.Axis.Y;
 +import static jalview.math.RotatableMatrix.Axis.Z;
 +
 +import java.awt.Color;
 +import java.awt.Font;
 +import java.awt.Rectangle;
 +import java.io.BufferedReader;
 +import java.io.ByteArrayInputStream;
 +import java.io.File;
 +import java.io.FileInputStream;
 +import java.io.FileOutputStream;
 +import java.io.IOException;
 +import java.io.InputStream;
 +import java.io.InputStreamReader;
 +import java.io.OutputStream;
 +import java.io.OutputStreamWriter;
 +import java.io.PrintWriter;
 +import java.lang.reflect.InvocationTargetException;
 +import java.math.BigInteger;
 +import java.net.MalformedURLException;
 +import java.net.URL;
 +import java.util.ArrayList;
 +import java.util.Arrays;
 +import java.util.Collections;
 +import java.util.Enumeration;
 +import java.util.GregorianCalendar;
 +import java.util.HashMap;
 +import java.util.HashSet;
 +import java.util.Hashtable;
 +import java.util.IdentityHashMap;
 +import java.util.Iterator;
 +import java.util.LinkedHashMap;
 +import java.util.List;
 +import java.util.Locale;
 +import java.util.Map;
 +import java.util.Map.Entry;
 +import java.util.Set;
 +import java.util.Vector;
 +import java.util.jar.JarEntry;
 +import java.util.jar.JarInputStream;
 +import java.util.jar.JarOutputStream;
 +
 +import javax.swing.JInternalFrame;
 +import javax.swing.SwingUtilities;
 +import javax.xml.bind.JAXBContext;
 +import javax.xml.bind.JAXBElement;
 +import javax.xml.bind.Marshaller;
 +import javax.xml.datatype.DatatypeConfigurationException;
 +import javax.xml.datatype.DatatypeFactory;
 +import javax.xml.datatype.XMLGregorianCalendar;
 +import javax.xml.stream.XMLInputFactory;
 +import javax.xml.stream.XMLStreamReader;
 +
 +import jalview.analysis.Conservation;
 +import jalview.analysis.PCA;
 +import jalview.analysis.scoremodels.ScoreModels;
 +import jalview.analysis.scoremodels.SimilarityParams;
 +import jalview.api.FeatureColourI;
 +import jalview.api.ViewStyleI;
 +import jalview.api.analysis.ScoreModelI;
 +import jalview.api.analysis.SimilarityParamsI;
 +import jalview.api.structures.JalviewStructureDisplayI;
 +import jalview.bin.Cache;
 +import jalview.bin.Console;
 +import jalview.datamodel.AlignedCodonFrame;
 +import jalview.datamodel.Alignment;
 +import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.DBRefEntry;
 +import jalview.datamodel.GeneLocus;
 +import jalview.datamodel.GraphLine;
 +import jalview.datamodel.PDBEntry;
 +import jalview.datamodel.Point;
 +import jalview.datamodel.RnaViewerModel;
 +import jalview.datamodel.SequenceFeature;
 +import jalview.datamodel.SequenceGroup;
 +import jalview.datamodel.SequenceI;
 +import jalview.datamodel.StructureViewerModel;
 +import jalview.datamodel.StructureViewerModel.StructureData;
 +import jalview.datamodel.features.FeatureMatcher;
 +import jalview.datamodel.features.FeatureMatcherI;
 +import jalview.datamodel.features.FeatureMatcherSet;
 +import jalview.datamodel.features.FeatureMatcherSetI;
++import jalview.ext.archaeopteryx.AptxInit;
++import jalview.ext.treeviewer.TreeFrameI;
++import jalview.ext.treeviewer.TreeI;
++import jalview.ext.treeviewer.TreeViewerUtils;
 +import jalview.ext.varna.RnaModel;
 +import jalview.gui.AlignFrame;
 +import jalview.gui.AlignViewport;
 +import jalview.gui.AlignmentPanel;
 +import jalview.gui.AppVarna;
 +import jalview.gui.Desktop;
 +import jalview.gui.JvOptionPane;
 +import jalview.gui.OOMWarning;
 +import jalview.gui.PCAPanel;
 +import jalview.gui.PaintRefresher;
 +import jalview.gui.SplitFrame;
 +import jalview.gui.StructureViewer;
 +import jalview.gui.StructureViewer.ViewerType;
 +import jalview.gui.StructureViewerBase;
 +import jalview.gui.TreePanel;
 +import jalview.io.BackupFiles;
 +import jalview.io.DataSourceType;
 +import jalview.io.FileFormat;
 +import jalview.io.NewickFile;
 +import jalview.math.Matrix;
 +import jalview.math.MatrixI;
 +import jalview.renderer.ResidueShaderI;
 +import jalview.schemes.AnnotationColourGradient;
 +import jalview.schemes.ColourSchemeI;
 +import jalview.schemes.ColourSchemeProperty;
 +import jalview.schemes.FeatureColour;
 +import jalview.schemes.ResidueProperties;
 +import jalview.schemes.UserColourScheme;
 +import jalview.structure.StructureSelectionManager;
 +import jalview.structures.models.AAStructureBindingModel;
 +import jalview.util.Format;
 +import jalview.util.HttpUtils;
 +import jalview.util.MessageManager;
 +import jalview.util.Platform;
 +import jalview.util.StringUtils;
 +import jalview.util.jarInputStreamProvider;
 +import jalview.util.matcher.Condition;
 +import jalview.viewmodel.AlignmentViewport;
 +import jalview.viewmodel.PCAModel;
 +import jalview.viewmodel.ViewportRanges;
 +import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 +import jalview.viewmodel.seqfeatures.FeatureRendererSettings;
 +import jalview.viewmodel.seqfeatures.FeaturesDisplayed;
 +import jalview.ws.jws2.Jws2Discoverer;
 +import jalview.ws.jws2.dm.AAConSettings;
 +import jalview.ws.jws2.jabaws2.Jws2Instance;
 +import jalview.ws.params.ArgumentI;
 +import jalview.ws.params.AutoCalcSetting;
 +import jalview.ws.params.WsParamSetI;
 +import jalview.xml.binding.jalview.AlcodonFrame;
 +import jalview.xml.binding.jalview.AlcodonFrame.AlcodMap;
 +import jalview.xml.binding.jalview.Annotation;
 +import jalview.xml.binding.jalview.Annotation.ThresholdLine;
 +import jalview.xml.binding.jalview.AnnotationColourScheme;
 +import jalview.xml.binding.jalview.AnnotationElement;
 +import jalview.xml.binding.jalview.DoubleMatrix;
 +import jalview.xml.binding.jalview.DoubleVector;
 +import jalview.xml.binding.jalview.Feature;
 +import jalview.xml.binding.jalview.Feature.OtherData;
 +import jalview.xml.binding.jalview.FeatureMatcherSet.CompoundMatcher;
 +import jalview.xml.binding.jalview.FilterBy;
 +import jalview.xml.binding.jalview.JalviewModel;
 +import jalview.xml.binding.jalview.JalviewModel.FeatureSettings;
 +import jalview.xml.binding.jalview.JalviewModel.FeatureSettings.Group;
 +import jalview.xml.binding.jalview.JalviewModel.FeatureSettings.Setting;
 +import jalview.xml.binding.jalview.JalviewModel.JGroup;
 +import jalview.xml.binding.jalview.JalviewModel.JSeq;
 +import jalview.xml.binding.jalview.JalviewModel.JSeq.Pdbids;
 +import jalview.xml.binding.jalview.JalviewModel.JSeq.Pdbids.StructureState;
 +import jalview.xml.binding.jalview.JalviewModel.JSeq.RnaViewer;
 +import jalview.xml.binding.jalview.JalviewModel.JSeq.RnaViewer.SecondaryStructure;
 +import jalview.xml.binding.jalview.JalviewModel.PcaViewer;
 +import jalview.xml.binding.jalview.JalviewModel.PcaViewer.Axis;
 +import jalview.xml.binding.jalview.JalviewModel.PcaViewer.SeqPointMax;
 +import jalview.xml.binding.jalview.JalviewModel.PcaViewer.SeqPointMin;
 +import jalview.xml.binding.jalview.JalviewModel.PcaViewer.SequencePoint;
 +import jalview.xml.binding.jalview.JalviewModel.Tree;
 +import jalview.xml.binding.jalview.JalviewModel.UserColours;
 +import jalview.xml.binding.jalview.JalviewModel.Viewport;
 +import jalview.xml.binding.jalview.JalviewModel.Viewport.CalcIdParam;
 +import jalview.xml.binding.jalview.JalviewModel.Viewport.HiddenColumns;
 +import jalview.xml.binding.jalview.JalviewUserColours;
 +import jalview.xml.binding.jalview.JalviewUserColours.Colour;
 +import jalview.xml.binding.jalview.MapListType.MapListFrom;
 +import jalview.xml.binding.jalview.MapListType.MapListTo;
 +import jalview.xml.binding.jalview.Mapping;
 +import jalview.xml.binding.jalview.NoValueColour;
 +import jalview.xml.binding.jalview.ObjectFactory;
 +import jalview.xml.binding.jalview.PcaDataType;
 +import jalview.xml.binding.jalview.Pdbentry.Property;
 +import jalview.xml.binding.jalview.Sequence;
 +import jalview.xml.binding.jalview.Sequence.DBRef;
 +import jalview.xml.binding.jalview.SequenceSet;
 +import jalview.xml.binding.jalview.SequenceSet.SequenceSetProperties;
 +import jalview.xml.binding.jalview.ThresholdType;
 +import jalview.xml.binding.jalview.VAMSAS;
 +
 +/**
 + * Write out the current jalview desktop state as a Jalview XML stream.
 + * 
 + * Note: the vamsas objects referred to here are primitive versions of the
 + * VAMSAS project schema elements - they are not the same and most likely never
 + * will be :)
 + * 
 + * @author $author$
 + * @version $Revision: 1.134 $
 + */
 +public class Jalview2XML
 +{
 +
 +  // BH 2018 we add the .jvp binary extension to J2S so that
 +  // it will declare that binary when we do the file save from the browser
 +
 +  static
 +  {
 +    Platform.addJ2SBinaryType(".jvp?");
 +  }
 +
 +  private static final String VIEWER_PREFIX = "viewer_";
 +
 +  private static final String RNA_PREFIX = "rna_";
 +
 +  private static final String UTF_8 = "UTF-8";
 +
 +  /**
 +   * prefix for recovering datasets for alignments with multiple views where
 +   * non-existent dataset IDs were written for some views
 +   */
 +  private static final String UNIQSEQSETID = "uniqueSeqSetId.";
 +
 +  // use this with nextCounter() to make unique names for entities
 +  private int counter = 0;
 +
 +  /*
 +   * SequenceI reference -> XML ID string in jalview XML. Populated as XML reps
 +   * of sequence objects are created.
 +   */
 +  IdentityHashMap<SequenceI, String> seqsToIds = null;
 +
 +  /**
 +   * jalview XML Sequence ID to jalview sequence object reference (both dataset
 +   * and alignment sequences. Populated as XML reps of sequence objects are
 +   * created.)
 +   */
 +  Map<String, SequenceI> seqRefIds = null;
 +
 +  Map<String, SequenceI> incompleteSeqs = null;
 +
 +  List<SeqFref> frefedSequence = null;
 +
 +  boolean raiseGUI = true; // whether errors are raised in dialog boxes or not
 +
 +  /*
 +   * Map of reconstructed AlignFrame objects that appear to have come from
 +   * SplitFrame objects (have a dna/protein complement view).
 +   */
 +  private Map<Viewport, AlignFrame> splitFrameCandidates = new HashMap<>();
 +
 +  /*
 +   * Map from displayed rna structure models to their saved session state jar
 +   * entry names
 +   */
 +  private Map<RnaModel, String> rnaSessions = new HashMap<>();
 +
 +  /**
 +   * A helper method for safely using the value of an optional attribute that
 +   * may be null if not present in the XML. Answers the boolean value, or false
 +   * if null.
 +   * 
 +   * @param b
 +   * @return
 +   */
 +  public static boolean safeBoolean(Boolean b)
 +  {
 +    return b == null ? false : b.booleanValue();
 +  }
 +
 +  /**
 +   * A helper method for safely using the value of an optional attribute that
 +   * may be null if not present in the XML. Answers the integer value, or zero
 +   * if null.
 +   * 
 +   * @param i
 +   * @return
 +   */
 +  public static int safeInt(Integer i)
 +  {
 +    return i == null ? 0 : i.intValue();
 +  }
 +
 +  /**
 +   * A helper method for safely using the value of an optional attribute that
 +   * may be null if not present in the XML. Answers the float value, or zero if
 +   * null.
 +   * 
 +   * @param f
 +   * @return
 +   */
 +  public static float safeFloat(Float f)
 +  {
 +    return f == null ? 0f : f.floatValue();
 +  }
 +
 +  /**
 +   * create/return unique hash string for sq
 +   * 
 +   * @param sq
 +   * @return new or existing unique string for sq
 +   */
 +  String seqHash(SequenceI sq)
 +  {
 +    if (seqsToIds == null)
 +    {
 +      initSeqRefs();
 +    }
 +    if (seqsToIds.containsKey(sq))
 +    {
 +      return seqsToIds.get(sq);
 +    }
 +    else
 +    {
 +      // create sequential key
 +      String key = "sq" + (seqsToIds.size() + 1);
 +      key = makeHashCode(sq, key); // check we don't have an external reference
 +      // for it already.
 +      seqsToIds.put(sq, key);
 +      return key;
 +    }
 +  }
 +
 +  void initSeqRefs()
 +  {
 +    if (seqsToIds == null)
 +    {
 +      seqsToIds = new IdentityHashMap<>();
 +    }
 +    if (seqRefIds == null)
 +    {
 +      seqRefIds = new HashMap<>();
 +    }
 +    if (incompleteSeqs == null)
 +    {
 +      incompleteSeqs = new HashMap<>();
 +    }
 +    if (frefedSequence == null)
 +    {
 +      frefedSequence = new ArrayList<>();
 +    }
 +  }
 +
 +  public Jalview2XML()
 +  {
 +  }
 +
 +  public Jalview2XML(boolean raiseGUI)
 +  {
 +    this.raiseGUI = raiseGUI;
 +  }
 +
 +  /**
 +   * base class for resolving forward references to sequences by their ID
 +   * 
 +   * @author jprocter
 +   *
 +   */
 +  abstract class SeqFref
 +  {
 +    String sref;
 +
 +    String type;
 +
 +    public SeqFref(String _sref, String type)
 +    {
 +      sref = _sref;
 +      this.type = type;
 +    }
 +
 +    public String getSref()
 +    {
 +      return sref;
 +    }
 +
 +    public SequenceI getSrefSeq()
 +    {
 +      return seqRefIds.get(sref);
 +    }
 +
 +    public boolean isResolvable()
 +    {
 +      return seqRefIds.get(sref) != null;
 +    }
 +
 +    public SequenceI getSrefDatasetSeq()
 +    {
 +      SequenceI sq = seqRefIds.get(sref);
 +      if (sq != null)
 +      {
 +        while (sq.getDatasetSequence() != null)
 +        {
 +          sq = sq.getDatasetSequence();
 +        }
 +      }
 +      return sq;
 +    }
 +
 +    /**
 +     * @return true if the forward reference was fully resolved
 +     */
 +    abstract boolean resolve();
 +
 +    @Override
 +    public String toString()
 +    {
 +      return type + " reference to " + sref;
 +    }
 +  }
 +
 +  /**
 +   * create forward reference for a mapping
 +   * 
 +   * @param sref
 +   * @param _jmap
 +   * @return
 +   */
 +  public SeqFref newMappingRef(final String sref,
 +          final jalview.datamodel.Mapping _jmap)
 +  {
 +    SeqFref fref = new SeqFref(sref, "Mapping")
 +    {
 +      public jalview.datamodel.Mapping jmap = _jmap;
 +
 +      @Override
 +      boolean resolve()
 +      {
 +        SequenceI seq = getSrefDatasetSeq();
 +        if (seq == null)
 +        {
 +          return false;
 +        }
 +        jmap.setTo(seq);
 +        return true;
 +      }
 +    };
 +    return fref;
 +  }
 +
 +  public SeqFref newAlcodMapRef(final String sref,
 +          final AlignedCodonFrame _cf,
 +          final jalview.datamodel.Mapping _jmap)
 +  {
 +
 +    SeqFref fref = new SeqFref(sref, "Codon Frame")
 +    {
 +      AlignedCodonFrame cf = _cf;
 +
 +      public jalview.datamodel.Mapping mp = _jmap;
 +
 +      @Override
 +      public boolean isResolvable()
 +      {
 +        return super.isResolvable() && mp.getTo() != null;
 +      }
 +
 +      @Override
 +      boolean resolve()
 +      {
 +        SequenceI seq = getSrefDatasetSeq();
 +        if (seq == null)
 +        {
 +          return false;
 +        }
 +        cf.addMap(seq, mp.getTo(), mp.getMap());
 +        return true;
 +      }
 +    };
 +    return fref;
 +  }
 +
 +  public void resolveFrefedSequences()
 +  {
 +    Iterator<SeqFref> nextFref = frefedSequence.iterator();
 +    int toresolve = frefedSequence.size();
 +    int unresolved = 0, failedtoresolve = 0;
 +    while (nextFref.hasNext())
 +    {
 +      SeqFref ref = nextFref.next();
 +      if (ref.isResolvable())
 +      {
 +        try
 +        {
 +          if (ref.resolve())
 +          {
 +            nextFref.remove();
 +          }
 +          else
 +          {
 +            failedtoresolve++;
 +          }
 +        } catch (Exception x)
 +        {
 +          System.err.println(
 +                  "IMPLEMENTATION ERROR: Failed to resolve forward reference for sequence "
 +                          + ref.getSref());
 +          x.printStackTrace();
 +          failedtoresolve++;
 +        }
 +      }
 +      else
 +      {
 +        unresolved++;
 +      }
 +    }
 +    if (unresolved > 0)
 +    {
 +      System.err.println("Jalview Project Import: There were " + unresolved
 +              + " forward references left unresolved on the stack.");
 +    }
 +    if (failedtoresolve > 0)
 +    {
 +      System.err.println("SERIOUS! " + failedtoresolve
 +              + " resolvable forward references failed to resolve.");
 +    }
 +    if (incompleteSeqs != null && incompleteSeqs.size() > 0)
 +    {
 +      System.err.println(
 +              "Jalview Project Import: There are " + incompleteSeqs.size()
 +                      + " sequences which may have incomplete metadata.");
 +      if (incompleteSeqs.size() < 10)
 +      {
 +        for (SequenceI s : incompleteSeqs.values())
 +        {
 +          System.err.println(s.toString());
 +        }
 +      }
 +      else
 +      {
 +        System.err.println(
 +                "Too many to report. Skipping output of incomplete sequences.");
 +      }
 +    }
 +  }
 +
 +  /**
 +   * This maintains a map of viewports, the key being the seqSetId. Important to
 +   * set historyItem and redoList for multiple views
 +   */
 +  Map<String, AlignViewport> viewportsAdded = new HashMap<>();
 +
 +  Map<String, AlignmentAnnotation> annotationIds = new HashMap<>();
 +
 +  String uniqueSetSuffix = "";
 +
 +  /**
 +   * List of pdbfiles added to Jar
 +   */
 +  List<String> pdbfiles = null;
 +
 +  // SAVES SEVERAL ALIGNMENT WINDOWS TO SAME JARFILE
 +  public void saveState(File statefile)
 +  {
 +    FileOutputStream fos = null;
 +
 +    try
 +    {
 +
 +      fos = new FileOutputStream(statefile);
 +
 +      JarOutputStream jout = new JarOutputStream(fos);
 +      saveState(jout);
 +      fos.close();
 +
 +    } catch (Exception e)
 +    {
 +      Console.error("Couln't write Jalview state to " + statefile, e);
 +      // TODO: inform user of the problem - they need to know if their data was
 +      // not saved !
 +      if (errorMessage == null)
 +      {
 +        errorMessage = "Did't write Jalview Archive to output file '"
 +                + statefile + "' - See console error log for details";
 +      }
 +      else
 +      {
 +        errorMessage += "(Didn't write Jalview Archive to output file '"
 +                + statefile + ")";
 +      }
 +      e.printStackTrace();
 +    } finally
 +    {
 +      if (fos != null)
 +      {
 +        try
 +        {
 +          fos.close();
 +        } catch (IOException e)
 +        {
 +          // ignore
 +        }
 +      }
 +    }
 +    reportErrors();
 +  }
 +
 +  /**
 +   * Writes a jalview project archive to the given Jar output stream.
 +   * 
 +   * @param jout
 +   */
 +  public void saveState(JarOutputStream jout)
 +  {
 +    AlignFrame[] frames = Desktop.getAlignFrames();
 +
 +    if (frames == null)
 +    {
 +      return;
 +    }
 +    saveAllFrames(Arrays.asList(frames), jout);
 +  }
 +
 +  /**
 +   * core method for storing state for a set of AlignFrames.
 +   * 
 +   * @param frames
 +   *          - frames involving all data to be exported (including containing
 +   *          splitframes)
 +   * @param jout
 +   *          - project output stream
 +   */
 +  private void saveAllFrames(List<AlignFrame> frames, JarOutputStream jout)
 +  {
 +    Hashtable<String, AlignFrame> dsses = new Hashtable<>();
 +
 +    /*
 +     * ensure cached data is clear before starting
 +     */
 +    // todo tidy up seqRefIds, seqsToIds initialisation / reset
 +    rnaSessions.clear();
 +    splitFrameCandidates.clear();
 +
 +    try
 +    {
 +
 +      // NOTE UTF-8 MUST BE USED FOR WRITING UNICODE CHARS
 +      // //////////////////////////////////////////////////
 +
 +      List<String> shortNames = new ArrayList<>();
 +      List<String> viewIds = new ArrayList<>();
 +
 +      // REVERSE ORDER
 +      for (int i = frames.size() - 1; i > -1; i--)
 +      {
 +        AlignFrame af = frames.get(i);
 +        // skip ?
 +        if (skipList != null && skipList
 +                .containsKey(af.getViewport().getSequenceSetId()))
 +        {
 +          continue;
 +        }
 +
 +        String shortName = makeFilename(af, shortNames);
 +
 +        int apSize = af.getAlignPanels().size();
 +
 +        for (int ap = 0; ap < apSize; ap++)
 +        {
 +          AlignmentPanel apanel = (AlignmentPanel) af.getAlignPanels()
 +                  .get(ap);
 +          String fileName = apSize == 1 ? shortName : ap + shortName;
 +          if (!fileName.endsWith(".xml"))
 +          {
 +            fileName = fileName + ".xml";
 +          }
 +
 +          saveState(apanel, fileName, jout, viewIds);
 +
 +          String dssid = getDatasetIdRef(
 +                  af.getViewport().getAlignment().getDataset());
 +          if (!dsses.containsKey(dssid))
 +          {
 +            dsses.put(dssid, af);
 +          }
 +        }
 +      }
 +
 +      writeDatasetFor(dsses, "" + jout.hashCode() + " " + uniqueSetSuffix,
 +              jout);
 +
 +      try
 +      {
 +        jout.flush();
 +      } catch (Exception foo)
 +      {
 +      }
 +      jout.close();
 +    } catch (Exception ex)
 +    {
 +      // TODO: inform user of the problem - they need to know if their data was
 +      // not saved !
 +      if (errorMessage == null)
 +      {
 +        errorMessage = "Couldn't write Jalview Archive - see error output for details";
 +      }
 +      ex.printStackTrace();
 +    }
 +  }
 +
 +  /**
 +   * Generates a distinct file name, based on the title of the AlignFrame, by
 +   * appending _n for increasing n until an unused name is generated. The new
 +   * name (without its extension) is added to the list.
 +   * 
 +   * @param af
 +   * @param namesUsed
 +   * @return the generated name, with .xml extension
 +   */
 +  protected String makeFilename(AlignFrame af, List<String> namesUsed)
 +  {
 +    String shortName = af.getTitle();
 +
 +    if (shortName.indexOf(File.separatorChar) > -1)
 +    {
 +      shortName = shortName
 +              .substring(shortName.lastIndexOf(File.separatorChar) + 1);
 +    }
 +
 +    int count = 1;
 +
 +    while (namesUsed.contains(shortName))
 +    {
 +      if (shortName.endsWith("_" + (count - 1)))
 +      {
 +        shortName = shortName.substring(0, shortName.lastIndexOf("_"));
 +      }
 +
 +      shortName = shortName.concat("_" + count);
 +      count++;
 +    }
 +
 +    namesUsed.add(shortName);
 +
 +    if (!shortName.endsWith(".xml"))
 +    {
 +      shortName = shortName + ".xml";
 +    }
 +    return shortName;
 +  }
 +
 +  // USE THIS METHOD TO SAVE A SINGLE ALIGNMENT WINDOW
 +  public boolean saveAlignment(AlignFrame af, String jarFile,
 +          String fileName)
 +  {
 +    try
 +    {
 +      // create backupfiles object and get new temp filename destination
 +      boolean doBackup = BackupFiles.getEnabled();
 +      BackupFiles backupfiles = doBackup ? new BackupFiles(jarFile) : null;
 +      FileOutputStream fos = new FileOutputStream(
 +              doBackup ? backupfiles.getTempFilePath() : jarFile);
 +
 +      JarOutputStream jout = new JarOutputStream(fos);
 +      List<AlignFrame> frames = new ArrayList<>();
 +
 +      // resolve splitframes
 +      if (af.getViewport().getCodingComplement() != null)
 +      {
 +        frames = ((SplitFrame) af.getSplitViewContainer()).getAlignFrames();
 +      }
 +      else
 +      {
 +        frames.add(af);
 +      }
 +      saveAllFrames(frames, jout);
 +      try
 +      {
 +        jout.flush();
 +      } catch (Exception foo)
 +      {
 +      }
 +      jout.close();
 +      boolean success = true;
 +
 +      if (doBackup)
 +      {
 +        backupfiles.setWriteSuccess(success);
 +        success = backupfiles.rollBackupsAndRenameTempFile();
 +      }
 +
 +      return success;
 +    } catch (Exception ex)
 +    {
 +      errorMessage = "Couldn't Write alignment view to Jalview Archive - see error output for details";
 +      ex.printStackTrace();
 +      return false;
 +    }
 +  }
 +
 +  private void writeDatasetFor(Hashtable<String, AlignFrame> dsses,
 +          String fileName, JarOutputStream jout)
 +  {
 +
 +    for (String dssids : dsses.keySet())
 +    {
 +      AlignFrame _af = dsses.get(dssids);
 +      String jfileName = fileName + " Dataset for " + _af.getTitle();
 +      if (!jfileName.endsWith(".xml"))
 +      {
 +        jfileName = jfileName + ".xml";
 +      }
 +      saveState(_af.alignPanel, jfileName, true, jout, null);
 +    }
 +  }
 +
 +  /**
 +   * create a JalviewModel from an alignment view and marshall it to a
 +   * JarOutputStream
 +   * 
 +   * @param ap
 +   *          panel to create jalview model for
 +   * @param fileName
 +   *          name of alignment panel written to output stream
 +   * @param jout
 +   *          jar output stream
 +   * @param viewIds
 +   * @param out
 +   *          jar entry name
 +   */
 +  public JalviewModel saveState(AlignmentPanel ap, String fileName,
 +          JarOutputStream jout, List<String> viewIds)
 +  {
 +    return saveState(ap, fileName, false, jout, viewIds);
 +  }
 +
 +  /**
 +   * create a JalviewModel from an alignment view and marshall it to a
 +   * JarOutputStream
 +   * 
 +   * @param ap
 +   *          panel to create jalview model for
 +   * @param fileName
 +   *          name of alignment panel written to output stream
 +   * @param storeDS
 +   *          when true, only write the dataset for the alignment, not the data
 +   *          associated with the view.
 +   * @param jout
 +   *          jar output stream
 +   * @param out
 +   *          jar entry name
 +   */
 +  public JalviewModel saveState(AlignmentPanel ap, String fileName,
 +          boolean storeDS, JarOutputStream jout, List<String> viewIds)
 +  {
 +    if (viewIds == null)
 +    {
 +      viewIds = new ArrayList<>();
 +    }
 +
 +    initSeqRefs();
 +
 +    List<UserColourScheme> userColours = new ArrayList<>();
 +
 +    AlignViewport av = ap.av;
 +    ViewportRanges vpRanges = av.getRanges();
 +
 +    final ObjectFactory objectFactory = new ObjectFactory();
 +    JalviewModel object = objectFactory.createJalviewModel();
 +    object.setVamsasModel(new VAMSAS());
 +
 +    // object.setCreationDate(new java.util.Date(System.currentTimeMillis()));
 +    try
 +    {
 +      GregorianCalendar c = new GregorianCalendar();
 +      DatatypeFactory datatypeFactory = DatatypeFactory.newInstance();
 +      XMLGregorianCalendar now = datatypeFactory.newXMLGregorianCalendar(c);// gregorianCalendar);
 +      object.setCreationDate(now);
 +    } catch (DatatypeConfigurationException e)
 +    {
 +      System.err.println("error writing date: " + e.toString());
 +    }
 +    object.setVersion(Cache.getDefault("VERSION", "Development Build"));
 +
 +    /**
 +     * rjal is full height alignment, jal is actual alignment with full metadata
 +     * but excludes hidden sequences.
 +     */
 +    jalview.datamodel.AlignmentI rjal = av.getAlignment(), jal = rjal;
 +
 +    if (av.hasHiddenRows())
 +    {
 +      rjal = jal.getHiddenSequences().getFullAlignment();
 +    }
 +
 +    SequenceSet vamsasSet = new SequenceSet();
 +    Sequence vamsasSeq;
 +    // JalviewModelSequence jms = new JalviewModelSequence();
 +
 +    vamsasSet.setGapChar(jal.getGapCharacter() + "");
 +
 +    if (jal.getDataset() != null)
 +    {
 +      // dataset id is the dataset's hashcode
 +      vamsasSet.setDatasetId(getDatasetIdRef(jal.getDataset()));
 +      if (storeDS)
 +      {
 +        // switch jal and the dataset
 +        jal = jal.getDataset();
 +        rjal = jal;
 +      }
 +    }
 +    if (jal.getProperties() != null)
 +    {
 +      Enumeration en = jal.getProperties().keys();
 +      while (en.hasMoreElements())
 +      {
 +        String key = en.nextElement().toString();
 +        SequenceSetProperties ssp = new SequenceSetProperties();
 +        ssp.setKey(key);
 +        ssp.setValue(jal.getProperties().get(key).toString());
 +        // vamsasSet.addSequenceSetProperties(ssp);
 +        vamsasSet.getSequenceSetProperties().add(ssp);
 +      }
 +    }
 +
 +    JSeq jseq;
 +    Set<String> calcIdSet = new HashSet<>();
 +    // record the set of vamsas sequence XML POJO we create.
 +    HashMap<String, Sequence> vamsasSetIds = new HashMap<>();
 +    // SAVE SEQUENCES
 +    for (final SequenceI jds : rjal.getSequences())
 +    {
 +      final SequenceI jdatasq = jds.getDatasetSequence() == null ? jds
 +              : jds.getDatasetSequence();
 +      String id = seqHash(jds);
 +      if (vamsasSetIds.get(id) == null)
 +      {
 +        if (seqRefIds.get(id) != null && !storeDS)
 +        {
 +          // This happens for two reasons: 1. multiple views are being
 +          // serialised.
 +          // 2. the hashCode has collided with another sequence's code. This
 +          // DOES
 +          // HAPPEN! (PF00072.15.stk does this)
 +          // JBPNote: Uncomment to debug writing out of files that do not read
 +          // back in due to ArrayOutOfBoundExceptions.
 +          // System.err.println("vamsasSeq backref: "+id+"");
 +          // System.err.println(jds.getName()+"
 +          // "+jds.getStart()+"-"+jds.getEnd()+" "+jds.getSequenceAsString());
 +          // System.err.println("Hashcode: "+seqHash(jds));
 +          // SequenceI rsq = (SequenceI) seqRefIds.get(id + "");
 +          // System.err.println(rsq.getName()+"
 +          // "+rsq.getStart()+"-"+rsq.getEnd()+" "+rsq.getSequenceAsString());
 +          // System.err.println("Hashcode: "+seqHash(rsq));
 +        }
 +        else
 +        {
 +          vamsasSeq = createVamsasSequence(id, jds);
 +          // vamsasSet.addSequence(vamsasSeq);
 +          vamsasSet.getSequence().add(vamsasSeq);
 +          vamsasSetIds.put(id, vamsasSeq);
 +          seqRefIds.put(id, jds);
 +        }
 +      }
 +      jseq = new JSeq();
 +      jseq.setStart(jds.getStart());
 +      jseq.setEnd(jds.getEnd());
 +      jseq.setColour(av.getSequenceColour(jds).getRGB());
 +
 +      jseq.setId(id); // jseq id should be a string not a number
 +      if (!storeDS)
 +      {
 +        // Store any sequences this sequence represents
 +        if (av.hasHiddenRows())
 +        {
 +          // use rjal, contains the full height alignment
 +          jseq.setHidden(
 +                  av.getAlignment().getHiddenSequences().isHidden(jds));
 +
 +          if (av.isHiddenRepSequence(jds))
 +          {
 +            jalview.datamodel.SequenceI[] reps = av
 +                    .getRepresentedSequences(jds).getSequencesInOrder(rjal);
 +
 +            for (int h = 0; h < reps.length; h++)
 +            {
 +              if (reps[h] != jds)
 +              {
 +                // jseq.addHiddenSequences(rjal.findIndex(reps[h]));
 +                jseq.getHiddenSequences().add(rjal.findIndex(reps[h]));
 +              }
 +            }
 +          }
 +        }
 +        // mark sequence as reference - if it is the reference for this view
 +        if (jal.hasSeqrep())
 +        {
 +          jseq.setViewreference(jds == jal.getSeqrep());
 +        }
 +      }
 +
 +      // TODO: omit sequence features from each alignment view's XML dump if we
 +      // are storing dataset
 +      List<SequenceFeature> sfs = jds.getSequenceFeatures();
 +      for (SequenceFeature sf : sfs)
 +      {
 +        // Features features = new Features();
 +        Feature features = new Feature();
 +
 +        features.setBegin(sf.getBegin());
 +        features.setEnd(sf.getEnd());
 +        features.setDescription(sf.getDescription());
 +        features.setType(sf.getType());
 +        features.setFeatureGroup(sf.getFeatureGroup());
 +        features.setScore(sf.getScore());
 +        if (sf.links != null)
 +        {
 +          for (int l = 0; l < sf.links.size(); l++)
 +          {
 +            OtherData keyValue = new OtherData();
 +            keyValue.setKey("LINK_" + l);
 +            keyValue.setValue(sf.links.elementAt(l).toString());
 +            // features.addOtherData(keyValue);
 +            features.getOtherData().add(keyValue);
 +          }
 +        }
 +        if (sf.otherDetails != null)
 +        {
 +          /*
 +           * save feature attributes, which may be simple strings or
 +           * map valued (have sub-attributes)
 +           */
 +          for (Entry<String, Object> entry : sf.otherDetails.entrySet())
 +          {
 +            String key = entry.getKey();
 +            Object value = entry.getValue();
 +            if (value instanceof Map<?, ?>)
 +            {
 +              for (Entry<String, Object> subAttribute : ((Map<String, Object>) value)
 +                      .entrySet())
 +              {
 +                OtherData otherData = new OtherData();
 +                otherData.setKey(key);
 +                otherData.setKey2(subAttribute.getKey());
 +                otherData.setValue(subAttribute.getValue().toString());
 +                // features.addOtherData(otherData);
 +                features.getOtherData().add(otherData);
 +              }
 +            }
 +            else
 +            {
 +              OtherData otherData = new OtherData();
 +              otherData.setKey(key);
 +              otherData.setValue(value.toString());
 +              // features.addOtherData(otherData);
 +              features.getOtherData().add(otherData);
 +            }
 +          }
 +        }
 +
 +        // jseq.addFeatures(features);
 +        jseq.getFeatures().add(features);
 +      }
 +
 +      if (jdatasq.getAllPDBEntries() != null)
 +      {
 +        Enumeration<PDBEntry> en = jdatasq.getAllPDBEntries().elements();
 +        while (en.hasMoreElements())
 +        {
 +          Pdbids pdb = new Pdbids();
 +          jalview.datamodel.PDBEntry entry = en.nextElement();
 +
 +          String pdbId = entry.getId();
 +          pdb.setId(pdbId);
 +          pdb.setType(entry.getType());
 +
 +          /*
 +           * Store any structure views associated with this sequence. This
 +           * section copes with duplicate entries in the project, so a dataset
 +           * only view *should* be coped with sensibly.
 +           */
 +          // This must have been loaded, is it still visible?
 +          JInternalFrame[] frames = Desktop.desktop.getAllFrames();
 +          String matchedFile = null;
 +          for (int f = frames.length - 1; f > -1; f--)
 +          {
 +            if (frames[f] instanceof StructureViewerBase)
 +            {
 +              StructureViewerBase viewFrame = (StructureViewerBase) frames[f];
 +              matchedFile = saveStructureViewer(ap, jds, pdb, entry,
 +                      viewIds, matchedFile, viewFrame);
 +              /*
 +               * Only store each structure viewer's state once in the project
 +               * jar. First time through only (storeDS==false)
 +               */
 +              String viewId = viewFrame.getViewId();
 +              String viewerType = viewFrame.getViewerType().toString();
 +              if (!storeDS && !viewIds.contains(viewId))
 +              {
 +                viewIds.add(viewId);
 +                File viewerState = viewFrame.saveSession();
 +                if (viewerState != null)
 +                {
 +                  copyFileToJar(jout, viewerState.getPath(),
 +                          getViewerJarEntryName(viewId), viewerType);
 +                }
 +                else
 +                {
 +                  Console.error(
 +                          "Failed to save viewer state for " + viewerType);
 +                }
 +              }
 +            }
 +          }
 +
 +          if (matchedFile != null || entry.getFile() != null)
 +          {
 +            if (entry.getFile() != null)
 +            {
 +              // use entry's file
 +              matchedFile = entry.getFile();
 +            }
 +            pdb.setFile(matchedFile); // entry.getFile());
 +            if (pdbfiles == null)
 +            {
 +              pdbfiles = new ArrayList<>();
 +            }
 +
 +            if (!pdbfiles.contains(pdbId))
 +            {
 +              pdbfiles.add(pdbId);
 +              copyFileToJar(jout, matchedFile, pdbId, pdbId);
 +            }
 +          }
 +
 +          Enumeration<String> props = entry.getProperties();
 +          if (props.hasMoreElements())
 +          {
 +            // PdbentryItem item = new PdbentryItem();
 +            while (props.hasMoreElements())
 +            {
 +              Property prop = new Property();
 +              String key = props.nextElement();
 +              prop.setName(key);
 +              prop.setValue(entry.getProperty(key).toString());
 +              // item.addProperty(prop);
 +              pdb.getProperty().add(prop);
 +            }
 +            // pdb.addPdbentryItem(item);
 +          }
 +
 +          // jseq.addPdbids(pdb);
 +          jseq.getPdbids().add(pdb);
 +        }
 +      }
 +
 +      saveRnaViewers(jout, jseq, jds, viewIds, ap, storeDS);
 +
 +      // jms.addJSeq(jseq);
 +      object.getJSeq().add(jseq);
 +    }
 +
 +    if (!storeDS && av.hasHiddenRows())
 +    {
 +      jal = av.getAlignment();
 +    }
 +    // SAVE MAPPINGS
 +    // FOR DATASET
 +    if (storeDS && jal.getCodonFrames() != null)
 +    {
 +      List<AlignedCodonFrame> jac = jal.getCodonFrames();
 +      for (AlignedCodonFrame acf : jac)
 +      {
 +        AlcodonFrame alc = new AlcodonFrame();
 +        if (acf.getProtMappings() != null
 +                && acf.getProtMappings().length > 0)
 +        {
 +          boolean hasMap = false;
 +          SequenceI[] dnas = acf.getdnaSeqs();
 +          jalview.datamodel.Mapping[] pmaps = acf.getProtMappings();
 +          for (int m = 0; m < pmaps.length; m++)
 +          {
 +            AlcodMap alcmap = new AlcodMap();
 +            alcmap.setDnasq(seqHash(dnas[m]));
 +            alcmap.setMapping(
 +                    createVamsasMapping(pmaps[m], dnas[m], null, false));
 +            // alc.addAlcodMap(alcmap);
 +            alc.getAlcodMap().add(alcmap);
 +            hasMap = true;
 +          }
 +          if (hasMap)
 +          {
 +            // vamsasSet.addAlcodonFrame(alc);
 +            vamsasSet.getAlcodonFrame().add(alc);
 +          }
 +        }
 +        // TODO: delete this ? dead code from 2.8.3->2.9 ?
 +        // {
 +        // AlcodonFrame alc = new AlcodonFrame();
 +        // vamsasSet.addAlcodonFrame(alc);
 +        // for (int p = 0; p < acf.aaWidth; p++)
 +        // {
 +        // Alcodon cmap = new Alcodon();
 +        // if (acf.codons[p] != null)
 +        // {
 +        // // Null codons indicate a gapped column in the translated peptide
 +        // // alignment.
 +        // cmap.setPos1(acf.codons[p][0]);
 +        // cmap.setPos2(acf.codons[p][1]);
 +        // cmap.setPos3(acf.codons[p][2]);
 +        // }
 +        // alc.addAlcodon(cmap);
 +        // }
 +        // if (acf.getProtMappings() != null
 +        // && acf.getProtMappings().length > 0)
 +        // {
 +        // SequenceI[] dnas = acf.getdnaSeqs();
 +        // jalview.datamodel.Mapping[] pmaps = acf.getProtMappings();
 +        // for (int m = 0; m < pmaps.length; m++)
 +        // {
 +        // AlcodMap alcmap = new AlcodMap();
 +        // alcmap.setDnasq(seqHash(dnas[m]));
 +        // alcmap.setMapping(createVamsasMapping(pmaps[m], dnas[m], null,
 +        // false));
 +        // alc.addAlcodMap(alcmap);
 +        // }
 +        // }
 +      }
 +    }
 +
 +    // SAVE TREES
 +    // /////////////////////////////////
 +    if (!storeDS && av.getCurrentTree() != null)
 +    {
 +      // FIND ANY ASSOCIATED TREES
 +      // NOT IMPLEMENTED FOR HEADLESS STATE AT PRESENT
 +      if (Desktop.desktop != null)
 +      {
 +        JInternalFrame[] frames = Desktop.desktop.getAllFrames();
 +
 +        for (int t = 0; t < frames.length; t++)
 +        {
 +          if (frames[t] instanceof TreePanel)
 +          {
 +            TreePanel tp = (TreePanel) frames[t];
 +
 +            if (tp.getTreeCanvas().getViewport().getAlignment() == jal)
 +            {
 +              JalviewModel.Tree tree = new JalviewModel.Tree();
 +              tree.setTitle(tp.getTitle());
 +              tree.setCurrentTree((av.getCurrentTree() == tp.getTree()));
 +              tree.setNewick(tp.getTree().print());
 +              tree.setThreshold(tp.getTreeCanvas().getThreshold());
 +
 +              tree.setFitToWindow(tp.fitToWindow.getState());
 +              tree.setFontName(tp.getTreeFont().getName());
 +              tree.setFontSize(tp.getTreeFont().getSize());
 +              tree.setFontStyle(tp.getTreeFont().getStyle());
 +              tree.setMarkUnlinked(tp.placeholdersMenu.getState());
 +
 +              tree.setShowBootstrap(tp.bootstrapMenu.getState());
 +              tree.setShowDistances(tp.distanceMenu.getState());
 +
 +              tree.setHeight(tp.getHeight());
 +              tree.setWidth(tp.getWidth());
 +              tree.setXpos(tp.getX());
 +              tree.setYpos(tp.getY());
 +              tree.setId(makeHashCode(tp, null));
 +              tree.setLinkToAllViews(
 +                      tp.getTreeCanvas().isApplyToAllViews());
 +
 +              // jms.addTree(tree);
 +              object.getTree().add(tree);
 +            }
 +          }
++
 +        }
 +      }
 +    }
++    if (!storeDS && av.getCurrentExtTree() != null)
++    {
++      Set<TreeFrameI> externalTreeViews = TreeViewerUtils
++              .getActiveTreeViews()
++              .keySet();
++      for (TreeFrameI treeView : externalTreeViews)
++      {
++        TreeI tree = treeView.getTree();
++        try
++        {
++          tree.outputAsFile(new File("word"));
++          copyFileToJar(jout, "word", "aptx-test","Archeopteryx Tree Session");
++
++
++        } catch (IOException e)
++        {
++          // TODO Auto-generated catch block
++          e.printStackTrace();
++        }
++
++      }
++
++    }
++
++
 +
 +    /*
 +     * save PCA viewers
 +     */
 +    if (!storeDS && Desktop.desktop != null)
 +    {
 +      for (JInternalFrame frame : Desktop.desktop.getAllFrames())
 +      {
 +        if (frame instanceof PCAPanel)
 +        {
 +          PCAPanel panel = (PCAPanel) frame;
 +          if (panel.getAlignViewport().getAlignment() == jal)
 +          {
 +            savePCA(panel, object);
 +          }
 +        }
 +      }
 +    }
 +
 +    // SAVE ANNOTATIONS
 +    /**
 +     * store forward refs from an annotationRow to any groups
 +     */
 +    IdentityHashMap<SequenceGroup, String> groupRefs = new IdentityHashMap<>();
 +    if (storeDS)
 +    {
 +      for (SequenceI sq : jal.getSequences())
 +      {
 +        // Store annotation on dataset sequences only
 +        AlignmentAnnotation[] aa = sq.getAnnotation();
 +        if (aa != null && aa.length > 0)
 +        {
 +          storeAlignmentAnnotation(aa, groupRefs, av, calcIdSet, storeDS,
 +                  vamsasSet);
 +        }
 +      }
 +    }
 +    else
 +    {
 +      if (jal.getAlignmentAnnotation() != null)
 +      {
 +        // Store the annotation shown on the alignment.
 +        AlignmentAnnotation[] aa = jal.getAlignmentAnnotation();
 +        storeAlignmentAnnotation(aa, groupRefs, av, calcIdSet, storeDS,
 +                vamsasSet);
 +      }
 +    }
 +    // SAVE GROUPS
 +    if (jal.getGroups() != null)
 +    {
 +      JGroup[] groups = new JGroup[jal.getGroups().size()];
 +      int i = -1;
 +      for (jalview.datamodel.SequenceGroup sg : jal.getGroups())
 +      {
 +        JGroup jGroup = new JGroup();
 +        groups[++i] = jGroup;
 +
 +        jGroup.setStart(sg.getStartRes());
 +        jGroup.setEnd(sg.getEndRes());
 +        jGroup.setName(sg.getName());
 +        if (groupRefs.containsKey(sg))
 +        {
 +          // group has references so set its ID field
 +          jGroup.setId(groupRefs.get(sg));
 +        }
 +        ColourSchemeI colourScheme = sg.getColourScheme();
 +        if (colourScheme != null)
 +        {
 +          ResidueShaderI groupColourScheme = sg.getGroupColourScheme();
 +          if (groupColourScheme.conservationApplied())
 +          {
 +            jGroup.setConsThreshold(groupColourScheme.getConservationInc());
 +
 +            if (colourScheme instanceof jalview.schemes.UserColourScheme)
 +            {
 +              jGroup.setColour(setUserColourScheme(colourScheme,
 +                      userColours, object));
 +            }
 +            else
 +            {
 +              jGroup.setColour(colourScheme.getSchemeName());
 +            }
 +          }
 +          else if (colourScheme instanceof jalview.schemes.AnnotationColourGradient)
 +          {
 +            jGroup.setColour("AnnotationColourGradient");
 +            jGroup.setAnnotationColours(constructAnnotationColours(
 +                    (jalview.schemes.AnnotationColourGradient) colourScheme,
 +                    userColours, object));
 +          }
 +          else if (colourScheme instanceof jalview.schemes.UserColourScheme)
 +          {
 +            jGroup.setColour(
 +                    setUserColourScheme(colourScheme, userColours, object));
 +          }
 +          else
 +          {
 +            jGroup.setColour(colourScheme.getSchemeName());
 +          }
 +
 +          jGroup.setPidThreshold(groupColourScheme.getThreshold());
 +        }
 +
 +        jGroup.setOutlineColour(sg.getOutlineColour().getRGB());
 +        jGroup.setDisplayBoxes(sg.getDisplayBoxes());
 +        jGroup.setDisplayText(sg.getDisplayText());
 +        jGroup.setColourText(sg.getColourText());
 +        jGroup.setTextCol1(sg.textColour.getRGB());
 +        jGroup.setTextCol2(sg.textColour2.getRGB());
 +        jGroup.setTextColThreshold(sg.thresholdTextColour);
 +        jGroup.setShowUnconserved(sg.getShowNonconserved());
 +        jGroup.setIgnoreGapsinConsensus(sg.getIgnoreGapsConsensus());
 +        jGroup.setShowConsensusHistogram(sg.isShowConsensusHistogram());
 +        jGroup.setShowSequenceLogo(sg.isShowSequenceLogo());
 +        jGroup.setNormaliseSequenceLogo(sg.isNormaliseSequenceLogo());
 +        for (SequenceI seq : sg.getSequences())
 +        {
 +          // jGroup.addSeq(seqHash(seq));
 +          jGroup.getSeq().add(seqHash(seq));
 +        }
 +      }
 +
 +      // jms.setJGroup(groups);
 +      Object group;
 +      for (JGroup grp : groups)
 +      {
 +        object.getJGroup().add(grp);
 +      }
 +    }
 +    if (!storeDS)
 +    {
 +      // /////////SAVE VIEWPORT
 +      Viewport view = new Viewport();
 +      view.setTitle(ap.alignFrame.getTitle());
 +      view.setSequenceSetId(
 +              makeHashCode(av.getSequenceSetId(), av.getSequenceSetId()));
 +      view.setId(av.getViewId());
 +      if (av.getCodingComplement() != null)
 +      {
 +        view.setComplementId(av.getCodingComplement().getViewId());
 +      }
 +      view.setViewName(av.getViewName());
 +      view.setGatheredViews(av.isGatherViewsHere());
 +
 +      Rectangle size = ap.av.getExplodedGeometry();
 +      Rectangle position = size;
 +      if (size == null)
 +      {
 +        size = ap.alignFrame.getBounds();
 +        if (av.getCodingComplement() != null)
 +        {
 +          position = ((SplitFrame) ap.alignFrame.getSplitViewContainer())
 +                  .getBounds();
 +        }
 +        else
 +        {
 +          position = size;
 +        }
 +      }
 +      view.setXpos(position.x);
 +      view.setYpos(position.y);
 +
 +      view.setWidth(size.width);
 +      view.setHeight(size.height);
 +
 +      view.setStartRes(vpRanges.getStartRes());
 +      view.setStartSeq(vpRanges.getStartSeq());
 +
 +      if (av.getGlobalColourScheme() instanceof jalview.schemes.UserColourScheme)
 +      {
 +        view.setBgColour(setUserColourScheme(av.getGlobalColourScheme(),
 +                userColours, object));
 +      }
 +      else if (av
 +              .getGlobalColourScheme() instanceof jalview.schemes.AnnotationColourGradient)
 +      {
 +        AnnotationColourScheme ac = constructAnnotationColours(
 +                (jalview.schemes.AnnotationColourGradient) av
 +                        .getGlobalColourScheme(),
 +                userColours, object);
 +
 +        view.setAnnotationColours(ac);
 +        view.setBgColour("AnnotationColourGradient");
 +      }
 +      else
 +      {
 +        view.setBgColour(ColourSchemeProperty
 +                .getColourName(av.getGlobalColourScheme()));
 +      }
 +
 +      ResidueShaderI vcs = av.getResidueShading();
 +      ColourSchemeI cs = av.getGlobalColourScheme();
 +
 +      if (cs != null)
 +      {
 +        if (vcs.conservationApplied())
 +        {
 +          view.setConsThreshold(vcs.getConservationInc());
 +          if (cs instanceof jalview.schemes.UserColourScheme)
 +          {
 +            view.setBgColour(setUserColourScheme(cs, userColours, object));
 +          }
 +        }
 +        view.setPidThreshold(vcs.getThreshold());
 +      }
 +
 +      view.setConservationSelected(av.getConservationSelected());
 +      view.setPidSelected(av.getAbovePIDThreshold());
 +      final Font font = av.getFont();
 +      view.setFontName(font.getName());
 +      view.setFontSize(font.getSize());
 +      view.setFontStyle(font.getStyle());
 +      view.setScaleProteinAsCdna(av.getViewStyle().isScaleProteinAsCdna());
 +      view.setRenderGaps(av.isRenderGaps());
 +      view.setShowAnnotation(av.isShowAnnotation());
 +      view.setShowBoxes(av.getShowBoxes());
 +      view.setShowColourText(av.getColourText());
 +      view.setShowFullId(av.getShowJVSuffix());
 +      view.setRightAlignIds(av.isRightAlignIds());
 +      view.setShowSequenceFeatures(av.isShowSequenceFeatures());
 +      view.setShowText(av.getShowText());
 +      view.setShowUnconserved(av.getShowUnconserved());
 +      view.setWrapAlignment(av.getWrapAlignment());
 +      view.setTextCol1(av.getTextColour().getRGB());
 +      view.setTextCol2(av.getTextColour2().getRGB());
 +      view.setTextColThreshold(av.getThresholdTextColour());
 +      view.setShowConsensusHistogram(av.isShowConsensusHistogram());
 +      view.setShowSequenceLogo(av.isShowSequenceLogo());
 +      view.setNormaliseSequenceLogo(av.isNormaliseSequenceLogo());
 +      view.setShowGroupConsensus(av.isShowGroupConsensus());
 +      view.setShowGroupConservation(av.isShowGroupConservation());
 +      view.setShowNPfeatureTooltip(av.isShowNPFeats());
 +      view.setShowDbRefTooltip(av.isShowDBRefs());
 +      view.setFollowHighlight(av.isFollowHighlight());
 +      view.setFollowSelection(av.followSelection);
 +      view.setIgnoreGapsinConsensus(av.isIgnoreGapsConsensus());
 +      view.setShowComplementFeatures(av.isShowComplementFeatures());
 +      view.setShowComplementFeaturesOnTop(
 +              av.isShowComplementFeaturesOnTop());
 +      if (av.getFeaturesDisplayed() != null)
 +      {
 +        FeatureSettings fs = new FeatureSettings();
 +
 +        FeatureRendererModel fr = ap.getSeqPanel().seqCanvas
 +                .getFeatureRenderer();
 +        String[] renderOrder = fr.getRenderOrder().toArray(new String[0]);
 +
 +        Vector<String> settingsAdded = new Vector<>();
 +        if (renderOrder != null)
 +        {
 +          for (String featureType : renderOrder)
 +          {
 +            FeatureSettings.Setting setting = new FeatureSettings.Setting();
 +            setting.setType(featureType);
 +
 +            /*
 +             * save any filter for the feature type
 +             */
 +            FeatureMatcherSetI filter = fr.getFeatureFilter(featureType);
 +            if (filter != null)
 +            {
 +              Iterator<FeatureMatcherI> filters = filter.getMatchers()
 +                      .iterator();
 +              FeatureMatcherI firstFilter = filters.next();
 +              setting.setMatcherSet(Jalview2XML.marshalFilter(firstFilter,
 +                      filters, filter.isAnded()));
 +            }
 +
 +            /*
 +             * save colour scheme for the feature type
 +             */
 +            FeatureColourI fcol = fr.getFeatureStyle(featureType);
 +            if (!fcol.isSimpleColour())
 +            {
 +              setting.setColour(fcol.getMaxColour().getRGB());
 +              setting.setMincolour(fcol.getMinColour().getRGB());
 +              setting.setMin(fcol.getMin());
 +              setting.setMax(fcol.getMax());
 +              setting.setColourByLabel(fcol.isColourByLabel());
 +              if (fcol.isColourByAttribute())
 +              {
 +                String[] attName = fcol.getAttributeName();
 +                setting.getAttributeName().add(attName[0]);
 +                if (attName.length > 1)
 +                {
 +                  setting.getAttributeName().add(attName[1]);
 +                }
 +              }
 +              setting.setAutoScale(fcol.isAutoScaled());
 +              setting.setThreshold(fcol.getThreshold());
 +              Color noColour = fcol.getNoColour();
 +              if (noColour == null)
 +              {
 +                setting.setNoValueColour(NoValueColour.NONE);
 +              }
 +              else if (noColour.equals(fcol.getMaxColour()))
 +              {
 +                setting.setNoValueColour(NoValueColour.MAX);
 +              }
 +              else
 +              {
 +                setting.setNoValueColour(NoValueColour.MIN);
 +              }
 +              // -1 = No threshold, 0 = Below, 1 = Above
 +              setting.setThreshstate(fcol.isAboveThreshold() ? 1
 +                      : (fcol.isBelowThreshold() ? 0 : -1));
 +            }
 +            else
 +            {
 +              setting.setColour(fcol.getColour().getRGB());
 +            }
 +
 +            setting.setDisplay(
 +                    av.getFeaturesDisplayed().isVisible(featureType));
 +            float rorder = fr.getOrder(featureType);
 +            if (rorder > -1)
 +            {
 +              setting.setOrder(rorder);
 +            }
 +            /// fs.addSetting(setting);
 +            fs.getSetting().add(setting);
 +            settingsAdded.addElement(featureType);
 +          }
 +        }
 +
 +        // is groups actually supposed to be a map here ?
 +        Iterator<String> en = fr.getFeatureGroups().iterator();
 +        Vector<String> groupsAdded = new Vector<>();
 +        while (en.hasNext())
 +        {
 +          String grp = en.next();
 +          if (groupsAdded.contains(grp))
 +          {
 +            continue;
 +          }
 +          Group g = new Group();
 +          g.setName(grp);
 +          g.setDisplay(((Boolean) fr.checkGroupVisibility(grp, false))
 +                  .booleanValue());
 +          // fs.addGroup(g);
 +          fs.getGroup().add(g);
 +          groupsAdded.addElement(grp);
 +        }
 +        // jms.setFeatureSettings(fs);
 +        object.setFeatureSettings(fs);
 +      }
 +
 +      if (av.hasHiddenColumns())
 +      {
 +        jalview.datamodel.HiddenColumns hidden = av.getAlignment()
 +                .getHiddenColumns();
 +        if (hidden == null)
 +        {
 +          Console.warn(
 +                  "REPORT BUG: avoided null columnselection bug (DMAM reported). Please contact Jim about this.");
 +        }
 +        else
 +        {
 +          Iterator<int[]> hiddenRegions = hidden.iterator();
 +          while (hiddenRegions.hasNext())
 +          {
 +            int[] region = hiddenRegions.next();
 +            HiddenColumns hc = new HiddenColumns();
 +            hc.setStart(region[0]);
 +            hc.setEnd(region[1]);
 +            // view.addHiddenColumns(hc);
 +            view.getHiddenColumns().add(hc);
 +          }
 +        }
 +      }
 +      if (calcIdSet.size() > 0)
 +      {
 +        for (String calcId : calcIdSet)
 +        {
 +          if (calcId.trim().length() > 0)
 +          {
 +            CalcIdParam cidp = createCalcIdParam(calcId, av);
 +            // Some calcIds have no parameters.
 +            if (cidp != null)
 +            {
 +              // view.addCalcIdParam(cidp);
 +              view.getCalcIdParam().add(cidp);
 +            }
 +          }
 +        }
 +      }
 +
 +      // jms.addViewport(view);
 +      object.getViewport().add(view);
 +    }
 +    // object.setJalviewModelSequence(jms);
 +    // object.getVamsasModel().addSequenceSet(vamsasSet);
 +    object.getVamsasModel().getSequenceSet().add(vamsasSet);
 +
 +    if (jout != null && fileName != null)
 +    {
 +      // We may not want to write the object to disk,
 +      // eg we can copy the alignViewport to a new view object
 +      // using save and then load
 +      try
 +      {
 +        fileName = fileName.replace('\\', '/');
 +        System.out.println("Writing jar entry " + fileName);
 +        JarEntry entry = new JarEntry(fileName);
 +        jout.putNextEntry(entry);
 +        PrintWriter pout = new PrintWriter(
 +                new OutputStreamWriter(jout, UTF_8));
 +        JAXBContext jaxbContext = JAXBContext
 +                .newInstance(JalviewModel.class);
 +        Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
 +
 +        // output pretty printed
 +        // jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
 +        jaxbMarshaller.marshal(
 +                new ObjectFactory().createJalviewModel(object), pout);
 +
 +        // jaxbMarshaller.marshal(object, pout);
 +        // marshaller.marshal(object);
 +        pout.flush();
 +        jout.closeEntry();
 +      } catch (Exception ex)
 +      {
 +        // TODO: raise error in GUI if marshalling failed.
 +        System.err.println("Error writing Jalview project");
 +        ex.printStackTrace();
 +      }
 +    }
 +    return object;
 +  }
 +
 +  /**
 +   * Writes PCA viewer attributes and computed values to an XML model object and
 +   * adds it to the JalviewModel. Any exceptions are reported by logging.
 +   */
 +  protected void savePCA(PCAPanel panel, JalviewModel object)
 +  {
 +    try
 +    {
 +      PcaViewer viewer = new PcaViewer();
 +      viewer.setHeight(panel.getHeight());
 +      viewer.setWidth(panel.getWidth());
 +      viewer.setXpos(panel.getX());
 +      viewer.setYpos(panel.getY());
 +      viewer.setTitle(panel.getTitle());
 +      PCAModel pcaModel = panel.getPcaModel();
 +      viewer.setScoreModelName(pcaModel.getScoreModelName());
 +      viewer.setXDim(panel.getSelectedDimensionIndex(X));
 +      viewer.setYDim(panel.getSelectedDimensionIndex(Y));
 +      viewer.setZDim(panel.getSelectedDimensionIndex(Z));
 +      viewer.setBgColour(
 +              panel.getRotatableCanvas().getBackgroundColour().getRGB());
 +      viewer.setScaleFactor(panel.getRotatableCanvas().getScaleFactor());
 +      float[] spMin = panel.getRotatableCanvas().getSeqMin();
 +      SeqPointMin spmin = new SeqPointMin();
 +      spmin.setXPos(spMin[0]);
 +      spmin.setYPos(spMin[1]);
 +      spmin.setZPos(spMin[2]);
 +      viewer.setSeqPointMin(spmin);
 +      float[] spMax = panel.getRotatableCanvas().getSeqMax();
 +      SeqPointMax spmax = new SeqPointMax();
 +      spmax.setXPos(spMax[0]);
 +      spmax.setYPos(spMax[1]);
 +      spmax.setZPos(spMax[2]);
 +      viewer.setSeqPointMax(spmax);
 +      viewer.setShowLabels(panel.getRotatableCanvas().isShowLabels());
 +      viewer.setLinkToAllViews(
 +              panel.getRotatableCanvas().isApplyToAllViews());
 +      SimilarityParamsI sp = pcaModel.getSimilarityParameters();
 +      viewer.setIncludeGaps(sp.includeGaps());
 +      viewer.setMatchGaps(sp.matchGaps());
 +      viewer.setIncludeGappedColumns(sp.includeGappedColumns());
 +      viewer.setDenominateByShortestLength(sp.denominateByShortestLength());
 +
 +      /*
 +       * sequence points on display
 +       */
 +      for (jalview.datamodel.SequencePoint spt : pcaModel
 +              .getSequencePoints())
 +      {
 +        SequencePoint point = new SequencePoint();
 +        point.setSequenceRef(seqHash(spt.getSequence()));
 +        point.setXPos(spt.coord.x);
 +        point.setYPos(spt.coord.y);
 +        point.setZPos(spt.coord.z);
 +        viewer.getSequencePoint().add(point);
 +      }
 +
 +      /*
 +       * (end points of) axes on display
 +       */
 +      for (Point p : panel.getRotatableCanvas().getAxisEndPoints())
 +      {
 +
 +        Axis axis = new Axis();
 +        axis.setXPos(p.x);
 +        axis.setYPos(p.y);
 +        axis.setZPos(p.z);
 +        viewer.getAxis().add(axis);
 +      }
 +
 +      /*
 +       * raw PCA data (note we are not restoring PCA inputs here -
 +       * alignment view, score model, similarity parameters)
 +       */
 +      PcaDataType data = new PcaDataType();
 +      viewer.setPcaData(data);
 +      PCA pca = pcaModel.getPcaData();
 +
 +      DoubleMatrix pm = new DoubleMatrix();
 +      saveDoubleMatrix(pca.getPairwiseScores(), pm);
 +      data.setPairwiseMatrix(pm);
 +
 +      DoubleMatrix tm = new DoubleMatrix();
 +      saveDoubleMatrix(pca.getTridiagonal(), tm);
 +      data.setTridiagonalMatrix(tm);
 +
 +      DoubleMatrix eigenMatrix = new DoubleMatrix();
 +      data.setEigenMatrix(eigenMatrix);
 +      saveDoubleMatrix(pca.getEigenmatrix(), eigenMatrix);
 +
 +      object.getPcaViewer().add(viewer);
 +    } catch (Throwable t)
 +    {
 +      Console.error("Error saving PCA: " + t.getMessage());
 +    }
 +  }
 +
 +  /**
 +   * Stores values from a matrix into an XML element, including (if present) the
 +   * D or E vectors
 +   * 
 +   * @param m
 +   * @param xmlMatrix
 +   * @see #loadDoubleMatrix(DoubleMatrix)
 +   */
 +  protected void saveDoubleMatrix(MatrixI m, DoubleMatrix xmlMatrix)
 +  {
 +    xmlMatrix.setRows(m.height());
 +    xmlMatrix.setColumns(m.width());
 +    for (int i = 0; i < m.height(); i++)
 +    {
 +      DoubleVector row = new DoubleVector();
 +      for (int j = 0; j < m.width(); j++)
 +      {
 +        row.getV().add(m.getValue(i, j));
 +      }
 +      xmlMatrix.getRow().add(row);
 +    }
 +    if (m.getD() != null)
 +    {
 +      DoubleVector dVector = new DoubleVector();
 +      for (double d : m.getD())
 +      {
 +        dVector.getV().add(d);
 +      }
 +      xmlMatrix.setD(dVector);
 +    }
 +    if (m.getE() != null)
 +    {
 +      DoubleVector eVector = new DoubleVector();
 +      for (double e : m.getE())
 +      {
 +        eVector.getV().add(e);
 +      }
 +      xmlMatrix.setE(eVector);
 +    }
 +  }
 +
 +  /**
 +   * Loads XML matrix data into a new Matrix object, including the D and/or E
 +   * vectors (if present)
 +   * 
 +   * @param mData
 +   * @return
 +   * @see Jalview2XML#saveDoubleMatrix(MatrixI, DoubleMatrix)
 +   */
 +  protected MatrixI loadDoubleMatrix(DoubleMatrix mData)
 +  {
 +    int rows = mData.getRows();
 +    double[][] vals = new double[rows][];
 +
 +    for (int i = 0; i < rows; i++)
 +    {
 +      List<Double> dVector = mData.getRow().get(i).getV();
 +      vals[i] = new double[dVector.size()];
 +      int dvi = 0;
 +      for (Double d : dVector)
 +      {
 +        vals[i][dvi++] = d;
 +      }
 +    }
 +
 +    MatrixI m = new Matrix(vals);
 +
 +    if (mData.getD() != null)
 +    {
 +      List<Double> dVector = mData.getD().getV();
 +      double[] vec = new double[dVector.size()];
 +      int dvi = 0;
 +      for (Double d : dVector)
 +      {
 +        vec[dvi++] = d;
 +      }
 +      m.setD(vec);
 +    }
 +    if (mData.getE() != null)
 +    {
 +      List<Double> dVector = mData.getE().getV();
 +      double[] vec = new double[dVector.size()];
 +      int dvi = 0;
 +      for (Double d : dVector)
 +      {
 +        vec[dvi++] = d;
 +      }
 +      m.setE(vec);
 +    }
 +
 +    return m;
 +  }
 +
 +  /**
 +   * Save any Varna viewers linked to this sequence. Writes an rnaViewer element
 +   * for each viewer, with
 +   * <ul>
 +   * <li>viewer geometry (position, size, split pane divider location)</li>
 +   * <li>index of the selected structure in the viewer (currently shows gapped
 +   * or ungapped)</li>
 +   * <li>the id of the annotation holding RNA secondary structure</li>
 +   * <li>(currently only one SS is shown per viewer, may be more in future)</li>
 +   * </ul>
 +   * Varna viewer state is also written out (in native Varna XML) to separate
 +   * project jar entries. A separate entry is written for each RNA structure
 +   * displayed, with the naming convention
 +   * <ul>
 +   * <li>rna_viewId_sequenceId_annotationId_[gapped|trimmed]</li>
 +   * </ul>
 +   * 
 +   * @param jout
 +   * @param jseq
 +   * @param jds
 +   * @param viewIds
 +   * @param ap
 +   * @param storeDataset
 +   */
 +  protected void saveRnaViewers(JarOutputStream jout, JSeq jseq,
 +          final SequenceI jds, List<String> viewIds, AlignmentPanel ap,
 +          boolean storeDataset)
 +  {
 +    if (Desktop.desktop == null)
 +    {
 +      return;
 +    }
 +    JInternalFrame[] frames = Desktop.desktop.getAllFrames();
 +    for (int f = frames.length - 1; f > -1; f--)
 +    {
 +      if (frames[f] instanceof AppVarna)
 +      {
 +        AppVarna varna = (AppVarna) frames[f];
 +        /*
 +         * link the sequence to every viewer that is showing it and is linked to
 +         * its alignment panel
 +         */
 +        if (varna.isListeningFor(jds) && ap == varna.getAlignmentPanel())
 +        {
 +          String viewId = varna.getViewId();
 +          RnaViewer rna = new RnaViewer();
 +          rna.setViewId(viewId);
 +          rna.setTitle(varna.getTitle());
 +          rna.setXpos(varna.getX());
 +          rna.setYpos(varna.getY());
 +          rna.setWidth(varna.getWidth());
 +          rna.setHeight(varna.getHeight());
 +          rna.setDividerLocation(varna.getDividerLocation());
 +          rna.setSelectedRna(varna.getSelectedIndex());
 +          // jseq.addRnaViewer(rna);
 +          jseq.getRnaViewer().add(rna);
 +
 +          /*
 +           * Store each Varna panel's state once in the project per sequence.
 +           * First time through only (storeDataset==false)
 +           */
 +          // boolean storeSessions = false;
 +          // String sequenceViewId = viewId + seqsToIds.get(jds);
 +          // if (!storeDataset && !viewIds.contains(sequenceViewId))
 +          // {
 +          // viewIds.add(sequenceViewId);
 +          // storeSessions = true;
 +          // }
 +          for (RnaModel model : varna.getModels())
 +          {
 +            if (model.seq == jds)
 +            {
 +              /*
 +               * VARNA saves each view (sequence or alignment secondary
 +               * structure, gapped or trimmed) as a separate XML file
 +               */
 +              String jarEntryName = rnaSessions.get(model);
 +              if (jarEntryName == null)
 +              {
 +
 +                String varnaStateFile = varna.getStateInfo(model.rna);
 +                jarEntryName = RNA_PREFIX + viewId + "_" + nextCounter();
 +                copyFileToJar(jout, varnaStateFile, jarEntryName, "Varna");
 +                rnaSessions.put(model, jarEntryName);
 +              }
 +              SecondaryStructure ss = new SecondaryStructure();
 +              String annotationId = varna.getAnnotation(jds).annotationId;
 +              ss.setAnnotationId(annotationId);
 +              ss.setViewerState(jarEntryName);
 +              ss.setGapped(model.gapped);
 +              ss.setTitle(model.title);
 +              // rna.addSecondaryStructure(ss);
 +              rna.getSecondaryStructure().add(ss);
 +            }
 +          }
 +        }
 +      }
 +    }
 +  }
 +
 +  /**
 +   * Copy the contents of a file to a new entry added to the output jar
 +   * 
 +   * @param jout
 +   * @param infilePath
 +   * @param jarEntryName
 +   * @param msg
 +   *          additional identifying info to log to the console
 +   */
 +  protected void copyFileToJar(JarOutputStream jout, String infilePath,
 +          String jarEntryName, String msg)
 +  {
 +    try (InputStream is = new FileInputStream(infilePath))
 +    {
 +      File file = new File(infilePath);
 +      if (file.exists() && jout != null)
 +      {
 +        System.out.println(
 +                "Writing jar entry " + jarEntryName + " (" + msg + ")");
 +        jout.putNextEntry(new JarEntry(jarEntryName));
 +        copyAll(is, jout);
 +        jout.closeEntry();
 +        // dis = new DataInputStream(new FileInputStream(file));
 +        // byte[] data = new byte[(int) file.length()];
 +        // dis.readFully(data);
 +        // writeJarEntry(jout, jarEntryName, data);
 +      }
 +    } catch (Exception ex)
 +    {
 +      ex.printStackTrace();
 +    }
 +  }
 +
 +  /**
 +   * Copies input to output, in 4K buffers; handles any data (text or binary)
 +   * 
 +   * @param in
 +   * @param out
 +   * @throws IOException
 +   */
 +  protected void copyAll(InputStream in, OutputStream out)
 +          throws IOException
 +  {
 +    byte[] buffer = new byte[4096];
 +    int bytesRead = 0;
 +    while ((bytesRead = in.read(buffer)) != -1)
 +    {
 +      out.write(buffer, 0, bytesRead);
 +    }
 +  }
 +
 +  /**
 +   * Save the state of a structure viewer
 +   * 
 +   * @param ap
 +   * @param jds
 +   * @param pdb
 +   *          the archive XML element under which to save the state
 +   * @param entry
 +   * @param viewIds
 +   * @param matchedFile
 +   * @param viewFrame
 +   * @return
 +   */
 +  protected String saveStructureViewer(AlignmentPanel ap, SequenceI jds,
 +          Pdbids pdb, PDBEntry entry, List<String> viewIds,
 +          String matchedFile, StructureViewerBase viewFrame)
 +  {
 +    final AAStructureBindingModel bindingModel = viewFrame.getBinding();
 +
 +    /*
 +     * Look for any bindings for this viewer to the PDB file of interest
 +     * (including part matches excluding chain id)
 +     */
 +    for (int peid = 0; peid < bindingModel.getPdbCount(); peid++)
 +    {
 +      final PDBEntry pdbentry = bindingModel.getPdbEntry(peid);
 +      final String pdbId = pdbentry.getId();
 +      if (!pdbId.equals(entry.getId()) && !(entry.getId().length() > 4
 +              && entry.getId().toLowerCase(Locale.ROOT)
 +                      .startsWith(pdbId.toLowerCase(Locale.ROOT))))
 +      {
 +        /*
 +         * not interested in a binding to a different PDB entry here
 +         */
 +        continue;
 +      }
 +      if (matchedFile == null)
 +      {
 +        matchedFile = pdbentry.getFile();
 +      }
 +      else if (!matchedFile.equals(pdbentry.getFile()))
 +      {
 +        Console.warn(
 +                "Probably lost some PDB-Sequence mappings for this structure file (which apparently has same PDB Entry code): "
 +                        + pdbentry.getFile());
 +      }
 +      // record the
 +      // file so we
 +      // can get at it if the ID
 +      // match is ambiguous (e.g.
 +      // 1QIP==1qipA)
 +
 +      for (int smap = 0; smap < viewFrame.getBinding()
 +              .getSequence()[peid].length; smap++)
 +      {
 +        // if (jal.findIndex(jmol.jmb.sequence[peid][smap]) > -1)
 +        if (jds == viewFrame.getBinding().getSequence()[peid][smap])
 +        {
 +          StructureState state = new StructureState();
 +          state.setVisible(true);
 +          state.setXpos(viewFrame.getX());
 +          state.setYpos(viewFrame.getY());
 +          state.setWidth(viewFrame.getWidth());
 +          state.setHeight(viewFrame.getHeight());
 +          final String viewId = viewFrame.getViewId();
 +          state.setViewId(viewId);
 +          state.setAlignwithAlignPanel(viewFrame.isUsedforaligment(ap));
 +          state.setColourwithAlignPanel(viewFrame.isUsedForColourBy(ap));
 +          state.setColourByJmol(viewFrame.isColouredByViewer());
 +          state.setType(viewFrame.getViewerType().toString());
 +          // pdb.addStructureState(state);
 +          pdb.getStructureState().add(state);
 +        }
 +      }
 +    }
 +    return matchedFile;
 +  }
 +
 +  /**
 +   * Populates the AnnotationColourScheme xml for save. This captures the
 +   * settings of the options in the 'Colour by Annotation' dialog.
 +   * 
 +   * @param acg
 +   * @param userColours
 +   * @param jm
 +   * @return
 +   */
 +  private AnnotationColourScheme constructAnnotationColours(
 +          AnnotationColourGradient acg, List<UserColourScheme> userColours,
 +          JalviewModel jm)
 +  {
 +    AnnotationColourScheme ac = new AnnotationColourScheme();
 +    ac.setAboveThreshold(acg.getAboveThreshold());
 +    ac.setThreshold(acg.getAnnotationThreshold());
 +    // 2.10.2 save annotationId (unique) not annotation label
 +    ac.setAnnotation(acg.getAnnotation().annotationId);
 +    if (acg.getBaseColour() instanceof UserColourScheme)
 +    {
 +      ac.setColourScheme(
 +              setUserColourScheme(acg.getBaseColour(), userColours, jm));
 +    }
 +    else
 +    {
 +      ac.setColourScheme(
 +              ColourSchemeProperty.getColourName(acg.getBaseColour()));
 +    }
 +
 +    ac.setMaxColour(acg.getMaxColour().getRGB());
 +    ac.setMinColour(acg.getMinColour().getRGB());
 +    ac.setPerSequence(acg.isSeqAssociated());
 +    ac.setPredefinedColours(acg.isPredefinedColours());
 +    return ac;
 +  }
 +
 +  private void storeAlignmentAnnotation(AlignmentAnnotation[] aa,
 +          IdentityHashMap<SequenceGroup, String> groupRefs,
 +          AlignmentViewport av, Set<String> calcIdSet, boolean storeDS,
 +          SequenceSet vamsasSet)
 +  {
 +
 +    for (int i = 0; i < aa.length; i++)
 +    {
 +      Annotation an = new Annotation();
 +
 +      AlignmentAnnotation annotation = aa[i];
 +      if (annotation.annotationId != null)
 +      {
 +        annotationIds.put(annotation.annotationId, annotation);
 +      }
 +
 +      an.setId(annotation.annotationId);
 +
 +      an.setVisible(annotation.visible);
 +
 +      an.setDescription(annotation.description);
 +
 +      if (annotation.sequenceRef != null)
 +      {
 +        // 2.9 JAL-1781 xref on sequence id rather than name
 +        an.setSequenceRef(seqsToIds.get(annotation.sequenceRef));
 +      }
 +      if (annotation.groupRef != null)
 +      {
 +        String groupIdr = groupRefs.get(annotation.groupRef);
 +        if (groupIdr == null)
 +        {
 +          // make a locally unique String
 +          groupRefs.put(annotation.groupRef,
 +                  groupIdr = ("" + System.currentTimeMillis()
 +                          + annotation.groupRef.getName()
 +                          + groupRefs.size()));
 +        }
 +        an.setGroupRef(groupIdr.toString());
 +      }
 +
 +      // store all visualization attributes for annotation
 +      an.setGraphHeight(annotation.graphHeight);
 +      an.setCentreColLabels(annotation.centreColLabels);
 +      an.setScaleColLabels(annotation.scaleColLabel);
 +      an.setShowAllColLabels(annotation.showAllColLabels);
 +      an.setBelowAlignment(annotation.belowAlignment);
 +
 +      if (annotation.graph > 0)
 +      {
 +        an.setGraph(true);
 +        an.setGraphType(annotation.graph);
 +        an.setGraphGroup(annotation.graphGroup);
 +        if (annotation.getThreshold() != null)
 +        {
 +          ThresholdLine line = new ThresholdLine();
 +          line.setLabel(annotation.getThreshold().label);
 +          line.setValue(annotation.getThreshold().value);
 +          line.setColour(annotation.getThreshold().colour.getRGB());
 +          an.setThresholdLine(line);
 +        }
 +      }
 +      else
 +      {
 +        an.setGraph(false);
 +      }
 +
 +      an.setLabel(annotation.label);
 +
 +      if (annotation == av.getAlignmentQualityAnnot()
 +              || annotation == av.getAlignmentConservationAnnotation()
 +              || annotation == av.getAlignmentConsensusAnnotation()
 +              || annotation.autoCalculated)
 +      {
 +        // new way of indicating autocalculated annotation -
 +        an.setAutoCalculated(annotation.autoCalculated);
 +      }
 +      if (annotation.hasScore())
 +      {
 +        an.setScore(annotation.getScore());
 +      }
 +
 +      if (annotation.getCalcId() != null)
 +      {
 +        calcIdSet.add(annotation.getCalcId());
 +        an.setCalcId(annotation.getCalcId());
 +      }
 +      if (annotation.hasProperties())
 +      {
 +        for (String pr : annotation.getProperties())
 +        {
 +          jalview.xml.binding.jalview.Annotation.Property prop = new jalview.xml.binding.jalview.Annotation.Property();
 +          prop.setName(pr);
 +          prop.setValue(annotation.getProperty(pr));
 +          // an.addProperty(prop);
 +          an.getProperty().add(prop);
 +        }
 +      }
 +
 +      AnnotationElement ae;
 +      if (annotation.annotations != null)
 +      {
 +        an.setScoreOnly(false);
 +        for (int a = 0; a < annotation.annotations.length; a++)
 +        {
 +          if ((annotation == null) || (annotation.annotations[a] == null))
 +          {
 +            continue;
 +          }
 +
 +          ae = new AnnotationElement();
 +          if (annotation.annotations[a].description != null)
 +          {
 +            ae.setDescription(annotation.annotations[a].description);
 +          }
 +          if (annotation.annotations[a].displayCharacter != null)
 +          {
 +            ae.setDisplayCharacter(
 +                    annotation.annotations[a].displayCharacter);
 +          }
 +
 +          if (!Float.isNaN(annotation.annotations[a].value))
 +          {
 +            ae.setValue(annotation.annotations[a].value);
 +          }
 +
 +          ae.setPosition(a);
 +          if (annotation.annotations[a].secondaryStructure > ' ')
 +          {
 +            ae.setSecondaryStructure(
 +                    annotation.annotations[a].secondaryStructure + "");
 +          }
 +
 +          if (annotation.annotations[a].colour != null
 +                  && annotation.annotations[a].colour != java.awt.Color.black)
 +          {
 +            ae.setColour(annotation.annotations[a].colour.getRGB());
 +          }
 +
 +          // an.addAnnotationElement(ae);
 +          an.getAnnotationElement().add(ae);
 +          if (annotation.autoCalculated)
 +          {
 +            // only write one non-null entry into the annotation row -
 +            // sufficient to get the visualization attributes necessary to
 +            // display data
 +            continue;
 +          }
 +        }
 +      }
 +      else
 +      {
 +        an.setScoreOnly(true);
 +      }
 +      if (!storeDS || (storeDS && !annotation.autoCalculated))
 +      {
 +        // skip autocalculated annotation - these are only provided for
 +        // alignments
 +        // vamsasSet.addAnnotation(an);
 +        vamsasSet.getAnnotation().add(an);
 +      }
 +    }
 +
 +  }
 +
 +  private CalcIdParam createCalcIdParam(String calcId, AlignViewport av)
 +  {
 +    AutoCalcSetting settings = av.getCalcIdSettingsFor(calcId);
 +    if (settings != null)
 +    {
 +      CalcIdParam vCalcIdParam = new CalcIdParam();
 +      vCalcIdParam.setCalcId(calcId);
 +      // vCalcIdParam.addServiceURL(settings.getServiceURI());
 +      vCalcIdParam.getServiceURL().add(settings.getServiceURI());
 +      // generic URI allowing a third party to resolve another instance of the
 +      // service used for this calculation
 +      for (String url : settings.getServiceURLs())
 +      {
 +        // vCalcIdParam.addServiceURL(urls);
 +        vCalcIdParam.getServiceURL().add(url);
 +      }
 +      vCalcIdParam.setVersion("1.0");
 +      if (settings.getPreset() != null)
 +      {
 +        WsParamSetI setting = settings.getPreset();
 +        vCalcIdParam.setName(setting.getName());
 +        vCalcIdParam.setDescription(setting.getDescription());
 +      }
 +      else
 +      {
 +        vCalcIdParam.setName("");
 +        vCalcIdParam.setDescription("Last used parameters");
 +      }
 +      // need to be able to recover 1) settings 2) user-defined presets or
 +      // recreate settings from preset 3) predefined settings provided by
 +      // service - or settings that can be transferred (or discarded)
 +      vCalcIdParam.setParameters(
 +              settings.getWsParamFile().replace("\n", "|\\n|"));
 +      vCalcIdParam.setAutoUpdate(settings.isAutoUpdate());
 +      // todo - decide if updateImmediately is needed for any projects.
 +
 +      return vCalcIdParam;
 +    }
 +    return null;
 +  }
 +
 +  private boolean recoverCalcIdParam(CalcIdParam calcIdParam,
 +          AlignViewport av)
 +  {
 +    if (calcIdParam.getVersion().equals("1.0"))
 +    {
 +      final String[] calcIds = calcIdParam.getServiceURL()
 +              .toArray(new String[0]);
 +      Jws2Instance service = Jws2Discoverer.getDiscoverer()
 +              .getPreferredServiceFor(calcIds);
 +      if (service != null)
 +      {
 +        WsParamSetI parmSet = null;
 +        try
 +        {
 +          parmSet = service.getParamStore().parseServiceParameterFile(
 +                  calcIdParam.getName(), calcIdParam.getDescription(),
 +                  calcIds,
 +                  calcIdParam.getParameters().replace("|\\n|", "\n"));
 +        } catch (IOException x)
 +        {
 +          Console.warn("Couldn't parse parameter data for "
 +                  + calcIdParam.getCalcId(), x);
 +          return false;
 +        }
 +        List<ArgumentI> argList = null;
 +        if (calcIdParam.getName().length() > 0)
 +        {
 +          parmSet = service.getParamStore()
 +                  .getPreset(calcIdParam.getName());
 +          if (parmSet != null)
 +          {
 +            // TODO : check we have a good match with settings in AACon -
 +            // otherwise we'll need to create a new preset
 +          }
 +        }
 +        else
 +        {
 +          argList = parmSet.getArguments();
 +          parmSet = null;
 +        }
 +        AAConSettings settings = new AAConSettings(
 +                calcIdParam.isAutoUpdate(), service, parmSet, argList);
 +        av.setCalcIdSettingsFor(calcIdParam.getCalcId(), settings,
 +                calcIdParam.isNeedsUpdate());
 +        return true;
 +      }
 +      else
 +      {
 +        Console.warn(
 +                "Cannot resolve a service for the parameters used in this project. Try configuring a JABAWS server.");
 +        return false;
 +      }
 +    }
 +    throw new Error(MessageManager.formatMessage(
 +            "error.unsupported_version_calcIdparam", new Object[]
 +            { calcIdParam.toString() }));
 +  }
 +
 +  /**
 +   * External mapping between jalview objects and objects yielding a valid and
 +   * unique object ID string. This is null for normal Jalview project IO, but
 +   * non-null when a jalview project is being read or written as part of a
 +   * vamsas session.
 +   */
 +  IdentityHashMap jv2vobj = null;
 +
 +  /**
 +   * Construct a unique ID for jvobj using either existing bindings or if none
 +   * exist, the result of the hashcode call for the object.
 +   * 
 +   * @param jvobj
 +   *          jalview data object
 +   * @return unique ID for referring to jvobj
 +   */
 +  private String makeHashCode(Object jvobj, String altCode)
 +  {
 +    if (jv2vobj != null)
 +    {
 +      Object id = jv2vobj.get(jvobj);
 +      if (id != null)
 +      {
 +        return id.toString();
 +      }
 +      // check string ID mappings
 +      if (jvids2vobj != null && jvobj instanceof String)
 +      {
 +        id = jvids2vobj.get(jvobj);
 +      }
 +      if (id != null)
 +      {
 +        return id.toString();
 +      }
 +      // give up and warn that something has gone wrong
 +      Console.warn(
 +              "Cannot find ID for object in external mapping : " + jvobj);
 +    }
 +    return altCode;
 +  }
 +
 +  /**
 +   * return local jalview object mapped to ID, if it exists
 +   * 
 +   * @param idcode
 +   *          (may be null)
 +   * @return null or object bound to idcode
 +   */
 +  private Object retrieveExistingObj(String idcode)
 +  {
 +    if (idcode != null && vobj2jv != null)
 +    {
 +      return vobj2jv.get(idcode);
 +    }
 +    return null;
 +  }
 +
 +  /**
 +   * binding from ID strings from external mapping table to jalview data model
 +   * objects.
 +   */
 +  private Hashtable vobj2jv;
 +
 +  private Sequence createVamsasSequence(String id, SequenceI jds)
 +  {
 +    return createVamsasSequence(true, id, jds, null);
 +  }
 +
 +  private Sequence createVamsasSequence(boolean recurse, String id,
 +          SequenceI jds, SequenceI parentseq)
 +  {
 +    Sequence vamsasSeq = new Sequence();
 +    vamsasSeq.setId(id);
 +    vamsasSeq.setName(jds.getName());
 +    vamsasSeq.setSequence(jds.getSequenceAsString());
 +    vamsasSeq.setDescription(jds.getDescription());
 +    List<DBRefEntry> dbrefs = null;
 +    if (jds.getDatasetSequence() != null)
 +    {
 +      vamsasSeq.setDsseqid(seqHash(jds.getDatasetSequence()));
 +    }
 +    else
 +    {
 +      // seqId==dsseqid so we can tell which sequences really are
 +      // dataset sequences only
 +      vamsasSeq.setDsseqid(id);
 +      dbrefs = jds.getDBRefs();
 +      if (parentseq == null)
 +      {
 +        parentseq = jds;
 +      }
 +    }
 +
 +    /*
 +     * save any dbrefs; special subclass GeneLocus is flagged as 'locus'
 +     */
 +    if (dbrefs != null)
 +    {
 +      for (int d = 0, nd = dbrefs.size(); d < nd; d++)
 +      {
 +        DBRef dbref = new DBRef();
 +        DBRefEntry ref = dbrefs.get(d);
 +        dbref.setSource(ref.getSource());
 +        dbref.setVersion(ref.getVersion());
 +        dbref.setAccessionId(ref.getAccessionId());
 +        dbref.setCanonical(ref.isCanonical());
 +        if (ref instanceof GeneLocus)
 +        {
 +          dbref.setLocus(true);
 +        }
 +        if (ref.hasMap())
 +        {
 +          Mapping mp = createVamsasMapping(ref.getMap(), parentseq, jds,
 +                  recurse);
 +          dbref.setMapping(mp);
 +        }
 +        vamsasSeq.getDBRef().add(dbref);
 +      }
 +    }
 +    return vamsasSeq;
 +  }
 +
 +  private Mapping createVamsasMapping(jalview.datamodel.Mapping jmp,
 +          SequenceI parentseq, SequenceI jds, boolean recurse)
 +  {
 +    Mapping mp = null;
 +    if (jmp.getMap() != null)
 +    {
 +      mp = new Mapping();
 +
 +      jalview.util.MapList mlst = jmp.getMap();
 +      List<int[]> r = mlst.getFromRanges();
 +      for (int[] range : r)
 +      {
 +        MapListFrom mfrom = new MapListFrom();
 +        mfrom.setStart(range[0]);
 +        mfrom.setEnd(range[1]);
 +        // mp.addMapListFrom(mfrom);
 +        mp.getMapListFrom().add(mfrom);
 +      }
 +      r = mlst.getToRanges();
 +      for (int[] range : r)
 +      {
 +        MapListTo mto = new MapListTo();
 +        mto.setStart(range[0]);
 +        mto.setEnd(range[1]);
 +        // mp.addMapListTo(mto);
 +        mp.getMapListTo().add(mto);
 +      }
 +      mp.setMapFromUnit(BigInteger.valueOf(mlst.getFromRatio()));
 +      mp.setMapToUnit(BigInteger.valueOf(mlst.getToRatio()));
 +      if (jmp.getTo() != null)
 +      {
 +        // MappingChoice mpc = new MappingChoice();
 +
 +        // check/create ID for the sequence referenced by getTo()
 +
 +        String jmpid = "";
 +        SequenceI ps = null;
 +        if (parentseq != jmp.getTo()
 +                && parentseq.getDatasetSequence() != jmp.getTo())
 +        {
 +          // chaining dbref rather than a handshaking one
 +          jmpid = seqHash(ps = jmp.getTo());
 +        }
 +        else
 +        {
 +          jmpid = seqHash(ps = parentseq);
 +        }
 +        // mpc.setDseqFor(jmpid);
 +        mp.setDseqFor(jmpid);
 +        if (!seqRefIds.containsKey(jmpid))
 +        {
 +          Console.debug("creatign new DseqFor ID");
 +          seqRefIds.put(jmpid, ps);
 +        }
 +        else
 +        {
 +          Console.debug("reusing DseqFor ID");
 +        }
 +
 +        // mp.setMappingChoice(mpc);
 +      }
 +    }
 +    return mp;
 +  }
 +
 +  String setUserColourScheme(jalview.schemes.ColourSchemeI cs,
 +          List<UserColourScheme> userColours, JalviewModel jm)
 +  {
 +    String id = null;
 +    jalview.schemes.UserColourScheme ucs = (jalview.schemes.UserColourScheme) cs;
 +    boolean newucs = false;
 +    if (!userColours.contains(ucs))
 +    {
 +      userColours.add(ucs);
 +      newucs = true;
 +    }
 +    id = "ucs" + userColours.indexOf(ucs);
 +    if (newucs)
 +    {
 +      // actually create the scheme's entry in the XML model
 +      java.awt.Color[] colours = ucs.getColours();
 +      UserColours uc = new UserColours();
 +      // UserColourScheme jbucs = new UserColourScheme();
 +      JalviewUserColours jbucs = new JalviewUserColours();
 +
 +      for (int i = 0; i < colours.length; i++)
 +      {
 +        Colour col = new Colour();
 +        col.setName(ResidueProperties.aa[i]);
 +        col.setRGB(jalview.util.Format.getHexString(colours[i]));
 +        // jbucs.addColour(col);
 +        jbucs.getColour().add(col);
 +      }
 +      if (ucs.getLowerCaseColours() != null)
 +      {
 +        colours = ucs.getLowerCaseColours();
 +        for (int i = 0; i < colours.length; i++)
 +        {
 +          Colour col = new Colour();
 +          col.setName(ResidueProperties.aa[i].toLowerCase(Locale.ROOT));
 +          col.setRGB(jalview.util.Format.getHexString(colours[i]));
 +          // jbucs.addColour(col);
 +          jbucs.getColour().add(col);
 +        }
 +      }
 +
 +      uc.setId(id);
 +      uc.setUserColourScheme(jbucs);
 +      // jm.addUserColours(uc);
 +      jm.getUserColours().add(uc);
 +    }
 +
 +    return id;
 +  }
 +
 +  jalview.schemes.UserColourScheme getUserColourScheme(JalviewModel jm,
 +          String id)
 +  {
 +    List<UserColours> uc = jm.getUserColours();
 +    UserColours colours = null;
 +    /*
 +    for (int i = 0; i < uc.length; i++)
 +    {
 +      if (uc[i].getId().equals(id))
 +      {
 +        colours = uc[i];
 +        break;
 +      }
 +    }
 +    */
 +    for (UserColours c : uc)
 +    {
 +      if (c.getId().equals(id))
 +      {
 +        colours = c;
 +        break;
 +      }
 +    }
 +
 +    java.awt.Color[] newColours = new java.awt.Color[24];
 +
 +    for (int i = 0; i < 24; i++)
 +    {
 +      newColours[i] = new java.awt.Color(Integer.parseInt(
 +              // colours.getUserColourScheme().getColour(i).getRGB(), 16));
 +              colours.getUserColourScheme().getColour().get(i).getRGB(),
 +              16));
 +    }
 +
 +    jalview.schemes.UserColourScheme ucs = new jalview.schemes.UserColourScheme(
 +            newColours);
 +
 +    if (colours.getUserColourScheme().getColour().size()/*Count()*/ > 24)
 +    {
 +      newColours = new java.awt.Color[23];
 +      for (int i = 0; i < 23; i++)
 +      {
 +        newColours[i] = new java.awt.Color(
 +                Integer.parseInt(colours.getUserColourScheme().getColour()
 +                        .get(i + 24).getRGB(), 16));
 +      }
 +      ucs.setLowerCaseColours(newColours);
 +    }
 +
 +    return ucs;
 +  }
 +
 +  /**
 +   * contains last error message (if any) encountered by XML loader.
 +   */
 +  String errorMessage = null;
 +
 +  /**
 +   * flag to control whether the Jalview2XML_V1 parser should be deferred to if
 +   * exceptions are raised during project XML parsing
 +   */
 +  public boolean attemptversion1parse = false;
 +
 +  /**
 +   * Load a jalview project archive from a jar file
 +   * 
 +   * @param file
 +   *          - HTTP URL or filename
 +   */
 +  public AlignFrame loadJalviewAlign(final Object file)
 +  {
 +
 +    jalview.gui.AlignFrame af = null;
 +
 +    try
 +    {
 +      // create list to store references for any new Jmol viewers created
 +      newStructureViewers = new Vector<>();
 +      // UNMARSHALLER SEEMS TO CLOSE JARINPUTSTREAM, MOST ANNOYING
 +      // Workaround is to make sure caller implements the JarInputStreamProvider
 +      // interface
 +      // so we can re-open the jar input stream for each entry.
 +
 +      jarInputStreamProvider jprovider = createjarInputStreamProvider(file);
 +      af = loadJalviewAlign(jprovider);
 +      if (af != null)
 +      {
 +        af.setMenusForViewport();
 +      }
 +    } catch (MalformedURLException e)
 +    {
 +      errorMessage = "Invalid URL format for '" + file + "'";
 +      reportErrors();
 +    } finally
 +    {
 +      try
 +      {
 +        SwingUtilities.invokeAndWait(new Runnable()
 +        {
 +          @Override
 +          public void run()
 +          {
 +            setLoadingFinishedForNewStructureViewers();
 +          }
 +        });
 +      } catch (Exception x)
 +      {
 +        System.err.println("Error loading alignment: " + x.getMessage());
 +      }
 +    }
 +    return af;
 +  }
 +
 +  @SuppressWarnings("unused")
 +  private jarInputStreamProvider createjarInputStreamProvider(
 +          final Object ofile) throws MalformedURLException
 +  {
 +
 +    // BH 2018 allow for bytes already attached to File object
 +    try
 +    {
 +      String file = (ofile instanceof File
 +              ? ((File) ofile).getCanonicalPath()
 +              : ofile.toString());
 +      byte[] bytes = Platform.isJS() ? Platform.getFileBytes((File) ofile)
 +              : null;
 +      URL url = null;
 +      errorMessage = null;
 +      uniqueSetSuffix = null;
 +      seqRefIds = null;
 +      viewportsAdded.clear();
 +      frefedSequence = null;
 +
 +      if (HttpUtils.startsWithHttpOrHttps(file))
 +      {
 +        url = new URL(file);
 +      }
 +      final URL _url = url;
 +      return new jarInputStreamProvider()
 +      {
 +
 +        @Override
 +        public JarInputStream getJarInputStream() throws IOException
 +        {
 +          if (bytes != null)
 +          {
 +            // System.out.println("Jalview2XML: opening byte jarInputStream for
 +            // bytes.length=" + bytes.length);
 +            return new JarInputStream(new ByteArrayInputStream(bytes));
 +          }
 +          if (_url != null)
 +          {
 +            // System.out.println("Jalview2XML: opening url jarInputStream for "
 +            // + _url);
 +            return new JarInputStream(_url.openStream());
 +          }
 +          else
 +          {
 +            // System.out.println("Jalview2XML: opening file jarInputStream for
 +            // " + file);
 +            return new JarInputStream(new FileInputStream(file));
 +          }
 +        }
 +
 +        @Override
 +        public String getFilename()
 +        {
 +          return file;
 +        }
 +      };
 +    } catch (IOException e)
 +    {
 +      e.printStackTrace();
 +      return null;
 +    }
 +  }
 +
 +  /**
 +   * Recover jalview session from a jalview project archive. Caller may
 +   * initialise uniqueSetSuffix, seqRefIds, viewportsAdded and frefedSequence
 +   * themselves. Any null fields will be initialised with default values,
 +   * non-null fields are left alone.
 +   * 
 +   * @param jprovider
 +   * @return
 +   */
 +  public AlignFrame loadJalviewAlign(final jarInputStreamProvider jprovider)
 +  {
 +    errorMessage = null;
 +    if (uniqueSetSuffix == null)
 +    {
 +      uniqueSetSuffix = System.currentTimeMillis() % 100000 + "";
 +    }
 +    if (seqRefIds == null)
 +    {
 +      initSeqRefs();
 +    }
 +    AlignFrame af = null, _af = null;
 +    IdentityHashMap<AlignmentI, AlignmentI> importedDatasets = new IdentityHashMap<>();
 +    Map<String, AlignFrame> gatherToThisFrame = new HashMap<>();
 +    final String file = jprovider.getFilename();
 +    try
 +    {
 +      JarInputStream jin = null;
 +      JarEntry jarentry = null;
 +      int entryCount = 1;
 +
 +      do
 +      {
 +        jin = jprovider.getJarInputStream();
 +        for (int i = 0; i < entryCount; i++)
 +        {
 +          jarentry = jin.getNextJarEntry();
 +        }
 +
 +        if (jarentry != null && jarentry.getName().endsWith(".xml"))
 +        {
 +          JAXBContext jc = JAXBContext
 +                  .newInstance("jalview.xml.binding.jalview");
 +          XMLStreamReader streamReader = XMLInputFactory.newInstance()
 +                  .createXMLStreamReader(jin);
 +          javax.xml.bind.Unmarshaller um = jc.createUnmarshaller();
 +          JAXBElement<JalviewModel> jbe = um.unmarshal(streamReader,
 +                  JalviewModel.class);
 +          JalviewModel object = jbe.getValue();
 +
 +          if (true) // !skipViewport(object))
 +          {
 +            _af = loadFromObject(object, file, true, jprovider);
 +            if (_af != null && object.getViewport().size() > 0)
 +            // getJalviewModelSequence().getViewportCount() > 0)
 +            {
 +              if (af == null)
 +              {
 +                // store a reference to the first view
 +                af = _af;
 +              }
 +              if (_af.getViewport().isGatherViewsHere())
 +              {
 +                // if this is a gathered view, keep its reference since
 +                // after gathering views, only this frame will remain
 +                af = _af;
 +                gatherToThisFrame.put(_af.getViewport().getSequenceSetId(),
 +                        _af);
 +              }
 +              // Save dataset to register mappings once all resolved
 +              importedDatasets.put(
 +                      af.getViewport().getAlignment().getDataset(),
 +                      af.getViewport().getAlignment().getDataset());
 +            }
 +          }
 +          entryCount++;
 +        }
 +        else if (jarentry != null)
 +        {
 +          // Some other file here.
 +          entryCount++;
 +        }
 +      } while (jarentry != null);
 +      jin.close();
 +      resolveFrefedSequences();
 +    } catch (IOException ex)
 +    {
 +      ex.printStackTrace();
 +      errorMessage = "Couldn't locate Jalview XML file : " + file;
 +      System.err.println(
 +              "Exception whilst loading jalview XML file : " + ex + "\n");
 +    } catch (Exception ex)
 +    {
 +      System.err.println("Parsing as Jalview Version 2 file failed.");
 +      ex.printStackTrace(System.err);
 +      if (attemptversion1parse)
 +      {
 +        // used to attempt to parse as V1 castor-generated xml
 +      }
 +      if (Desktop.instance != null)
 +      {
 +        Desktop.instance.stopLoading();
 +      }
 +      if (af != null)
 +      {
 +        System.out.println("Successfully loaded archive file");
 +        return af;
 +      }
 +      ex.printStackTrace();
 +
 +      System.err.println(
 +              "Exception whilst loading jalview XML file : " + ex + "\n");
 +    } catch (OutOfMemoryError e)
 +    {
 +      // Don't use the OOM Window here
 +      errorMessage = "Out of memory loading jalview XML file";
 +      System.err.println("Out of memory whilst loading jalview XML file");
 +      e.printStackTrace();
 +    }
 +
 +    /*
 +     * Regather multiple views (with the same sequence set id) to the frame (if
 +     * any) that is flagged as the one to gather to, i.e. convert them to tabbed
 +     * views instead of separate frames. Note this doesn't restore a state where
 +     * some expanded views in turn have tabbed views - the last "first tab" read
 +     * in will play the role of gatherer for all.
 +     */
 +    for (AlignFrame fr : gatherToThisFrame.values())
 +    {
 +      Desktop.instance.gatherViews(fr);
 +    }
 +
 +    restoreSplitFrames();
 +    for (AlignmentI ds : importedDatasets.keySet())
 +    {
 +      if (ds.getCodonFrames() != null)
 +      {
 +        StructureSelectionManager
 +                .getStructureSelectionManager(Desktop.instance)
 +                .registerMappings(ds.getCodonFrames());
 +      }
 +    }
 +    if (errorMessage != null)
 +    {
 +      reportErrors();
 +    }
 +
 +    if (Desktop.instance != null)
 +    {
 +      Desktop.instance.stopLoading();
 +    }
 +
 +    return af;
 +  }
 +
 +  /**
 +   * Try to reconstruct and display SplitFrame windows, where each contains
 +   * complementary dna and protein alignments. Done by pairing up AlignFrame
 +   * objects (created earlier) which have complementary viewport ids associated.
 +   */
 +  protected void restoreSplitFrames()
 +  {
 +    List<SplitFrame> gatherTo = new ArrayList<>();
 +    List<AlignFrame> addedToSplitFrames = new ArrayList<>();
 +    Map<String, AlignFrame> dna = new HashMap<>();
 +
 +    /*
 +     * Identify the DNA alignments
 +     */
 +    for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
 +            .entrySet())
 +    {
 +      AlignFrame af = candidate.getValue();
 +      if (af.getViewport().getAlignment().isNucleotide())
 +      {
 +        dna.put(candidate.getKey().getId(), af);
 +      }
 +    }
 +
 +    /*
 +     * Try to match up the protein complements
 +     */
 +    for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
 +            .entrySet())
 +    {
 +      AlignFrame af = candidate.getValue();
 +      if (!af.getViewport().getAlignment().isNucleotide())
 +      {
 +        String complementId = candidate.getKey().getComplementId();
 +        // only non-null complements should be in the Map
 +        if (complementId != null && dna.containsKey(complementId))
 +        {
 +          final AlignFrame dnaFrame = dna.get(complementId);
 +          SplitFrame sf = createSplitFrame(dnaFrame, af);
 +          addedToSplitFrames.add(dnaFrame);
 +          addedToSplitFrames.add(af);
 +          dnaFrame.setMenusForViewport();
 +          af.setMenusForViewport();
 +          if (af.getViewport().isGatherViewsHere())
 +          {
 +            gatherTo.add(sf);
 +          }
 +        }
 +      }
 +    }
 +
 +    /*
 +     * Open any that we failed to pair up (which shouldn't happen!) as
 +     * standalone AlignFrame's.
 +     */
 +    for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
 +            .entrySet())
 +    {
 +      AlignFrame af = candidate.getValue();
 +      if (!addedToSplitFrames.contains(af))
 +      {
 +        Viewport view = candidate.getKey();
 +        Desktop.addInternalFrame(af, view.getTitle(),
 +                safeInt(view.getWidth()), safeInt(view.getHeight()));
 +        af.setMenusForViewport();
 +        System.err.println("Failed to restore view " + view.getTitle()
 +                + " to split frame");
 +      }
 +    }
 +
 +    /*
 +     * Gather back into tabbed views as flagged.
 +     */
 +    for (SplitFrame sf : gatherTo)
 +    {
 +      Desktop.instance.gatherViews(sf);
 +    }
 +
 +    splitFrameCandidates.clear();
 +  }
 +
 +  /**
 +   * Construct and display one SplitFrame holding DNA and protein alignments.
 +   * 
 +   * @param dnaFrame
 +   * @param proteinFrame
 +   * @return
 +   */
 +  protected SplitFrame createSplitFrame(AlignFrame dnaFrame,
 +          AlignFrame proteinFrame)
 +  {
 +    SplitFrame splitFrame = new SplitFrame(dnaFrame, proteinFrame);
 +    String title = MessageManager.getString("label.linked_view_title");
 +    int width = (int) dnaFrame.getBounds().getWidth();
 +    int height = (int) (dnaFrame.getBounds().getHeight()
 +            + proteinFrame.getBounds().getHeight() + 50);
 +
 +    /*
 +     * SplitFrame location is saved to both enclosed frames
 +     */
 +    splitFrame.setLocation(dnaFrame.getX(), dnaFrame.getY());
 +    Desktop.addInternalFrame(splitFrame, title, width, height);
 +
 +    /*
 +     * And compute cDNA consensus (couldn't do earlier with consensus as
 +     * mappings were not yet present)
 +     */
 +    proteinFrame.getViewport().alignmentChanged(proteinFrame.alignPanel);
 +
 +    return splitFrame;
 +  }
 +
 +  /**
 +   * check errorMessage for a valid error message and raise an error box in the
 +   * GUI or write the current errorMessage to stderr and then clear the error
 +   * state.
 +   */
 +  protected void reportErrors()
 +  {
 +    reportErrors(false);
 +  }
 +
 +  protected void reportErrors(final boolean saving)
 +  {
 +    if (errorMessage != null)
 +    {
 +      final String finalErrorMessage = errorMessage;
 +      if (raiseGUI)
 +      {
 +        javax.swing.SwingUtilities.invokeLater(new Runnable()
 +        {
 +          @Override
 +          public void run()
 +          {
 +            JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 +                    finalErrorMessage,
 +                    "Error " + (saving ? "saving" : "loading")
 +                            + " Jalview file",
 +                    JvOptionPane.WARNING_MESSAGE);
 +          }
 +        });
 +      }
 +      else
 +      {
 +        System.err.println("Problem loading Jalview file: " + errorMessage);
 +      }
 +    }
 +    errorMessage = null;
 +  }
 +
 +  Map<String, String> alreadyLoadedPDB = new HashMap<>();
 +
 +  /**
 +   * when set, local views will be updated from view stored in JalviewXML
 +   * Currently (28th Sep 2008) things will go horribly wrong in vamsas document
 +   * sync if this is set to true.
 +   */
 +  private final boolean updateLocalViews = false;
 +
 +  /**
 +   * Returns the path to a temporary file holding the PDB file for the given PDB
 +   * id. The first time of asking, searches for a file of that name in the
 +   * Jalview project jar, and copies it to a new temporary file. Any repeat
 +   * requests just return the path to the file previously created.
 +   * 
 +   * @param jprovider
 +   * @param pdbId
 +   * @return
 +   */
 +  String loadPDBFile(jarInputStreamProvider jprovider, String pdbId,
 +          String origFile)
 +  {
 +    if (alreadyLoadedPDB.containsKey(pdbId))
 +    {
 +      return alreadyLoadedPDB.get(pdbId).toString();
 +    }
 +
 +    String tempFile = copyJarEntry(jprovider, pdbId, "jalview_pdb",
 +            origFile);
 +    if (tempFile != null)
 +    {
 +      alreadyLoadedPDB.put(pdbId, tempFile);
 +    }
 +    return tempFile;
 +  }
 +
 +  /**
 +   * Copies the jar entry of given name to a new temporary file and returns the
 +   * path to the file, or null if the entry is not found.
 +   * 
 +   * @param jprovider
 +   * @param jarEntryName
 +   * @param prefix
 +   *          a prefix for the temporary file name, must be at least three
 +   *          characters long
 +   * @param suffixModel
 +   *          null or original file - so new file can be given the same suffix
 +   *          as the old one
 +   * @return
 +   */
 +  protected String copyJarEntry(jarInputStreamProvider jprovider,
 +          String jarEntryName, String prefix, String suffixModel)
 +  {
 +    String suffix = ".tmp";
 +    if (suffixModel == null)
 +    {
 +      suffixModel = jarEntryName;
 +    }
 +    int sfpos = suffixModel.lastIndexOf(".");
 +    if (sfpos > -1 && sfpos < (suffixModel.length() - 1))
 +    {
 +      suffix = "." + suffixModel.substring(sfpos + 1);
 +    }
 +
 +    try (JarInputStream jin = jprovider.getJarInputStream())
 +    {
 +      JarEntry entry = null;
 +      do
 +      {
 +        entry = jin.getNextJarEntry();
 +      } while (entry != null && !entry.getName().equals(jarEntryName));
 +
 +      if (entry != null)
 +      {
 +        // in = new BufferedReader(new InputStreamReader(jin, UTF_8));
 +        File outFile = File.createTempFile(prefix, suffix);
 +        outFile.deleteOnExit();
 +        try (OutputStream os = new FileOutputStream(outFile))
 +        {
 +          copyAll(jin, os);
 +        }
 +        String t = outFile.getAbsolutePath();
 +        return t;
 +      }
 +      else
 +      {
 +        Console.warn(
 +                "Couldn't find entry in Jalview Jar for " + jarEntryName);
 +      }
 +    } catch (Exception ex)
 +    {
 +      ex.printStackTrace();
 +    }
 +
 +    return null;
 +  }
 +
 +  private class JvAnnotRow
 +  {
 +    public JvAnnotRow(int i, AlignmentAnnotation jaa)
 +    {
 +      order = i;
 +      template = jaa;
 +    }
 +
 +    /**
 +     * persisted version of annotation row from which to take vis properties
 +     */
 +    public jalview.datamodel.AlignmentAnnotation template;
 +
 +    /**
 +     * original position of the annotation row in the alignment
 +     */
 +    public int order;
 +  }
 +
 +  /**
 +   * Load alignment frame from jalview XML DOM object
 +   * 
 +   * @param jalviewModel
 +   *          DOM
 +   * @param file
 +   *          filename source string
 +   * @param loadTreesAndStructures
 +   *          when false only create Viewport
 +   * @param jprovider
 +   *          data source provider
 +   * @return alignment frame created from view stored in DOM
 +   */
 +  AlignFrame loadFromObject(JalviewModel jalviewModel, String file,
 +          boolean loadTreesAndStructures, jarInputStreamProvider jprovider)
 +  {
 +    SequenceSet vamsasSet = jalviewModel.getVamsasModel().getSequenceSet()
 +            .get(0);
 +    List<Sequence> vamsasSeqs = vamsasSet.getSequence();
 +
 +    // JalviewModelSequence jms = object.getJalviewModelSequence();
 +
 +    // Viewport view = (jms.getViewportCount() > 0) ? jms.getViewport(0)
 +    // : null;
 +    Viewport view = (jalviewModel.getViewport().size() > 0)
 +            ? jalviewModel.getViewport().get(0)
 +            : null;
 +
 +    // ////////////////////////////////
 +    // INITIALISE ALIGNMENT SEQUENCESETID AND VIEWID
 +    //
 +    //
 +    // If we just load in the same jar file again, the sequenceSetId
 +    // will be the same, and we end up with multiple references
 +    // to the same sequenceSet. We must modify this id on load
 +    // so that each load of the file gives a unique id
 +
 +    /**
 +     * used to resolve correct alignment dataset for alignments with multiple
 +     * views
 +     */
 +    String uniqueSeqSetId = null;
 +    String viewId = null;
 +    if (view != null)
 +    {
 +      uniqueSeqSetId = view.getSequenceSetId() + uniqueSetSuffix;
 +      viewId = (view.getId() == null ? null
 +              : view.getId() + uniqueSetSuffix);
 +    }
 +
 +    // ////////////////////////////////
 +    // LOAD SEQUENCES
 +
 +    List<SequenceI> hiddenSeqs = null;
 +
 +    List<SequenceI> tmpseqs = new ArrayList<>();
 +
 +    boolean multipleView = false;
 +    SequenceI referenceseqForView = null;
 +    // JSeq[] jseqs = object.getJalviewModelSequence().getJSeq();
 +    List<JSeq> jseqs = jalviewModel.getJSeq();
 +    int vi = 0; // counter in vamsasSeq array
 +    for (int i = 0; i < jseqs.size(); i++)
 +    {
 +      JSeq jseq = jseqs.get(i);
 +      String seqId = jseq.getId();
 +
 +      SequenceI tmpSeq = seqRefIds.get(seqId);
 +      if (tmpSeq != null)
 +      {
 +        if (!incompleteSeqs.containsKey(seqId))
 +        {
 +          // may not need this check, but keep it for at least 2.9,1 release
 +          if (tmpSeq.getStart() != jseq.getStart()
 +                  || tmpSeq.getEnd() != jseq.getEnd())
 +          {
 +            System.err.println(String.format(
 +                    "Warning JAL-2154 regression: updating start/end for sequence %s from %d/%d to %d/%d",
 +                    tmpSeq.getName(), tmpSeq.getStart(), tmpSeq.getEnd(),
 +                    jseq.getStart(), jseq.getEnd()));
 +          }
 +        }
 +        else
 +        {
 +          incompleteSeqs.remove(seqId);
 +        }
 +        if (vamsasSeqs.size() > vi
 +                && vamsasSeqs.get(vi).getId().equals(seqId))
 +        {
 +          // most likely we are reading a dataset XML document so
 +          // update from vamsasSeq section of XML for this sequence
 +          tmpSeq.setName(vamsasSeqs.get(vi).getName());
 +          tmpSeq.setDescription(vamsasSeqs.get(vi).getDescription());
 +          tmpSeq.setSequence(vamsasSeqs.get(vi).getSequence());
 +          vi++;
 +        }
 +        else
 +        {
 +          // reading multiple views, so vamsasSeq set is a subset of JSeq
 +          multipleView = true;
 +        }
 +        tmpSeq.setStart(jseq.getStart());
 +        tmpSeq.setEnd(jseq.getEnd());
 +        tmpseqs.add(tmpSeq);
 +      }
 +      else
 +      {
 +        Sequence vamsasSeq = vamsasSeqs.get(vi);
 +        tmpSeq = new jalview.datamodel.Sequence(vamsasSeq.getName(),
 +                vamsasSeq.getSequence());
 +        tmpSeq.setDescription(vamsasSeq.getDescription());
 +        tmpSeq.setStart(jseq.getStart());
 +        tmpSeq.setEnd(jseq.getEnd());
 +        tmpSeq.setVamsasId(uniqueSetSuffix + seqId);
 +        seqRefIds.put(vamsasSeq.getId(), tmpSeq);
 +        tmpseqs.add(tmpSeq);
 +        vi++;
 +      }
 +
 +      if (safeBoolean(jseq.isViewreference()))
 +      {
 +        referenceseqForView = tmpseqs.get(tmpseqs.size() - 1);
 +      }
 +
 +      if (jseq.isHidden() != null && jseq.isHidden().booleanValue())
 +      {
 +        if (hiddenSeqs == null)
 +        {
 +          hiddenSeqs = new ArrayList<>();
 +        }
 +
 +        hiddenSeqs.add(tmpSeq);
 +      }
 +    }
 +
 +    // /
 +    // Create the alignment object from the sequence set
 +    // ///////////////////////////////
 +    SequenceI[] orderedSeqs = tmpseqs
 +            .toArray(new SequenceI[tmpseqs.size()]);
 +
 +    AlignmentI al = null;
 +    // so we must create or recover the dataset alignment before going further
 +    // ///////////////////////////////
 +    if (vamsasSet.getDatasetId() == null || vamsasSet.getDatasetId() == "")
 +    {
 +      // older jalview projects do not have a dataset - so creat alignment and
 +      // dataset
 +      al = new Alignment(orderedSeqs);
 +      al.setDataset(null);
 +    }
 +    else
 +    {
 +      boolean isdsal = jalviewModel.getViewport().isEmpty();
 +      if (isdsal)
 +      {
 +        // we are importing a dataset record, so
 +        // recover reference to an alignment already materialsed as dataset
 +        al = getDatasetFor(vamsasSet.getDatasetId());
 +      }
 +      if (al == null)
 +      {
 +        // materialse the alignment
 +        al = new Alignment(orderedSeqs);
 +      }
 +      if (isdsal)
 +      {
 +        addDatasetRef(vamsasSet.getDatasetId(), al);
 +      }
 +
 +      // finally, verify all data in vamsasSet is actually present in al
 +      // passing on flag indicating if it is actually a stored dataset
 +      recoverDatasetFor(vamsasSet, al, isdsal, uniqueSeqSetId);
 +    }
 +
 +    if (referenceseqForView != null)
 +    {
 +      al.setSeqrep(referenceseqForView);
 +    }
 +    // / Add the alignment properties
 +    for (int i = 0; i < vamsasSet.getSequenceSetProperties().size(); i++)
 +    {
 +      SequenceSetProperties ssp = vamsasSet.getSequenceSetProperties()
 +              .get(i);
 +      al.setProperty(ssp.getKey(), ssp.getValue());
 +    }
 +
 +    // ///////////////////////////////
 +
 +    Hashtable pdbloaded = new Hashtable(); // TODO nothing writes to this??
 +    if (!multipleView)
 +    {
 +      // load sequence features, database references and any associated PDB
 +      // structures for the alignment
 +      //
 +      // prior to 2.10, this part would only be executed the first time a
 +      // sequence was encountered, but not afterwards.
 +      // now, for 2.10 projects, this is also done if the xml doc includes
 +      // dataset sequences not actually present in any particular view.
 +      //
 +      for (int i = 0; i < vamsasSeqs.size(); i++)
 +      {
 +        JSeq jseq = jseqs.get(i);
 +        if (jseq.getFeatures().size() > 0)
 +        {
 +          List<Feature> features = jseq.getFeatures();
 +          for (int f = 0; f < features.size(); f++)
 +          {
 +            Feature feat = features.get(f);
 +            SequenceFeature sf = new SequenceFeature(feat.getType(),
 +                    feat.getDescription(), feat.getBegin(), feat.getEnd(),
 +                    safeFloat(feat.getScore()), feat.getFeatureGroup());
 +            sf.setStatus(feat.getStatus());
 +
 +            /*
 +             * load any feature attributes - include map-valued attributes
 +             */
 +            Map<String, Map<String, String>> mapAttributes = new HashMap<>();
 +            for (int od = 0; od < feat.getOtherData().size(); od++)
 +            {
 +              OtherData keyValue = feat.getOtherData().get(od);
 +              String attributeName = keyValue.getKey();
 +              String attributeValue = keyValue.getValue();
 +              if (attributeName.startsWith("LINK"))
 +              {
 +                sf.addLink(attributeValue);
 +              }
 +              else
 +              {
 +                String subAttribute = keyValue.getKey2();
 +                if (subAttribute == null)
 +                {
 +                  // simple string-valued attribute
 +                  sf.setValue(attributeName, attributeValue);
 +                }
 +                else
 +                {
 +                  // attribute 'key' has sub-attribute 'key2'
 +                  if (!mapAttributes.containsKey(attributeName))
 +                  {
 +                    mapAttributes.put(attributeName, new HashMap<>());
 +                  }
 +                  mapAttributes.get(attributeName).put(subAttribute,
 +                          attributeValue);
 +                }
 +              }
 +            }
 +            for (Entry<String, Map<String, String>> mapAttribute : mapAttributes
 +                    .entrySet())
 +            {
 +              sf.setValue(mapAttribute.getKey(), mapAttribute.getValue());
 +            }
 +
 +            // adds feature to datasequence's feature set (since Jalview 2.10)
 +            al.getSequenceAt(i).addSequenceFeature(sf);
 +          }
 +        }
 +        if (vamsasSeqs.get(i).getDBRef().size() > 0)
 +        {
 +          // adds dbrefs to datasequence's set (since Jalview 2.10)
 +          addDBRefs(
 +                  al.getSequenceAt(i).getDatasetSequence() == null
 +                          ? al.getSequenceAt(i)
 +                          : al.getSequenceAt(i).getDatasetSequence(),
 +                  vamsasSeqs.get(i));
 +        }
 +        if (jseq.getPdbids().size() > 0)
 +        {
 +          List<Pdbids> ids = jseq.getPdbids();
 +          for (int p = 0; p < ids.size(); p++)
 +          {
 +            Pdbids pdbid = ids.get(p);
 +            jalview.datamodel.PDBEntry entry = new jalview.datamodel.PDBEntry();
 +            entry.setId(pdbid.getId());
 +            if (pdbid.getType() != null)
 +            {
 +              if (PDBEntry.Type.getType(pdbid.getType()) != null)
 +              {
 +                entry.setType(PDBEntry.Type.getType(pdbid.getType()));
 +              }
 +              else
 +              {
 +                entry.setType(PDBEntry.Type.FILE);
 +              }
 +            }
 +            // jprovider is null when executing 'New View'
 +            if (pdbid.getFile() != null && jprovider != null)
 +            {
 +              if (!pdbloaded.containsKey(pdbid.getFile()))
 +              {
 +                entry.setFile(loadPDBFile(jprovider, pdbid.getId(),
 +                        pdbid.getFile()));
 +              }
 +              else
 +              {
 +                entry.setFile(pdbloaded.get(pdbid.getId()).toString());
 +              }
 +            }
 +            /*
 +            if (pdbid.getPdbentryItem() != null)
 +            {
 +              for (PdbentryItem item : pdbid.getPdbentryItem())
 +              {
 +                for (Property pr : item.getProperty())
 +                {
 +                  entry.setProperty(pr.getName(), pr.getValue());
 +                }
 +              }
 +            }
 +            */
 +            for (Property prop : pdbid.getProperty())
 +            {
 +              entry.setProperty(prop.getName(), prop.getValue());
 +            }
 +            StructureSelectionManager
 +                    .getStructureSelectionManager(Desktop.instance)
 +                    .registerPDBEntry(entry);
 +            // adds PDBEntry to datasequence's set (since Jalview 2.10)
 +            if (al.getSequenceAt(i).getDatasetSequence() != null)
 +            {
 +              al.getSequenceAt(i).getDatasetSequence().addPDBId(entry);
 +            }
 +            else
 +            {
 +              al.getSequenceAt(i).addPDBId(entry);
 +            }
 +          }
 +        }
 +      }
 +    } // end !multipleview
 +
 +    // ///////////////////////////////
 +    // LOAD SEQUENCE MAPPINGS
 +
 +    if (vamsasSet.getAlcodonFrame().size() > 0)
 +    {
 +      // TODO Potentially this should only be done once for all views of an
 +      // alignment
 +      List<AlcodonFrame> alc = vamsasSet.getAlcodonFrame();
 +      for (int i = 0; i < alc.size(); i++)
 +      {
 +        AlignedCodonFrame cf = new AlignedCodonFrame();
 +        if (alc.get(i).getAlcodMap().size() > 0)
 +        {
 +          List<AlcodMap> maps = alc.get(i).getAlcodMap();
 +          for (int m = 0; m < maps.size(); m++)
 +          {
 +            AlcodMap map = maps.get(m);
 +            SequenceI dnaseq = seqRefIds.get(map.getDnasq());
 +            // Load Mapping
 +            jalview.datamodel.Mapping mapping = null;
 +            // attach to dna sequence reference.
 +            if (map.getMapping() != null)
 +            {
 +              mapping = addMapping(map.getMapping());
 +              if (dnaseq != null && mapping.getTo() != null)
 +              {
 +                cf.addMap(dnaseq, mapping.getTo(), mapping.getMap());
 +              }
 +              else
 +              {
 +                // defer to later
 +                frefedSequence
 +                        .add(newAlcodMapRef(map.getDnasq(), cf, mapping));
 +              }
 +            }
 +          }
 +          al.addCodonFrame(cf);
 +        }
 +      }
 +    }
 +
 +    // ////////////////////////////////
 +    // LOAD ANNOTATIONS
 +    List<JvAnnotRow> autoAlan = new ArrayList<>();
 +
 +    /*
 +     * store any annotations which forward reference a group's ID
 +     */
 +    Map<String, List<AlignmentAnnotation>> groupAnnotRefs = new Hashtable<>();
 +
 +    if (vamsasSet.getAnnotation().size()/*Count()*/ > 0)
 +    {
 +      List<Annotation> an = vamsasSet.getAnnotation();
 +
 +      for (int i = 0; i < an.size(); i++)
 +      {
 +        Annotation annotation = an.get(i);
 +
 +        /**
 +         * test if annotation is automatically calculated for this view only
 +         */
 +        boolean autoForView = false;
 +        if (annotation.getLabel().equals("Quality")
 +                || annotation.getLabel().equals("Conservation")
 +                || annotation.getLabel().equals("Consensus"))
 +        {
 +          // Kludge for pre 2.5 projects which lacked the autocalculated flag
 +          autoForView = true;
 +          // JAXB has no has() test; schema defaults value to false
 +          // if (!annotation.hasAutoCalculated())
 +          // {
 +          // annotation.setAutoCalculated(true);
 +          // }
 +        }
 +        if (autoForView || annotation.isAutoCalculated())
 +        {
 +          // remove ID - we don't recover annotation from other views for
 +          // view-specific annotation
 +          annotation.setId(null);
 +        }
 +
 +        // set visibility for other annotation in this view
 +        String annotationId = annotation.getId();
 +        if (annotationId != null && annotationIds.containsKey(annotationId))
 +        {
 +          AlignmentAnnotation jda = annotationIds.get(annotationId);
 +          // in principle Visible should always be true for annotation displayed
 +          // in multiple views
 +          if (annotation.isVisible() != null)
 +          {
 +            jda.visible = annotation.isVisible();
 +          }
 +
 +          al.addAnnotation(jda);
 +
 +          continue;
 +        }
 +        // Construct new annotation from model.
 +        List<AnnotationElement> ae = annotation.getAnnotationElement();
 +        jalview.datamodel.Annotation[] anot = null;
 +        java.awt.Color firstColour = null;
 +        int anpos;
 +        if (!annotation.isScoreOnly())
 +        {
 +          anot = new jalview.datamodel.Annotation[al.getWidth()];
 +          for (int aa = 0; aa < ae.size() && aa < anot.length; aa++)
 +          {
 +            AnnotationElement annElement = ae.get(aa);
 +            anpos = annElement.getPosition();
 +
 +            if (anpos >= anot.length)
 +            {
 +              continue;
 +            }
 +
 +            float value = safeFloat(annElement.getValue());
 +            anot[anpos] = new jalview.datamodel.Annotation(
 +                    annElement.getDisplayCharacter(),
 +                    annElement.getDescription(),
 +                    (annElement.getSecondaryStructure() == null
 +                            || annElement.getSecondaryStructure()
 +                                    .length() == 0)
 +                                            ? ' '
 +                                            : annElement
 +                                                    .getSecondaryStructure()
 +                                                    .charAt(0),
 +                    value);
 +            anot[anpos].colour = new Color(safeInt(annElement.getColour()));
 +            if (firstColour == null)
 +            {
 +              firstColour = anot[anpos].colour;
 +            }
 +          }
 +        }
 +        jalview.datamodel.AlignmentAnnotation jaa = null;
 +
 +        if (annotation.isGraph())
 +        {
 +          float llim = 0, hlim = 0;
 +          // if (autoForView || an[i].isAutoCalculated()) {
 +          // hlim=11f;
 +          // }
 +          jaa = new jalview.datamodel.AlignmentAnnotation(
 +                  annotation.getLabel(), annotation.getDescription(), anot,
 +                  llim, hlim, safeInt(annotation.getGraphType()));
 +
 +          jaa.graphGroup = safeInt(annotation.getGraphGroup());
 +          jaa._linecolour = firstColour;
 +          if (annotation.getThresholdLine() != null)
 +          {
 +            jaa.setThreshold(new jalview.datamodel.GraphLine(
 +                    safeFloat(annotation.getThresholdLine().getValue()),
 +                    annotation.getThresholdLine().getLabel(),
 +                    new java.awt.Color(safeInt(
 +                            annotation.getThresholdLine().getColour()))));
 +          }
 +          if (autoForView || annotation.isAutoCalculated())
 +          {
 +            // Hardwire the symbol display line to ensure that labels for
 +            // histograms are displayed
 +            jaa.hasText = true;
 +          }
 +        }
 +        else
 +        {
 +          jaa = new jalview.datamodel.AlignmentAnnotation(
 +                  annotation.getLabel(), annotation.getDescription(), anot);
 +          jaa._linecolour = firstColour;
 +        }
 +        // register new annotation
 +        if (annotation.getId() != null)
 +        {
 +          annotationIds.put(annotation.getId(), jaa);
 +          jaa.annotationId = annotation.getId();
 +        }
 +        // recover sequence association
 +        String sequenceRef = annotation.getSequenceRef();
 +        if (sequenceRef != null)
 +        {
 +          // from 2.9 sequenceRef is to sequence id (JAL-1781)
 +          SequenceI sequence = seqRefIds.get(sequenceRef);
 +          if (sequence == null)
 +          {
 +            // in pre-2.9 projects sequence ref is to sequence name
 +            sequence = al.findName(sequenceRef);
 +          }
 +          if (sequence != null)
 +          {
 +            jaa.createSequenceMapping(sequence, 1, true);
 +            sequence.addAlignmentAnnotation(jaa);
 +          }
 +        }
 +        // and make a note of any group association
 +        if (annotation.getGroupRef() != null
 +                && annotation.getGroupRef().length() > 0)
 +        {
 +          List<jalview.datamodel.AlignmentAnnotation> aal = groupAnnotRefs
 +                  .get(annotation.getGroupRef());
 +          if (aal == null)
 +          {
 +            aal = new ArrayList<>();
 +            groupAnnotRefs.put(annotation.getGroupRef(), aal);
 +          }
 +          aal.add(jaa);
 +        }
 +
 +        if (annotation.getScore() != null)
 +        {
 +          jaa.setScore(annotation.getScore().doubleValue());
 +        }
 +        if (annotation.isVisible() != null)
 +        {
 +          jaa.visible = annotation.isVisible().booleanValue();
 +        }
 +
 +        if (annotation.isCentreColLabels() != null)
 +        {
 +          jaa.centreColLabels = annotation.isCentreColLabels()
 +                  .booleanValue();
 +        }
 +
 +        if (annotation.isScaleColLabels() != null)
 +        {
 +          jaa.scaleColLabel = annotation.isScaleColLabels().booleanValue();
 +        }
 +        if (annotation.isAutoCalculated())
 +        {
 +          // newer files have an 'autoCalculated' flag and store calculation
 +          // state in viewport properties
 +          jaa.autoCalculated = true; // means annotation will be marked for
 +          // update at end of load.
 +        }
 +        if (annotation.getGraphHeight() != null)
 +        {
 +          jaa.graphHeight = annotation.getGraphHeight().intValue();
 +        }
 +        jaa.belowAlignment = annotation.isBelowAlignment();
 +        jaa.setCalcId(annotation.getCalcId());
 +        if (annotation.getProperty().size() > 0)
 +        {
 +          for (Annotation.Property prop : annotation.getProperty())
 +          {
 +            jaa.setProperty(prop.getName(), prop.getValue());
 +          }
 +        }
 +        if (jaa.autoCalculated)
 +        {
 +          autoAlan.add(new JvAnnotRow(i, jaa));
 +        }
 +        else
 +        // if (!autoForView)
 +        {
 +          // add autocalculated group annotation and any user created annotation
 +          // for the view
 +          al.addAnnotation(jaa);
 +        }
 +      }
 +    }
 +    // ///////////////////////
 +    // LOAD GROUPS
 +    // Create alignment markup and styles for this view
 +    if (jalviewModel.getJGroup().size() > 0)
 +    {
 +      List<JGroup> groups = jalviewModel.getJGroup();
 +      boolean addAnnotSchemeGroup = false;
 +      for (int i = 0; i < groups.size(); i++)
 +      {
 +        JGroup jGroup = groups.get(i);
 +        ColourSchemeI cs = null;
 +        if (jGroup.getColour() != null)
 +        {
 +          if (jGroup.getColour().startsWith("ucs"))
 +          {
 +            cs = getUserColourScheme(jalviewModel, jGroup.getColour());
 +          }
 +          else if (jGroup.getColour().equals("AnnotationColourGradient")
 +                  && jGroup.getAnnotationColours() != null)
 +          {
 +            addAnnotSchemeGroup = true;
 +          }
 +          else
 +          {
 +            cs = ColourSchemeProperty.getColourScheme(null, al,
 +                    jGroup.getColour());
 +          }
 +        }
 +        int pidThreshold = safeInt(jGroup.getPidThreshold());
 +
 +        Vector<SequenceI> seqs = new Vector<>();
 +
 +        for (int s = 0; s < jGroup.getSeq().size(); s++)
 +        {
 +          String seqId = jGroup.getSeq().get(s);
 +          SequenceI ts = seqRefIds.get(seqId);
 +
 +          if (ts != null)
 +          {
 +            seqs.addElement(ts);
 +          }
 +        }
 +
 +        if (seqs.size() < 1)
 +        {
 +          continue;
 +        }
 +
 +        SequenceGroup sg = new SequenceGroup(seqs, jGroup.getName(), cs,
 +                safeBoolean(jGroup.isDisplayBoxes()),
 +                safeBoolean(jGroup.isDisplayText()),
 +                safeBoolean(jGroup.isColourText()),
 +                safeInt(jGroup.getStart()), safeInt(jGroup.getEnd()));
 +        sg.getGroupColourScheme().setThreshold(pidThreshold, true);
 +        sg.getGroupColourScheme()
 +                .setConservationInc(safeInt(jGroup.getConsThreshold()));
 +        sg.setOutlineColour(new Color(safeInt(jGroup.getOutlineColour())));
 +
 +        sg.textColour = new Color(safeInt(jGroup.getTextCol1()));
 +        sg.textColour2 = new Color(safeInt(jGroup.getTextCol2()));
 +        sg.setShowNonconserved(safeBoolean(jGroup.isShowUnconserved()));
 +        sg.thresholdTextColour = safeInt(jGroup.getTextColThreshold());
 +        // attributes with a default in the schema are never null
 +        sg.setShowConsensusHistogram(jGroup.isShowConsensusHistogram());
 +        sg.setshowSequenceLogo(jGroup.isShowSequenceLogo());
 +        sg.setNormaliseSequenceLogo(jGroup.isNormaliseSequenceLogo());
 +        sg.setIgnoreGapsConsensus(jGroup.isIgnoreGapsinConsensus());
 +        if (jGroup.getConsThreshold() != null
 +                && jGroup.getConsThreshold().intValue() != 0)
 +        {
 +          Conservation c = new Conservation("All", sg.getSequences(null), 0,
 +                  sg.getWidth() - 1);
 +          c.calculate();
 +          c.verdict(false, 25);
 +          sg.cs.setConservation(c);
 +        }
 +
 +        if (jGroup.getId() != null && groupAnnotRefs.size() > 0)
 +        {
 +          // re-instate unique group/annotation row reference
 +          List<AlignmentAnnotation> jaal = groupAnnotRefs
 +                  .get(jGroup.getId());
 +          if (jaal != null)
 +          {
 +            for (AlignmentAnnotation jaa : jaal)
 +            {
 +              jaa.groupRef = sg;
 +              if (jaa.autoCalculated)
 +              {
 +                // match up and try to set group autocalc alignment row for this
 +                // annotation
 +                if (jaa.label.startsWith("Consensus for "))
 +                {
 +                  sg.setConsensus(jaa);
 +                }
 +                // match up and try to set group autocalc alignment row for this
 +                // annotation
 +                if (jaa.label.startsWith("Conservation for "))
 +                {
 +                  sg.setConservationRow(jaa);
 +                }
 +              }
 +            }
 +          }
 +        }
 +        al.addGroup(sg);
 +        if (addAnnotSchemeGroup)
 +        {
 +          // reconstruct the annotation colourscheme
 +          sg.setColourScheme(
 +                  constructAnnotationColour(jGroup.getAnnotationColours(),
 +                          null, al, jalviewModel, false));
 +        }
 +      }
 +    }
 +    if (view == null)
 +    {
 +      // only dataset in this model, so just return.
 +      return null;
 +    }
 +    // ///////////////////////////////
 +    // LOAD VIEWPORT
 +
 +    AlignFrame af = null;
 +    AlignViewport av = null;
 +    // now check to see if we really need to create a new viewport.
 +    if (multipleView && viewportsAdded.size() == 0)
 +    {
 +      // We recovered an alignment for which a viewport already exists.
 +      // TODO: fix up any settings necessary for overlaying stored state onto
 +      // state recovered from another document. (may not be necessary).
 +      // we may need a binding from a viewport in memory to one recovered from
 +      // XML.
 +      // and then recover its containing af to allow the settings to be applied.
 +      // TODO: fix for vamsas demo
 +      System.err.println(
 +              "About to recover a viewport for existing alignment: Sequence set ID is "
 +                      + uniqueSeqSetId);
 +      Object seqsetobj = retrieveExistingObj(uniqueSeqSetId);
 +      if (seqsetobj != null)
 +      {
 +        if (seqsetobj instanceof String)
 +        {
 +          uniqueSeqSetId = (String) seqsetobj;
 +          System.err.println(
 +                  "Recovered extant sequence set ID mapping for ID : New Sequence set ID is "
 +                          + uniqueSeqSetId);
 +        }
 +        else
 +        {
 +          System.err.println(
 +                  "Warning : Collision between sequence set ID string and existing jalview object mapping.");
 +        }
 +
 +      }
 +    }
 +    /**
 +     * indicate that annotation colours are applied across all groups (pre
 +     * Jalview 2.8.1 behaviour)
 +     */
 +    boolean doGroupAnnColour = Jalview2XML.isVersionStringLaterThan("2.8.1",
 +            jalviewModel.getVersion());
 +
 +    AlignmentPanel ap = null;
 +    boolean isnewview = true;
 +    if (viewId != null)
 +    {
 +      // Check to see if this alignment already has a view id == viewId
 +      jalview.gui.AlignmentPanel views[] = Desktop
 +              .getAlignmentPanels(uniqueSeqSetId);
 +      if (views != null && views.length > 0)
 +      {
 +        for (int v = 0; v < views.length; v++)
 +        {
 +          if (views[v].av.getViewId().equalsIgnoreCase(viewId))
 +          {
 +            // recover the existing alignpanel, alignframe, viewport
 +            af = views[v].alignFrame;
 +            av = views[v].av;
 +            ap = views[v];
 +            // TODO: could even skip resetting view settings if we don't want to
 +            // change the local settings from other jalview processes
 +            isnewview = false;
 +          }
 +        }
 +      }
 +    }
 +
 +    if (isnewview)
 +    {
 +      af = loadViewport(file, jseqs, hiddenSeqs, al, jalviewModel, view,
 +              uniqueSeqSetId, viewId, autoAlan);
 +      av = af.getViewport();
 +      ap = af.alignPanel;
 +    }
 +
 +    /*
 +     * Load any trees, PDB structures and viewers
 +     * 
 +     * Not done if flag is false (when this method is used for New View)
 +     */
 +    if (loadTreesAndStructures)
 +    {
 +      loadTrees(jalviewModel, view, af, av, ap);
++      loadExternalTrees(jprovider, jalviewModel, av);
 +      loadPCAViewers(jalviewModel, ap);
 +      loadPDBStructures(jprovider, jseqs, af, ap);
 +      loadRnaViewers(jprovider, jseqs, ap);
 +    }
 +    // and finally return.
 +    return af;
 +  }
++    
++  private void loadExternalTrees(jarInputStreamProvider jprovider,
++          JalviewModel jms, AlignViewport av)
++  {
++    // TODO: allow more than one archeopteryx session per project
++    String treeFile = copyJarEntry(jprovider, "aptx-test", "aptx", null);
++    if (treeFile != null)
++    {
++    try
++    {
++      AptxInit.createInstancesFromFile(treeFile, av);
++    } catch (IOException e)
++    {
++      // TODO Auto-generated catch block
++      e.printStackTrace();
++      }
++    }
++    
++  }
++
++
 +
 +  /**
 +   * Instantiate and link any saved RNA (Varna) viewers. The state of the Varna
 +   * panel is restored from separate jar entries, two (gapped and trimmed) per
 +   * sequence and secondary structure.
 +   * 
 +   * Currently each viewer shows just one sequence and structure (gapped and
 +   * trimmed), however this method is designed to support multiple sequences or
 +   * structures in viewers if wanted in future.
 +   * 
 +   * @param jprovider
 +   * @param jseqs
 +   * @param ap
 +   */
 +  private void loadRnaViewers(jarInputStreamProvider jprovider,
 +          List<JSeq> jseqs, AlignmentPanel ap)
 +  {
 +    /*
 +     * scan the sequences for references to viewers; create each one the first
 +     * time it is referenced, add Rna models to existing viewers
 +     */
 +    for (JSeq jseq : jseqs)
 +    {
 +      for (int i = 0; i < jseq.getRnaViewer().size(); i++)
 +      {
 +        RnaViewer viewer = jseq.getRnaViewer().get(i);
 +        AppVarna appVarna = findOrCreateVarnaViewer(viewer, uniqueSetSuffix,
 +                ap);
 +
 +        for (int j = 0; j < viewer.getSecondaryStructure().size(); j++)
 +        {
 +          SecondaryStructure ss = viewer.getSecondaryStructure().get(j);
 +          SequenceI seq = seqRefIds.get(jseq.getId());
 +          AlignmentAnnotation ann = this.annotationIds
 +                  .get(ss.getAnnotationId());
 +
 +          /*
 +           * add the structure to the Varna display (with session state copied
 +           * from the jar to a temporary file)
 +           */
 +          boolean gapped = safeBoolean(ss.isGapped());
 +          String rnaTitle = ss.getTitle();
 +          String sessionState = ss.getViewerState();
 +          String tempStateFile = copyJarEntry(jprovider, sessionState,
 +                  "varna", null);
 +          RnaModel rna = new RnaModel(rnaTitle, ann, seq, null, gapped);
 +          appVarna.addModelSession(rna, rnaTitle, tempStateFile);
 +        }
 +        appVarna.setInitialSelection(safeInt(viewer.getSelectedRna()));
 +      }
 +    }
 +  }
 +
 +  /**
 +   * Locate and return an already instantiated matching AppVarna, or create one
 +   * if not found
 +   * 
 +   * @param viewer
 +   * @param viewIdSuffix
 +   * @param ap
 +   * @return
 +   */
 +  protected AppVarna findOrCreateVarnaViewer(RnaViewer viewer,
 +          String viewIdSuffix, AlignmentPanel ap)
 +  {
 +    /*
 +     * on each load a suffix is appended to the saved viewId, to avoid conflicts
 +     * if load is repeated
 +     */
 +    String postLoadId = viewer.getViewId() + viewIdSuffix;
 +    for (JInternalFrame frame : getAllFrames())
 +    {
 +      if (frame instanceof AppVarna)
 +      {
 +        AppVarna varna = (AppVarna) frame;
 +        if (postLoadId.equals(varna.getViewId()))
 +        {
 +          // this viewer is already instantiated
 +          // could in future here add ap as another 'parent' of the
 +          // AppVarna window; currently just 1-to-many
 +          return varna;
 +        }
 +      }
 +    }
 +
 +    /*
 +     * viewer not found - make it
 +     */
 +    RnaViewerModel model = new RnaViewerModel(postLoadId, viewer.getTitle(),
 +            safeInt(viewer.getXpos()), safeInt(viewer.getYpos()),
 +            safeInt(viewer.getWidth()), safeInt(viewer.getHeight()),
 +            safeInt(viewer.getDividerLocation()));
 +    AppVarna varna = new AppVarna(model, ap);
 +
 +    return varna;
 +  }
 +
 +  /**
 +   * Load any saved trees
 +   * 
 +   * @param jm
 +   * @param view
 +   * @param af
 +   * @param av
 +   * @param ap
 +   */
 +  protected void loadTrees(JalviewModel jm, Viewport view, AlignFrame af,
 +          AlignViewport av, AlignmentPanel ap)
 +  {
 +    // TODO result of automated refactoring - are all these parameters needed?
 +    try
 +    {
 +      for (int t = 0; t < jm.getTree().size(); t++)
 +      {
 +
 +        Tree tree = jm.getTree().get(t);
 +
++        TreeFrameI externalViewer = AptxInit.createInstanceFromNhx(
++                tree.getTitle(), tree.getNewick(),
++                av);
++
 +        TreePanel tp = (TreePanel) retrieveExistingObj(tree.getId());
 +        if (tp == null)
 +        {
 +          tp = af.showNewickTree(new NewickFile(tree.getNewick()),
 +                  tree.getTitle(), safeInt(tree.getWidth()),
 +                  safeInt(tree.getHeight()), safeInt(tree.getXpos()),
 +                  safeInt(tree.getYpos()));
 +          if (tree.getId() != null)
 +          {
 +            // perhaps bind the tree id to something ?
 +          }
 +        }
 +        else
 +        {
 +          // update local tree attributes ?
 +          // TODO: should check if tp has been manipulated by user - if so its
 +          // settings shouldn't be modified
 +          tp.setTitle(tree.getTitle());
 +          tp.setBounds(new Rectangle(safeInt(tree.getXpos()),
 +                  safeInt(tree.getYpos()), safeInt(tree.getWidth()),
 +                  safeInt(tree.getHeight())));
 +          tp.setViewport(av); // af.viewport;
 +          // TODO: verify 'associate with all views' works still
 +          tp.getTreeCanvas().setViewport(av); // af.viewport;
 +          tp.getTreeCanvas().setAssociatedPanel(ap); // af.alignPanel;
 +        }
 +        tp.getTreeCanvas().setApplyToAllViews(tree.isLinkToAllViews());
 +        if (tp == null)
 +        {
 +          Console.warn(
 +                  "There was a problem recovering stored Newick tree: \n"
 +                          + tree.getNewick());
 +          continue;
 +        }
 +
 +        tp.fitToWindow.setState(safeBoolean(tree.isFitToWindow()));
 +        tp.fitToWindow_actionPerformed(null);
 +
 +        if (tree.getFontName() != null)
 +        {
 +          tp.setTreeFont(
 +                  new Font(tree.getFontName(), safeInt(tree.getFontStyle()),
 +                          safeInt(tree.getFontSize())));
 +        }
 +        else
 +        {
 +          tp.setTreeFont(
 +                  new Font(view.getFontName(), safeInt(view.getFontStyle()),
 +                          safeInt(view.getFontSize())));
 +        }
 +
 +        tp.showPlaceholders(safeBoolean(tree.isMarkUnlinked()));
 +        tp.showBootstrap(safeBoolean(tree.isShowBootstrap()));
 +        tp.showDistances(safeBoolean(tree.isShowDistances()));
 +
 +        tp.getTreeCanvas().setThreshold(safeFloat(tree.getThreshold()));
 +
 +        if (safeBoolean(tree.isCurrentTree()))
 +        {
 +          af.getViewport().setCurrentTree(tp.getTree());
 +        }
 +      }
 +
 +    } catch (Exception ex)
 +    {
 +      ex.printStackTrace();
 +    }
 +  }
 +
 +  /**
 +   * Load and link any saved structure viewers.
 +   * 
 +   * @param jprovider
 +   * @param jseqs
 +   * @param af
 +   * @param ap
 +   */
 +  protected void loadPDBStructures(jarInputStreamProvider jprovider,
 +          List<JSeq> jseqs, AlignFrame af, AlignmentPanel ap)
 +  {
 +    /*
 +     * Run through all PDB ids on the alignment, and collect mappings between
 +     * distinct view ids and all sequences referring to that view.
 +     */
 +    Map<String, StructureViewerModel> structureViewers = new LinkedHashMap<>();
 +
 +    for (int i = 0; i < jseqs.size(); i++)
 +    {
 +      JSeq jseq = jseqs.get(i);
 +      if (jseq.getPdbids().size() > 0)
 +      {
 +        List<Pdbids> ids = jseq.getPdbids();
 +        for (int p = 0; p < ids.size(); p++)
 +        {
 +          Pdbids pdbid = ids.get(p);
 +          final int structureStateCount = pdbid.getStructureState().size();
 +          for (int s = 0; s < structureStateCount; s++)
 +          {
 +            // check to see if we haven't already created this structure view
 +            final StructureState structureState = pdbid.getStructureState()
 +                    .get(s);
 +            String sviewid = (structureState.getViewId() == null) ? null
 +                    : structureState.getViewId() + uniqueSetSuffix;
 +            jalview.datamodel.PDBEntry jpdb = new jalview.datamodel.PDBEntry();
 +            // Originally : pdbid.getFile()
 +            // : TODO: verify external PDB file recovery still works in normal
 +            // jalview project load
 +            jpdb.setFile(
 +                    loadPDBFile(jprovider, pdbid.getId(), pdbid.getFile()));
 +            jpdb.setId(pdbid.getId());
 +
 +            int x = safeInt(structureState.getXpos());
 +            int y = safeInt(structureState.getYpos());
 +            int width = safeInt(structureState.getWidth());
 +            int height = safeInt(structureState.getHeight());
 +
 +            // Probably don't need to do this anymore...
 +            // Desktop.desktop.getComponentAt(x, y);
 +            // TODO: NOW: check that this recovers the PDB file correctly.
 +            String pdbFile = loadPDBFile(jprovider, pdbid.getId(),
 +                    pdbid.getFile());
 +            jalview.datamodel.SequenceI seq = seqRefIds
 +                    .get(jseq.getId() + "");
 +            if (sviewid == null)
 +            {
 +              sviewid = "_jalview_pre2_4_" + x + "," + y + "," + width + ","
 +                      + height;
 +            }
 +            if (!structureViewers.containsKey(sviewid))
 +            {
 +              String viewerType = structureState.getType();
 +              if (viewerType == null) // pre Jalview 2.9
 +              {
 +                viewerType = ViewerType.JMOL.toString();
 +              }
 +              structureViewers.put(sviewid,
 +                      new StructureViewerModel(x, y, width, height, false,
 +                              false, true, structureState.getViewId(),
 +                              viewerType));
 +              // Legacy pre-2.7 conversion JAL-823 :
 +              // do not assume any view has to be linked for colour by
 +              // sequence
 +            }
 +
 +            // assemble String[] { pdb files }, String[] { id for each
 +            // file }, orig_fileloc, SequenceI[][] {{ seqs_file 1 }, {
 +            // seqs_file 2}, boolean[] {
 +            // linkAlignPanel,superposeWithAlignpanel}} from hash
 +            StructureViewerModel jmoldat = structureViewers.get(sviewid);
 +            jmoldat.setAlignWithPanel(jmoldat.isAlignWithPanel()
 +                    || structureState.isAlignwithAlignPanel());
 +
 +            /*
 +             * Default colour by linked panel to false if not specified (e.g.
 +             * for pre-2.7 projects)
 +             */
 +            boolean colourWithAlignPanel = jmoldat.isColourWithAlignPanel();
 +            colourWithAlignPanel |= structureState.isColourwithAlignPanel();
 +            jmoldat.setColourWithAlignPanel(colourWithAlignPanel);
 +
 +            /*
 +             * Default colour by viewer to true if not specified (e.g. for
 +             * pre-2.7 projects)
 +             */
 +            boolean colourByViewer = jmoldat.isColourByViewer();
 +            colourByViewer &= structureState.isColourByJmol();
 +            jmoldat.setColourByViewer(colourByViewer);
 +
 +            if (jmoldat.getStateData().length() < structureState.getValue()
 +                    /*Content()*/.length())
 +            {
 +              jmoldat.setStateData(structureState.getValue());// Content());
 +            }
 +            if (pdbid.getFile() != null)
 +            {
 +              File mapkey = new File(pdbid.getFile());
 +              StructureData seqstrmaps = jmoldat.getFileData().get(mapkey);
 +              if (seqstrmaps == null)
 +              {
 +                jmoldat.getFileData().put(mapkey,
 +                        seqstrmaps = jmoldat.new StructureData(pdbFile,
 +                                pdbid.getId()));
 +              }
 +              if (!seqstrmaps.getSeqList().contains(seq))
 +              {
 +                seqstrmaps.getSeqList().add(seq);
 +                // TODO and chains?
 +              }
 +            }
 +            else
 +            {
 +              errorMessage = ("The Jmol views in this project were imported\nfrom an older version of Jalview.\nPlease review the sequence colour associations\nin the Colour by section of the Jmol View menu.\n\nIn the case of problems, see note at\nhttp://issues.jalview.org/browse/JAL-747");
 +              Console.warn(errorMessage);
 +            }
 +          }
 +        }
 +      }
 +    }
 +    // Instantiate the associated structure views
 +    for (Entry<String, StructureViewerModel> entry : structureViewers
 +            .entrySet())
 +    {
 +      try
 +      {
 +        createOrLinkStructureViewer(entry, af, ap, jprovider);
 +      } catch (Exception e)
 +      {
 +        System.err.println(
 +                "Error loading structure viewer: " + e.getMessage());
 +        // failed - try the next one
 +      }
 +    }
 +  }
 +
 +  /**
 +   * 
 +   * @param viewerData
 +   * @param af
 +   * @param ap
 +   * @param jprovider
 +   */
 +  protected void createOrLinkStructureViewer(
 +          Entry<String, StructureViewerModel> viewerData, AlignFrame af,
 +          AlignmentPanel ap, jarInputStreamProvider jprovider)
 +  {
 +    final StructureViewerModel stateData = viewerData.getValue();
 +
 +    /*
 +     * Search for any viewer windows already open from other alignment views
 +     * that exactly match the stored structure state
 +     */
 +    StructureViewerBase comp = findMatchingViewer(viewerData);
 +
 +    if (comp != null)
 +    {
 +      linkStructureViewer(ap, comp, stateData);
 +      return;
 +    }
 +
 +    String type = stateData.getType();
 +    try
 +    {
 +      ViewerType viewerType = ViewerType.valueOf(type);
 +      createStructureViewer(viewerType, viewerData, af, jprovider);
 +    } catch (IllegalArgumentException | NullPointerException e)
 +    {
 +      // TODO JAL-3619 show error dialog / offer an alternative viewer
 +      Console.error("Invalid structure viewer type: " + type);
 +    }
 +  }
 +
 +  /**
 +   * Generates a name for the entry in the project jar file to hold state
 +   * information for a structure viewer
 +   * 
 +   * @param viewId
 +   * @return
 +   */
 +  protected String getViewerJarEntryName(String viewId)
 +  {
 +    return VIEWER_PREFIX + viewId;
 +  }
 +
 +  /**
 +   * Returns any open frame that matches given structure viewer data. The match
 +   * is based on the unique viewId, or (for older project versions) the frame's
 +   * geometry.
 +   * 
 +   * @param viewerData
 +   * @return
 +   */
 +  protected StructureViewerBase findMatchingViewer(
 +          Entry<String, StructureViewerModel> viewerData)
 +  {
 +    final String sviewid = viewerData.getKey();
 +    final StructureViewerModel svattrib = viewerData.getValue();
 +    StructureViewerBase comp = null;
 +    JInternalFrame[] frames = getAllFrames();
 +    for (JInternalFrame frame : frames)
 +    {
 +      if (frame instanceof StructureViewerBase)
 +      {
 +        /*
 +         * Post jalview 2.4 schema includes structure view id
 +         */
 +        if (sviewid != null && ((StructureViewerBase) frame).getViewId()
 +                .equals(sviewid))
 +        {
 +          comp = (StructureViewerBase) frame;
 +          break; // break added in 2.9
 +        }
 +        /*
 +         * Otherwise test for matching position and size of viewer frame
 +         */
 +        else if (frame.getX() == svattrib.getX()
 +                && frame.getY() == svattrib.getY()
 +                && frame.getHeight() == svattrib.getHeight()
 +                && frame.getWidth() == svattrib.getWidth())
 +        {
 +          comp = (StructureViewerBase) frame;
 +          // no break in faint hope of an exact match on viewId
 +        }
 +      }
 +    }
 +    return comp;
 +  }
 +
 +  /**
 +   * Link an AlignmentPanel to an existing structure viewer.
 +   * 
 +   * @param ap
 +   * @param viewer
 +   * @param oldFiles
 +   * @param useinViewerSuperpos
 +   * @param usetoColourbyseq
 +   * @param viewerColouring
 +   */
 +  protected void linkStructureViewer(AlignmentPanel ap,
 +          StructureViewerBase viewer, StructureViewerModel stateData)
 +  {
 +    // NOTE: if the jalview project is part of a shared session then
 +    // view synchronization should/could be done here.
 +
 +    final boolean useinViewerSuperpos = stateData.isAlignWithPanel();
 +    final boolean usetoColourbyseq = stateData.isColourWithAlignPanel();
 +    final boolean viewerColouring = stateData.isColourByViewer();
 +    Map<File, StructureData> oldFiles = stateData.getFileData();
 +
 +    /*
 +     * Add mapping for sequences in this view to an already open viewer
 +     */
 +    final AAStructureBindingModel binding = viewer.getBinding();
 +    for (File id : oldFiles.keySet())
 +    {
 +      // add this and any other pdb files that should be present in the
 +      // viewer
 +      StructureData filedat = oldFiles.get(id);
 +      String pdbFile = filedat.getFilePath();
 +      SequenceI[] seq = filedat.getSeqList().toArray(new SequenceI[0]);
 +      binding.getSsm().setMapping(seq, null, pdbFile, DataSourceType.FILE,
 +              null);
 +      binding.addSequenceForStructFile(pdbFile, seq);
 +    }
 +    // and add the AlignmentPanel's reference to the view panel
 +    viewer.addAlignmentPanel(ap);
 +    if (useinViewerSuperpos)
 +    {
 +      viewer.useAlignmentPanelForSuperposition(ap);
 +    }
 +    else
 +    {
 +      viewer.excludeAlignmentPanelForSuperposition(ap);
 +    }
 +    if (usetoColourbyseq)
 +    {
 +      viewer.useAlignmentPanelForColourbyseq(ap, !viewerColouring);
 +    }
 +    else
 +    {
 +      viewer.excludeAlignmentPanelForColourbyseq(ap);
 +    }
 +  }
 +
 +  /**
 +   * Get all frames within the Desktop.
 +   * 
 +   * @return
 +   */
 +  protected JInternalFrame[] getAllFrames()
 +  {
 +    JInternalFrame[] frames = null;
 +    // TODO is this necessary - is it safe - risk of hanging?
 +    do
 +    {
 +      try
 +      {
 +        frames = Desktop.desktop.getAllFrames();
 +      } catch (ArrayIndexOutOfBoundsException e)
 +      {
 +        // occasional No such child exceptions are thrown here...
 +        try
 +        {
 +          Thread.sleep(10);
 +        } catch (InterruptedException f)
 +        {
 +        }
 +      }
 +    } while (frames == null);
 +    return frames;
 +  }
 +
 +  /**
 +   * Answers true if 'version' is equal to or later than 'supported', where each
 +   * is formatted as major/minor versions like "2.8.3" or "2.3.4b1" for bugfix
 +   * changes. Development and test values for 'version' are leniently treated
 +   * i.e. answer true.
 +   * 
 +   * @param supported
 +   *          - minimum version we are comparing against
 +   * @param version
 +   *          - version of data being processsed
 +   * @return
 +   */
 +  public static boolean isVersionStringLaterThan(String supported,
 +          String version)
 +  {
 +    if (supported == null || version == null
 +            || version.equalsIgnoreCase("DEVELOPMENT BUILD")
 +            || version.equalsIgnoreCase("Test")
 +            || version.equalsIgnoreCase("AUTOMATED BUILD"))
 +    {
 +      System.err.println("Assuming project file with "
 +              + (version == null ? "null" : version)
 +              + " is compatible with Jalview version " + supported);
 +      return true;
 +    }
 +    else
 +    {
 +      return StringUtils.compareVersions(version, supported, "b") >= 0;
 +    }
 +  }
 +
 +  Vector<JalviewStructureDisplayI> newStructureViewers = null;
 +
 +  protected void addNewStructureViewer(JalviewStructureDisplayI sview)
 +  {
 +    if (newStructureViewers != null)
 +    {
 +      sview.getBinding().setFinishedLoadingFromArchive(false);
 +      newStructureViewers.add(sview);
 +    }
 +  }
 +
 +  protected void setLoadingFinishedForNewStructureViewers()
 +  {
 +    if (newStructureViewers != null)
 +    {
 +      for (JalviewStructureDisplayI sview : newStructureViewers)
 +      {
 +        sview.getBinding().setFinishedLoadingFromArchive(true);
 +      }
 +      newStructureViewers.clear();
 +      newStructureViewers = null;
 +    }
 +  }
 +
 +  AlignFrame loadViewport(String file, List<JSeq> JSEQ,
 +          List<SequenceI> hiddenSeqs, AlignmentI al, JalviewModel jm,
 +          Viewport view, String uniqueSeqSetId, String viewId,
 +          List<JvAnnotRow> autoAlan)
 +  {
 +    AlignFrame af = null;
 +    af = new AlignFrame(al, safeInt(view.getWidth()),
 +            safeInt(view.getHeight()), uniqueSeqSetId, viewId)
 +    // {
 +    //
 +    // @Override
 +    // protected void processKeyEvent(java.awt.event.KeyEvent e) {
 +    // System.out.println("Jalview2XML AF " + e);
 +    // super.processKeyEvent(e);
 +    //
 +    // }
 +    //
 +    // }
 +    ;
 +
 +    af.setFileName(file, FileFormat.Jalview);
 +
 +    final AlignViewport viewport = af.getViewport();
 +    for (int i = 0; i < JSEQ.size(); i++)
 +    {
 +      int colour = safeInt(JSEQ.get(i).getColour());
 +      viewport.setSequenceColour(viewport.getAlignment().getSequenceAt(i),
 +              new Color(colour));
 +    }
 +
 +    if (al.hasSeqrep())
 +    {
 +      viewport.setColourByReferenceSeq(true);
 +      viewport.setDisplayReferenceSeq(true);
 +    }
 +
 +    viewport.setGatherViewsHere(safeBoolean(view.isGatheredViews()));
 +
 +    if (view.getSequenceSetId() != null)
 +    {
 +      AlignmentViewport av = viewportsAdded.get(uniqueSeqSetId);
 +
 +      viewport.setSequenceSetId(uniqueSeqSetId);
 +      if (av != null)
 +      {
 +        // propagate shared settings to this new view
 +        viewport.setHistoryList(av.getHistoryList());
 +        viewport.setRedoList(av.getRedoList());
 +      }
 +      else
 +      {
 +        viewportsAdded.put(uniqueSeqSetId, viewport);
 +      }
 +      // TODO: check if this method can be called repeatedly without
 +      // side-effects if alignpanel already registered.
 +      PaintRefresher.Register(af.alignPanel, uniqueSeqSetId);
 +    }
 +    // apply Hidden regions to view.
 +    if (hiddenSeqs != null)
 +    {
 +      for (int s = 0; s < JSEQ.size(); s++)
 +      {
 +        SequenceGroup hidden = new SequenceGroup();
 +        boolean isRepresentative = false;
 +        for (int r = 0; r < JSEQ.get(s).getHiddenSequences().size(); r++)
 +        {
 +          isRepresentative = true;
 +          SequenceI sequenceToHide = al
 +                  .getSequenceAt(JSEQ.get(s).getHiddenSequences().get(r));
 +          hidden.addSequence(sequenceToHide, false);
 +          // remove from hiddenSeqs list so we don't try to hide it twice
 +          hiddenSeqs.remove(sequenceToHide);
 +        }
 +        if (isRepresentative)
 +        {
 +          SequenceI representativeSequence = al.getSequenceAt(s);
 +          hidden.addSequence(representativeSequence, false);
 +          viewport.hideRepSequences(representativeSequence, hidden);
 +        }
 +      }
 +
 +      SequenceI[] hseqs = hiddenSeqs
 +              .toArray(new SequenceI[hiddenSeqs.size()]);
 +      viewport.hideSequence(hseqs);
 +
 +    }
 +    // recover view properties and display parameters
 +
 +    viewport.setShowAnnotation(safeBoolean(view.isShowAnnotation()));
 +    viewport.setAbovePIDThreshold(safeBoolean(view.isPidSelected()));
 +    final int pidThreshold = safeInt(view.getPidThreshold());
 +    viewport.setThreshold(pidThreshold);
 +
 +    viewport.setColourText(safeBoolean(view.isShowColourText()));
 +
 +    viewport.setConservationSelected(
 +            safeBoolean(view.isConservationSelected()));
 +    viewport.setIncrement(safeInt(view.getConsThreshold()));
 +    viewport.setShowJVSuffix(safeBoolean(view.isShowFullId()));
 +    viewport.setRightAlignIds(safeBoolean(view.isRightAlignIds()));
 +    viewport.setFont(new Font(view.getFontName(),
 +            safeInt(view.getFontStyle()), safeInt(view.getFontSize())),
 +            true);
 +    ViewStyleI vs = viewport.getViewStyle();
 +    vs.setScaleProteinAsCdna(view.isScaleProteinAsCdna());
 +    viewport.setViewStyle(vs);
 +    // TODO: allow custom charWidth/Heights to be restored by updating them
 +    // after setting font - which means set above to false
 +    viewport.setRenderGaps(safeBoolean(view.isRenderGaps()));
 +    viewport.setWrapAlignment(safeBoolean(view.isWrapAlignment()));
 +    viewport.setShowAnnotation(safeBoolean(view.isShowAnnotation()));
 +
 +    viewport.setShowBoxes(safeBoolean(view.isShowBoxes()));
 +
 +    viewport.setShowText(safeBoolean(view.isShowText()));
 +
 +    viewport.setTextColour(new Color(safeInt(view.getTextCol1())));
 +    viewport.setTextColour2(new Color(safeInt(view.getTextCol2())));
 +    viewport.setThresholdTextColour(safeInt(view.getTextColThreshold()));
 +    viewport.setShowUnconserved(view.isShowUnconserved());
 +    viewport.getRanges().setStartRes(safeInt(view.getStartRes()));
 +
 +    if (view.getViewName() != null)
 +    {
 +      viewport.setViewName(view.getViewName());
 +      af.setInitialTabVisible();
 +    }
 +    af.setBounds(safeInt(view.getXpos()), safeInt(view.getYpos()),
 +            safeInt(view.getWidth()), safeInt(view.getHeight()));
 +    // startSeq set in af.alignPanel.updateLayout below
 +    af.alignPanel.updateLayout();
 +    ColourSchemeI cs = null;
 +    // apply colourschemes
 +    if (view.getBgColour() != null)
 +    {
 +      if (view.getBgColour().startsWith("ucs"))
 +      {
 +        cs = getUserColourScheme(jm, view.getBgColour());
 +      }
 +      else if (view.getBgColour().startsWith("Annotation"))
 +      {
 +        AnnotationColourScheme viewAnnColour = view.getAnnotationColours();
 +        cs = constructAnnotationColour(viewAnnColour, af, al, jm, true);
 +
 +        // annpos
 +
 +      }
 +      else
 +      {
 +        cs = ColourSchemeProperty.getColourScheme(af.getViewport(), al,
 +                view.getBgColour());
 +      }
 +    }
 +
 +    /*
 +     * turn off 'alignment colour applies to all groups'
 +     * while restoring global colour scheme
 +     */
 +    viewport.setColourAppliesToAllGroups(false);
 +    viewport.setGlobalColourScheme(cs);
 +    viewport.getResidueShading().setThreshold(pidThreshold,
 +            view.isIgnoreGapsinConsensus());
 +    viewport.getResidueShading()
 +            .setConsensus(viewport.getSequenceConsensusHash());
 +    if (safeBoolean(view.isConservationSelected()) && cs != null)
 +    {
 +      viewport.getResidueShading()
 +              .setConservationInc(safeInt(view.getConsThreshold()));
 +    }
 +    af.changeColour(cs);
 +    viewport.setColourAppliesToAllGroups(true);
 +
 +    viewport.setShowSequenceFeatures(
 +            safeBoolean(view.isShowSequenceFeatures()));
 +
 +    viewport.setCentreColumnLabels(view.isCentreColumnLabels());
 +    viewport.setIgnoreGapsConsensus(view.isIgnoreGapsinConsensus(), null);
 +    viewport.setFollowHighlight(view.isFollowHighlight());
 +    viewport.followSelection = view.isFollowSelection();
 +    viewport.setShowConsensusHistogram(view.isShowConsensusHistogram());
 +    viewport.setShowSequenceLogo(view.isShowSequenceLogo());
 +    viewport.setNormaliseSequenceLogo(view.isNormaliseSequenceLogo());
 +    viewport.setShowDBRefs(safeBoolean(view.isShowDbRefTooltip()));
 +    viewport.setShowNPFeats(safeBoolean(view.isShowNPfeatureTooltip()));
 +    viewport.setShowGroupConsensus(view.isShowGroupConsensus());
 +    viewport.setShowGroupConservation(view.isShowGroupConservation());
 +    viewport.setShowComplementFeatures(view.isShowComplementFeatures());
 +    viewport.setShowComplementFeaturesOnTop(
 +            view.isShowComplementFeaturesOnTop());
 +
 +    // recover feature settings
 +    if (jm.getFeatureSettings() != null)
 +    {
 +      FeatureRendererModel fr = af.alignPanel.getSeqPanel().seqCanvas
 +              .getFeatureRenderer();
 +      FeaturesDisplayed fdi;
 +      viewport.setFeaturesDisplayed(fdi = new FeaturesDisplayed());
 +      String[] renderOrder = new String[jm.getFeatureSettings().getSetting()
 +              .size()];
 +      Map<String, FeatureColourI> featureColours = new Hashtable<>();
 +      Map<String, Float> featureOrder = new Hashtable<>();
 +
 +      for (int fs = 0; fs < jm.getFeatureSettings().getSetting()
 +              .size(); fs++)
 +      {
 +        Setting setting = jm.getFeatureSettings().getSetting().get(fs);
 +        String featureType = setting.getType();
 +
 +        /*
 +         * restore feature filters (if any)
 +         */
 +        jalview.xml.binding.jalview.FeatureMatcherSet filters = setting
 +                .getMatcherSet();
 +        if (filters != null)
 +        {
 +          FeatureMatcherSetI filter = Jalview2XML.parseFilter(featureType,
 +                  filters);
 +          if (!filter.isEmpty())
 +          {
 +            fr.setFeatureFilter(featureType, filter);
 +          }
 +        }
 +
 +        /*
 +         * restore feature colour scheme
 +         */
 +        Color maxColour = new Color(setting.getColour());
 +        if (setting.getMincolour() != null)
 +        {
 +          /*
 +           * minColour is always set unless a simple colour
 +           * (including for colour by label though it doesn't use it)
 +           */
 +          Color minColour = new Color(setting.getMincolour().intValue());
 +          Color noValueColour = minColour;
 +          NoValueColour noColour = setting.getNoValueColour();
 +          if (noColour == NoValueColour.NONE)
 +          {
 +            noValueColour = null;
 +          }
 +          else if (noColour == NoValueColour.MAX)
 +          {
 +            noValueColour = maxColour;
 +          }
 +          float min = safeFloat(safeFloat(setting.getMin()));
 +          float max = setting.getMax() == null ? 1f
 +                  : setting.getMax().floatValue();
 +          FeatureColourI gc = new FeatureColour(maxColour, minColour,
 +                  maxColour, noValueColour, min, max);
 +          if (setting.getAttributeName().size() > 0)
 +          {
 +            gc.setAttributeName(setting.getAttributeName().toArray(
 +                    new String[setting.getAttributeName().size()]));
 +          }
 +          if (setting.getThreshold() != null)
 +          {
 +            gc.setThreshold(setting.getThreshold().floatValue());
 +            int threshstate = safeInt(setting.getThreshstate());
 +            // -1 = None, 0 = Below, 1 = Above threshold
 +            if (threshstate == 0)
 +            {
 +              gc.setBelowThreshold(true);
 +            }
 +            else if (threshstate == 1)
 +            {
 +              gc.setAboveThreshold(true);
 +            }
 +          }
 +          gc.setAutoScaled(true); // default
 +          if (setting.isAutoScale() != null)
 +          {
 +            gc.setAutoScaled(setting.isAutoScale());
 +          }
 +          if (setting.isColourByLabel() != null)
 +          {
 +            gc.setColourByLabel(setting.isColourByLabel());
 +          }
 +          // and put in the feature colour table.
 +          featureColours.put(featureType, gc);
 +        }
 +        else
 +        {
 +          featureColours.put(featureType, new FeatureColour(maxColour));
 +        }
 +        renderOrder[fs] = featureType;
 +        if (setting.getOrder() != null)
 +        {
 +          featureOrder.put(featureType, setting.getOrder().floatValue());
 +        }
 +        else
 +        {
 +          featureOrder.put(featureType, Float.valueOf(
 +                  fs / jm.getFeatureSettings().getSetting().size()));
 +        }
 +        if (safeBoolean(setting.isDisplay()))
 +        {
 +          fdi.setVisible(featureType);
 +        }
 +      }
 +      Map<String, Boolean> fgtable = new Hashtable<>();
 +      for (int gs = 0; gs < jm.getFeatureSettings().getGroup().size(); gs++)
 +      {
 +        Group grp = jm.getFeatureSettings().getGroup().get(gs);
 +        fgtable.put(grp.getName(), Boolean.valueOf(grp.isDisplay()));
 +      }
 +      // FeatureRendererSettings frs = new FeatureRendererSettings(renderOrder,
 +      // fgtable, featureColours, jms.getFeatureSettings().hasTransparency() ?
 +      // jms.getFeatureSettings().getTransparency() : 0.0, featureOrder);
 +      FeatureRendererSettings frs = new FeatureRendererSettings(renderOrder,
 +              fgtable, featureColours, 1.0f, featureOrder);
 +      fr.transferSettings(frs);
 +    }
 +
 +    if (view.getHiddenColumns().size() > 0)
 +    {
 +      for (int c = 0; c < view.getHiddenColumns().size(); c++)
 +      {
 +        final HiddenColumns hc = view.getHiddenColumns().get(c);
 +        viewport.hideColumns(safeInt(hc.getStart()),
 +                safeInt(hc.getEnd()) /* +1 */);
 +      }
 +    }
 +    if (view.getCalcIdParam() != null)
 +    {
 +      for (CalcIdParam calcIdParam : view.getCalcIdParam())
 +      {
 +        if (calcIdParam != null)
 +        {
 +          if (recoverCalcIdParam(calcIdParam, viewport))
 +          {
 +          }
 +          else
 +          {
 +            Console.warn("Couldn't recover parameters for "
 +                    + calcIdParam.getCalcId());
 +          }
 +        }
 +      }
 +    }
 +    af.setMenusFromViewport(viewport);
 +    af.setTitle(view.getTitle());
 +    // TODO: we don't need to do this if the viewport is aready visible.
 +    /*
 +     * Add the AlignFrame to the desktop (it may be 'gathered' later), unless it
 +     * has a 'cdna/protein complement' view, in which case save it in order to
 +     * populate a SplitFrame once all views have been read in.
 +     */
 +    String complementaryViewId = view.getComplementId();
 +    if (complementaryViewId == null)
 +    {
 +      Desktop.addInternalFrame(af, view.getTitle(),
 +              safeInt(view.getWidth()), safeInt(view.getHeight()));
 +      // recompute any autoannotation
 +      af.alignPanel.updateAnnotation(false, true);
 +      reorderAutoannotation(af, al, autoAlan);
 +      af.alignPanel.alignmentChanged();
 +    }
 +    else
 +    {
 +      splitFrameCandidates.put(view, af);
 +    }
 +    return af;
 +  }
 +
 +  /**
 +   * Reads saved data to restore Colour by Annotation settings
 +   * 
 +   * @param viewAnnColour
 +   * @param af
 +   * @param al
 +   * @param model
 +   * @param checkGroupAnnColour
 +   * @return
 +   */
 +  private ColourSchemeI constructAnnotationColour(
 +          AnnotationColourScheme viewAnnColour, AlignFrame af,
 +          AlignmentI al, JalviewModel model, boolean checkGroupAnnColour)
 +  {
 +    boolean propagateAnnColour = false;
 +    AlignmentI annAlignment = af != null ? af.getViewport().getAlignment()
 +            : al;
 +    if (checkGroupAnnColour && al.getGroups() != null
 +            && al.getGroups().size() > 0)
 +    {
 +      // pre 2.8.1 behaviour
 +      // check to see if we should transfer annotation colours
 +      propagateAnnColour = true;
 +      for (SequenceGroup sg : al.getGroups())
 +      {
 +        if (sg.getColourScheme() instanceof AnnotationColourGradient)
 +        {
 +          propagateAnnColour = false;
 +        }
 +      }
 +    }
 +
 +    /*
 +     * 2.10.2- : saved annotationId is AlignmentAnnotation.annotationId
 +     */
 +    String annotationId = viewAnnColour.getAnnotation();
 +    AlignmentAnnotation matchedAnnotation = annotationIds.get(annotationId);
 +
 +    /*
 +     * pre 2.10.2: saved annotationId is AlignmentAnnotation.label
 +     */
 +    if (matchedAnnotation == null
 +            && annAlignment.getAlignmentAnnotation() != null)
 +    {
 +      for (int i = 0; i < annAlignment.getAlignmentAnnotation().length; i++)
 +      {
 +        if (annotationId
 +                .equals(annAlignment.getAlignmentAnnotation()[i].label))
 +        {
 +          matchedAnnotation = annAlignment.getAlignmentAnnotation()[i];
 +          break;
 +        }
 +      }
 +    }
 +    if (matchedAnnotation == null)
 +    {
 +      System.err.println("Failed to match annotation colour scheme for "
 +              + annotationId);
 +      return null;
 +    }
 +    if (matchedAnnotation.getThreshold() == null)
 +    {
 +      matchedAnnotation.setThreshold(
 +              new GraphLine(safeFloat(viewAnnColour.getThreshold()),
 +                      "Threshold", Color.black));
 +    }
 +
 +    AnnotationColourGradient cs = null;
 +    if (viewAnnColour.getColourScheme().equals("None"))
 +    {
 +      cs = new AnnotationColourGradient(matchedAnnotation,
 +              new Color(safeInt(viewAnnColour.getMinColour())),
 +              new Color(safeInt(viewAnnColour.getMaxColour())),
 +              safeInt(viewAnnColour.getAboveThreshold()));
 +    }
 +    else if (viewAnnColour.getColourScheme().startsWith("ucs"))
 +    {
 +      cs = new AnnotationColourGradient(matchedAnnotation,
 +              getUserColourScheme(model, viewAnnColour.getColourScheme()),
 +              safeInt(viewAnnColour.getAboveThreshold()));
 +    }
 +    else
 +    {
 +      cs = new AnnotationColourGradient(matchedAnnotation,
 +              ColourSchemeProperty.getColourScheme(af.getViewport(), al,
 +                      viewAnnColour.getColourScheme()),
 +              safeInt(viewAnnColour.getAboveThreshold()));
 +    }
 +
 +    boolean perSequenceOnly = safeBoolean(viewAnnColour.isPerSequence());
 +    boolean useOriginalColours = safeBoolean(
 +            viewAnnColour.isPredefinedColours());
 +    cs.setSeqAssociated(perSequenceOnly);
 +    cs.setPredefinedColours(useOriginalColours);
 +
 +    if (propagateAnnColour && al.getGroups() != null)
 +    {
 +      // Also use these settings for all the groups
 +      for (int g = 0; g < al.getGroups().size(); g++)
 +      {
 +        SequenceGroup sg = al.getGroups().get(g);
 +        if (sg.getGroupColourScheme() == null)
 +        {
 +          continue;
 +        }
 +
 +        AnnotationColourGradient groupScheme = new AnnotationColourGradient(
 +                matchedAnnotation, sg.getColourScheme(),
 +                safeInt(viewAnnColour.getAboveThreshold()));
 +        sg.setColourScheme(groupScheme);
 +        groupScheme.setSeqAssociated(perSequenceOnly);
 +        groupScheme.setPredefinedColours(useOriginalColours);
 +      }
 +    }
 +    return cs;
 +  }
 +
 +  private void reorderAutoannotation(AlignFrame af, AlignmentI al,
 +          List<JvAnnotRow> autoAlan)
 +  {
 +    // copy over visualization settings for autocalculated annotation in the
 +    // view
 +    if (al.getAlignmentAnnotation() != null)
 +    {
 +      /**
 +       * Kludge for magic autoannotation names (see JAL-811)
 +       */
 +      String[] magicNames = new String[] { "Consensus", "Quality",
 +          "Conservation" };
 +      JvAnnotRow nullAnnot = new JvAnnotRow(-1, null);
 +      Hashtable<String, JvAnnotRow> visan = new Hashtable<>();
 +      for (String nm : magicNames)
 +      {
 +        visan.put(nm, nullAnnot);
 +      }
 +      for (JvAnnotRow auan : autoAlan)
 +      {
 +        visan.put(auan.template.label
 +                + (auan.template.getCalcId() == null ? ""
 +                        : "\t" + auan.template.getCalcId()),
 +                auan);
 +      }
 +      int hSize = al.getAlignmentAnnotation().length;
 +      List<JvAnnotRow> reorder = new ArrayList<>();
 +      // work through any autoCalculated annotation already on the view
 +      // removing it if it should be placed in a different location on the
 +      // annotation panel.
 +      List<String> remains = new ArrayList<>(visan.keySet());
 +      for (int h = 0; h < hSize; h++)
 +      {
 +        jalview.datamodel.AlignmentAnnotation jalan = al
 +                .getAlignmentAnnotation()[h];
 +        if (jalan.autoCalculated)
 +        {
 +          String k;
 +          JvAnnotRow valan = visan.get(k = jalan.label);
 +          if (jalan.getCalcId() != null)
 +          {
 +            valan = visan.get(k = jalan.label + "\t" + jalan.getCalcId());
 +          }
 +
 +          if (valan != null)
 +          {
 +            // delete the auto calculated row from the alignment
 +            al.deleteAnnotation(jalan, false);
 +            remains.remove(k);
 +            hSize--;
 +            h--;
 +            if (valan != nullAnnot)
 +            {
 +              if (jalan != valan.template)
 +              {
 +                // newly created autoannotation row instance
 +                // so keep a reference to the visible annotation row
 +                // and copy over all relevant attributes
 +                if (valan.template.graphHeight >= 0)
 +
 +                {
 +                  jalan.graphHeight = valan.template.graphHeight;
 +                }
 +                jalan.visible = valan.template.visible;
 +              }
 +              reorder.add(new JvAnnotRow(valan.order, jalan));
 +            }
 +          }
 +        }
 +      }
 +      // Add any (possibly stale) autocalculated rows that were not appended to
 +      // the view during construction
 +      for (String other : remains)
 +      {
 +        JvAnnotRow othera = visan.get(other);
 +        if (othera != nullAnnot && othera.template.getCalcId() != null
 +                && othera.template.getCalcId().length() > 0)
 +        {
 +          reorder.add(othera);
 +        }
 +      }
 +      // now put the automatic annotation in its correct place
 +      int s = 0, srt[] = new int[reorder.size()];
 +      JvAnnotRow[] rws = new JvAnnotRow[reorder.size()];
 +      for (JvAnnotRow jvar : reorder)
 +      {
 +        rws[s] = jvar;
 +        srt[s++] = jvar.order;
 +      }
 +      reorder.clear();
 +      jalview.util.QuickSort.sort(srt, rws);
 +      // and re-insert the annotation at its correct position
 +      for (JvAnnotRow jvar : rws)
 +      {
 +        al.addAnnotation(jvar.template, jvar.order);
 +      }
 +      af.alignPanel.adjustAnnotationHeight();
 +    }
 +  }
 +
 +  Hashtable skipList = null;
 +
 +  /**
 +   * TODO remove this method
 +   * 
 +   * @param view
 +   * @return AlignFrame bound to sequenceSetId from view, if one exists. private
 +   *         AlignFrame getSkippedFrame(Viewport view) { if (skipList==null) {
 +   *         throw new Error("Implementation Error. No skipList defined for this
 +   *         Jalview2XML instance."); } return (AlignFrame)
 +   *         skipList.get(view.getSequenceSetId()); }
 +   */
 +
 +  /**
 +   * Check if the Jalview view contained in object should be skipped or not.
 +   * 
 +   * @param object
 +   * @return true if view's sequenceSetId is a key in skipList
 +   */
 +  private boolean skipViewport(JalviewModel object)
 +  {
 +    if (skipList == null)
 +    {
 +      return false;
 +    }
 +    String id = object.getViewport().get(0).getSequenceSetId();
 +    if (skipList.containsKey(id))
 +    {
 +      Console.debug("Skipping seuqence set id " + id);
 +      return true;
 +    }
 +    return false;
 +  }
 +
 +  public void addToSkipList(AlignFrame af)
 +  {
 +    if (skipList == null)
 +    {
 +      skipList = new Hashtable();
 +    }
 +    skipList.put(af.getViewport().getSequenceSetId(), af);
 +  }
 +
 +  public void clearSkipList()
 +  {
 +    if (skipList != null)
 +    {
 +      skipList.clear();
 +      skipList = null;
 +    }
 +  }
 +
 +  private void recoverDatasetFor(SequenceSet vamsasSet, AlignmentI al,
 +          boolean ignoreUnrefed, String uniqueSeqSetId)
 +  {
 +    jalview.datamodel.AlignmentI ds = getDatasetFor(
 +            vamsasSet.getDatasetId());
 +    AlignmentI xtant_ds = ds;
 +    if (xtant_ds == null)
 +    {
 +      // good chance we are about to create a new dataset, but check if we've
 +      // seen some of the dataset sequence IDs before.
 +      // TODO: skip this check if we are working with project generated by
 +      // version 2.11 or later
 +      xtant_ds = checkIfHasDataset(vamsasSet.getSequence());
 +      if (xtant_ds != null)
 +      {
 +        ds = xtant_ds;
 +        addDatasetRef(vamsasSet.getDatasetId(), ds);
 +      }
 +    }
 +    Vector<SequenceI> dseqs = null;
 +    if (!ignoreUnrefed)
 +    {
 +      // recovering an alignment View
 +      AlignmentI seqSetDS = getDatasetFor(UNIQSEQSETID + uniqueSeqSetId);
 +      if (seqSetDS != null)
 +      {
 +        if (ds != null && ds != seqSetDS)
 +        {
 +          Console.warn(
 +                  "JAL-3171 regression: Overwriting a dataset reference for an alignment"
 +                          + " - CDS/Protein crossreference data may be lost");
 +          if (xtant_ds != null)
 +          {
 +            // This can only happen if the unique sequence set ID was bound to a
 +            // dataset that did not contain any of the sequences in the view
 +            // currently being restored.
 +            Console.warn(
 +                    "JAL-3171 SERIOUS!  TOTAL CONFUSION - please consider contacting the Jalview Development team so they can investigate why your project caused this message to be displayed.");
 +          }
 +        }
 +        ds = seqSetDS;
 +        addDatasetRef(vamsasSet.getDatasetId(), ds);
 +      }
 +    }
 +    if (ds == null)
 +    {
 +      // try even harder to restore dataset
 +      AlignmentI xtantDS = checkIfHasDataset(vamsasSet.getSequence());
 +      // create a list of new dataset sequences
 +      dseqs = new Vector<>();
 +    }
 +    for (int i = 0, iSize = vamsasSet.getSequence().size(); i < iSize; i++)
 +    {
 +      Sequence vamsasSeq = vamsasSet.getSequence().get(i);
 +      ensureJalviewDatasetSequence(vamsasSeq, ds, dseqs, ignoreUnrefed, i);
 +    }
 +    // create a new dataset
 +    if (ds == null)
 +    {
 +      SequenceI[] dsseqs = new SequenceI[dseqs.size()];
 +      dseqs.copyInto(dsseqs);
 +      ds = new jalview.datamodel.Alignment(dsseqs);
 +      Console.debug("Created new dataset " + vamsasSet.getDatasetId()
 +              + " for alignment " + System.identityHashCode(al));
 +      addDatasetRef(vamsasSet.getDatasetId(), ds);
 +    }
 +    // set the dataset for the newly imported alignment.
 +    if (al.getDataset() == null && !ignoreUnrefed)
 +    {
 +      al.setDataset(ds);
 +      // register dataset for the alignment's uniqueSeqSetId for legacy projects
 +      addDatasetRef(UNIQSEQSETID + uniqueSeqSetId, ds);
 +    }
 +    updateSeqDatasetBinding(vamsasSet.getSequence(), ds);
 +  }
 +
 +  /**
 +   * XML dataset sequence ID to materialised dataset reference
 +   */
 +  HashMap<String, AlignmentI> seqToDataset = new HashMap<>();
 +
 +  /**
 +   * @return the first materialised dataset reference containing a dataset
 +   *         sequence referenced in the given view
 +   * @param list
 +   *          - sequences from the view
 +   */
 +  AlignmentI checkIfHasDataset(List<Sequence> list)
 +  {
 +    for (Sequence restoredSeq : list)
 +    {
 +      AlignmentI datasetFor = seqToDataset.get(restoredSeq.getDsseqid());
 +      if (datasetFor != null)
 +      {
 +        return datasetFor;
 +      }
 +    }
 +    return null;
 +  }
 +
 +  /**
 +   * Register ds as the containing dataset for the dataset sequences referenced
 +   * by sequences in list
 +   * 
 +   * @param list
 +   *          - sequences in a view
 +   * @param ds
 +   */
 +  void updateSeqDatasetBinding(List<Sequence> list, AlignmentI ds)
 +  {
 +    for (Sequence restoredSeq : list)
 +    {
 +      AlignmentI prevDS = seqToDataset.put(restoredSeq.getDsseqid(), ds);
 +      if (prevDS != null && prevDS != ds)
 +      {
 +        Console.warn("Dataset sequence appears in many datasets: "
 +                + restoredSeq.getDsseqid());
 +        // TODO: try to merge!
 +      }
 +    }
 +  }
 +
 +  /**
 +   * 
 +   * @param vamsasSeq
 +   *          sequence definition to create/merge dataset sequence for
 +   * @param ds
 +   *          dataset alignment
 +   * @param dseqs
 +   *          vector to add new dataset sequence to
 +   * @param ignoreUnrefed
 +   *          - when true, don't create new sequences from vamsasSeq if it's id
 +   *          doesn't already have an asssociated Jalview sequence.
 +   * @param vseqpos
 +   *          - used to reorder the sequence in the alignment according to the
 +   *          vamsasSeq array ordering, to preserve ordering of dataset
 +   */
 +  private void ensureJalviewDatasetSequence(Sequence vamsasSeq,
 +          AlignmentI ds, Vector<SequenceI> dseqs, boolean ignoreUnrefed,
 +          int vseqpos)
 +  {
 +    // JBP TODO: Check this is called for AlCodonFrames to support recovery of
 +    // xRef Codon Maps
 +    SequenceI sq = seqRefIds.get(vamsasSeq.getId());
 +    boolean reorder = false;
 +    SequenceI dsq = null;
 +    if (sq != null && sq.getDatasetSequence() != null)
 +    {
 +      dsq = sq.getDatasetSequence();
 +    }
 +    else
 +    {
 +      reorder = true;
 +    }
 +    if (sq == null && ignoreUnrefed)
 +    {
 +      return;
 +    }
 +    String sqid = vamsasSeq.getDsseqid();
 +    if (dsq == null)
 +    {
 +      // need to create or add a new dataset sequence reference to this sequence
 +      if (sqid != null)
 +      {
 +        dsq = seqRefIds.get(sqid);
 +      }
 +      // check again
 +      if (dsq == null)
 +      {
 +        // make a new dataset sequence
 +        dsq = sq.createDatasetSequence();
 +        if (sqid == null)
 +        {
 +          // make up a new dataset reference for this sequence
 +          sqid = seqHash(dsq);
 +        }
 +        dsq.setVamsasId(uniqueSetSuffix + sqid);
 +        seqRefIds.put(sqid, dsq);
 +        if (ds == null)
 +        {
 +          if (dseqs != null)
 +          {
 +            dseqs.addElement(dsq);
 +          }
 +        }
 +        else
 +        {
 +          ds.addSequence(dsq);
 +        }
 +      }
 +      else
 +      {
 +        if (sq != dsq)
 +        { // make this dataset sequence sq's dataset sequence
 +          sq.setDatasetSequence(dsq);
 +          // and update the current dataset alignment
 +          if (ds == null)
 +          {
 +            if (dseqs != null)
 +            {
 +              if (!dseqs.contains(dsq))
 +              {
 +                dseqs.add(dsq);
 +              }
 +            }
 +            else
 +            {
 +              if (ds.findIndex(dsq) < 0)
 +              {
 +                ds.addSequence(dsq);
 +              }
 +            }
 +          }
 +        }
 +      }
 +    }
 +    // TODO: refactor this as a merge dataset sequence function
 +    // now check that sq (the dataset sequence) sequence really is the union of
 +    // all references to it
 +    // boolean pre = sq.getStart() < dsq.getStart();
 +    // boolean post = sq.getEnd() > dsq.getEnd();
 +    // if (pre || post)
 +    if (sq != dsq)
 +    {
 +      // StringBuffer sb = new StringBuffer();
 +      String newres = jalview.analysis.AlignSeq.extractGaps(
 +              jalview.util.Comparison.GapChars, sq.getSequenceAsString());
 +      if (!newres.equalsIgnoreCase(dsq.getSequenceAsString())
 +              && newres.length() > dsq.getLength())
 +      {
 +        // Update with the longer sequence.
 +        synchronized (dsq)
 +        {
 +          /*
 +           * if (pre) { sb.insert(0, newres .substring(0, dsq.getStart() -
 +           * sq.getStart())); dsq.setStart(sq.getStart()); } if (post) {
 +           * sb.append(newres.substring(newres.length() - sq.getEnd() -
 +           * dsq.getEnd())); dsq.setEnd(sq.getEnd()); }
 +           */
 +          dsq.setSequence(newres);
 +        }
 +        // TODO: merges will never happen if we 'know' we have the real dataset
 +        // sequence - this should be detected when id==dssid
 +        System.err.println(
 +                "DEBUG Notice:  Merged dataset sequence (if you see this often, post at http://issues.jalview.org/browse/JAL-1474)"); // ("
 +        // + (pre ? "prepended" : "") + " "
 +        // + (post ? "appended" : ""));
 +      }
 +    }
 +    else
 +    {
 +      // sequence refs are identical. We may need to update the existing dataset
 +      // alignment with this one, though.
 +      if (ds != null && dseqs == null)
 +      {
 +        int opos = ds.findIndex(dsq);
 +        SequenceI tseq = null;
 +        if (opos != -1 && vseqpos != opos)
 +        {
 +          // remove from old position
 +          ds.deleteSequence(dsq);
 +        }
 +        if (vseqpos < ds.getHeight())
 +        {
 +          if (vseqpos != opos)
 +          {
 +            // save sequence at destination position
 +            tseq = ds.getSequenceAt(vseqpos);
 +            ds.replaceSequenceAt(vseqpos, dsq);
 +            ds.addSequence(tseq);
 +          }
 +        }
 +        else
 +        {
 +          ds.addSequence(dsq);
 +        }
 +      }
 +    }
 +  }
 +
 +  /*
 +   * TODO use AlignmentI here and in related methods - needs
 +   * AlignmentI.getDataset() changed to return AlignmentI instead of Alignment
 +   */
 +  Hashtable<String, AlignmentI> datasetIds = null;
 +
 +  IdentityHashMap<AlignmentI, String> dataset2Ids = null;
 +
 +  private AlignmentI getDatasetFor(String datasetId)
 +  {
 +    if (datasetIds == null)
 +    {
 +      datasetIds = new Hashtable<>();
 +      return null;
 +    }
 +    if (datasetIds.containsKey(datasetId))
 +    {
 +      return datasetIds.get(datasetId);
 +    }
 +    return null;
 +  }
 +
 +  private void addDatasetRef(String datasetId, AlignmentI dataset)
 +  {
 +    if (datasetIds == null)
 +    {
 +      datasetIds = new Hashtable<>();
 +    }
 +    datasetIds.put(datasetId, dataset);
 +  }
 +
 +  /**
 +   * make a new dataset ID for this jalview dataset alignment
 +   * 
 +   * @param dataset
 +   * @return
 +   */
 +  private String getDatasetIdRef(AlignmentI dataset)
 +  {
 +    if (dataset.getDataset() != null)
 +    {
 +      Console.warn(
 +              "Serious issue!  Dataset Object passed to getDatasetIdRef is not a Jalview DATASET alignment...");
 +    }
 +    String datasetId = makeHashCode(dataset, null);
 +    if (datasetId == null)
 +    {
 +      // make a new datasetId and record it
 +      if (dataset2Ids == null)
 +      {
 +        dataset2Ids = new IdentityHashMap<>();
 +      }
 +      else
 +      {
 +        datasetId = dataset2Ids.get(dataset);
 +      }
 +      if (datasetId == null)
 +      {
 +        datasetId = "ds" + dataset2Ids.size() + 1;
 +        dataset2Ids.put(dataset, datasetId);
 +      }
 +    }
 +    return datasetId;
 +  }
 +
 +  /**
 +   * Add any saved DBRefEntry's to the sequence. An entry flagged as 'locus' is
 +   * constructed as a special subclass GeneLocus.
 +   * 
 +   * @param datasetSequence
 +   * @param sequence
 +   */
 +  private void addDBRefs(SequenceI datasetSequence, Sequence sequence)
 +  {
 +    for (int d = 0; d < sequence.getDBRef().size(); d++)
 +    {
 +      DBRef dr = sequence.getDBRef().get(d);
 +      DBRefEntry entry;
 +      if (dr.isLocus())
 +      {
 +        entry = new GeneLocus(dr.getSource(), dr.getVersion(),
 +                dr.getAccessionId());
 +      }
 +      else
 +      {
 +        entry = new DBRefEntry(dr.getSource(), dr.getVersion(),
 +                dr.getAccessionId());
 +      }
 +      if (dr.getMapping() != null)
 +      {
 +        entry.setMap(addMapping(dr.getMapping()));
 +      }
 +      entry.setCanonical(dr.isCanonical());
 +      datasetSequence.addDBRef(entry);
 +    }
 +  }
 +
 +  private jalview.datamodel.Mapping addMapping(Mapping m)
 +  {
 +    SequenceI dsto = null;
 +    // Mapping m = dr.getMapping();
 +    int fr[] = new int[m.getMapListFrom().size() * 2];
 +    Iterator<MapListFrom> from = m.getMapListFrom().iterator();// enumerateMapListFrom();
 +    for (int _i = 0; from.hasNext(); _i += 2)
 +    {
 +      MapListFrom mf = from.next();
 +      fr[_i] = mf.getStart();
 +      fr[_i + 1] = mf.getEnd();
 +    }
 +    int fto[] = new int[m.getMapListTo().size() * 2];
 +    Iterator<MapListTo> to = m.getMapListTo().iterator();// enumerateMapListTo();
 +    for (int _i = 0; to.hasNext(); _i += 2)
 +    {
 +      MapListTo mf = to.next();
 +      fto[_i] = mf.getStart();
 +      fto[_i + 1] = mf.getEnd();
 +    }
 +    jalview.datamodel.Mapping jmap = new jalview.datamodel.Mapping(dsto, fr,
 +            fto, m.getMapFromUnit().intValue(),
 +            m.getMapToUnit().intValue());
 +
 +    /*
 +     * (optional) choice of dseqFor or Sequence
 +     */
 +    if (m.getDseqFor() != null)
 +    {
 +      String dsfor = m.getDseqFor();
 +      if (seqRefIds.containsKey(dsfor))
 +      {
 +        /*
 +         * recover from hash
 +         */
 +        jmap.setTo(seqRefIds.get(dsfor));
 +      }
 +      else
 +      {
 +        frefedSequence.add(newMappingRef(dsfor, jmap));
 +      }
 +    }
 +    else if (m.getSequence() != null)
 +    {
 +      /*
 +       * local sequence definition
 +       */
 +      Sequence ms = m.getSequence();
 +      SequenceI djs = null;
 +      String sqid = ms.getDsseqid();
 +      if (sqid != null && sqid.length() > 0)
 +      {
 +        /*
 +         * recover dataset sequence
 +         */
 +        djs = seqRefIds.get(sqid);
 +      }
 +      else
 +      {
 +        System.err.println(
 +                "Warning - making up dataset sequence id for DbRef sequence map reference");
 +        sqid = ((Object) ms).toString(); // make up a new hascode for
 +        // undefined dataset sequence hash
 +        // (unlikely to happen)
 +      }
 +
 +      if (djs == null)
 +      {
 +        /**
 +         * make a new dataset sequence and add it to refIds hash
 +         */
 +        djs = new jalview.datamodel.Sequence(ms.getName(),
 +                ms.getSequence());
 +        djs.setStart(jmap.getMap().getToLowest());
 +        djs.setEnd(jmap.getMap().getToHighest());
 +        djs.setVamsasId(uniqueSetSuffix + sqid);
 +        jmap.setTo(djs);
 +        incompleteSeqs.put(sqid, djs);
 +        seqRefIds.put(sqid, djs);
 +
 +      }
 +      Console.debug("about to recurse on addDBRefs.");
 +      addDBRefs(djs, ms);
 +
 +    }
 +
 +    return jmap;
 +  }
 +
 +  /**
 +   * Provides a 'copy' of an alignment view (on action New View) by 'saving' the
 +   * view as XML (but not to file), and then reloading it
 +   * 
 +   * @param ap
 +   * @return
 +   */
 +  public AlignmentPanel copyAlignPanel(AlignmentPanel ap)
 +  {
 +    initSeqRefs();
 +    JalviewModel jm = saveState(ap, null, null, null);
 +
 +    addDatasetRef(
 +            jm.getVamsasModel().getSequenceSet().get(0).getDatasetId(),
 +            ap.getAlignment().getDataset());
 +
 +    uniqueSetSuffix = "";
 +    // jm.getJalviewModelSequence().getViewport(0).setId(null);
 +    jm.getViewport().get(0).setId(null);
 +    // we don't overwrite the view we just copied
 +
 +    if (this.frefedSequence == null)
 +    {
 +      frefedSequence = new Vector<>();
 +    }
 +
 +    viewportsAdded.clear();
 +
 +    AlignFrame af = loadFromObject(jm, null, false, null);
 +    af.getAlignPanels().clear();
 +    af.closeMenuItem_actionPerformed(true);
 +
 +    /*
 +     * if(ap.av.getAlignment().getAlignmentAnnotation()!=null) { for(int i=0;
 +     * i<ap.av.getAlignment().getAlignmentAnnotation().length; i++) {
 +     * if(!ap.av.getAlignment().getAlignmentAnnotation()[i].autoCalculated) {
 +     * af.alignPanel.av.getAlignment().getAlignmentAnnotation()[i] =
 +     * ap.av.getAlignment().getAlignmentAnnotation()[i]; } } }
 +     */
 +
 +    return af.alignPanel;
 +  }
 +
 +  private Hashtable jvids2vobj;
 +
 +  /**
 +   * set the object to ID mapping tables used to write/recover objects and XML
 +   * ID strings for the jalview project. If external tables are provided then
 +   * finalize and clearSeqRefs will not clear the tables when the Jalview2XML
 +   * object goes out of scope. - also populates the datasetIds hashtable with
 +   * alignment objects containing dataset sequences
 +   * 
 +   * @param vobj2jv
 +   *          Map from ID strings to jalview datamodel
 +   * @param jv2vobj
 +   *          Map from jalview datamodel to ID strings
 +   * 
 +   * 
 +   */
 +  public void setObjectMappingTables(Hashtable vobj2jv,
 +          IdentityHashMap jv2vobj)
 +  {
 +    this.jv2vobj = jv2vobj;
 +    this.vobj2jv = vobj2jv;
 +    Iterator ds = jv2vobj.keySet().iterator();
 +    String id;
 +    while (ds.hasNext())
 +    {
 +      Object jvobj = ds.next();
 +      id = jv2vobj.get(jvobj).toString();
 +      if (jvobj instanceof jalview.datamodel.Alignment)
 +      {
 +        if (((jalview.datamodel.Alignment) jvobj).getDataset() == null)
 +        {
 +          addDatasetRef(id, (jalview.datamodel.Alignment) jvobj);
 +        }
 +      }
 +      else if (jvobj instanceof jalview.datamodel.Sequence)
 +      {
 +        // register sequence object so the XML parser can recover it.
 +        if (seqRefIds == null)
 +        {
 +          seqRefIds = new HashMap<>();
 +        }
 +        if (seqsToIds == null)
 +        {
 +          seqsToIds = new IdentityHashMap<>();
 +        }
 +        seqRefIds.put(jv2vobj.get(jvobj).toString(), (SequenceI) jvobj);
 +        seqsToIds.put((SequenceI) jvobj, id);
 +      }
 +      else if (jvobj instanceof jalview.datamodel.AlignmentAnnotation)
 +      {
 +        String anid;
 +        AlignmentAnnotation jvann = (AlignmentAnnotation) jvobj;
 +        annotationIds.put(anid = jv2vobj.get(jvobj).toString(), jvann);
 +        if (jvann.annotationId == null)
 +        {
 +          jvann.annotationId = anid;
 +        }
 +        if (!jvann.annotationId.equals(anid))
 +        {
 +          // TODO verify that this is the correct behaviour
 +          Console.warn("Overriding Annotation ID for " + anid
 +                  + " from different id : " + jvann.annotationId);
 +          jvann.annotationId = anid;
 +        }
 +      }
 +      else if (jvobj instanceof String)
 +      {
 +        if (jvids2vobj == null)
 +        {
 +          jvids2vobj = new Hashtable();
 +          jvids2vobj.put(jvobj, jv2vobj.get(jvobj).toString());
 +        }
 +      }
 +      else
 +      {
 +        Console.debug("Ignoring " + jvobj.getClass() + " (ID = " + id);
 +      }
 +    }
 +  }
 +
 +  /**
 +   * set the uniqueSetSuffix used to prefix/suffix object IDs for jalview
 +   * objects created from the project archive. If string is null (default for
 +   * construction) then suffix will be set automatically.
 +   * 
 +   * @param string
 +   */
 +  public void setUniqueSetSuffix(String string)
 +  {
 +    uniqueSetSuffix = string;
 +
 +  }
 +
 +  /**
 +   * uses skipList2 as the skipList for skipping views on sequence sets
 +   * associated with keys in the skipList
 +   * 
 +   * @param skipList2
 +   */
 +  public void setSkipList(Hashtable skipList2)
 +  {
 +    skipList = skipList2;
 +  }
 +
 +  /**
 +   * Reads the jar entry of given name and returns its contents, or null if the
 +   * entry is not found.
 +   * 
 +   * @param jprovider
 +   * @param jarEntryName
 +   * @return
 +   */
 +  protected String readJarEntry(jarInputStreamProvider jprovider,
 +          String jarEntryName)
 +  {
 +    String result = null;
 +    BufferedReader in = null;
 +
 +    try
 +    {
 +      /*
 +       * Reopen the jar input stream and traverse its entries to find a matching
 +       * name
 +       */
 +      JarInputStream jin = jprovider.getJarInputStream();
 +      JarEntry entry = null;
 +      do
 +      {
 +        entry = jin.getNextJarEntry();
 +      } while (entry != null && !entry.getName().equals(jarEntryName));
 +
 +      if (entry != null)
 +      {
 +        StringBuilder out = new StringBuilder(256);
 +        in = new BufferedReader(new InputStreamReader(jin, UTF_8));
 +        String data;
 +
 +        while ((data = in.readLine()) != null)
 +        {
 +          out.append(data);
 +        }
 +        result = out.toString();
 +      }
 +      else
 +      {
 +        Console.warn(
 +                "Couldn't find entry in Jalview Jar for " + jarEntryName);
 +      }
 +    } catch (Exception ex)
 +    {
 +      ex.printStackTrace();
 +    } finally
 +    {
 +      if (in != null)
 +      {
 +        try
 +        {
 +          in.close();
 +        } catch (IOException e)
 +        {
 +          // ignore
 +        }
 +      }
 +    }
 +
 +    return result;
 +  }
 +
 +  /**
 +   * Returns an incrementing counter (0, 1, 2...)
 +   * 
 +   * @return
 +   */
 +  private synchronized int nextCounter()
 +  {
 +    return counter++;
 +  }
 +
 +  /**
 +   * Loads any saved PCA viewers
 +   * 
 +   * @param jms
 +   * @param ap
 +   */
 +  protected void loadPCAViewers(JalviewModel model, AlignmentPanel ap)
 +  {
 +    try
 +    {
 +      List<PcaViewer> pcaviewers = model.getPcaViewer();
 +      for (PcaViewer viewer : pcaviewers)
 +      {
 +        String modelName = viewer.getScoreModelName();
 +        SimilarityParamsI params = new SimilarityParams(
 +                viewer.isIncludeGappedColumns(), viewer.isMatchGaps(),
 +                viewer.isIncludeGaps(),
 +                viewer.isDenominateByShortestLength());
 +
 +        /*
 +         * create the panel (without computing the PCA)
 +         */
 +        PCAPanel panel = new PCAPanel(ap, modelName, params);
 +
 +        panel.setTitle(viewer.getTitle());
 +        panel.setBounds(new Rectangle(viewer.getXpos(), viewer.getYpos(),
 +                viewer.getWidth(), viewer.getHeight()));
 +
 +        boolean showLabels = viewer.isShowLabels();
 +        panel.setShowLabels(showLabels);
 +        panel.getRotatableCanvas().setShowLabels(showLabels);
 +        panel.getRotatableCanvas()
 +                .setBgColour(new Color(viewer.getBgColour()));
 +        panel.getRotatableCanvas()
 +                .setApplyToAllViews(viewer.isLinkToAllViews());
 +
 +        /*
 +         * load PCA output data
 +         */
 +        ScoreModelI scoreModel = ScoreModels.getInstance()
 +                .getScoreModel(modelName, ap);
 +        PCA pca = new PCA(null, scoreModel, params);
 +        PcaDataType pcaData = viewer.getPcaData();
 +
 +        MatrixI pairwise = loadDoubleMatrix(pcaData.getPairwiseMatrix());
 +        pca.setPairwiseScores(pairwise);
 +
 +        MatrixI triDiag = loadDoubleMatrix(pcaData.getTridiagonalMatrix());
 +        pca.setTridiagonal(triDiag);
 +
 +        MatrixI result = loadDoubleMatrix(pcaData.getEigenMatrix());
 +        pca.setEigenmatrix(result);
 +
 +        panel.getPcaModel().setPCA(pca);
 +
 +        /*
 +         * we haven't saved the input data! (JAL-2647 to do)
 +         */
 +        panel.setInputData(null);
 +
 +        /*
 +         * add the sequence points for the PCA display
 +         */
 +        List<jalview.datamodel.SequencePoint> seqPoints = new ArrayList<>();
 +        for (SequencePoint sp : viewer.getSequencePoint())
 +        {
 +          String seqId = sp.getSequenceRef();
 +          SequenceI seq = seqRefIds.get(seqId);
 +          if (seq == null)
 +          {
 +            throw new IllegalStateException(
 +                    "Unmatched seqref for PCA: " + seqId);
 +          }
 +          Point pt = new Point(sp.getXPos(), sp.getYPos(), sp.getZPos());
 +          jalview.datamodel.SequencePoint seqPoint = new jalview.datamodel.SequencePoint(
 +                  seq, pt);
 +          seqPoints.add(seqPoint);
 +        }
 +        panel.getRotatableCanvas().setPoints(seqPoints, seqPoints.size());
 +
 +        /*
 +         * set min-max ranges and scale after setPoints (which recomputes them)
 +         */
 +        panel.getRotatableCanvas().setScaleFactor(viewer.getScaleFactor());
 +        SeqPointMin spMin = viewer.getSeqPointMin();
 +        float[] min = new float[] { spMin.getXPos(), spMin.getYPos(),
 +            spMin.getZPos() };
 +        SeqPointMax spMax = viewer.getSeqPointMax();
 +        float[] max = new float[] { spMax.getXPos(), spMax.getYPos(),
 +            spMax.getZPos() };
 +        panel.getRotatableCanvas().setSeqMinMax(min, max);
 +
 +        // todo: hold points list in PCAModel only
 +        panel.getPcaModel().setSequencePoints(seqPoints);
 +
 +        panel.setSelectedDimensionIndex(viewer.getXDim(), X);
 +        panel.setSelectedDimensionIndex(viewer.getYDim(), Y);
 +        panel.setSelectedDimensionIndex(viewer.getZDim(), Z);
 +
 +        // is this duplication needed?
 +        panel.setTop(seqPoints.size() - 1);
 +        panel.getPcaModel().setTop(seqPoints.size() - 1);
 +
 +        /*
 +         * add the axes' end points for the display
 +         */
 +        for (int i = 0; i < 3; i++)
 +        {
 +          Axis axis = viewer.getAxis().get(i);
 +          panel.getRotatableCanvas().getAxisEndPoints()[i] = new Point(
 +                  axis.getXPos(), axis.getYPos(), axis.getZPos());
 +        }
 +
 +        Desktop.addInternalFrame(panel, MessageManager.formatMessage(
 +                "label.calc_title", "PCA", modelName), 475, 450);
 +      }
 +    } catch (Exception ex)
 +    {
 +      Console.error("Error loading PCA: " + ex.toString());
 +    }
 +  }
 +
 +  /**
 +   * Creates a new structure viewer window
 +   * 
 +   * @param viewerType
 +   * @param viewerData
 +   * @param af
 +   * @param jprovider
 +   */
 +  protected void createStructureViewer(ViewerType viewerType,
 +          final Entry<String, StructureViewerModel> viewerData,
 +          AlignFrame af, jarInputStreamProvider jprovider)
 +  {
 +    final StructureViewerModel viewerModel = viewerData.getValue();
 +    String sessionFilePath = null;
 +
 +    if (viewerType == ViewerType.JMOL)
 +    {
 +      sessionFilePath = rewriteJmolSession(viewerModel, jprovider);
 +    }
 +    else
 +    {
 +      String viewerJarEntryName = getViewerJarEntryName(
 +              viewerModel.getViewId());
 +      sessionFilePath = copyJarEntry(jprovider, viewerJarEntryName,
 +              "viewerSession", ".tmp");
 +    }
 +    final String sessionPath = sessionFilePath;
 +    final String sviewid = viewerData.getKey();
 +    try
 +    {
 +      SwingUtilities.invokeAndWait(new Runnable()
 +      {
 +        @Override
 +        public void run()
 +        {
 +          JalviewStructureDisplayI sview = null;
 +          try
 +          {
 +            sview = StructureViewer.createView(viewerType, af.alignPanel,
 +                    viewerModel, sessionPath, sviewid);
 +            addNewStructureViewer(sview);
 +          } catch (OutOfMemoryError ex)
 +          {
 +            new OOMWarning("Restoring structure view for " + viewerType,
 +                    (OutOfMemoryError) ex.getCause());
 +            if (sview != null && sview.isVisible())
 +            {
 +              sview.closeViewer(false);
 +              sview.setVisible(false);
 +              sview.dispose();
 +            }
 +          }
 +        }
 +      });
 +    } catch (InvocationTargetException | InterruptedException ex)
 +    {
 +      Console.warn("Unexpected error when opening " + viewerType
 +              + " structure viewer", ex);
 +    }
 +  }
 +
 +  /**
 +   * Rewrites a Jmol session script, saves it to a temporary file, and returns
 +   * the path of the file. "load file" commands are rewritten to change the
 +   * original PDB file names to those created as the Jalview project is loaded.
 +   * 
 +   * @param svattrib
 +   * @param jprovider
 +   * @return
 +   */
 +  private String rewriteJmolSession(StructureViewerModel svattrib,
 +          jarInputStreamProvider jprovider)
 +  {
 +    String state = svattrib.getStateData(); // Jalview < 2.9
 +    if (state == null || state.isEmpty()) // Jalview >= 2.9
 +    {
 +      String jarEntryName = getViewerJarEntryName(svattrib.getViewId());
 +      state = readJarEntry(jprovider, jarEntryName);
 +    }
 +    // TODO or simpler? for each key in oldFiles,
 +    // replace key.getPath() in state with oldFiles.get(key).getFilePath()
 +    // (allowing for different path escapings)
 +    StringBuilder rewritten = new StringBuilder(state.length());
 +    int cp = 0, ncp, ecp;
 +    Map<File, StructureData> oldFiles = svattrib.getFileData();
 +    while ((ncp = state.indexOf("load ", cp)) > -1)
 +    {
 +      do
 +      {
 +        // look for next filename in load statement
 +        rewritten.append(state.substring(cp,
 +                ncp = (state.indexOf("\"", ncp + 1) + 1)));
 +        String oldfilenam = state.substring(ncp,
 +                ecp = state.indexOf("\"", ncp));
 +        // recover the new mapping data for this old filename
 +        // have to normalize filename - since Jmol and jalview do
 +        // filename translation differently.
 +        StructureData filedat = oldFiles.get(new File(oldfilenam));
 +        if (filedat == null)
 +        {
 +          String reformatedOldFilename = oldfilenam.replaceAll("/", "\\\\");
 +          filedat = oldFiles.get(new File(reformatedOldFilename));
 +        }
 +        rewritten.append(Platform.escapeBackslashes(filedat.getFilePath()));
 +        rewritten.append("\"");
 +        cp = ecp + 1; // advance beyond last \" and set cursor so we can
 +                      // look for next file statement.
 +      } while ((ncp = state.indexOf("/*file*/", cp)) > -1);
 +    }
 +    if (cp > 0)
 +    {
 +      // just append rest of state
 +      rewritten.append(state.substring(cp));
 +    }
 +    else
 +    {
 +      System.err.print("Ignoring incomplete Jmol state for PDB ids: ");
 +      rewritten = new StringBuilder(state);
 +      rewritten.append("; load append ");
 +      for (File id : oldFiles.keySet())
 +      {
 +        // add pdb files that should be present in the viewer
 +        StructureData filedat = oldFiles.get(id);
 +        rewritten.append(" \"").append(filedat.getFilePath()).append("\"");
 +      }
 +      rewritten.append(";");
 +    }
 +
 +    if (rewritten.length() == 0)
 +    {
 +      return null;
 +    }
 +    final String history = "history = ";
 +    int historyIndex = rewritten.indexOf(history);
 +    if (historyIndex > -1)
 +    {
 +      /*
 +       * change "history = [true|false];" to "history = [1|0];"
 +       */
 +      historyIndex += history.length();
 +      String val = rewritten.substring(historyIndex, historyIndex + 5);
 +      if (val.startsWith("true"))
 +      {
 +        rewritten.replace(historyIndex, historyIndex + 4, "1");
 +      }
 +      else if (val.startsWith("false"))
 +      {
 +        rewritten.replace(historyIndex, historyIndex + 5, "0");
 +      }
 +    }
 +
 +    try
 +    {
 +      File tmp = File.createTempFile("viewerSession", ".tmp");
 +      try (OutputStream os = new FileOutputStream(tmp))
 +      {
 +        InputStream is = new ByteArrayInputStream(
 +                rewritten.toString().getBytes());
 +        copyAll(is, os);
 +        return tmp.getAbsolutePath();
 +      }
 +    } catch (IOException e)
 +    {
 +      Console.error("Error restoring Jmol session: " + e.toString());
 +    }
 +    return null;
 +  }
 +
 +  /**
 +   * Populates an XML model of the feature colour scheme for one feature type
 +   * 
 +   * @param featureType
 +   * @param fcol
 +   * @return
 +   */
 +  public static Colour marshalColour(String featureType,
 +          FeatureColourI fcol)
 +  {
 +    Colour col = new Colour();
 +    if (fcol.isSimpleColour())
 +    {
 +      col.setRGB(Format.getHexString(fcol.getColour()));
 +    }
 +    else
 +    {
 +      col.setRGB(Format.getHexString(fcol.getMaxColour()));
 +      col.setMin(fcol.getMin());
 +      col.setMax(fcol.getMax());
 +      col.setMinRGB(jalview.util.Format.getHexString(fcol.getMinColour()));
 +      col.setAutoScale(fcol.isAutoScaled());
 +      col.setThreshold(fcol.getThreshold());
 +      col.setColourByLabel(fcol.isColourByLabel());
 +      col.setThreshType(fcol.isAboveThreshold() ? ThresholdType.ABOVE
 +              : (fcol.isBelowThreshold() ? ThresholdType.BELOW
 +                      : ThresholdType.NONE));
 +      if (fcol.isColourByAttribute())
 +      {
 +        final String[] attName = fcol.getAttributeName();
 +        col.getAttributeName().add(attName[0]);
 +        if (attName.length > 1)
 +        {
 +          col.getAttributeName().add(attName[1]);
 +        }
 +      }
 +      Color noColour = fcol.getNoColour();
 +      if (noColour == null)
 +      {
 +        col.setNoValueColour(NoValueColour.NONE);
 +      }
 +      else if (noColour == fcol.getMaxColour())
 +      {
 +        col.setNoValueColour(NoValueColour.MAX);
 +      }
 +      else
 +      {
 +        col.setNoValueColour(NoValueColour.MIN);
 +      }
 +    }
 +    col.setName(featureType);
 +    return col;
 +  }
 +
 +  /**
 +   * Populates an XML model of the feature filter(s) for one feature type
 +   * 
 +   * @param firstMatcher
 +   *          the first (or only) match condition)
 +   * @param filter
 +   *          remaining match conditions (if any)
 +   * @param and
 +   *          if true, conditions are and-ed, else or-ed
 +   */
 +  public static jalview.xml.binding.jalview.FeatureMatcherSet marshalFilter(
 +          FeatureMatcherI firstMatcher, Iterator<FeatureMatcherI> filters,
 +          boolean and)
 +  {
 +    jalview.xml.binding.jalview.FeatureMatcherSet result = new jalview.xml.binding.jalview.FeatureMatcherSet();
 +
 +    if (filters.hasNext())
 +    {
 +      /*
 +       * compound matcher
 +       */
 +      CompoundMatcher compound = new CompoundMatcher();
 +      compound.setAnd(and);
 +      jalview.xml.binding.jalview.FeatureMatcherSet matcher1 = marshalFilter(
 +              firstMatcher, Collections.emptyIterator(), and);
 +      // compound.addMatcherSet(matcher1);
 +      compound.getMatcherSet().add(matcher1);
 +      FeatureMatcherI nextMatcher = filters.next();
 +      jalview.xml.binding.jalview.FeatureMatcherSet matcher2 = marshalFilter(
 +              nextMatcher, filters, and);
 +      // compound.addMatcherSet(matcher2);
 +      compound.getMatcherSet().add(matcher2);
 +      result.setCompoundMatcher(compound);
 +    }
 +    else
 +    {
 +      /*
 +       * single condition matcher
 +       */
 +      // MatchCondition matcherModel = new MatchCondition();
 +      jalview.xml.binding.jalview.FeatureMatcher matcherModel = new jalview.xml.binding.jalview.FeatureMatcher();
 +      matcherModel.setCondition(
 +              firstMatcher.getMatcher().getCondition().getStableName());
 +      matcherModel.setValue(firstMatcher.getMatcher().getPattern());
 +      if (firstMatcher.isByAttribute())
 +      {
 +        matcherModel.setBy(FilterBy.BY_ATTRIBUTE);
 +        // matcherModel.setAttributeName(firstMatcher.getAttribute());
 +        String[] attName = firstMatcher.getAttribute();
 +        matcherModel.getAttributeName().add(attName[0]); // attribute
 +        if (attName.length > 1)
 +        {
 +          matcherModel.getAttributeName().add(attName[1]); // sub-attribute
 +        }
 +      }
 +      else if (firstMatcher.isByLabel())
 +      {
 +        matcherModel.setBy(FilterBy.BY_LABEL);
 +      }
 +      else if (firstMatcher.isByScore())
 +      {
 +        matcherModel.setBy(FilterBy.BY_SCORE);
 +      }
 +      result.setMatchCondition(matcherModel);
 +    }
 +
 +    return result;
 +  }
 +
 +  /**
 +   * Loads one XML model of a feature filter to a Jalview object
 +   * 
 +   * @param featureType
 +   * @param matcherSetModel
 +   * @return
 +   */
 +  public static FeatureMatcherSetI parseFilter(String featureType,
 +          jalview.xml.binding.jalview.FeatureMatcherSet matcherSetModel)
 +  {
 +    FeatureMatcherSetI result = new FeatureMatcherSet();
 +    try
 +    {
 +      parseFilterConditions(result, matcherSetModel, true);
 +    } catch (IllegalStateException e)
 +    {
 +      // mixing AND and OR conditions perhaps
 +      System.err.println(
 +              String.format("Error reading filter conditions for '%s': %s",
 +                      featureType, e.getMessage()));
 +      // return as much as was parsed up to the error
 +    }
 +
 +    return result;
 +  }
 +
 +  /**
 +   * Adds feature match conditions to matcherSet as unmarshalled from XML
 +   * (possibly recursively for compound conditions)
 +   * 
 +   * @param matcherSet
 +   * @param matcherSetModel
 +   * @param and
 +   *          if true, multiple conditions are AND-ed, else they are OR-ed
 +   * @throws IllegalStateException
 +   *           if AND and OR conditions are mixed
 +   */
 +  protected static void parseFilterConditions(FeatureMatcherSetI matcherSet,
 +          jalview.xml.binding.jalview.FeatureMatcherSet matcherSetModel,
 +          boolean and)
 +  {
 +    jalview.xml.binding.jalview.FeatureMatcher mc = matcherSetModel
 +            .getMatchCondition();
 +    if (mc != null)
 +    {
 +      /*
 +       * single condition
 +       */
 +      FilterBy filterBy = mc.getBy();
 +      Condition cond = Condition.fromString(mc.getCondition());
 +      String pattern = mc.getValue();
 +      FeatureMatcherI matchCondition = null;
 +      if (filterBy == FilterBy.BY_LABEL)
 +      {
 +        matchCondition = FeatureMatcher.byLabel(cond, pattern);
 +      }
 +      else if (filterBy == FilterBy.BY_SCORE)
 +      {
 +        matchCondition = FeatureMatcher.byScore(cond, pattern);
 +
 +      }
 +      else if (filterBy == FilterBy.BY_ATTRIBUTE)
 +      {
 +        final List<String> attributeName = mc.getAttributeName();
 +        String[] attNames = attributeName
 +                .toArray(new String[attributeName.size()]);
 +        matchCondition = FeatureMatcher.byAttribute(cond, pattern,
 +                attNames);
 +      }
 +
 +      /*
 +       * note this throws IllegalStateException if AND-ing to a 
 +       * previously OR-ed compound condition, or vice versa
 +       */
 +      if (and)
 +      {
 +        matcherSet.and(matchCondition);
 +      }
 +      else
 +      {
 +        matcherSet.or(matchCondition);
 +      }
 +    }
 +    else
 +    {
 +      /*
 +       * compound condition
 +       */
 +      List<jalview.xml.binding.jalview.FeatureMatcherSet> matchers = matcherSetModel
 +              .getCompoundMatcher().getMatcherSet();
 +      boolean anded = matcherSetModel.getCompoundMatcher().isAnd();
 +      if (matchers.size() == 2)
 +      {
 +        parseFilterConditions(matcherSet, matchers.get(0), anded);
 +        parseFilterConditions(matcherSet, matchers.get(1), anded);
 +      }
 +      else
 +      {
 +        System.err.println("Malformed compound filter condition");
 +      }
 +    }
 +  }
 +
 +  /**
 +   * Loads one XML model of a feature colour to a Jalview object
 +   * 
 +   * @param colourModel
 +   * @return
 +   */
 +  public static FeatureColourI parseColour(Colour colourModel)
 +  {
 +    FeatureColourI colour = null;
 +
 +    if (colourModel.getMax() != null)
 +    {
 +      Color mincol = null;
 +      Color maxcol = null;
 +      Color noValueColour = null;
 +
 +      try
 +      {
 +        mincol = new Color(Integer.parseInt(colourModel.getMinRGB(), 16));
 +        maxcol = new Color(Integer.parseInt(colourModel.getRGB(), 16));
 +      } catch (Exception e)
 +      {
 +        Console.warn("Couldn't parse out graduated feature color.", e);
 +      }
 +
 +      NoValueColour noCol = colourModel.getNoValueColour();
 +      if (noCol == NoValueColour.MIN)
 +      {
 +        noValueColour = mincol;
 +      }
 +      else if (noCol == NoValueColour.MAX)
 +      {
 +        noValueColour = maxcol;
 +      }
 +
 +      colour = new FeatureColour(maxcol, mincol, maxcol, noValueColour,
 +              safeFloat(colourModel.getMin()),
 +              safeFloat(colourModel.getMax()));
 +      final List<String> attributeName = colourModel.getAttributeName();
 +      String[] attributes = attributeName
 +              .toArray(new String[attributeName.size()]);
 +      if (attributes != null && attributes.length > 0)
 +      {
 +        colour.setAttributeName(attributes);
 +      }
 +      if (colourModel.isAutoScale() != null)
 +      {
 +        colour.setAutoScaled(colourModel.isAutoScale().booleanValue());
 +      }
 +      if (colourModel.isColourByLabel() != null)
 +      {
 +        colour.setColourByLabel(
 +                colourModel.isColourByLabel().booleanValue());
 +      }
 +      if (colourModel.getThreshold() != null)
 +      {
 +        colour.setThreshold(colourModel.getThreshold().floatValue());
 +      }
 +      ThresholdType ttyp = colourModel.getThreshType();
 +      if (ttyp == ThresholdType.ABOVE)
 +      {
 +        colour.setAboveThreshold(true);
 +      }
 +      else if (ttyp == ThresholdType.BELOW)
 +      {
 +        colour.setBelowThreshold(true);
 +      }
 +    }
 +    else
 +    {
 +      Color color = new Color(Integer.parseInt(colourModel.getRGB(), 16));
 +      colour = new FeatureColour(color);
 +    }
 +
 +    return colour;
 +  }
 +}
@@@ -125,11 -125,11 +125,11 @@@ public class AWTConsole extends WindowA
  
      // Starting two seperate threads to read from the PipedInputStreams
      //
-     reader = new Thread(this, "AWTConsoleReader1Thread");
+     reader = new Thread(this, "AWTConsoleReader1");
      reader.setDaemon(true);
      reader.start();
      //
-     reader2 = new Thread(this, "AWTConsoleReader2Thread");
+     reader2 = new Thread(this, "AWTConsoleReader2");
      reader2.setDaemon(true);
      reader2.start();
  
      // We do it with a seperate Thread becasue we don't wan't to break a Thread
      // used by the Console.
      System.out.println("\nLets throw an error on this console");
-     errorThrower = new Thread(this, "AWTConsoleErrorLogThread");
+     errorThrower = new Thread(this, "AWTConsoleErrorLog");
      errorThrower.setDaemon(true);
      errorThrower.start();
    }
      return input;
    }
  
 +  /**
 +   * 
 +   * @param arg
 +   * @j2sIgnore
 +   */
    public static void main(String[] arg)
    {
      new AWTConsole(); // create console with not reference
   */
  package jalview.util;
  
 -import jalview.datamodel.DBRefEntry;
 -import jalview.datamodel.DBRefSource;
 -import jalview.datamodel.PDBEntry;
 -import jalview.datamodel.SequenceI;
 +import java.util.Locale;
  
  import java.util.ArrayList;
 -import java.util.Arrays;
 +import java.util.BitSet;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  
  import com.stevesoft.pat.Regex;
  
 +import jalview.datamodel.DBRefEntry;
 +import jalview.datamodel.DBRefSource;
 +import jalview.datamodel.Mapping;
 +import jalview.datamodel.PDBEntry;
 +import jalview.datamodel.SequenceI;
 +
  /**
   * Utilities for handling DBRef objects and their collections.
   */
@@@ -47,18 -45,7 +47,18 @@@ public class DBRefUtil
     */
    private static Map<String, String> canonicalSourceNameLookup = new HashMap<>();
  
 -  private static Map<String, String> dasCoordinateSystemsLookup = new HashMap<>();
 +  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;
  
    static
    {
      canonicalSourceNameLookup.put("ensembl-tr", DBRefSource.ENSEMBL);
      canonicalSourceNameLookup.put("ensembl-gn", DBRefSource.ENSEMBL);
  
++    // TODO keep ? (in phyloviewer branch only)
+     canonicalSourceNameLookup.put("pfam", DBRefSource.PFAM);
 -    // Make sure we have lowercase entries for all canonical string lookups
 -    Set<String> keys = canonicalSourceNameLookup.keySet();
 -    for (String k : keys)
 +    // guarantee we always have lowercase entries for canonical string lookups
 +    for (String k : canonicalSourceNameLookup.keySet())
      {
 -      canonicalSourceNameLookup.put(k.toLowerCase(),
 +      canonicalSourceNameLookup.put(k.toLowerCase(Locale.ROOT),
                canonicalSourceNameLookup.get(k));
      }
 -
 -    dasCoordinateSystemsLookup.put("pdbresnum", DBRefSource.PDB);
 -    dasCoordinateSystemsLookup.put("uniprot", DBRefSource.UNIPROT);
 -    dasCoordinateSystemsLookup.put("embl", DBRefSource.EMBL);
 -    // dasCoordinateSystemsLookup.put("embl", DBRefSource.EMBLCDS);
    }
  
    /**
     *          array of sources to select
     * @return
     */
 -  public static DBRefEntry[] selectRefs(DBRefEntry[] dbrefs,
 +  public static List<DBRefEntry> selectRefs(List<DBRefEntry> dbrefs,
            String[] sources)
    {
      if (dbrefs == null || sources == null)
      {
        return dbrefs;
      }
 -    HashSet<String> srcs = new HashSet<>();
 +
 +    // BH TODO (what?)
 +    HashSet<String> srcs = new HashSet<String>();
      for (String src : sources)
      {
 -      srcs.add(src.toUpperCase());
 +      srcs.add(src.toUpperCase(Locale.ROOT));
      }
  
 -    List<DBRefEntry> res = new ArrayList<>();
 -    for (DBRefEntry dbr : dbrefs)
 +    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()))
 +      if (srcs.contains(source.toUpperCase(Locale.ROOT)))
        {
          res.add(dbr);
        }
      }
 -
      if (res.size() > 0)
      {
 -      DBRefEntry[] reply = new DBRefEntry[res.size()];
 -      return res.toArray(reply);
 +      // List<DBRefEntry> reply = new DBRefEntry[res.size()];
 +      return res;// .toArray(reply);
      }
      return null;
    }
  
 +  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();
 +  }
 +
    /**
 -   * isDasCoordinateSystem
 +   * Returns a (possibly empty) list of those references that match the given
 +   * entry, according to the given comparator.
     * 
 -   * @param string
 -   *          String
 -   * @param dBRefEntry
 -   *          DBRefEntry
 -   * @return boolean true if Source DBRefEntry is compatible with DAS
 -   *         CoordinateSystem name
 +   * @param refs
 +   *          an array of database references to search
 +   * @param entry
 +   *          an entry to compare against
 +   * @param comparator
 +   * @return
     */
 -
 -  public static boolean isDasCoordinateSystem(String string,
 -          DBRefEntry dBRefEntry)
 +  static List<DBRefEntry> searchRefs(DBRefEntry[] refs, DBRefEntry entry,
 +          DbRefComp comparator)
    {
 -    if (string == null || dBRefEntry == null)
 +    List<DBRefEntry> rfs = new ArrayList<>();
 +    if (refs == null || entry == null)
      {
 -      return false;
 +      return rfs;
      }
 -    String coordsys = dasCoordinateSystemsLookup.get(string.toLowerCase());
 -    return coordsys == null ? false
 -            : coordsys.equals(dBRefEntry.getSource());
 +    for (int i = 0; i < refs.length; i++)
 +    {
 +      if (comparator.matches(entry, refs[i]))
 +      {
 +        rfs.add(refs[i]);
 +      }
 +    }
 +    return rfs;
    }
  
    /**
      {
        return null;
      }
 -    String canonical = canonicalSourceNameLookup.get(source.toLowerCase());
 +    String canonical = canonicalSourceNameLookup
 +            .get(source.toLowerCase(Locale.ROOT));
      return canonical == null ? source : canonical;
    }
  
     *          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(DBRefEntry[] ref,
 -          DBRefEntry entry)
 +  public static List<DBRefEntry> searchRefs(List<DBRefEntry> ref,
 +          DBRefEntry entry, int mode)
    {
      return searchRefs(ref, entry,
 -            matchDbAndIdAndEitherMapOrEquivalentMapList);
 +            matchDbAndIdAndEitherMapOrEquivalentMapList, mode);
    }
  
    /**
     *          accession id to match
     * @return
     */
 -  public static List<DBRefEntry> searchRefs(DBRefEntry[] refs, String accId)
 +  public static List<DBRefEntry> searchRefs(List<DBRefEntry> refs,
 +          String accId)
    {
 -    return searchRefs(refs, new DBRefEntry("", "", accId), matchId);
 +    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);
    }
  
    /**
     * @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(DBRefEntry[] refs, DBRefEntry entry,
 -          DbRefComp comparator)
 +  static List<DBRefEntry> searchRefs(List<DBRefEntry> refs,
 +          DBRefEntry entry, DbRefComp comparator, int mode)
    {
-     List<DBRefEntry> rfs = new ArrayList<DBRefEntry>();
+     List<DBRefEntry> rfs = new ArrayList<>();
      if (refs == null || entry == null)
      {
        return rfs;
      }
 -    for (int i = 0; i < refs.length; i++)
 +    for (int i = 0, n = refs.size(); i < n; i++)
      {
 -      if (comparator.matches(entry, refs[i]))
 +      DBRefEntry e = refs.get(i);
 +      if (comparator.matches(entry, e, SEARCH_MODE_FULL))
        {
 -        rfs.add(refs[i]);
 +        rfs.add(e);
        }
      }
      return rfs;
  
    interface DbRefComp
    {
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb);
 +    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?
 +  // TODO unused - remove? would be broken by equating "" with null
    public static DbRefComp matchNonNullonA = new DbRefComp()
    {
      @Override
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb)
 +    public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode)
      {
 -      if (refa.getSource() == null
 +      if ((mode & DB_SOURCE) != 0 && (refa.getSource() == null
                || DBRefUtils.getCanonicalName(refb.getSource()).equals(
 -                      DBRefUtils.getCanonicalName(refa.getSource())))
 +                      DBRefUtils.getCanonicalName(refa.getSource()))))
        {
 -        if (refa.getVersion() == null
 -                || refb.getVersion().equals(refa.getVersion()))
 +        if ((mode & DB_VERSION) != 0 && (refa.getVersion() == null
 +                || refb.getVersion().equals(refa.getVersion())))
          {
 -          if (refa.getAccessionId() == null
 -                  || refb.getAccessionId().equals(refa.getAccessionId()))
 +          if ((mode & DB_ID) != 0 && (refa.getAccessionId() == null
 +                  || refb.getAccessionId().equals(refa.getAccessionId())))
            {
 -            if (refa.getMap() == null || (refb.getMap() != null
 -                    && refb.getMap().equals(refa.getMap())))
 +            if ((mode & DB_MAP) != 0
 +                    && (refa.getMap() == null || (refb.getMap() != null
 +                            && refb.getMap().equals(refa.getMap()))))
              {
                return true;
              }
    public static DbRefComp matchEitherNonNull = new DbRefComp()
    {
      @Override
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb)
 +    public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode)
      {
        if (nullOrEqualSource(refa.getSource(), refb.getSource())
                && nullOrEqual(refa.getVersion(), refb.getVersion())
        }
        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.trim());
 +      }
 +    }
 +    if (ref != null)
 +    {
 +      seq.addDBRef(ref);
 +    }
 +    return ref;
 +  }
 +
 +  /**
     * accession ID and DB must be identical. Version is ignored. Map is either
     * not defined or is a match (or is compatible?)
     */
    public static DbRefComp matchDbAndIdAndEitherMap = new DbRefComp()
    {
      @Override
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb)
 +    public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode)
      {
        if (refa.getSource() != null && refb.getSource() != null
                && DBRefUtils.getCanonicalName(refb.getSource()).equals(
    public static DbRefComp matchDbAndIdAndComplementaryMapList = new DbRefComp()
    {
      @Override
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb)
 +    public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode)
      {
        if (refa.getSource() != null && refb.getSource() != null
                && DBRefUtils.getCanonicalName(refb.getSource()).equals(
    public static DbRefComp matchDbAndIdAndEquivalentMapList = new DbRefComp()
    {
      @Override
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb)
 +    public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode)
      {
        if (refa.getSource() != null && refb.getSource() != null
                && DBRefUtils.getCanonicalName(refb.getSource()).equals(
    public static DbRefComp matchDbAndIdAndEitherMapOrEquivalentMapList = new DbRefComp()
    {
      @Override
 -    public boolean matches(DBRefEntry refa, DBRefEntry refb)
 +    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
                  || refa.getAccessionId().equals(refb.getAccessionId()))
          {
    };
  
    /**
 -   * 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)
 +   * Returns the (possibly empty) list of those supplied dbrefs which have the
 +   * specified source database, with a case-insensitive match of source name
     * 
 -   * @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)
 +   * @param dbRefs
 +   * @param source
 +   * @return
     */
 -  public static DBRefEntry parseToDbRef(SequenceI seq, String dbname,
 -          String version, String acn)
 +  public static List<DBRefEntry> searchRefsForSource(DBRefEntry[] dbRefs,
 +          String source)
    {
 -    DBRefEntry ref = null;
 -    if (dbname != null)
 +    List<DBRefEntry> matches = new ArrayList<>();
 +    if (dbRefs != null && source != null)
      {
 -      String locsrc = DBRefUtils.getCanonicalName(dbname);
 -      if (locsrc.equals(DBRefSource.PDB))
 +      for (DBRefEntry dbref : dbRefs)
        {
 -        /*
 -         * 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
 +        if (source.equalsIgnoreCase(dbref.getSource()))
          {
 -          System.err.println("Malformed PDB DR line:" + acn);
 +          matches.add(dbref);
          }
        }
 -      else
 -      {
 -        // default:
 -        ref = new DBRefEntry(locsrc, version, acn);
 -      }
 -    }
 -    if (ref != null)
 -    {
 -      seq.addDBRef(ref);
      }
 -    return ref;
 +    return matches;
    }
  
    /**
     *          a set of references to select from
     * @return
     */
 -  public static DBRefEntry[] selectDbRefs(boolean selectDna,
 -          DBRefEntry[] refs)
 +  public static List<DBRefEntry> selectDbRefs(boolean selectDna,
 +          List<DBRefEntry> refs)
    {
      return selectRefs(refs,
              selectDna ? DBRefSource.DNACODINGDBS : DBRefSource.PROTEINDBS);
     * @param source
     * @return
     */
 -  public static List<DBRefEntry> searchRefsForSource(DBRefEntry[] dbRefs,
 -          String source)
 +  public static List<DBRefEntry> searchRefsForSource(
 +          List<DBRefEntry> dbRefs, String source)
    {
-     List<DBRefEntry> matches = new ArrayList<DBRefEntry>();
+     List<DBRefEntry> matches = new ArrayList<>();
      if (dbRefs != null && source != null)
      {
        for (DBRefEntry dbref : dbRefs)
     * 
     * @param sequence
     */
 -  public static void ensurePrimaries(SequenceI sequence)
 +  public static void ensurePrimaries(SequenceI sequence,
 +          List<DBRefEntry> pr)
    {
 -    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));
 -    }
 +    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 (DBRefEntry p : pr)
 +    for (int ip = pr.size(); --ip >= 0;)
      {
 -      while (selfs.contains(p))
 +      DBRefEntry p = pr.get(ip);
 +      for (int i = bsSelect.nextSetBit(0); i >= 0; i = bsSelect
 +              .nextSetBit(i + 1))
        {
 -        selfs.remove(p);
 +        if (dbrefs.get(i) == p)
 +          bsSelect.clear(i);
        }
 +      // while (selfs.contains(p))
 +      // {
 +      // selfs.remove(p);
 +      // }
      }
 -    List<DBRefEntry> toPromote = new ArrayList<>();
 +    // List<DBRefEntry> toPromote = new ArrayList<DBRefEntry>();
  
 -    for (DBRefEntry p : pr)
 +    for (int ip = pr.size(), keys = 0; --ip >= 0
 +            && keys != DBRefSource.PRIMARY_MASK;)
      {
 -      List<String> promType = new ArrayList<>();
 -      if (sequence.isProtein())
 +      DBRefEntry p = pr.get(ip);
 +      if (isProtein)
        {
          switch (getCanonicalName(p.getSource()))
          {
          case DBRefSource.UNIPROT:
 -          // case DBRefSource.UNIPROTKB:
 -          // case DBRefSource.UP_NAME:
 -          // search for and promote ensembl
 -          promType.add(DBRefSource.ENSEMBL);
 +          keys |= DBRefSource.UNIPROT_MASK;
            break;
          case DBRefSource.ENSEMBL:
 -          // search for and promote Uniprot
 -          promType.add(DBRefSource.UNIPROT);
 +          keys |= DBRefSource.ENSEMBL_MASK;
            break;
          }
        }
        else
        {
 -        // TODO: promote transcript refs
 +        // 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)
 +      if (keys == 0 || !selectRefsBS(dbrefs, keys, bsSelect))
 +        return;
 +      // if (candidates != null)
        {
 -        for (DBRefEntry cand : candidates)
 +        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())
            {
 -            if (cand.getMap().getTo() != null
 -                    && cand.getMap().getTo() != sequence)
 +            Mapping map = cand.getMap();
 +            SequenceI cto = map.getTo();
 +            if (cto != null && cto != sequence)
              {
                // can't promote refs with mappings to other sequences
                continue;
              }
 -            if (cand.getMap().getMap().getFromLowest() != sequence
 -                    .getStart()
 -                    && cand.getMap().getMap().getFromHighest() != sequence
 -                            .getEnd())
 +            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
 +          // and promote - not that version must be non-null here,
 +          // as p must have passed isPrimaryCandidate()
            cand.setVersion(p.getVersion() + " (promoted)");
 -          selfs.remove(cand);
 -          toPromote.add(cand);
 +          bsSelect.clear(ic);
 +          // selfs.remove(cand);
 +          // toPromote.add(cand);
            if (!cand.isPrimaryCandidate())
            {
              System.out.println(
   */
  package jalview.util;
  
 +import java.util.ArrayList;
 +import java.util.Arrays;
 +import java.util.HashMap;
 +import java.util.Iterator;
 +import java.util.List;
 +import java.util.Map;
 +
  import jalview.analysis.AlignmentSorter;
  import jalview.api.AlignViewportI;
 +import jalview.bin.Console;
  import jalview.commands.CommandI;
  import jalview.commands.EditCommand;
  import jalview.commands.EditCommand.Action;
  import jalview.commands.EditCommand.Edit;
  import jalview.commands.OrderCommand;
  import jalview.datamodel.AlignedCodonFrame;
 +import jalview.datamodel.AlignedCodonFrame.SequenceToSequenceMapping;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.AlignmentOrder;
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.HiddenColumns;
 +import jalview.datamodel.Mapping;
  import jalview.datamodel.SearchResultMatchI;
  import jalview.datamodel.SearchResults;
  import jalview.datamodel.SearchResultsI;
@@@ -49,6 -39,13 +49,6 @@@ import jalview.datamodel.Sequence
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  
 -import java.util.ArrayList;
 -import java.util.Arrays;
 -import java.util.HashMap;
 -import java.util.Iterator;
 -import java.util.List;
 -import java.util.Map;
 -
  /**
   * Helper methods for manipulations involving sequence mappings.
   * 
@@@ -81,7 -78,7 +81,7 @@@ public final class MappingUtil
        action = action.getUndoAction();
      }
      // TODO write this
 -    System.err.println("MappingUtils.mapCutOrPaste not yet implemented");
 +    Console.error("MappingUtils.mapCutOrPaste not yet implemented");
    }
  
    /**
         */
        int startResiduePos = selected.findPosition(firstUngappedPos);
        int endResiduePos = selected.findPosition(lastUngappedPos);
 -
 -      for (AlignedCodonFrame acf : codonFrames)
 +      for (SequenceI seq : mapTo.getAlignment().getSequences())
        {
 -        SequenceI mappedSequence = targetIsNucleotide
 -                ? acf.getDnaForAaSeq(selected)
 -                : acf.getAaForDnaSeq(selected);
 -        if (mappedSequence != null)
 +        int mappedStartResidue = 0;
 +        int mappedEndResidue = 0;
 +        for (AlignedCodonFrame acf : codonFrames)
          {
 -          for (SequenceI seq : mapTo.getAlignment().getSequences())
 +          // rather than use acf.getCoveringMapping() we iterate through all
 +          // mappings to make sure all CDS are selected for a protein
 +          for (SequenceToSequenceMapping map : acf.getMappings())
            {
 -            int mappedStartResidue = 0;
 -            int mappedEndResidue = 0;
 -            if (seq.getDatasetSequence() == mappedSequence)
 +            if (map.covers(selected) && map.covers(seq))
              {
                /*
                 * Found a sequence mapping. Locate the start/end mapped residues.
                List<AlignedCodonFrame> mapping = Arrays
                        .asList(new AlignedCodonFrame[]
                        { acf });
 +              // locate start
                SearchResultsI sr = buildSearchResults(selected,
                        startResiduePos, mapping);
                for (SearchResultMatchI m : sr.getResults())
                  mappedStartResidue = m.getStart();
                  mappedEndResidue = m.getEnd();
                }
 +              // locate end - allowing for adjustment of start range
                sr = buildSearchResults(selected, endResiduePos, mapping);
                for (SearchResultMatchI m : sr.getResults())
                {
      {
        for (AlignedCodonFrame acf : mappings)
        {
 -        SequenceI mappedSeq = mappingToNucleotide ? acf.getDnaForAaSeq(seq)
 -                : acf.getAaForDnaSeq(seq);
 -        if (mappedSeq != null)
 +        for (SequenceI seq2 : mapTo.getSequences())
          {
 -          for (SequenceI seq2 : mapTo.getSequences())
 +          /*
 +           * the corresponding peptide / CDS is the one for which there is
 +           * a complete ('covering') mapping to 'seq'
 +           */
 +          SequenceI peptide = mappingToNucleotide ? seq2 : seq;
 +          SequenceI cds = mappingToNucleotide ? seq : seq2;
 +          SequenceToSequenceMapping s2s = acf.getCoveringMapping(cds,
 +                  peptide);
 +          if (s2s != null)
            {
 -            if (seq2.getDatasetSequence() == mappedSeq)
 -            {
 -              mappedOrder.add(seq2);
 -              j++;
 -              break;
 -            }
 +            mappedOrder.add(seq2);
 +            j++;
 +            break;
            }
          }
        }
  
      if (colsel == null)
      {
 -      return; // mappedColumns;
 +      return;
      }
  
      char fromGapChar = mapFrom.getAlignment().getGapCharacter();
                toSequences, fromGapChar);
      }
  
 -    for (int[] hidden : hiddencols.getHiddenColumnsCopy())
 +    Iterator<int[]> regions = hiddencols.iterator();
 +    while (regions.hasNext())
      {
 -      mapHiddenColumns(hidden, codonFrames, newHidden, fromSequences,
 -              toSequences, fromGapChar);
 +      mapHiddenColumns(regions.next(), codonFrames, newHidden,
 +              fromSequences, toSequences, fromGapChar);
      }
 -    return; // mappedColumns;
 +    return;
    }
  
    /**
           */
          for (SequenceI toSeq : toSequences)
          {
 -          if (toSeq.getDatasetSequence() == mappedSeq)
 +          if (toSeq.getDatasetSequence() == mappedSeq
 +                  && mappedStartResidue >= toSeq.getStart()
 +                  && mappedEndResidue <= toSeq.getEnd())
            {
              int mappedStartCol = toSeq.findIndex(mappedStartResidue);
              int mappedEndCol = toSeq.findIndex(mappedEndResidue);
      {
        if (range.length % 2 != 0)
        {
 -        System.err.println(
 +        Console.error(
                  "Error unbalance start/end ranges: " + ranges.toString());
          return 0;
        }
    }
  
    /**
 +   * Answers true if range's start-end positions include those of queryRange,
 +   * where either range might be in reverse direction, else false
 +   * 
 +   * @param range
 +   *          a start-end range
 +   * @param queryRange
 +   *          a candidate subrange of range (start2-end2)
 +   * @return
 +   */
 +  public static boolean rangeContains(int[] range, int[] queryRange)
 +  {
 +    if (range == null || queryRange == null || range.length != 2
 +            || queryRange.length != 2)
 +    {
 +      /*
 +       * invalid arguments
 +       */
 +      return false;
 +    }
 +
 +    int min = Math.min(range[0], range[1]);
 +    int max = Math.max(range[0], range[1]);
 +
 +    return (min <= queryRange[0] && max >= queryRange[0]
 +            && min <= queryRange[1] && max >= queryRange[1]);
 +  }
 +
 +  /**
     * Removes the specified number of positions from the given ranges. Provided
     * to allow a stop codon to be stripped from a CDS sequence so that it matches
     * the peptide translation length.
     *          a list of (single) [start, end] ranges
     * @return
     */
 -  public static void removeEndPositions(int positions,
 -          List<int[]> ranges)
 +  public static void removeEndPositions(int positions, List<int[]> ranges)
    {
      int toRemove = positions;
      Iterator<int[]> it = new ReverseListIterator<>(ranges);
          /*
           * not coded for [start1, end1, start2, end2, ...]
           */
 -        System.err
 -                .println("MappingUtils.removeEndPositions doesn't handle multiple  ranges");
 +        Console.error(
 +                "MappingUtils.removeEndPositions doesn't handle multiple  ranges");
          return;
        }
  
          /*
           * not coded for a reverse strand range (end < start)
           */
 -        System.err
 -                .println("MappingUtils.removeEndPositions doesn't handle reverse strand");
 +        Console.error(
 +                "MappingUtils.removeEndPositions doesn't handle reverse strand");
          return;
        }
        if (length > toRemove)
      }
    }
  
 +  /**
 +   * Converts a list of {@code start-end} ranges to a single array of
 +   * {@code start1, end1, start2, ... } ranges
 +   * 
 +   * @param ranges
 +   * @return
 +   */
 +  public static int[] rangeListToArray(List<int[]> ranges)
 +  {
 +    int rangeCount = ranges.size();
 +    int[] result = new int[rangeCount * 2];
 +    int j = 0;
 +    for (int i = 0; i < rangeCount; i++)
 +    {
 +      int[] range = ranges.get(i);
 +      result[j++] = range[0];
 +      result[j++] = range[1];
 +    }
 +    return result;
 +  }
 +
 +  /*
 +   * Returns the maximal start-end positions in the given (ordered) list of
 +   * ranges which is overlapped by the given begin-end range, or null if there
 +   * is no overlap.
 +   * 
 +   * <pre>
 +   * Examples:
 +   *   if ranges is {[4, 8], [10, 12], [16, 19]}
 +   * then
 +   *   findOverlap(ranges, 1, 20) == [4, 19]
 +   *   findOverlap(ranges, 6, 11) == [6, 11]
 +   *   findOverlap(ranges, 9, 15) == [10, 12]
 +   *   findOverlap(ranges, 13, 15) == null
 +   * </pre>
 +   * 
 +   * @param ranges
 +   * @param begin
 +   * @param end
 +   * @return
 +   */
 +  protected static int[] findOverlap(List<int[]> ranges, final int begin,
 +          final int end)
 +  {
 +    boolean foundStart = false;
 +    int from = 0;
 +    int to = 0;
  
 +    /*
 +     * traverse the ranges to find the first position (if any) >= begin,
 +     * and the last position (if any) <= end
 +     */
 +    for (int[] range : ranges)
 +    {
 +      if (!foundStart)
 +      {
 +        if (range[0] >= begin)
 +        {
 +          /*
 +           * first range that starts with, or follows, begin
 +           */
 +          foundStart = true;
 +          from = Math.max(range[0], begin);
 +        }
 +        else if (range[1] >= begin)
 +        {
 +          /*
 +           * first range that contains begin
 +           */
 +          foundStart = true;
 +          from = begin;
 +        }
 +      }
 +
 +      if (range[0] <= end)
 +      {
 +        to = Math.min(end, range[1]);
 +      }
 +    }
 +
 +    return foundStart && to >= from ? new int[] { from, to } : null;
 +  }
 +    
    public static <K, V> Map<K, V> putWithDuplicationCheck(Map<K, V> map, K key,
            V value)
    {
      }
      else
      {
--      jalview.bin.Cache.log.warn(
++      Console.warn(
                "Attempt to add duplicate entry detected for map with key: "
                        + key.toString() + " and value: " + value.toString());
      }
    
      return map;
 -    
    }
  }
@@@ -24,7 -24,6 +24,7 @@@ import jalview.analysis.AnnotationSorte
  import jalview.analysis.Conservation;
  import jalview.analysis.TreeModel;
  import jalview.api.AlignCalcManagerI;
 +import jalview.api.AlignExportSettingsI;
  import jalview.api.AlignViewportI;
  import jalview.api.AlignmentViewPanel;
  import jalview.api.FeaturesDisplayedI;
@@@ -32,7 -31,6 +32,7 @@@ import jalview.api.ViewStyleI
  import jalview.commands.CommandI;
  import jalview.datamodel.AlignedCodonFrame;
  import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.AlignmentExportData;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.AlignmentView;
  import jalview.datamodel.Annotation;
@@@ -45,6 -43,7 +45,7 @@@ import jalview.datamodel.Sequence
  import jalview.datamodel.SequenceCollectionI;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
+ import jalview.ext.treeviewer.TreeI;
  import jalview.renderer.ResidueShader;
  import jalview.renderer.ResidueShaderI;
  import jalview.schemes.ColourSchemeI;
@@@ -69,7 -68,6 +70,7 @@@ import java.util.BitSet
  import java.util.Deque;
  import java.util.HashMap;
  import java.util.Hashtable;
 +import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  
@@@ -664,7 -662,7 +665,7 @@@ public abstract class AlignmentViewpor
           * retain any colour thresholds per group while
           * changing choice of colour scheme (JAL-2386)
           */
 -        sg.setColourScheme(cs);
 +        sg.setColourScheme(cs == null ? null : cs.getInstance(this, sg));
          if (cs != null)
          {
            sg.getGroupColourScheme().alignmentChanged(sg,
    /**
     * results of cDNA complement consensus visible portion of view
     */
 -  protected Hashtable[] hcomplementConsensus = null;
 +  protected Hashtable<String, Object>[] hcomplementConsensus = null;
  
    /**
     * results of secondary structure base pair consensus for visible portion of
     * view
     */
 -  protected Hashtable[] hStrucConsensus = null;
 +  protected Hashtable<String, Object>[] hStrucConsensus = null;
  
    protected Conservation hconservation = null;
  
    }
  
    @Override
 -  public void setComplementConsensusHash(Hashtable[] hconsensus)
 +  public void setComplementConsensusHash(
 +          Hashtable<String, Object>[] hconsensus)
    {
      this.hcomplementConsensus = hconsensus;
    }
    }
  
    @Override
 -  public Hashtable[] getComplementConsensusHash()
 +  public Hashtable<String, Object>[] getComplementConsensusHash()
    {
      return hcomplementConsensus;
    }
  
    @Override
 -  public Hashtable[] getRnaStructureConsensusHash()
 +  public Hashtable<String, Object>[] getRnaStructureConsensusHash()
    {
      return hStrucConsensus;
    }
  
    @Override
 -  public void setRnaStructureConsensusHash(Hashtable[] hStrucConsensus)
 +  public void setRnaStructureConsensusHash(
 +          Hashtable<String, Object>[] hStrucConsensus)
    {
      this.hStrucConsensus = hStrucConsensus;
  
      changeSupport = null;
      ranges = null;
      currentTree = null;
+     currentExtTree = null;
      selectionGroup = null;
 +    colSel = null;
      setAlignment(null);
    }
  
    public void invertColumnSelection()
    {
      colSel.invertColumnSelection(0, alignment.getWidth(), alignment);
 +    isColSelChanged(true);
    }
  
    @Override
      if (alignment.getHiddenColumns() != null
              && alignment.getHiddenColumns().hasHiddenColumns())
      {
 -      selection = alignment.getHiddenColumns()
 -              .getVisibleSequenceStrings(start, end, seqs);
 +      for (i = 0; i < iSize; i++)
 +      {
 +        Iterator<int[]> blocks = alignment.getHiddenColumns()
 +                .getVisContigsIterator(start, end + 1, false);
 +        selection[i] = seqs[i].getSequenceStringFromIterator(blocks);
 +      }
      }
      else
      {
        {
          if (start == 0)
          {
 -          start = hidden.adjustForHiddenColumns(start);
 +          start = hidden.visibleToAbsoluteColumn(start);
          }
  
 -        end = hidden.getHiddenBoundaryRight(start);
 +        end = hidden.getNextHiddenBoundary(false, start);
          if (start == end)
          {
            end = max;
  
        if (hidden != null && hidden.hasHiddenColumns())
        {
 -        start = hidden.adjustForHiddenColumns(end);
 -        start = hidden.getHiddenBoundaryLeft(start) + 1;
 +        start = hidden.visibleToAbsoluteColumn(end);
 +        start = hidden.getNextHiddenBoundary(true, start) + 1;
        }
      } while (end < max);
  
 -    int[][] startEnd = new int[regions.size()][2];
 +    // int[][] startEnd = new int[regions.size()][2];
  
      return regions;
    }
          AlignmentAnnotation clone = new AlignmentAnnotation(annot);
          if (selectedOnly && selectionGroup != null)
          {
 -          alignment.getHiddenColumns().makeVisibleAnnotation(
 -                  selectionGroup.getStartRes(), selectionGroup.getEndRes(),
 -                  clone);
 +          clone.makeVisibleAnnotation(selectionGroup.getStartRes(),
 +                  selectionGroup.getEndRes(), alignment.getHiddenColumns());
          }
          else
          {
 -          alignment.getHiddenColumns().makeVisibleAnnotation(clone);
 +          clone.makeVisibleAnnotation(alignment.getHiddenColumns());
          }
          ala.add(clone);
        }
       * TODO reorder the annotation rows according to group/sequence ordering on
       * alignment
       */
 -    boolean sortg = true;
 +    // boolean sortg = true;
  
      // remove old automatic annotation
      // add any new annotation
    public void clearSequenceColours()
    {
      sequenceColours.clear();
 -  };
 +  }
  
    @Override
    public AlignViewportI getCodingComplement()
      viewStyle.setProteinFontAsCdna(b);
    }
  
 +  @Override
 +  public void setShowComplementFeatures(boolean b)
 +  {
 +    viewStyle.setShowComplementFeatures(b);
 +  }
 +
 +  @Override
 +  public boolean isShowComplementFeatures()
 +  {
 +    return viewStyle.isShowComplementFeatures();
 +  }
 +
 +  @Override
 +  public void setShowComplementFeaturesOnTop(boolean b)
 +  {
 +    viewStyle.setShowComplementFeaturesOnTop(b);
 +  }
 +
 +  @Override
 +  public boolean isShowComplementFeaturesOnTop()
 +  {
 +    return viewStyle.isShowComplementFeaturesOnTop();
 +  }
 +
    /**
     * @return true if view should scroll to show the highlighted region of a
     *         sequence
      int lastSeq = alignment.getHeight() - 1;
      List<AlignedCodonFrame> seqMappings = null;
      for (int seqNo = ranges
 -            .getStartSeq(); seqNo < lastSeq; seqNo++, seqOffset++)
 +            .getStartSeq(); seqNo <= lastSeq; seqNo++, seqOffset++)
      {
        sequence = getAlignment().getSequenceAt(seqNo);
        if (hiddenSequences != null && hiddenSequences.isHidden(sequence))
  
    protected TreeModel currentTree = null;
  
+   protected TreeI currentExtTree = null;
    @Override
    public boolean hasSearchResults()
    {
      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);
 +    }
 +  }
 +
 +  @Override
 +  public Iterator<int[]> getViewAsVisibleContigs(boolean selectedRegionOnly)
 +  {
 +    int start = 0;
 +    int end = 0;
 +    if (selectedRegionOnly && selectionGroup != null)
 +    {
 +      start = selectionGroup.getStartRes();
 +      end = selectionGroup.getEndRes() + 1;
 +    }
 +    else
 +    {
 +      end = alignment.getWidth();
 +    }
 +    return (alignment.getHiddenColumns().getVisContigsIterator(start, end,
 +            false));
 +  }
+   public TreeI getCurrentExtTree()
+   {
+     return currentExtTree;
+   }
+   public void setCurrentExtTree(TreeI externalTree)
+   {
+     currentExtTree = externalTree;
+   }
  }
   */
  package jalview.ws;
  
 +import java.util.Locale;
 +
 +import java.util.ArrayList;
 +import java.util.Arrays;
 +import java.util.Enumeration;
 +import java.util.HashMap;
 +import java.util.Hashtable;
 +import java.util.List;
 +import java.util.Map;
 +import java.util.StringTokenizer;
 +import java.util.Vector;
 +import java.util.regex.Matcher;
 +import java.util.regex.Pattern;
 +
  import jalview.analysis.AlignSeq;
 +import jalview.api.FeatureSettingsModelI;
  import jalview.bin.Cache;
 +import jalview.bin.Console;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.DBRefEntry;
  import jalview.datamodel.DBRefSource;
  import jalview.datamodel.Mapping;
  import jalview.datamodel.SequenceI;
  import jalview.gui.CutAndPasteTransfer;
 -import jalview.gui.DasSourceBrowser;
  import jalview.gui.Desktop;
  import jalview.gui.FeatureSettings;
  import jalview.gui.IProgressIndicator;
  import jalview.gui.OOMWarning;
  import jalview.util.DBRefUtils;
  import jalview.util.MessageManager;
 -import jalview.ws.dbsources.das.api.jalviewSourceI;
 -import jalview.ws.dbsources.das.datamodel.DasSequenceSource;
  import jalview.ws.seqfetcher.DbSourceProxy;
 -
 -import java.util.ArrayList;
 -import java.util.Arrays;
 -import java.util.Enumeration;
 -import java.util.Hashtable;
 -import java.util.List;
 -import java.util.StringTokenizer;
 -import java.util.Vector;
 -
  import uk.ac.ebi.picr.model.UPEntry;
  import uk.ac.ebi.www.picr.AccessionMappingService.AccessionMapperServiceLocator;
  
@@@ -65,8 -61,6 +65,8 @@@ public class DBRefFetcher implements Ru
  {
    private static final String NEWLINE = System.lineSeparator();
  
 +  public static final String TRIM_RETRIEVED_SEQUENCES = "TRIM_FETCHED_DATASET_SEQS";
 +
    public interface FetchFinishedListenerI
    {
      void finished();
@@@ -78,6 -72,8 +78,6 @@@
  
    CutAndPasteTransfer output = new CutAndPasteTransfer();
  
 -  boolean running = false;
 -
    /**
     * picr client instance
     */
      }
      this.dataset = ds;
      // TODO Jalview 2.5 lots of this code should be in the gui package!
 -    sfetcher = jalview.gui.SequenceFetcher
 -            .getSequenceFetcherSingleton(progressIndicatorFrame);
 +    sfetcher = jalview.gui.SequenceFetcher.getSequenceFetcherSingleton();
      // set default behaviour for transferring excess sequence data to the
      // dataset
 -    trimDsSeqs = Cache.getDefault("TRIM_FETCHED_DATASET_SEQS", true);
 +    trimDsSeqs = Cache.getDefault(TRIM_RETRIEVED_SEQUENCES, true);
      if (sources == null)
      {
        setDatabaseSources(featureSettings, isNucleotide);
      // af.featureSettings_actionPerformed(null);
      String[] defdb = null;
      List<DbSourceProxy> selsources = new ArrayList<>();
 -    Vector<jalviewSourceI> dasselsrc = (featureSettings != null)
 -            ? featureSettings.getSelectedSources()
 -            : new DasSourceBrowser().getSelectedSources();
 -
 -    for (jalviewSourceI src : dasselsrc)
 -    {
 -      List<DbSourceProxy> sp = src.getSequenceSourceProxies();
 -      if (sp != null)
 -      {
 -        selsources.addAll(sp);
 -        if (sp.size() > 1)
 -        {
 -          Cache.log.debug("Added many Db Sources for :" + src.getTitle());
 -        }
 -      }
 -    }
      // select appropriate databases based on alignFrame context.
      if (forNucleotide)
      {
    }
  
    /**
 -   * retrieve all the das sequence sources and add them to the list of db
 -   * sources to retrieve from
 -   */
 -  public void appendAllDasSources()
 -  {
 -    if (dbSources == null)
 -    {
 -      dbSources = new DbSourceProxy[0];
 -    }
 -    // append additional sources
 -    DbSourceProxy[] otherdb = sfetcher
 -            .getDbSourceProxyInstances(DasSequenceSource.class);
 -    if (otherdb != null && otherdb.length > 0)
 -    {
 -      DbSourceProxy[] newsrc = new DbSourceProxy[dbSources.length
 -              + otherdb.length];
 -      System.arraycopy(dbSources, 0, newsrc, 0, dbSources.length);
 -      System.arraycopy(otherdb, 0, newsrc, dbSources.length,
 -              otherdb.length);
 -      dbSources = newsrc;
 -    }
 -  }
 -
 -  /**
     * start the fetcher thread
     * 
     * @param waitTillFinished
     */
    public void fetchDBRefs(boolean waitTillFinished)
    {
 -    // TODO can we not simply write
 -    // if (waitTillFinished) { run(); } else { new Thread(this).start(); }
 -
 -    Thread thread = new Thread(this, "FetchDBRef");
 -    thread.start();
 -    running = true;
 -
      if (waitTillFinished)
      {
 -      while (running)
 -      {
 -        try
 -        {
 -          Thread.sleep(500);
 -        } catch (Exception ex)
 -        {
 -        }
 -      }
 +      run();
 +    }
 +    else
 +    {
-       new Thread(this).start();
++      new Thread(this,"FetchDBRef").start();
      }
    }
  
     */
    void addSeqId(SequenceI seq, String key)
    {
 -    key = key.toUpperCase();
 +    key = key.toUpperCase(Locale.ROOT);
  
      Vector<SequenceI> seqs;
      if (seqRefs.containsKey(key))
        throw new Error(MessageManager
                .getString("error.implementation_error_must_init_dbsources"));
      }
 -    running = true;
      long startTime = System.currentTimeMillis();
      if (progressWindow != null)
      {
        e.printStackTrace();
      }
  
 -    Vector<SequenceI> sdataset = new Vector<>(
 -            Arrays.asList(dataset));
 +    Vector<SequenceI> sdataset = new Vector<>(Arrays.asList(dataset));
      List<String> warningMessages = new ArrayList<>();
  
 +    // clear any old feature display settings recorded from past sessions
 +    featureDisplaySettings = null;
 +
      int db = 0;
      while (sdataset.size() > 0 && db < dbSources.length)
      {
            AlignmentI retrieved = null;
            try
            {
 -            if (Cache.log.isDebugEnabled())
 +            if (Console.isDebugEnabled())
              {
 -              Cache.log.debug("Querying " + dbsource.getDbName()
 -                      + " with : '" + queryString.toString() + "'");
 +              Console.debug("Querying " + dbsource.getDbName() + " with : '"
 +                      + queryString.toString() + "'");
              }
              retrieved = dbsource.getSequenceRecords(queryString.toString());
            } catch (Exception ex)
            }
            if (retrieved != null)
            {
 -            transferReferences(sdataset, dbsource.getDbSource(), retrieved,
 -                    trimDsSeqs, warningMessages);
 +            transferReferences(sdataset, dbsource, retrieved, trimDsSeqs,
 +                    warningMessages);
            }
          }
          else
                    && (i < 50); seqIndex++, i++)
            {
              SequenceI sequence = dataset[seqIndex];
 -            DBRefEntry[] uprefs = DBRefUtils
 +            List<DBRefEntry> uprefs = DBRefUtils
                      .selectRefs(sequence.getDBRefs(), new String[]
                      { dbsource.getDbSource() }); // jalview.datamodel.DBRefSource.UNIPROT
              // });
              // check for existing dbrefs to use
 -            if (uprefs != null && uprefs.length > 0)
 +            if (uprefs != null && uprefs.size() > 0)
              {
 -              for (int j = 0; j < uprefs.length; j++)
 +              for (int j = 0, n = uprefs.size(); j < n; j++)
                {
 -                addSeqId(sequence, uprefs[j].getAccessionId());
 +                DBRefEntry upref = uprefs.get(j);
 +                addSeqId(sequence, upref.getAccessionId());
                  queries.addElement(
 -                        uprefs[j].getAccessionId().toUpperCase());
 +                        upref.getAccessionId().toUpperCase(Locale.ROOT));
                }
              }
              else
              {
 +              Pattern possibleIds = Pattern.compile("[A-Za-z0-9_]+");
                // generate queries from sequence ID string
 -              StringTokenizer st = new StringTokenizer(sequence.getName(),
 -                      "|");
 -              while (st.hasMoreTokens())
 +              Matcher tokens = possibleIds.matcher(sequence.getName());
 +              int p = 0;
 +              while (tokens.find(p))
                {
 -                String token = st.nextToken();
 +                String token = tokens.group();
 +                p = tokens.end();
                  UPEntry[] presp = null;
                  if (picrClient != null)
                  {
                            "Validated ID against PICR... (for what its worth):"
                                    + token);
                    addSeqId(sequence, token);
 -                  queries.addElement(token.toUpperCase());
 +                  queries.addElement(token.toUpperCase(Locale.ROOT));
                  }
                  else
                  {
                    // System.out.println("Not querying source with
                    // token="+token+"\n");
                    addSeqId(sequence, token);
 -                  queries.addElement(token.toUpperCase());
 +                  queries.addElement(token.toUpperCase(Locale.ROOT));
                  }
                }
              }
      {
        listener.finished();
      }
 -    running = false;
    }
  
    /**
     * @param warningMessages
     *          a list of messages to add to
     */
 -  boolean transferReferences(Vector<SequenceI> sdataset, String dbSource,
 -          AlignmentI retrievedAl, boolean trimDatasetSeqs,
 -          List<String> warningMessages)
 +  boolean transferReferences(Vector<SequenceI> sdataset,
 +          DbSourceProxy dbSourceProxy, AlignmentI retrievedAl,
 +          boolean trimDatasetSeqs, List<String> warningMessages)
    {
      // System.out.println("trimming ? " + trimDatasetSeqs);
      if (retrievedAl == null || retrievedAl.getHeight() == 0)
        return false;
      }
  
 +    String dbSource = dbSourceProxy.getDbName();
      boolean modified = false;
      SequenceI[] retrieved = recoverDbSequences(
              retrievedAl.getSequencesArray());
        // taking into account all accessionIds and names in the file
        Vector<SequenceI> sequenceMatches = new Vector<>();
        // look for corresponding accession ids
 -      DBRefEntry[] entryRefs = DBRefUtils
 +      List<DBRefEntry> entryRefs = DBRefUtils
                .selectRefs(retrievedSeq.getDBRefs(), new String[]
                { dbSource });
        if (entryRefs == null)
                          + dbSource + " on " + retrievedSeq.getName());
          continue;
        }
 -      for (int j = 0; j < entryRefs.length; j++)
 +      for (int j = 0, n = entryRefs.size(); j < n; j++)
        {
 -        String accessionId = entryRefs[j].getAccessionId();
 +        DBRefEntry ref = entryRefs.get(j);
 +        String accessionId = ref.getAccessionId();
          // match up on accessionId
 -        if (seqRefs.containsKey(accessionId.toUpperCase()))
 +        if (seqRefs.containsKey(accessionId.toUpperCase(Locale.ROOT)))
          {
            Vector<SequenceI> seqs = seqRefs.get(accessionId);
            for (int jj = 0; jj < seqs.size(); jj++)
        // could be useful to extend this so we try to find any 'significant'
        // information in common between two sequence objects.
        /*
 -       * DBRefEntry[] entryRefs =
 +       * List<DBRefEntry> entryRefs =
         * jalview.util.DBRefUtils.selectRefs(entry.getDBRef(), new String[] {
         * dbSource }); for (int j = 0; j < entry.getName().size(); j++) { String
         * name = entry.getName().elementAt(j).toString(); if
         * seqs.elementAt(jj); if (!sequenceMatches.contains(sequence)) {
         * sequenceMatches.addElement(sequence); } } } }
         */
 +      if (sequenceMatches.size() > 0)
 +      {
 +        addFeatureSettings(dbSourceProxy);
 +      }
        // sequenceMatches now contains the set of all sequences associated with
        // the returned db record
        final String retrievedSeqString = retrievedSeq.getSequenceAsString();
 -      String entrySeq = retrievedSeqString.toUpperCase();
 +      String entrySeq = retrievedSeqString.toUpperCase(Locale.ROOT);
        for (int m = 0; m < sequenceMatches.size(); m++)
        {
          sequence = sequenceMatches.elementAt(m);
          // TODO: test for legacy where uniprot or EMBL refs exist but no
          // mappings are made (but content matches retrieved set)
          boolean updateRefFrame = sequence.getDBRefs() == null
 -                || sequence.getDBRefs().length == 0;
 +                || sequence.getDBRefs().size() == 0;
          // TODO:
          // verify sequence against the entry sequence
  
          boolean remoteEnclosesLocal = false;
          String nonGapped = AlignSeq
                  .extractGaps("-. ", sequence.getSequenceAsString())
 -                .toUpperCase();
 +                .toUpperCase(Locale.ROOT);
          int absStart = entrySeq.indexOf(nonGapped);
          if (absStart == -1)
          {
              int startShift = absStart - sequenceStart + 1;
              if (startShift != 0)
              {
 -              modified |= sequence.getFeatures().shiftFeatures(startShift);
 +              modified |= sequence.getFeatures().shiftFeatures(1,
 +                      startShift);
              }
            }
          }
                String ngAlsq = AlignSeq
                        .extractGaps("-. ",
                                alseqs[alsq].getSequenceAsString())
 -                      .toUpperCase();
 +                      .toUpperCase(Locale.ROOT);
                int oldstrt = alseqs[alsq].getStart();
                alseqs[alsq].setStart(sequence.getSequenceAsString()
 -                      .toUpperCase().indexOf(ngAlsq) + sequence.getStart());
 +                      .toUpperCase(Locale.ROOT).indexOf(ngAlsq)
 +                      + sequence.getStart());
                if (oldstrt != alseqs[alsq].getStart())
                {
                  alseqs[alsq].setEnd(
          // and remove it from the rest
          // TODO: decide if we should remove annotated sequence from set
          sdataset.remove(sequence);
 -        // TODO: should we make a note of sequences that have received new DB
 -        // ids, so we can query all enabled DAS servers for them ?
        }
      }
      return modified;
    }
  
 +  Map<String, FeatureSettingsModelI> featureDisplaySettings = null;
 +
 +  private void addFeatureSettings(DbSourceProxy dbSourceProxy)
 +  {
 +    FeatureSettingsModelI fsettings = dbSourceProxy
 +            .getFeatureColourScheme();
 +    if (fsettings != null)
 +    {
 +      if (featureDisplaySettings == null)
 +      {
 +        featureDisplaySettings = new HashMap<>();
 +      }
 +      featureDisplaySettings.put(dbSourceProxy.getDbName(), fsettings);
 +    }
 +  }
 +
 +  /**
 +   * 
 +   * @return any feature settings associated with sources that have provided
 +   *         sequences
 +   */
 +  public List<FeatureSettingsModelI> getFeatureSettingsModels()
 +  {
 +    return featureDisplaySettings == null
 +            ? Arrays.asList(new FeatureSettingsModelI[0])
 +            : Arrays.asList(featureDisplaySettings.values()
 +                    .toArray(new FeatureSettingsModelI[1]));
 +  }
 +
    /**
     * Adds the message to the list unless it already contains it
     * 
     */
    private SequenceI[] recoverDbSequences(SequenceI[] sequencesArray)
    {
 -    Vector<SequenceI> nseq = new Vector<>();
 -    for (int i = 0; sequencesArray != null
 -            && i < sequencesArray.length; i++)
 +    int n;
 +    if (sequencesArray == null || (n = sequencesArray.length) == 0)
      {
 -      nseq.addElement(sequencesArray[i]);
 -      DBRefEntry[] dbr = sequencesArray[i].getDBRefs();
 +      return sequencesArray;
 +    }
 +    ArrayList<SequenceI> nseq = new ArrayList<>();
 +    for (int i = 0; i < n; i++)
 +    {
 +      nseq.add(sequencesArray[i]);
 +      List<DBRefEntry> dbr = sequencesArray[i].getDBRefs();
        Mapping map = null;
 -      for (int r = 0; (dbr != null) && r < dbr.length; r++)
 +      if (dbr != null)
        {
 -        if ((map = dbr[r].getMap()) != null)
 +        for (int r = 0, rn = dbr.size(); r < rn; r++)
          {
 -          if (map.getTo() != null && !nseq.contains(map.getTo()))
 +          if ((map = dbr.get(r).getMap()) != null)
            {
 -            nseq.addElement(map.getTo());
 +            if (map.getTo() != null && !nseq.contains(map.getTo()))
 +            {
 +              nseq.add(map.getTo());
 +            }
            }
          }
        }
      }
 +    // BH 2019.01.25 question here if this is the right logic. Return the
 +    // original if nothing found?
      if (nseq.size() > 0)
      {
 -      sequencesArray = new SequenceI[nseq.size()];
 -      nseq.toArray(sequencesArray);
 +      return nseq.toArray(new SequenceI[nseq.size()]);
      }
      return sequencesArray;
    }
@@@ -21,7 -21,6 +21,7 @@@
  package jalview.ws.jws2;
  
  import jalview.bin.Cache;
 +import jalview.bin.Console;
  import jalview.gui.AlignFrame;
  import jalview.gui.Desktop;
  import jalview.gui.JvSwingUtils;
@@@ -165,7 -164,7 +165,7 @@@ public class Jws2Discoverer implements 
        {
          try
          {
 -          Cache.log.debug(
 +          Console.debug(
                    "Waiting around for old discovery thread to finish.");
            // wait around until old discoverer dies
            Thread.sleep(100);
          }
        }
        aborted = false;
 -      Cache.log.debug("Old discovery thread has finished.");
 +      Console.debug("Old discovery thread has finished.");
      }
      running = true;
  
  
        }
        qrys.add(squery);
-       new Thread(squery, "JabaQueryThread").start();
+       new Thread(squery, "JabaQuery").start();
      }
      boolean finished = true;
      do
        } catch (Exception e)
        {
        }
 -      ;
        for (JabaWsServerQuery squery : qrys)
        {
          if (squery.isRunning())
        }
        if (aborted)
        {
 -        Cache.log.debug(
 +        Console.debug(
                  "Aborting " + qrys.size() + " JABAWS discovery threads.");
          for (JabaWsServerQuery squery : qrys)
          {
                    changeSupport.firePropertyChange("services",
                            new Vector<Jws2Instance>(), services);
                  };
-               }, "LoadPreferredServiceThread").start();
+               }, "LoadPreferredService").start();
 -
              }
            });
          }
        ArrayList<Jws2Instance> hostservices = hosts.get(service.getHost());
        if (hostservices == null)
        {
 -        hosts.put(service.getHost(),
 -                hostservices = new ArrayList<>());
 +        hosts.put(service.getHost(), hostservices = new ArrayList<>());
          hostlist.add(service.getHost());
        }
        hostservices.add(service);
      }
    }
  
 +  /**
 +   * 
 +   * @param args
 +   * @j2sIgnore
 +   */
    public static void main(String[] args)
    {
      if (args.length > 0)
        {
          testUrls.add(url);
        }
 -      ;
      }
      Thread runner = getDiscoverer()
              .startDiscoverer(new PropertyChangeListener()
        } catch (InterruptedException e)
        {
        }
 -      ;
      }
      try
      {
            }
            else
            {
 -            Cache.log.warn("Ignoring duplicate url " + url + " in "
 +            Console.warn("Ignoring duplicate url " + url + " in "
                      + JWS2HOSTURLS + " list");
            }
          } catch (MalformedURLException ex)
          {
 -          Cache.log.warn("Problem whilst trying to make a URL from '"
 +          Console.warn("Problem whilst trying to make a URL from '"
                    + ((url != null) ? url : "<null>") + "'");
 -          Cache.log.warn(
 +          Console.warn(
                    "This was probably due to a malformed comma separated list"
                            + " in the " + JWS2HOSTURLS
                            + " entry of $(HOME)/.jalview_properties)");
 -          Cache.log.debug("Exception was ", ex);
 +          Console.debug("Exception was ", ex);
          }
        }
      } catch (Exception ex)
      {
 -      Cache.log.warn("Error parsing comma separated list of urls in "
 +      Console.warn("Error parsing comma separated list of urls in "
                + JWS2HOSTURLS + " preference.", ex);
      }
      return urls;
      return match;
    }
  
 -  Map<String, Map<String, String>> preferredServiceMap = new HashMap<>();;
 +  Map<String, Map<String, String>> preferredServiceMap = new HashMap<>();
  
    /**
     * get current preferred service of the given type, or global default
@@@ -1,36 -1,8 +1,36 @@@
 +/*
 + * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
 + * Copyright (C) $$Year-Rel$$ The Jalview Authors
 + * 
 + * This file is part of Jalview.
 + * 
 + * Jalview is free software: you can redistribute it and/or
 + * modify it under the terms of the GNU General Public License 
 + * as published by the Free Software Foundation, either version 3
 + * of the License, or (at your option) any later version.
 + *  
 + * Jalview is distributed in the hope that it will be useful, but 
 + * WITHOUT ANY WARRANTY; without even the implied warranty 
 + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
 + * PURPOSE.  See the GNU General Public License for more details.
 + * 
 + * You should have received a copy of the GNU General Public License
 + * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
 + * The Jalview Authors are detailed in the 'AUTHORS' file.
 + */
  package jalview.gui;
  
 -import static org.testng.Assert.assertEquals;
 +import static org.testng.Assert.assertNotNull;
  import static org.testng.Assert.assertTrue;
  
 +import java.awt.event.MouseEvent;
 +import java.io.File;
 +import java.io.IOException;
 +import java.io.PrintStream;
 +
 +import org.testng.annotations.BeforeClass;
 +import org.testng.annotations.Test;
 +
  import jalview.analysis.AlignmentGenerator;
  import jalview.bin.Cache;
  import jalview.bin.Jalview;
@@@ -38,97 -10,58 +38,97 @@@ import jalview.datamodel.AlignmentI
  import jalview.datamodel.SequenceGroup;
  import jalview.io.DataSourceType;
  import jalview.io.FileLoader;
 +import junit.extensions.PA;
  
 -import java.io.File;
 -import java.io.IOException;
 -import java.io.PrintStream;
 -
 -import org.testng.annotations.BeforeClass;
 -import org.testng.annotations.Test;
 -
 +/**
 + * Provides a simple test that memory is released when all windows are closed.
 + * <ul>
 + * <li>generates a reasonably large alignment and loads it</li>
 + * <li>performs various operations on the alignment</li>
 + * <li>closes all windows</li>
 + * <li>requests garbage collection</li>
 + * <li>asserts that the remaining memory footprint (heap usage) is 'not large'
 + * </li>
 + * </ul>
 + * If the test fails, this means that reference(s) to large object(s) have
 + * failed to be garbage collected. In this case:
 + * <ul>
 + * <li>set a breakpoint just before the test assertion in
 + * {@code checkUsedMemory}</li>
 + * <li>if the test fails intermittently, make this breakpoint conditional on
 + * {@code usedMemory > expectedMax}</li>
 + * <li>run the test to this point (and check that it is about to fail i.e.
 + * {@code usedMemory > expectedMax})</li>
 + * <li>use <a href="https://visualvm.github.io/">visualvm</a> to obtain a heap
 + * dump from the suspended process (and kill the test or let it fail)</li>
 + * <li>inspect the heap dump using visualvm for large objects and their
 + * referers</li>
 + * <li>Tips:</li>
 + * <ul>
 + * <li>Perform GC from the Monitor view in visualvm before requesting the heap
 + * dump - test failure might be simply a delay to GC</li>
 + * <li>View 'Objects' and filter classes to {@code jalview}. Sort columns by
 + * Count, or Size, and look for anything suspicious. For example, if the object
 + * count for {@code Sequence} is non-zero (it shouldn't be), pick any instance,
 + * and follow the chain of {@code references} to find which class(es) still hold
 + * references to sequence objects</li>
 + * <li>If this chain is impracticably long, re-run the test with a smaller
 + * alignment (set width=100, height=10 in {@code generateAlignment()}), to
 + * capture a heap which is qualitatively the same, but much smaller, so easier
 + * to analyse; note this requires an unconditional breakpoint</li>
 + * </ul>
 + * </ul>
 + * <p>
 + * <h2>Fixing memory leaks</h2>
 + * <p>
 + * Experience shows that often a reference is retained (directly or indirectly)
 + * by a Swing (or related) component (for example a {@code MouseListener} or
 + * {@code ActionListener}). There are two possible approaches to fixing:
 + * <ul>
 + * <li>Purist: ensure that all listeners and similar objects are removed when no
 + * longer needed. May be difficult, to achieve and to maintain as code
 + * changes.</li>
 + * <li>Pragmatic: null references to potentially large objects from Jalview
 + * application classes when no longer needed, typically when a panel is closed.
 + * This ensures that even if the JVM keeps a reference to a panel or viewport,
 + * it does not retain a large heap footprint. This is the approach taken in, for
 + * example, {@code AlignmentPanel.closePanel()} and
 + * {@code AnnotationPanel.dispose()}.</li>
 + * <li>Adjust code if necessary; for example an {@code ActionListener} should
 + * act on {@code av.getAlignment()} and not directly on {@code alignment}, as
 + * the latter pattern could leave persistent references to the alignment</li>
 + * </ul>
 + * Add code to 'null unused large object references' until the test passes. For
 + * a final sanity check, capture the heap dump for a passing test, and satisfy
 + * yourself that only 'small' or 'harmless' {@code jalview} object instances
 + * (such as enums or singletons) are left in the heap.
 + */
  public class FreeUpMemoryTest
  {
    private static final int ONE_MB = 1000 * 1000;
  
 +  /*
 +   * maximum retained heap usage (in MB) for a passing test
 +   */
 +  private static int MAX_RESIDUAL_HEAP = 45;
 +
    /**
     * Configure (read-only) Jalview property settings for test
     */
    @BeforeClass(alwaysRun = true)
    public void setUp()
    {
 -    Jalview.main(new String[] { "-nonews", "-props",
 -        "test/jalview/testProps.jvprops" });
 -    Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS",
 -            Boolean.TRUE.toString());
 -    Cache.applicationProperties.setProperty("SHOW_QUALITY",
 -            Boolean.TRUE.toString());
 -    Cache.applicationProperties.setProperty("SHOW_CONSERVATION",
 -            Boolean.TRUE.toString());
 -    Cache.applicationProperties.setProperty("SHOW_OCCUPANCY",
 -            Boolean.TRUE.toString());
 -    Cache.applicationProperties.setProperty("SHOW_IDENTITY",
 -            Boolean.TRUE.toString());
 +    Jalview.main(
 +            new String[]
 +            { "-nonews", "-props", "test/jalview/testProps.jvprops" });
 +    String True = Boolean.TRUE.toString();
 +    Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS", True);
 +    Cache.applicationProperties.setProperty("SHOW_QUALITY", True);
 +    Cache.applicationProperties.setProperty("SHOW_CONSERVATION", True);
 +    Cache.applicationProperties.setProperty("SHOW_OCCUPANCY", True);
 +    Cache.applicationProperties.setProperty("SHOW_IDENTITY", True);
    }
  
 -  /**
 -   * A simple test that memory is released when all windows are closed.
 -   * <ul>
 -   * <li>generates a reasonably large alignment and loads it</li>
 -   * <li>performs various operations on the alignment</li>
 -   * <li>closes all windows</li>
 -   * <li>requests garbage collection</li>
 -   * <li>asserts that the remaining memory footprint (heap usage) is 'not large'
 -   * </li>
 -   * </ul>
 -   * If the test fails, this suggests that a reference to some large object
 -   * (perhaps the alignment data, or some annotation / Tree / PCA data) has
 -   * failed to be garbage collected. If this is the case, the heap will need to
 -   * be inspected manually (suggest using jvisualvm) in order to track down
 -   * where large objects are still referenced. The code (for example
 -   * AlignmentViewport.dispose()) should then be updated to ensure references to
 -   * large objects are set to null when they are no longer required.
 -   * 
 -   * @throws IOException
 -   */
    @Test(groups = "Memory")
    public void testFreeMemoryOnClose() throws IOException
    {
  
      Desktop.instance.closeAll_actionPerformed(null);
  
 -    checkUsedMemory(35L);
 +    checkUsedMemory(MAX_RESIDUAL_HEAP);
 +  }
 +
 +  /**
 +   * Returns the current total used memory (available memory - free memory),
 +   * rounded down to the nearest MB
 +   * 
 +   * @return
 +   */
 +  private static int getUsedMemory()
 +  {
 +    long availableMemory = Runtime.getRuntime().totalMemory();
 +    long freeMemory = Runtime.getRuntime().freeMemory();
 +    long usedMemory = availableMemory - freeMemory;
 +
 +    return (int) (usedMemory / ONE_MB);
    }
  
    /**
     * 
     * @param expectedMax
     */
 -  protected void checkUsedMemory(long expectedMax)
 +  protected void checkUsedMemory(int expectedMax)
    {
      /*
 -     * request garbage collection and wait briefly for it to run;
 +     * request garbage collection and wait for it to run (up to 3 times);
       * NB there is no guarantee when, or whether, it will do so
       */
 -    System.gc();
 -    waitFor(100);
 -
 -    /*
 -     * a second gc() call should not be necessary - but it is!
 -     * the test passes with it, and fails without it
 -     */
 -    System.gc();
 -    waitFor(100);
 -
 -    /*
 -     * check used memory is 'reasonably low'
 -     */
 -    long availableMemory = Runtime.getRuntime().totalMemory() / ONE_MB;
 -    long freeMemory = Runtime.getRuntime().freeMemory() / ONE_MB;
 -    long usedMemory = availableMemory - freeMemory;
 -
 -    /*
 -     * sanity check - fails if any frame was added after
 -     * closeAll_actionPerformed
 -     */
 -    assertEquals(Desktop.instance.getAllFrames().length, 0);
 +    long usedMemory = 0L;
 +    Long minUsedMemory = null;
 +    int gcCount = 0;
 +    while (gcCount < 3)
 +    {
 +      gcCount++;
 +      System.gc();
 +      waitFor(1500);
 +      usedMemory = getUsedMemory();
 +      if (minUsedMemory == null || usedMemory < minUsedMemory)
 +      {
 +        minUsedMemory = usedMemory;
 +      }
 +      if (usedMemory < expectedMax)
 +      {
 +        break;
 +      }
 +    }
  
      /*
 -     * if this assertion fails
 -     * - set a breakpoint here
 -     * - run jvisualvm to inspect a heap dump of Jalview
 -     * - identify large objects in the heap and their referers
 +     * if this assertion fails (reproducibly!)
 +     * - set a breakpoint here, conditional on (usedMemory > expectedMax)
 +     * - run VisualVM to inspect the heap usage, and run GC from VisualVM to check 
 +     *   it is not simply delayed garbage collection causing the test failure 
 +     * - take a heap dump and identify large objects in the heap and their referers
       * - fix code as necessary to null the references on close
       */
 -    System.out.println("Used memory after gc = " + usedMemory + "MB");
 -    assertTrue(usedMemory < expectedMax, String.format(
 +    System.out.println("(Minimum) Used memory after " + gcCount
 +            + " call(s) to gc() = " + minUsedMemory + "MB (should be <="
 +            + expectedMax + ")");
 +    assertTrue(usedMemory <= expectedMax, String.format(
              "Used memory %d should be less than %d (Recommend running test manually to verify)",
 -            usedMemory,
 -            expectedMax));
 +            usedMemory, expectedMax));
    }
  
    /**
      }
  
      /*
 +     * open an Overview window
 +     */
 +    af.overviewMenuItem_actionPerformed(null);
 +    assertNotNull(af.alignPanel.overviewPanel);
 +
 +    /*
 +     * exercise the pop-up menu in the Overview Panel (JAL-2864)
 +     */
 +    Object[] args = new Object[] {
 +        new MouseEvent(af, 0, 0, 0, 0, 0, 1, true) };
 +    PA.invokeMethod(af.alignPanel.overviewPanel,
 +            "showPopupMenu(java.awt.event.MouseEvent)", args);
 +
 +    /*
       * set a selection group - potential memory leak if it retains
       * a reference to the alignment
       */
      af.openTreePcaDialog();
      CalculationChooser dialog = af.alignPanel.getCalculationDialog();
      dialog.openPcaPanel("BLOSUM62", dialog.getSimilarityParameters(true));
-     dialog.createTree("BLOSUM62",dialog.getSimilarityParameters(false));
+     try
+     {
+       dialog.createTree("BLOSUM62", dialog.getSimilarityParameters(false));
+     } catch (IOException e)
+     {
+       // TODO Auto-generated catch block
+       e.printStackTrace();
+     }
  
      /*
       * wait until Tree and PCA have been computed
       */
      while (af.viewport.getCurrentTree() == null
 -            && dialog.getPcaPanel().isWorking())
 +            || dialog.getPcaPanel().isWorking())
      {
        waitFor(10);
      }
      int width = 100000;
      int height = 100;
      ag.generate(width, height, 0, 10, 15);
 +    ps.close();
      return f;
    }
  }