Merge branch 'JAL-3878_ws-overhaul-3' into mmw/Release_2_12_ws_merge
authorMateusz Warowny <mmzwarowny@dundee.ac.uk>
Mon, 6 Feb 2023 13:04:59 +0000 (14:04 +0100)
committerMateusz Warowny <mmzwarowny@dundee.ac.uk>
Mon, 6 Feb 2023 13:10:03 +0000 (14:10 +0100)
1  2 
src/jalview/datamodel/Sequence.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/Desktop.java
src/jalview/gui/PCAPanel.java
src/jalview/gui/SlivkaPreferences.java
src/jalview/gui/StructureChooser.java
src/jalview/gui/WebserviceInfo.java
src/jalview/gui/WsJobParameters.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/workers/AlignCalcManager2.java

@@@ -86,6 -86,7 +86,6 @@@ public class Sequence extends ASequenc
    HiddenMarkovModel hmm;
  
    boolean isHMMConsensusSequence = false;
 -
    private DBModList<DBRefEntry> dbrefs; // controlled access
  
    /**
    }
  
    /**
+    * Create a new sequence object from a characters array using default values
+    * of 1 and -1 for start and end. The array used to create the sequence is
+    * copied and is not stored internally.
+    * 
+    * @param name
+    *          sequence name
+    * @param sequence
+    *          list of residues
+    */
+   public Sequence(String name, char[] sequence)
+   {
+     this(name, Arrays.copyOf(sequence, sequence.length), 1, -1);
+   }
+   /**
     * Creates a new Sequence object with new AlignmentAnnotations but inherits
     * any existing dataset sequence reference. If non exists, everything is
     * copied.
      {
        this.hmm = new HiddenMarkovModel(seq.getHMM(), this);
      }
 -
    }
  
    @Override
    @Override
    public void addDBRef(DBRefEntry entry)
    {
 +    // TODO JAL-3980 maintain as sorted list
      if (datasetSequence != null)
      {
        datasetSequence.addDBRef(entry);
      {
        dbrefs = new DBModList<>();
      }
 +    // TODO JAL-3979 LOOK UP RATHER THAN SWEEP FOR EFFICIENCY
  
      for (int ib = 0, nb = dbrefs.size(); ib < nb; ib++)
      {
    public List<AlignmentAnnotation> getAlignmentAnnotations(String calcId,
            String label)
    {
 +    return getAlignmentAnnotations(calcId, label, null, true);
 +  }
 +
 +  @Override
 +  public List<AlignmentAnnotation> getAlignmentAnnotations(String calcId,
 +          String label, String description)
 +  {
 +    return getAlignmentAnnotations(calcId, label, description, false);
 +  }
 +
 +  private List<AlignmentAnnotation> getAlignmentAnnotations(String calcId,
 +          String label, String description, boolean ignoreDescription)
 +  {
      List<AlignmentAnnotation> result = new ArrayList<>();
      if (this.annotation != null)
      {
        for (AlignmentAnnotation ann : annotation)
        {
          String id = ann.getCalcId();
 -        if (id != null && id.equals(calcId)
 -                && ann.label != null && ann.label.equals(label))
 +        if ((id != null && id.equals(calcId))
 +                && (ann.label != null && ann.label.equals(label))
 +                && ((ignoreDescription && description == null)
 +                        || (ann.description != null
 +                                && ann.description.equals(description))))
          {
            result.add(ann);
          }
      }
      return false;
    }
 -
    /**
     * {@inheritDoc}
     */
   */
  package jalview.gui;
  
 +import java.util.Locale;
 +
 +import java.io.IOException;
 +import java.util.HashSet;
 +import java.util.Set;
 +
 +import javax.swing.JFileChooser;
 +import javax.swing.JOptionPane;
 +import java.awt.BorderLayout;
 +import java.awt.Color;
 +import java.awt.Component;
 +import java.awt.Dimension;
 +import java.awt.Rectangle;
 +import java.awt.Toolkit;
 +import java.awt.datatransfer.Clipboard;
 +import java.awt.datatransfer.DataFlavor;
 +import java.awt.datatransfer.StringSelection;
 +import java.awt.datatransfer.Transferable;
 +import java.awt.dnd.DnDConstants;
 +import java.awt.dnd.DropTargetDragEvent;
 +import java.awt.dnd.DropTargetDropEvent;
 +import java.awt.dnd.DropTargetEvent;
 +import java.awt.dnd.DropTargetListener;
 +import java.awt.event.ActionEvent;
 +import java.awt.event.ActionListener;
 +import java.awt.event.FocusAdapter;
 +import java.awt.event.FocusEvent;
 +import java.awt.event.ItemEvent;
 +import java.awt.event.ItemListener;
 +import java.awt.event.KeyAdapter;
 +import java.awt.event.KeyEvent;
 +import java.awt.event.MouseEvent;
 +import java.awt.print.PageFormat;
 +import java.awt.print.PrinterJob;
 +import java.beans.PropertyChangeEvent;
 +import java.beans.PropertyChangeListener;
 +import java.io.File;
 +import java.io.FileWriter;
 +import java.io.PrintWriter;
 +import java.net.URL;
 +import java.util.ArrayList;
 +import java.util.Arrays;
 +import java.util.Collection;
 +import java.util.Deque;
 +import java.util.Enumeration;
 +import java.util.Hashtable;
 +import java.util.List;
 +import java.util.Vector;
 +
 +import javax.swing.ButtonGroup;
 +import javax.swing.JCheckBoxMenuItem;
 +import javax.swing.JComponent;
 +import javax.swing.JEditorPane;
 +import javax.swing.JInternalFrame;
 +import javax.swing.JLabel;
 +import javax.swing.JLayeredPane;
 +import javax.swing.JMenu;
 +import javax.swing.JMenuItem;
 +import javax.swing.JPanel;
 +import javax.swing.JScrollPane;
 +import javax.swing.SwingUtilities;
 +import javax.swing.event.InternalFrameAdapter;
 +import javax.swing.event.InternalFrameEvent;
 +
 +import ext.vamsas.ServiceHandle;
  import jalview.analysis.AlignmentSorter;
  import jalview.analysis.AlignmentUtils;
  import jalview.analysis.CrossRef;
@@@ -103,7 -38,6 +103,7 @@@ import jalview.api.SplitContainerI
  import jalview.api.ViewStyleI;
  import jalview.api.analysis.SimilarityParamsI;
  import jalview.bin.Cache;
 +import jalview.bin.Console;
  import jalview.bin.Jalview;
  import jalview.commands.CommandI;
  import jalview.commands.EditCommand;
@@@ -164,7 -98,6 +164,7 @@@ import jalview.schemes.ColourSchemeI
  import jalview.schemes.ColourSchemes;
  import jalview.schemes.ResidueColourScheme;
  import jalview.schemes.TCoffeeColourScheme;
 +import jalview.util.HttpUtils;
  import jalview.util.ImageMaker.TYPE;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
@@@ -182,7 -115,10 +182,10 @@@ import jalview.ws.params.ArgumentI
  import jalview.ws.params.ParamDatastoreI;
  import jalview.ws.params.WsParamSetI;
  import jalview.ws.seqfetcher.DbSourceProxy;
- import jalview.ws.slivkaws.SlivkaWSDiscoverer;
+ import jalview.ws2.client.api.WebServiceDiscovererI;
+ import jalview.ws2.client.slivka.SlivkaWSDiscoverer;
+ import jalview.ws2.gui.WebServicesMenuManager;
  import java.io.IOException;
  import java.util.HashSet;
  import java.util.Set;
@@@ -261,6 -197,7 +264,6 @@@ public class AlignFrame extends GAlignF
  {
  
    public static int frameCount;
 -
    public static final int DEFAULT_WIDTH = 700;
  
    public static final int DEFAULT_HEIGHT = 500;
    private int id;
  
    private DataSourceType protocol ;
 -
    /**
     * Creates a new AlignFrame object with specific width and height.
     * 
    public AlignFrame(AlignmentI al, HiddenColumns hiddenColumns, int width,
            int height, String sequenceSetId, String viewId)
    {
 -
      id = (++frameCount);
 -
      setSize(width, height);
  
      if (al.getDataset() == null)
     * initalise the alignframe from the underlying viewport data and the
     * configurations
     */
 -
    void init()
    {
      boolean newPanel = (alignPanel == null);
        alignPanel = new AlignmentPanel(this, viewport);
      }
      addAlignmentPanel(alignPanel, newPanel);
 -
      // setBackground(Color.white); // BH 2019
  
      if (!Jalview.isHeadlessMode())
        // modifyPID.setEnabled(false);
      }
  
 -    String sortby = jalview.bin.Cache.getDefault(Preferences.SORT_ALIGNMENT,
 +    String sortby = Cache.getDefault(Preferences.SORT_ALIGNMENT,
              "No sort");
  
      if (sortby.equals("Id"))
        wrapMenuItem_actionPerformed(null);
      }
  
 -    if (jalview.bin.Cache.getDefault(Preferences.SHOW_OVERVIEW, false))
 +    if (Cache.getDefault(Preferences.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);
      }
      addFocusListener(new FocusAdapter()
      {
 -
        @Override
        public void focusGained(FocusEvent e)
        {
     * @param format
     *          format of file
     */
 -
    @Deprecated
    public void setFileName(String file, FileFormatI format)
    {
     * 
     * @param file
     */
 -
    public void setFileObject(File file)
    {
      this.fileObject = file;
     * Add a KeyListener with handlers for various KeyPressed and KeyReleased
     * events
     */
 -
    void addKeyListener()
    {
      addKeyListener(new KeyAdapter()
      {
 -
        @Override
        public void keyPressed(KeyEvent evt)
        {
            }
            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;
  
    {
      buildWebServicesMenu();
    }
+   private WebServiceDiscovererI.ServicesChangeListener slivkaServiceChangeListener =
+       (discoverer, services) -> {
+         // run when slivka services change
+         var menu = AlignFrame.this.slivkaMenu;
+         menu.setServices(discoverer);
+         menu.setInProgress(discoverer.isRunning());
+         menu.setNoServices(services.isEmpty() && discoverer.isDone());
+       };
    /* Set up intrinsic listeners for dynamically generated GUI bits. */
    private void addServiceListeners()
    {
      if (Cache.getDefault("SHOW_SLIVKA_SERVICES", true))
      {
-       WSDiscovererI discoverer = SlivkaWSDiscoverer.getInstance();
-       discoverer.addServiceChangeListener(this);
+       WebServiceDiscovererI discoverer = SlivkaWSDiscoverer.getInstance();
+       discoverer.addServicesChangeListener(slivkaServiceChangeListener);
      }
      if (Cache.getDefault("SHOW_JWS2_SERVICES", true))
      {
        @Override
        public void internalFrameClosed(InternalFrameEvent e) {
          System.out.println("deregistering discoverer listener");
-         SlivkaWSDiscoverer.getInstance().removeServiceChangeListener(AlignFrame.this);
+         SlivkaWSDiscoverer.getInstance().removeServicesChangeListener(slivkaServiceChangeListener);
          Jws2Discoverer.getInstance().removeServiceChangeListener(AlignFrame.this);
          Desktop.getInstance().removeJalviewPropertyChangeListener("services", legacyListener);
          closeMenuItem_actionPerformed(true);
     * Configure menu items that vary according to whether the alignment is
     * nucleotide or protein
     */
 -
    public void setGUINucleotide()
    {
      AlignmentI al = getViewport().getAlignment();
     * operation that affects the data in the current view (selection changed,
     * etc) to update the menus to reflect the new state.
     */
 -
    @Override
    public void setMenusForViewport()
    {
     * @param av
     *          AlignViewport
     */
 -
    public void setMenusFromViewport(AlignViewport av)
    {
      padGapsMenuitem.setSelected(av.isPadGaps());
     * 
     * @param b
     */
 -
    public void setGroovyEnabled(boolean b)
    {
      runGroovy.setEnabled(b);
     * 
     * @see jalview.gui.IProgressIndicator#setProgressBar(java.lang.String, long)
     */
 -
    @Override
    public void setProgressBar(String message, long id)
    {
    }
    
    @Override
+   public void addProgressBar(long id, String message)
+   {
+     progressBar.addProgressBar(id, message);
+   }
+   @Override
    public void removeProgressBar(long id)
    {
      progressBar.removeProgressBar(id);
     * 
     * @return true if any progress bars are still active
     */
 -
    @Override
    public boolean operationInProgress()
    {
     * will cause the status bar to be hidden, with possibly undesirable flicker
     * of the screen layout.
     */
 -
    @Override
    public void setStatus(String text)
    {
    /*
     * Added so Castor Mapping file can obtain Jalview Version
     */
 -
    public String getVersion()
    {
 -    return jalview.bin.Cache.getProperty("VERSION");
 +    return Cache.getProperty("VERSION");
    }
  
    public FeatureRenderer getFeatureRenderer()
        Desktop.getInstance().closeAssociatedWindows();
  
        FileLoader loader = new FileLoader();
 -//      DataSourceType protocol = fileName.startsWith("http:")
 -//              ? DataSourceType.URL
 -//              : DataSourceType.FILE;
 -        loader.LoadFile(viewport, (fileObject == null ? fileName : fileObject), protocol, currentFileFormat);
 +      loader.LoadFile(viewport, (fileObject == null ? fileName : fileObject), protocol, currentFileFormat);
      }
      else
      {
          final FeatureSettings nfs = newframe.featureSettings;
          SwingUtilities.invokeLater(new Runnable()
          {
 -
            @Override
            public void run()
            {
        }
        this.closeMenuItem_actionPerformed(true);
      }
 -
    }
  
    @Override
    public void save_actionPerformed(ActionEvent e)
    {
      if (fileName == null || (currentFileFormat == null)
 -            || fileName.startsWith("http"))
 +            || HttpUtils.startsWithHttpOrHttps(fileName))
      {
        saveAs_actionPerformed();
      }
     * 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()
    {
     *
     * @return true if last call to saveAlignment(file, format) was successful.
     */
 -
    public boolean isSaveAlignmentSuccessful()
    {
  
      if (!lastSaveSuccessful)
      {
 -      JvOptionPane.showInternalMessageDialog(this, MessageManager
 -              .formatMessage("label.couldnt_save_file", new Object[]
 -              { lastFilenameSaved }),
 -              MessageManager.getString("label.error_saving_file"),
 -              JvOptionPane.WARNING_MESSAGE);
 +      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
      {
     * @param file
     * @param format
     */
 -
    public void saveAlignment(String file, FileFormatI format)
    {
      lastSaveSuccessful = true;
      AlignExportSettingsI options = new AlignExportSettingsAdapter(false);
      Runnable cancelAction = new Runnable()
      {
 -
        @Override
        public void run()
        {
      };
      Runnable outputAction = new Runnable()
      {
 -
        @Override
        public void run()
        {
          {
            // create backupfiles object and get new temp filename destination
            boolean doBackup = BackupFiles.getEnabled();
 -          BackupFiles backupfiles = doBackup ? new BackupFiles(file) : null;
 +          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 (backupfiles != null)
 +            {
 +              Console.trace("ALIGNFRAME about to write to temp file "
 +                      + backupfiles.getTempFilePath());
 +            }
 +
              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;
 -            ex.printStackTrace();
 +            Console.error(
 +                    "ALIGNFRAME Something unexpected happened writing the temp file");
 +            Console.error(ex.getMessage());
 +            Console.debug(Cache.getStackTraceString(ex));
            }
  
            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");
            }
          }
        }
     * 
     * @param fileFormatName
     */
 -
    @Override
    protected void outputText_actionPerformed(String fileFormatName)
    {
      AlignExportSettingsI options = new AlignExportSettingsAdapter(false);
      Runnable outputAction = new Runnable()
      {
 -
        @Override
        public void run()
        {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    protected void htmlMenuItem_actionPerformed(ActionEvent e)
    {
    }
  
    // ??
 -
    public void createImageMap(File file, String image)
    {
      alignPanel.makePNGImageMap(file, image);
     * 
     * @param f
     */
 -
    @Override
    public void createPNG(File f)
    {
     * 
     * @param f
     */
 -
    @Override
    public void createEPS(File f)
    {
     * 
     * @param f
     */
 -
    @Override
    public void createSVG(File f)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void printMenuItem_actionPerformed(ActionEvent e)
    {
            throws IOException, InterruptedException
    {
      final JalviewFileChooser chooser = new JalviewFileChooser(
 -            jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
 +            Cache.getProperty("LAST_DIRECTORY"));
      chooser.setFileView(new JalviewFileView());
      String tooltip = MessageManager
              .getString("label.load_jalview_annotations");
      chooser.setToolTipText(tooltip);
      chooser.setResponseHandler(0, new Runnable()
      {
 -
        @Override
        public void run()
        {
          String choice = chooser.getSelectedFile().getPath();
 -        jalview.bin.Cache.setProperty("LAST_DIRECTORY", choice);
 +        Cache.setProperty("LAST_DIRECTORY", choice);
          loadJalviewDataFile(chooser.getSelectedFile(), null, null, null);
        }
      });
     * 
     * @param closeAllTabs
     */
 -
    @Override
    public void closeMenuItem_actionPerformed(boolean closeAllTabs)
    {
     * 
     * @param panelToClose
     */
 -
    public void closeView(AlignmentPanel panelToClose)
    {
      int index = tabbedPane.getSelectedIndex();
    /**
     * DOCUMENT ME!
     */
 -
    void updateEditMenuBar()
    {
  
     * 
     * @return alignment objects for all views
     */
 -
    AlignmentI[] getViewAlignments()
    {
      if (alignPanels != null)
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    protected void undoMenuItem_actionPerformed(ActionEvent e)
    {
      {
        if (originalSource != viewport)
        {
 -        Cache.log.warn(
 +        Console.warn(
                  "Implementation worry: mismatch of viewport origin for undo");
        }
        originalSource.updateHiddenColumns();
        // viewport.getColumnSelection()
        // .getHiddenColumns().size() > 0);
        originalSource.notifyAlignment();
 -
      }
    }
  
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    protected void redoMenuItem_actionPerformed(ActionEvent e)
    {
  
        if (originalSource != viewport)
        {
 -        Cache.log.warn(
 +        Console.warn(
                  "Implementation worry: mismatch of viewport origin for redo");
        }
        originalSource.updateHiddenColumns();
        // viewport.getColumnSelection()
        // .getHiddenColumns().size() > 0);
        originalSource.notifyAlignment();
 -
      }
    }
  
    }
  
    /**
 -   * 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)
    {
      SequenceGroup sg = viewport.getSelectionGroup();
  
      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);
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    protected void copy_actionPerformed()
    {
                .setContents(new StringSelection(""), null);
  
        Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss,
 -              Desktop.getInstance());
 +              d);
      } catch (OutOfMemoryError er)
      {
        new OOMWarning("copying region", er);
     * @throws InterruptedException
     * @throws IOException
     */
 -
    @Override
    protected void pasteNew_actionPerformed(ActionEvent e)
            throws IOException, InterruptedException
     * @throws InterruptedException
     * @throws IOException
     */
 -
    @Override
    protected void pasteThis_actionPerformed(ActionEvent e)
            throws IOException, InterruptedException
        System.out.println("Exception whilst pasting: " + ex);
        // could be anything being pasted in here
      }
 +
    }
  
    @Override
        AlignFrame af = new AlignFrame(alignment, DEFAULT_WIDTH,
                DEFAULT_HEIGHT);
        String newtitle = new String("Flanking alignment");
 +
        Desktop d = Desktop.getInstance();
        if (d.jalviewClipboard != null && d.jalviewClipboard[2] != null)
        {
    /**
     * Action Cut (delete and copy) the selected region
     */
 -
    @Override
    protected void cut_actionPerformed()
    {
    /**
     * Performs menu option to Delete the currently selected region
     */
 -
    @Override
    protected void delete_actionPerformed()
    {
  
      Runnable okAction = new Runnable()
      {
 -
        @Override
        public void run()
        {
          viewport.getAlignment().deleteGroup(sg);
  
          viewport.notifyAlignment();
 -
          if (viewport.getAlignment().getHeight() < 1)
          {
            try
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    protected void deleteGroups_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void selectAllSequenceMenuItem_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void deselectAllSequenceMenuItem_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void invertSequenceMenuItem_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void remove2LeftMenuItem_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void remove2RightMenuItem_actionPerformed(ActionEvent e)
    {
        }
  
        viewport.notifyAlignment();
 -
      }
    }
  
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void removeGappedColumnMenuItem_actionPerformed(ActionEvent e)
    {
      ranges.setStartRes(seq.findIndex(startRes) - 1);
      viewport.notifyAlignment();
  
 -
    }
  
    /**
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void removeAllGapsMenuItem_actionPerformed(ActionEvent e)
    {
              viewport.getAlignment()));
  
      viewport.getRanges().setStartRes(seq.findIndex(startRes) - 1);
 +
      viewport.notifyAlignment();
  
    }
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void padGapsMenuitem_actionPerformed(ActionEvent e)
    {
      viewport.setPadGaps(padGapsMenuitem.isSelected());
      viewport.notifyAlignment();
    }
  
    /**
 -   * 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 view of the current alignment.
     */
 -
    @Override
    public void newView_actionPerformed(ActionEvent e)
    {
     *          if true then duplicate all annnotation, groups and settings
     * @return new alignment panel, already displayed.
     */
 -
    public AlignmentPanel newView(String viewTitle, boolean copyAnnotation)
    {
      /*
     * @param viewTitle
     * @return
     */
 -
    protected String getNewViewName(String viewTitle)
    {
      int index = Desktop.getViewCount(viewport.getSequenceSetId());
     * @param comps
     * @return
     */
 -
    protected List<String> getExistingViewNames(List<Component> comps)
    {
      List<String> existingNames = new ArrayList<>();
    /**
     * Explode tabbed views into separate windows.
     */
 -
    @Override
    public void expandViews_actionPerformed(ActionEvent e)
    {
    /**
     * Gather views in separate windows back into a tabbed presentation.
     */
 -
    @Override
    public void gatherViews_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void font_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    protected void seqLimit_actionPerformed(ActionEvent e)
    {
     * 
     * @see jalview.jbgui.GAlignFrame#followHighlight_actionPerformed()
     */
 -
    @Override
    protected void followHighlight_actionPerformed()
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    protected void colourTextMenuItem_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void wrapMenuItem_actionPerformed(ActionEvent e)
    {
     * @param toggleSeqs
     * @param toggleCols
     */
 -
    protected void toggleHiddenRegions(boolean toggleSeqs, boolean toggleCols)
    {
  
     * jalview.jbgui.GAlignFrame#hideAllButSelection_actionPerformed(java.awt.
     * event.ActionEvent)
     */
 -
    @Override
    public void hideAllButSelection_actionPerformed(ActionEvent e)
    {
     * jalview.jbgui.GAlignFrame#hideAllSelection_actionPerformed(java.awt.event
     * .ActionEvent)
     */
 -
    @Override
    public void hideAllSelection_actionPerformed(ActionEvent e)
    {
     * jalview.jbgui.GAlignFrame#showAllhidden_actionPerformed(java.awt.event.
     * ActionEvent)
     */
 -
    @Override
    public void showAllhidden_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    protected void scaleAbove_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    protected void scaleLeft_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    protected void scaleRight_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void viewBoxesMenuItem_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void viewTextMenuItem_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    protected void renderGapsMenuItem_actionPerformed(ActionEvent e)
    {
     * @param evt
     *          DOCUMENT ME!
     */
 -
    @Override
    public void showSeqFeatures_actionPerformed(ActionEvent evt)
    {
     * 
     * @param e
     */
 -
    @Override
    public void annotationPanelMenuItem_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void overviewMenuItem_actionPerformed(ActionEvent e)
    {
      }
  
      JInternalFrame frame = new JInternalFrame();
 -
      // BH 2019.07.26 we allow for an embedded
      // undecorated overview with defined size
      frame.setName(Platform.getAppID("overview"));
        dim = null; // hidden, not embedded
      }
      OverviewPanel overview = new OverviewPanel(alignPanel, dim);
 -
      frame.setContentPane(overview);
      if (dim == null)
      {
      frame.addInternalFrameListener(
              new javax.swing.event.InternalFrameAdapter()
              {
 -
                @Override
                public void internalFrameClosed(
                        javax.swing.event.InternalFrameEvent evt)
     * CovariationColourScheme(viewport.getAlignment().getAlignmentAnnotation
     * ()[0])); }
     */
 -
    @Override
    public void annotationColour_actionPerformed()
    {
     * 
     * @param selected
     */
 -
    @Override
    public void applyToAllGroups_actionPerformed(boolean selected)
    {
     * @param name
     *          the name (not the menu item label!) of the colour scheme
     */
 -
    @Override
    public void changeColour_actionPerformed(String name)
    {
     * 
     * @param cs
     */
 -
    @Override
    public void changeColour(ColourSchemeI cs)
    {
    /**
     * Show the PID threshold slider panel
     */
 -
    @Override
    protected void modifyPID_actionPerformed()
    {
    /**
     * Show the Conservation slider panel
     */
 -
    @Override
    protected void modifyConservation_actionPerformed()
    {
    /**
     * Action on selecting or deselecting (Colour) By Conservation
     */
 -
    @Override
    public void conservationMenuItem_actionPerformed(boolean selected)
    {
    /**
     * Action on selecting or deselecting (Colour) Above PID Threshold
     */
 -
    @Override
    public void abovePIDThreshold_actionPerformed(boolean selected)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void sortPairwiseMenuItem_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void sortIDMenuItem_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void sortLengthMenuItem_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void sortGroupMenuItem_actionPerformed(ActionEvent e)
    {
      alignPanel.paintAlignment(true, false);
  
    }
 -
    /**
     * DOCUMENT ME!
     * 
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void removeRedundancyMenuItem_actionPerformed(ActionEvent e)
    {
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    public void pairwiseAlignmentMenuItem_actionPerformed(ActionEvent e)
    {
      viewport.setAutoCalculateConsensusAndConservation(
              autoCalculate.isSelected());
      if (viewport.getAutoCalculateConsensusAndConservation())
 -    // ??
 -    // viewport.autoCalculateConsensus = autoCalculate.isSelected();
 -    // if (viewport.autoCalculateConsensus)
      {
        viewport.notifyAlignment();
      }
     * @param options
     *          parameters for the distance or similarity calculation
     */
 -
    void newTreePanel(String type, String modelName,
            SimilarityParamsI options)
    {
     * @param order
     *          DOCUMENT ME!
     */
 -
    public void addSortByOrderMenuItem(String title,
            final AlignmentOrder order)
    {
      sort.add(item);
      item.addActionListener(new java.awt.event.ActionListener()
      {
 -
        @Override
        public void actionPerformed(ActionEvent e)
        {
     *          the label used to retrieve scores for each sequence on the
     *          alignment
     */
 -
    public void addSortByAnnotScoreMenuItem(JMenu sort,
            final String scoreLabel)
    {
      sort.add(item);
      item.addActionListener(new java.awt.event.ActionListener()
      {
 -
        @Override
        public void actionPerformed(ActionEvent e)
        {
     * rebuilding in subsequence calls.
     * 
     */
 -
    @Override
    public void buildSortByAnnotationScoresMenu()
    {
     * closed, and adjust the tree leaf to sequence mapping when the alignment is
     * modified.
     */
 -
    @Override
    public void buildTreeSortMenu()
    {
        final JMenuItem item = new JMenuItem(tp.getTitle());
        item.addActionListener(new java.awt.event.ActionListener()
        {
 -
          @Override
          public void actionPerformed(ActionEvent e)
          {
      }
      return treePanels;
    }
 -
    public boolean sortBy(AlignmentOrder alorder, String undoname)
    {
      SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
     * be submitted for multiple alignment.
     * 
     */
 -
    public jalview.datamodel.AlignmentView gatherSequencesForAlignment()
    {
      // Now, check we have enough sequences
     * region or the whole alignment. (where the first sequence in the set is the
     * one that the prediction will be for).
     */
 -
    public AlignmentView gatherSeqOrMsaForSecStrPrediction()
    {
      AlignmentView seqs = null;
     * @param e
     *          DOCUMENT ME!
     */
 -
    @Override
    protected void loadTreeMenuItem_actionPerformed(ActionEvent e)
    {
      // 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_newick_like_tree_file"));
  
      chooser.setResponseHandler(0, new Runnable()
      {
 -
        @Override
        public void run()
        {
     *          position
     * @return TreePanel handle
     */
 -
    public TreePanel showNewickTree(NewickFile nf, String treeTitle,
            AlignmentView input, int w, int h, int x, int y)
    {
        if (nf.getTree() != null)
        {
          tp = new TreePanel(alignPanel, nf, treeTitle, input);
 +
          Dimension dim = Platform.getDimIfEmbedded(tp, -1, -1);
          if (dim == null)
          {
      return tp;
    }
  
+   private WebServicesMenuManager slivkaMenu = new WebServicesMenuManager("slivka", this);
  
    /**
     * Schedule the web services menu rebuild to the event dispatch thread.
    public void buildWebServicesMenu()
    {
      SwingUtilities.invokeLater(() -> {
 -      Cache.log.info("Rebuiling WS menu");
 +      Console.info("Rebuiling WS menu");
        webService.removeAll();
        if (Cache.getDefault("SHOW_SLIVKA_SERVICES", true))
        {
 -        Cache.log.info("Building web service menu for slivka");
 +        Console.info("Building web service menu for slivka");
          SlivkaWSDiscoverer discoverer = SlivkaWSDiscoverer.getInstance();
-         JMenu submenu = new JMenu("Slivka");
-         buildWebServicesMenu(discoverer, submenu);
-         webService.add(submenu);
+         slivkaMenu.setServices(discoverer);
+         slivkaMenu.setInProgress(discoverer.isRunning());
+         slivkaMenu.setNoServices(discoverer.isDone() && !discoverer.hasServices());
+         webService.add(slivkaMenu.getMenu());
        }
        if (Cache.getDefault("SHOW_JWS2_SERVICES", true))
        {
          buildWebServicesMenu(jws2servs, submenu);
          webService.add(submenu);
        }
+       build_urlServiceMenu(webService);
        build_fetchdbmenu(webService);
      });
    }
     * 
     * @param webService
     */
 -
    protected void build_urlServiceMenu(JMenu webService)
    {
      // TODO: remove this code when 2.7 is released
     * 
     * @return true if Show Cross-references menu should be enabled
     */
 -
    public boolean canShowProducts()
    {
      SequenceI[] seqs = viewport.getAlignment().getSequencesArray();
          JMenuItem xtype = new JMenuItem(source);
          xtype.addActionListener(new ActionListener()
          {
 -
            @Override
            public void actionPerformed(ActionEvent e)
            {
        showProducts.setEnabled(showp);
      } catch (Exception e)
      {
 -      Cache.log.warn(
 +      Console.warn(
                "canShowProducts threw an exception - please report to help@jalview.org",
                e);
        return false;
     * @param source
     *          the database to show cross-references for
     */
 -
    protected void showProductsFor(final SequenceI[] sel, final boolean _odna,
            final String source)
    {
     * Construct and display a new frame containing the translation of this
     * frame's DNA sequences to their aligned protein (amino acid) equivalents.
     */
 -
    @Override
    public void showTranslation_actionPerformed(GeneticCodeI codeTable)
    {
        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
     * 
     * @param format
     */
 -
    public void setFileFormat(FileFormatI format)
    {
      this.currentFileFormat = format;
     *          access mode of file (see jalview.io.AlignFile)
     * @return true if features file was parsed correctly.
     */
 -
    public boolean parseFeaturesFile(Object file, DataSourceType sourceType)
    {
      // BH 2018
                          + " with " + toassoc.getDisplayId(true));
                  assocfiles++;
                }
 +
              }
              // TODO: do we need to update overview ? only if features are
              // shown I guess
     * @throws InterruptedException
     * @throws IOException
     */
 -
    public void loadJalviewDataFile(Object file, DataSourceType sourceType,
            FileFormatI format, SequenceI assocSeq)
    {
            }
          } catch (Exception x)
          {
 -          Cache.log.debug(
 +          Console.debug(
                    "Exception when processing data source as T-COFFEE score file",
                    x);
            tcf = null;
        }
        if (isAnnotation)
        {
 +
          updateForAnnotations();
        }
      } catch (Exception ex)
     * Method invoked by the ChangeListener on the tabbed pane, in other words
     * when a different tabbed pane is selected by the user or programmatically.
     */
 -
    @Override
    public void tabSelectionChanged(int index)
    {
    /**
     * On right mouse click on view tab, prompt for and set new view name.
     */
 -
    @Override
    public void tabbedPane_mousePressed(MouseEvent e)
    {
    /**
     * Open the dialog for regex description parsing.
     */
 -
    @Override
    protected void extractScores_actionPerformed(ActionEvent e)
    {
     * jalview.jbgui.GAlignFrame#showDbRefs_actionPerformed(java.awt.event.ActionEvent
     * )
     */
 -
    @Override
    protected void showDbRefs_actionPerformed(ActionEvent e)
    {
     * @seejalview.jbgui.GAlignFrame#showNpFeats_actionPerformed(java.awt.event.
     * ActionEvent)
     */
 -
    @Override
    protected void showNpFeats_actionPerformed(ActionEvent e)
    {
     * 
     * @param av
     */
 -
    public boolean closeView(AlignViewportI av)
    {
      if (viewport == av)
              Cache.getDefault(DBRefFetcher.TRIM_RETRIEVED_SEQUENCES, true));
      trimrs.addActionListener(new ActionListener()
      {
 -
        @Override
        public void actionPerformed(ActionEvent e)
        {
        {
          new Thread(new Runnable()
          {
 -
            @Override
            public void run()
            {
                      alignPanel.alignFrame.featureSettings, isNucleotide);
              dbRefFetcher.addListener(new FetchFinishedListenerI()
              {
 -
                @Override
                public void finished()
                {
      rfetch.add(fetchr);
      new Thread(new Runnable()
      {
 -
        @Override
        public void run()
        {
          // .getSequenceFetcherSingleton();
          javax.swing.SwingUtilities.invokeLater(new Runnable()
          {
 -
            @Override
            public void run()
            {
                          dbRefFetcher
                                  .addListener(new FetchFinishedListenerI()
                                  {
 -
                                    @Override
                                    public void finished()
                                    {
                          { src.getDbSource() }));
                  fetchr.addActionListener(new ActionListener()
                  {
 -
                    @Override
                    public void actionPerformed(ActionEvent e)
                    {
                          dbRefFetcher
                                  .addListener(new FetchFinishedListenerI()
                                  {
 -
                                    @Override
                                    public void finished()
                                    {
                            dbRefFetcher
                                    .addListener(new FetchFinishedListenerI()
                                    {
 -
                                      @Override
                                      public void finished()
                                      {
    /**
     * Left justify the whole alignment.
     */
 -
    @Override
    protected void justifyLeftMenuItem_actionPerformed(ActionEvent e)
    {
    /**
     * Right justify the whole alignment.
     */
 -
    @Override
    protected void justifyRightMenuItem_actionPerformed(ActionEvent e)
    {
     * jalview.jbgui.GAlignFrame#showUnconservedMenuItem_actionPerformed(java.
     * awt.event.ActionEvent)
     */
 -
    @Override
    protected void showUnconservedMenuItem_actionPerformed(ActionEvent e)
    {
     * jalview.jbgui.GAlignFrame#showGroupConsensus_actionPerformed(java.awt.event
     * .ActionEvent)
     */
 -
    @Override
    protected void showGroupConsensus_actionPerformed(ActionEvent e)
    {
     * jalview.jbgui.GAlignFrame#showGroupConservation_actionPerformed(java.awt
     * .event.ActionEvent)
     */
 -
    @Override
    protected void showGroupConservation_actionPerformed(ActionEvent e)
    {
     * jalview.jbgui.GAlignFrame#showConsensusHistogram_actionPerformed(java.awt
     * .event.ActionEvent)
     */
 -
    @Override
    protected void showConsensusHistogram_actionPerformed(ActionEvent e)
    {
     * jalview.jbgui.GAlignFrame#showConsensusProfile_actionPerformed(java.awt
     * .event.ActionEvent)
     */
 -
    @Override
    protected void showSequenceLogo_actionPerformed(ActionEvent e)
    {
     * jalview.jbgui.GAlignFrame#makeGrpsFromSelection_actionPerformed(java.awt
     * .event.ActionEvent)
     */
 -
    @Override
    protected void makeGrpsFromSelection_actionPerformed(ActionEvent e)
    {
     * 
     * @param alignmentPanel
     */
 -
    public void setDisplayedView(AlignmentPanel alignmentPanel)
    {
      if (!viewport.getSequenceSetId()
     * @param forAlignment
     *          update non-sequence-related annotations
     */
 -
    @Override
    protected void setAnnotationsVisibility(boolean visible,
            boolean forSequences, boolean forAlignment)
    /**
     * Store selected annotation sort order for the view and repaint.
     */
 -
    @Override
    protected void sortAnnotations_actionPerformed()
    {
     * 
     * @return alignment panels in this alignment frame
     */
 -
    public List<? extends AlignmentViewPanel> getAlignPanels()
    {
      // alignPanels is never null
     * Open a new alignment window, with the cDNA associated with this (protein)
     * alignment, aligned as is the protein.
     */
 -
    protected void viewAsCdna_actionPerformed()
    {
      // TODO no longer a menu action - refactor as required
     * 
     * @param show
     */
 -
    @Override
    protected void showComplement_actionPerformed(boolean show)
    {
     * Generate the reverse (optionally complemented) of the selected sequences,
     * and add them to the alignment
     */
 -
    @Override
    protected void showReverse_actionPerformed(boolean complement)
    {
     * AlignFrame is set as currentAlignFrame in Desktop, to allow the script to
     * be targeted at this alignment.
     */
 -
    @Override
    protected void runGroovy_actionPerformed()
    {
     * @param columnsContaining
     * @return
     */
 -
    public boolean hideFeatureColumns(String featureType,
            boolean columnsContaining)
    {
     * Rebuilds the Colour menu, including any user-defined colours which have
     * been loaded either on startup or during the session
     */
 -
    public void buildColourMenu()
    {
      colourMenu.removeAll();
     * Open a dialog (if not already open) that allows the user to select and
     * calculate PCA or Tree analysis
     */
 -
    protected void openTreePcaDialog()
    {
      if (alignPanel.getCalculationDialog() == null)
    {
      hmmerMenu.setEnabled(HmmerCommand.isHmmerAvailable());
    }
 -
    @Override
    protected void loadVcf_actionPerformed()
    {
      final AlignFrame us = this;
      chooser.setResponseHandler(0, new Runnable()
      {
 -
        @Override
        public void run()
        {
      }
    }
  }
 -
   */
  package jalview.gui;
  
 +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;
@@@ -49,16 -47,13 +49,16 @@@ 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.File;
  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;
@@@ -119,7 -114,6 +119,7 @@@ import jalview.io.FormatAdapter
  import jalview.io.IdentifyFile;
  import jalview.io.JalviewFileChooser;
  import jalview.io.JalviewFileView;
 +import jalview.jbgui.APQHandlers;
  import jalview.jbgui.GDesktop;
  import jalview.jbgui.GSplitFrame;
  import jalview.jbgui.GStructureViewer;
@@@ -127,9 -121,7 +127,9 @@@ 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.LaunchUtils;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
  import jalview.util.UrlConstants;
@@@ -151,29 -143,12 +151,29 @@@ public class Desktop extends GDeskto
          StructureSelectionManagerProvider, ApplicationSingletonI
  
  {
 -  private static final String CITATION = "<br><br>Development managed by The Barton Group, University of Dundee, Scotland, UK.<br>"
 -          + "<br><br>For help, see the FAQ at <a href=\"http://www.jalview.org/faq\">www.jalview.org/faq</a> and/or join the jalview-discuss@jalview.org mailing list"
 -          + "<br><br>If  you use Jalview, please cite:"
 -          + "<br>Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)"
 -          + "<br>Jalview Version 2 - a multiple sequence alignment editor and analysis workbench"
 -          + "<br>Bioinformatics doi: 10.1093/bioinformatics/btp033";
 +  private static final String CITATION;
 +  static {
 +    URL bg_logo_url = ChannelProperties.getImageURL("bg_logo." + String.valueOf(SplashScreen.logoSize));
 +    URL uod_logo_url = ChannelProperties.getImageURL("uod_banner." + String.valueOf(SplashScreen.logoSize));
 +    boolean logo = (bg_logo_url != null || uod_logo_url != null);
 +    StringBuilder sb = new StringBuilder();
 +    sb.append(
 +            "<br><br>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 final String EXPERIMENTAL_FEATURES = "EXPERIMENTAL_FEATURES";
  
 -  protected static final String CONFIRM_KEYBOARD_QUIT = "CONFIRM_KEYBOARD_QUIT";
 +  public static final String CONFIRM_KEYBOARD_QUIT = "CONFIRM_KEYBOARD_QUIT";
  
    public static HashMap<String, FileWriter> savingFiles = new HashMap<String, FileWriter>();
  
    @SuppressWarnings("deprecation")
    private JalviewChangeSupport changeSupport = new JalviewChangeSupport();
  
 +  public static boolean nosplash = false;
 +
    /**
     * news reader - null if it was never started.
     */
     */
    private Desktop()
    {
 -    Cache.initLogger();
 +    super();
      try
      {
        /**
         */
  
        doConfigureStructurePrefs();
 -      setTitle("Jalview " + Cache.getProperty("VERSION"));
 -      /*
 -      if (!Platform.isAMac())
 -      {
 -      // this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
 -      }
 -      else
 +    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())
 +    {
 +      if (LaunchUtils.getJavaVersion() >= 11)
        {
 -       this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
 +        jalview.bin.Console.info(
 +                "Linux platform only! You may have the following warning next: \"WARNING: An illegal reflective access operation has occurred\"\nThis is expected and cannot be avoided, sorry about that.");
        }
 -      */
 -
        try
        {
 -        APQHandlers.setAPQHandlers(this);
 -      } catch (Throwable t)
 -      {
 -        System.out.println("Error setting APQHandlers: " + t.toString());
 -        // t.printStackTrace();
 -      }
 +        Toolkit xToolkit = Toolkit.getDefaultToolkit();
 +        Field[] declaredFields = xToolkit.getClass().getDeclaredFields();
 +        Field awtAppClassNameField = null;
  
 -      addWindowListener(new WindowAdapter()
 -      {
 +        if (Arrays.stream(declaredFields).anyMatch(f -> f.getName().equals("awtAppClassName"))) {
 +          awtAppClassNameField = xToolkit.getClass().getDeclaredField("awtAppClassName");
 +        }
  
 -        @Override
 -        public void windowClosing(WindowEvent ev)
 +        String title = ChannelProperties.getProperty("app_name");
 +        if (awtAppClassNameField != null) {
 +          awtAppClassNameField.setAccessible(true);
 +          awtAppClassNameField.set(xToolkit, title);
 +        }
 +        else
          {
 -          quit();
 +          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);
  
        getContentPane().add(desktopPane, BorderLayout.CENTER);
        desktopPane.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
  
 +
        // This line prevents Windows Look&Feel resizing all new windows to
        // maximum
        // if previous window was maximised
          setBounds(xPos, yPos, 900, 650);
        }
  
 -      getIdentifiersOrgData();
 -
        if (!Platform.isJS())
        /**
         * Java only
          jconsole.setHeader(Cache.getVersionDetailsForConsole());
          showConsole(showjconsole);
  
 -        showNews.setVisible(false);
 +        showNews.setVisible(false); // not sure if we should only do this for interactive session?
  
          experimentalFeatures.setSelected(showExperimental());
  
 +        getIdentifiersOrgData();
 +
          if (Jalview.isInteractive())
          {
            // disabled for SeqCanvasTest
            checkURLLinks();
  
            // Spawn a thread that shows the splashscreen
 -
 +          if (!nosplash) {
            SwingUtilities.invokeLater(new Runnable()
 -          {
 -            @Override
 -            public void run()
 -            {
 -              new SplashScreen(true);
 -            }
 -          });
 +           {
 +             @Override
 +             public void run()
 +             {
 +               new SplashScreen(true);
 +             }
 +           });
 +          }
  
            // Thread off a new instance of the file chooser - this reduces the
            // time
              @Override
              public void run()
              {
 -              Cache.log.debug("Filechooser init thread started.");
 +              jalview.bin.Console.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.");
 +              jalview.bin.Console.debug("Filechooser init thread finished.");
              }
            }).start();
            // Add the service change listener
                      @Override
                      public void propertyChange(PropertyChangeEvent evt)
                      {
 -                      Cache.log.debug("Firing service changed event for "
 +                      jalview.bin.Console.debug("Firing service changed event for "
                                + evt.getNewValue());
                        JalviewServicesChanged(evt);
                      }
                    });
          }
        }
 +
        this.setDropTarget(new java.awt.dnd.DropTarget(desktopPane, this));
  
        this.addWindowListener(new WindowAdapter()
              showPasteMenu(evt.getX(), evt.getY());
            }
          }
 -
          @Override
          public void mouseReleased(MouseEvent evt)
          {
      {
        t.printStackTrace();
      }
 -
    }
  
    /**
    public void doConfigureStructurePrefs()
    {
      // configure services
 -    StructureSelectionManager ssm = StructureSelectionManager
 -            .getStructureSelectionManager(this);
 -    if (Cache.getDefault(Preferences.ADD_SS_ANN, true))
 -    {
 -      ssm.setAddTempFacAnnot(
 -              Cache.getDefault(Preferences.ADD_TEMPFACT_ANN, true));
 -      ssm.setProcessSecondaryStructure(
 -              Cache.getDefault(Preferences.STRUCT_FROM_PDB, true));
 -      ssm.setSecStructServices(
 -              Cache.getDefault(Preferences.USE_RNAVIEW, true));
 -    }
 -    else
 -    {
 +    StructureSelectionManager ssm = StructureSelectionManager.getStructureSelectionManager(this);
 +    if (Cache.getDefault(Preferences.ADD_SS_ANN, true)) {
 +      ssm.setAddTempFacAnnot(Cache.getDefault(Preferences.ADD_TEMPFACT_ANN, true));
 +      ssm.setProcessSecondaryStructure(Cache.getDefault(Preferences.STRUCT_FROM_PDB, true));
 +      // JAL-3915 - RNAView is no longer an option so this has no effect
 +      ssm.setSecStructServices(Cache.getDefault(Preferences.USE_RNAVIEW, false));
 +    } else {
        ssm.setAddTempFacAnnot(false);
        ssm.setProcessSecondaryStructure(false);
        ssm.setSecStructServices(false);
      }
    }
  
 -  public void checkForNews()
 -  {
 +  public void checkForNews() {
      final Desktop me = this;
      // Thread off the news reader, in case there are connection problems.
 -    new Thread(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
 -    new Thread(new Runnable()
 -    {
 -      @Override
 -      public void run()
 -      {
 -        Cache.log.debug("Downloading data from identifiers.org");
 -        try
 -        {
 -          UrlDownloadClient.download(IdOrgSettings.getUrl(),
 -                  IdOrgSettings.getDownloadLocation());
 -        } catch (IOException e)
 +  public void getIdentifiersOrgData() {
 +    if (Cache.getProperty("NOIDENTIFIERSSERVICE") == null) {
 +      // Thread off the identifiers fetcher
 +      new Thread(new Runnable() {
 +        @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();
 -
 +      }).start();
 +    }
    }
  
    @Override
 -  protected void showNews_actionPerformed(ActionEvent e)
 -  {
 +  protected void showNews_actionPerformed(ActionEvent e) {
      showNews(showNews.isSelected());
    }
  
    void showNews(boolean visible)
    {
 -    Cache.log.debug((visible ? "Showing" : "Hiding") + " news.");
 +    jalview.bin.Console.debug((visible ? "Showing" : "Hiding") + " news.");
      showNews.setSelected(visible);
      if (visible && !jvnews.isVisible())
      {
      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 (Cache.getProperty("SCREENGEOMETRY_WIDTH") != null)
 -      {
 +    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 (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(Cache.getProperty("SCREENGEOMETRY_WIDTH"))));
 -        double sh = ((1f * screenSize.height) / (1f * Integer
 -                .parseInt(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)
          {
 -          Cache.log.debug(
 +          jalview.bin.Console.debug(
                    "Window geometry location recall error: shifting horizontal to within screenbounds.");
            ix -= screenSize.width;
          }
          while (iy >= screenSize.height)
          {
 -          Cache.log.debug(
 +          jalview.bin.Console.debug(
                    "Window geometry location recall error: shifting vertical to within screenbounds.");
            iy -= screenSize.height;
          }
 -        Cache.log.debug(
 +        jalview.bin.Console.debug(
                  "Got last known dimensions for " + windowName + ": x:" + ix
                          + " y:" + iy + " width:" + iw + " height:" + ih);
        }
      }
    }
  
 -//  /**
 -//   * Add an internal frame to the Jalview desktop that is allowed to be resized,
 -//   * has a minimum size of 300px and might or might not be visible
 -//   * 
 -//   * @param frame
 -//   *          Frame to show
 -//   * @param title
 -//   *          Visible Title
 -//   * @param makeVisible
 -//   *          When true, display frame immediately, otherwise, caller must call
 -//   *          setVisible themselves.
 -//   * @param w
 -//   *          width
 -//   * @param h
 -//   *          height
 -//   */
 -//  @Deprecated
 -//  public static synchronized void addInternalFrame(
 -//          final JInternalFrame frame, String title, boolean makeVisible,
 -//          int w, int h)
 -//  {
 -//    // textbox, web services, sequenceFetcher, featureSettings
 -//    getInstance().addFrame(frame, title, makeVisible, w, h,
 -//            FRAME_ALLOW_RESIZE, FRAME_SET_MIN_SIZE_300);
 -//  }
 -//
 -//  /**
 -//   * Add an internal frame to the Jalview desktop that is visible, has a minimum
 -//   * size of 300px, and may or may not be resizable
 -//   * 
 -//   * @param frame
 -//   *          Frame to show
 -//   * @param title
 -//   *          Visible Title
 -//   * @param w
 -//   *          width
 -//   * @param h
 -//   *          height
 -//   * @param resizable
 -//   *          Allow resize
 -//   */
 -//  @Deprecated
 -//  public static synchronized void addInternalFrame(
 -//          final JInternalFrame frame, String title, int w, int h,
 -//          boolean resizable)
 -//  {
 -//    // annotation, font, calculation, user-defined colors
 -//    getInstance().addFrame(frame, title, FRAME_MAKE_VISIBLE, w, h,
 -//            resizable, FRAME_SET_MIN_SIZE_300);
 -//  }
 +
  
    /**
     * Adds and opens the given frame to the desktop that is visible, allowed to
    {
      // 15 classes call this method directly.
      
 +
      // TODO: allow callers to determine X and Y position of frame (eg. via
      // bounds object).
      // TODO: consider fixing method to update entries in the window submenu with
    {
  
      openFrameCount++;
 +
      
      boolean isEmbedded = (Platform.getEmbeddedAttribute(frame, "id") != null);
      boolean hasEmbeddedSize = (Platform.getDimIfEmbedded(frame, -1, -1) != null);
      
      if (!ignoreMinSize)
      {
 +
        // Set default dimension for Alignment Frame window.
        // The Alignment Frame window could be added from a number of places,
        // hence,
      frame.setMaximizable(resizable);
      frame.setIconifiable(resizable);
      frame.setOpaque(Platform.isJS());
 +
      if (!isEmbedded && frame.getX() < 1 && frame.getY() < 1)
      {
        frame.setLocation(xOffset * openFrameCount,
          } catch (java.beans.PropertyVetoException ex)
          {
            // System.err.println(ex.toString());
 +
          }
        }
      });
      {
      } 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);
      }
    }
     */
    private static void setKeyBindings(JInternalFrame frame)
    {
 +    @SuppressWarnings("serial")
      final Action closeAction = new AbstractAction()
      {
        @Override
      /*
       * set up key bindings for Ctrl-W and Cmd-W, with the same (Close) action
       */
 -    KeyStroke ctrlWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
 -            InputEvent.CTRL_DOWN_MASK);
 -    KeyStroke cmdWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
 -            Platform.SHORTCUT_KEY_MASK);
 +    KeyStroke ctrlWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK);
 +    KeyStroke cmdWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W, Platform.SHORTCUT_KEY_MASK);
  
      InputMap inputMap = frame
              .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
       * JS: (pending JAL-3038) a plain text field
       */
      JComponent history;
 -    String urlBase = "http://www.";
 +    String urlBase = "https://www.";
      if (Platform.isJS())
      {
        history = new JTextField(urlBase, 35);
        public void run()
        {
          @SuppressWarnings("unchecked")
 -        String url = (history instanceof JTextField
 -                ? ((JTextField) history).getText()
 -                : ((JComboBox<String>) history).getSelectedItem()
 -                        .toString());
 -
 -        if (url.toLowerCase().endsWith(".jar"))
 -        {
 -          if (viewport != null)
 -          {
 -            new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
 -                    FileFormat.Jalview);
 -          }
 -          else
 -          {
 -            new FileLoader().LoadFile(url, DataSourceType.URL,
 -                    FileFormat.Jalview);
 +        String url = (history instanceof JTextField ? ((JTextField) history).getText()
 +            : ((JComboBox<String>) history).getEditor().getItem().toString().trim());
 +
 +        if (url.toLowerCase(Locale.ROOT).endsWith(".jar")) {
 +          if (viewport != null) {
 +            new FileLoader().LoadFile(viewport, url, DataSourceType.URL, FileFormat.Jalview);
 +          } else {
 +            new FileLoader().LoadFile(url, DataSourceType.URL, FileFormat.Jalview);
            }
 -        }
 -        else
 -        {
 +        } else {
            FileFormatI format = null;
            try
            {
  
    private void storeLastKnownDimensions(String string, Rectangle jc)
    {
 -    Cache.log.debug("Storing last known dimensions for " + string + ": x:"
 -            + jc.x + " y:" + jc.y + " width:" + jc.width + " height:"
 -            + jc.height);
 +    jalview.bin.Console.debug("Storing last known dimensions for " + string
 +            + ": x:" + jc.x + " y:" + jc.y + " width:" + jc.width
 +            + " height:" + jc.height);
  
      Cache.setProperty(string + "SCREEN_X", jc.x + "");
      Cache.setProperty(string + "SCREEN_Y", jc.y + "");
    public String getAboutMessage()
    {
      StringBuilder message = new StringBuilder(1024);
 -    message.append("<h1><strong>Version: ")
 -            .append(Cache.getProperty("VERSION")).append("</strong></h1>")
 -            .append("<strong>Built: <em>")
 -            .append(Cache.getDefault("BUILD_DATE", "unknown"))
 -            .append("</em> from ").append(Cache.getBuildDetailsForSplash())
 -            .append("</strong>");
 +    message.append("<div style=\"font-family: sans-serif;\">").append("<h1><strong>Version: ")
 +        .append(Cache.getProperty("VERSION")).append("</strong></h1>").append("<strong>Built: <em>")
 +        .append(Cache.getDefault("BUILD_DATE", "unknown")).append("</em> from ")
 +        .append(Cache.getBuildDetailsForSplash()).append("</strong>");
  
      String latestVersion = Cache.getDefault("LATEST_VERSION", "Checking");
 -    if (latestVersion.equals("Checking"))
 -    {
 +    if (latestVersion.equals("Checking")) {
        // JBP removed this message for 2.11: May be reinstated in future version
        // message.append("<br>...Checking latest version...</br>");
 -    }
 -    else if (!latestVersion.equals(Cache.getProperty("VERSION")))
 -    {
 +    } else if (!latestVersion.equals(Cache.getProperty("VERSION"))) {
        boolean red = false;
 -      if (Cache.getProperty("VERSION").toLowerCase()
 -              .indexOf("automated build") == -1)
 -      {
 +      if (Cache.getProperty("VERSION").toLowerCase(Locale.ROOT).indexOf("automated build") == -1) {
          red = true;
          // Displayed when code version and jnlp version do not match and code
          // version is not a development build
          message.append("<div style=\"color: #FF0000;font-style: bold;\">");
        }
  
 -      message.append("<br>!! Version ")
 -              .append(Cache.getDefault("LATEST_VERSION", "..Checking.."))
 -              .append(" is available for download from ")
 -              .append(Cache.getDefault("www.jalview.org",
 -                      "http://www.jalview.org"))
 -              .append(" !!");
 -      if (red)
 -      {
 +      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(Cache.getDefault("AUTHORFNAMES", DEFAULT_AUTHORS));
      message.append(CITATION);
  
 +    message.append("</div>");
 +
      return message.toString();
    }
  
     * Action on requesting Help documentation
     */
    @Override
 -  public void documentationMenuItem_actionPerformed()
 -  {
 -    try
 -    {
 -      if (Platform.isJS())
 -      {
 -        BrowserLauncher.openURL("http://www.jalview.org/help.html");
 -      }
 -      else
 +  public void documentationMenuItem_actionPerformed() {
 +    try {
 +      if (Platform.isJS()) {
 +        BrowserLauncher.openURL("https://www.jalview.org/help.html");
 +      } else
        /**
         * Java only
         * 
        {
          Help.showHelpWindow();
        }
 -    } catch (Exception ex)
 -    {
 +    } catch (Exception ex) {
        System.err.println("Error opening help: " + ex.getMessage());
      }
    }
    protected void garbageCollect_actionPerformed(ActionEvent e)
    {
      // We simply collect the garbage
 -    Cache.log.debug("Collecting garbage...");
 +    jalview.bin.Console.debug("Collecting garbage...");
      System.gc();
 -    Cache.log.debug("Finished garbage collection.");
 +    jalview.bin.Console.debug("Finished garbage collection.");
    }
  
    /*
     *          DOCUMENT ME!
     */
    @Override
 -  protected void preferences_actionPerformed(ActionEvent e)
 -  {
 -    new Preferences();
 +  protected void preferences_actionPerformed(ActionEvent e) {
 +    Preferences.openPreferences();
    }
  
    /**
        }
      }
  
 -    if (approveSave || autoSave)
 -    {
 +    if (approveSave || autoSave) {
        final Desktop me = this;
        final java.io.File chosenFile = projectFile;
        new Thread(new Runnable()
                      + chosenFile.getName(), oom);
            } catch (Exception ex)
            {
 -            Cache.log.error("Problems whilst trying to save to "
 +            jalview.bin.Console.error("Problems whilst trying to save to "
                      + chosenFile.getName(), ex);
              JvOptionPane.showMessageDialog(me,
                      MessageManager.formatMessage(
                new OOMWarning("Whilst loading project from " + choice, oom);
              } catch (Exception ex)
              {
 -              Cache.log.error(
 +              jalview.bin.Console.error(
                        "Problems whilst loading project from " + choice, ex);
                JvOptionPane.showMessageDialog(getDesktopPane(),
                        MessageManager.formatMessage(
                        JvOptionPane.WARNING_MESSAGE);
              }
            }
 -        }).start();
 +        }, "Project Loader").start();
        }
      });
  
      {
        source.showFeatureSettingsUI();
      }
 +
    }
  
    public JInternalFrame[] getAllFrames()
                    10, getHeight() - fm.getHeight());
          }
        }
 +
 +      // output debug scale message. Important for jalview.bin.HiDPISettingTest2
 +      Desktop.debugScaleMessage(Desktop.getDesktopPane().getGraphics());
      }
    }
  
        openGroovyConsole();
      } catch (Exception ex)
      {
 -      Cache.log.error("Groovy Shell Creation failed.", ex);
 +      jalview.bin.Console.error("Groovy Shell Creation failed.", ex);
        JvOptionPane.showInternalMessageDialog(desktopPane,
  
                MessageManager.getString("label.couldnt_create_groovy_shell"),
    }
    
    @Override
+   public void addProgressBar(long id, String message)
+   {
+     // TODO
+     throw new UnsupportedOperationException("not implemented");
+   }
+   @Override
    public void removeProgressBar(long id)
    {
      //TODO
      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)
    {
 -    System.out.println("Starting service discovery");
 +    jalview.bin.Console.debug("Starting service discovery");
 +
      var tasks = new ArrayList<Future<?>>();
      // JAL-940 - JALVIEW 1 services are now being EOLed as of JABA 2.1 release
  
      }
      if (Cache.getDefault("SHOW_SLIVKA_SERVICES", true))
      {
-       tasks.add(jalview.ws.slivkaws.SlivkaWSDiscoverer.getInstance().startDiscoverer());
+       tasks.add(jalview.ws2.client.slivka.SlivkaWSDiscoverer
+           .getInstance().startDiscoverer());
      }
      if (blocking)
      {
          }
          else
          {
 -          Cache.log.error(
 +          jalview.bin.Console.error(
                    "Errors reported by JABA discovery service. Check web services preferences.\n"
                            + ermsg);
          }
        {
          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.");
 -          }
 +          // TODO does error send to stderr if no log exists ?
 +          jalview.bin.Console.error("Couldn't handle string " + url + " as a URL.");
          }
          // ignore any exceptions due to dud links.
        }
            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);
          }
        }
      });
        public void exportImage(Graphics g) throws Exception
        {
          paintAll(g);
 -        Cache.log.info("Successfully written snapshot to file "
 +        jalview.bin.Console.info("Successfully written snapshot to file "
                  + of.getAbsolutePath());
        }
      };
                "application/x-java-url; class=java.net.URL");
      } catch (ClassNotFoundException cfe)
      {
 -      Cache.log.debug("Couldn't instantiate the URL dataflavor.", cfe);
 +      jalview.bin.Console.debug("Couldn't instantiate the URL dataflavor.",
 +              cfe);
      }
  
      if (urlFlavour != null && t.isDataFlavorSupported(urlFlavour))
          {
            protocols.add(DataSourceType.URL);
            files.add(url.toString());
 -          Cache.log.debug("Drop handled as URL dataflavor "
 +          jalview.bin.Console.debug("Drop handled as URL dataflavor "
                    + files.get(files.size() - 1));
            return;
          }
          }
        } catch (Throwable ex)
        {
 -        Cache.log.debug("URL drop handler failed.", 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 (File file : (List<File>) t
                .getTransferData(DataFlavor.javaFileListFlavor))
        {
        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
          DataFlavor textDf = new DataFlavor(
                  "text/plain;class=java.lang.String");
            data = (String) t.getTransferData(textDf);
          }
  
 -        Cache.log.debug("Plain text drop content returned "
 +        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 "
 +          jalview.bin.Console.debug("Adding missing FILE protocol for "
                    + files.get(protocols.size()));
            protocols.add(DataSourceType.FILE);
          }
              continue;
            }
            java.net.URI uri = new java.net.URI(s);
 -          if (uri.getScheme().toLowerCase().startsWith("http"))
 +          if (uri.getScheme().toLowerCase(Locale.ROOT).startsWith("http"))
            {
              protocols.add(DataSourceType.URL);
              files.add(uri.toString());
          }
        }
  
 -      if (Cache.log.isDebugEnabled())
 +      if (jalview.bin.Console.isDebugEnabled())
        {
          if (data == null || !added)
          {
            if (t.getTransferDataFlavors() != null
                    && t.getTransferDataFlavors().length > 0)
            {
 -            Cache.log.debug(
 +            jalview.bin.Console.debug(
                      "Couldn't resolve drop data. Here are the supported flavors:");
              for (DataFlavor fl : t.getTransferDataFlavors())
              {
 -              Cache.log.debug(
 +              jalview.bin.Console.debug(
                        "Supported transfer dataflavor: " + fl.toString());
                Object df = t.getTransferData(fl);
                if (df != null)
                {
 -                Cache.log.debug("Retrieves: " + df);
 +                jalview.bin.Console.debug("Retrieves: " + df);
                }
                else
                {
 -                Cache.log.debug("Retrieved nothing");
 +                jalview.bin.Console.debug("Retrieved nothing");
                }
              }
            }
            else
            {
 -            Cache.log.debug("Couldn't resolve dataflavor for drop: "
 -                    + t.toString());
 +            jalview.bin.Console.debug("Couldn't resolve dataflavor for drop: "
 +                            + t.toString());
            }
          }
        }
      }
      if (Platform.isWindowsAndNotJS())
      {
 -      Cache.log.debug("Scanning dropped content for Windows Link Files");
 +      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();
 +        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")))
              File lf = (obj instanceof File ? (File) obj
                      : new File((String) obj));
              // process link file to get a URL
 -            Cache.log.debug("Found potential link file: " + lf);
 +            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);
 -            Cache.log.debug("Parsed real filename " + fullname
 +            jalview.bin.Console.debug("Parsed real filename " + fullname
                      + " to extract protocol: " + protocols.get(f));
            } catch (Exception ex)
            {
 -            Cache.log.error(
 +            jalview.bin.Console.error(
                      "Couldn't parse " + files.get(f) + " as a link file.",
                      ex);
            }
      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));
 +    }
 +  }
  }
@@@ -24,7 -24,7 +24,7 @@@ import jalview.analysis.scoremodels.Sco
  import jalview.api.AlignViewportI;
  import jalview.api.analysis.ScoreModelI;
  import jalview.api.analysis.SimilarityParamsI;
 -import jalview.bin.Cache;
 +import jalview.bin.Console;
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.AlignmentView;
@@@ -123,8 -123,8 +123,8 @@@ public class PCAPanel extends GPCAPane
  
      ScoreModelI scoreModel = ScoreModels.getInstance()
              .getScoreModel(modelName, ap);
 -    setPcaModel(new PCAModel(seqstrings, seqs, nucleotide, scoreModel,
 -            params));
 +    setPcaModel(
 +            new PCAModel(seqstrings, seqs, nucleotide, scoreModel, params));
      PaintRefresher.Register(this, av.getSequenceSetId());
  
      setRotatableCanvas(new RotatableCanvas(alignPanel));
    protected void close_actionPerformed()
    {
      setPcaModel(null);
 +    if (this.rc != null)
 +    {
 +      this.rc.sequencePoints = null;
 +      this.rc.setAxisEndPoints(null);
 +      this.rc = null;
 +    }
    }
  
    @Override
      // JAL-2647 disabled after load from project (until save to project done)
      if (getPcaModel().getInputData() == null)
      {
 -      Cache.log.info(
 +      Console.info(
                "Unexpected call to originalSeqData_actionPerformed - should have hidden this menu action.");
        return;
      }
        @Override
        public void exportImage(Graphics g) throws Exception
        {
 -      RotatableCanvas canvas = getRotatableCanvas();
 -      canvas.drawBackground(g);
 -      canvas.drawScene(g);
 +        RotatableCanvas canvas = getRotatableCanvas();
 +        canvas.drawBackground(g);
 +        canvas.drawScene(g);
          if (canvas.drawAxes)
          {
            canvas.drawAxes(g);
    }
    
    @Override
+   public void addProgressBar(long id, String message)
+   {
+     progressBar.addProgressBar(id, message);
+   }
+   @Override
    public void removeProgressBar(long id)
    {
      progressBar.removeProgressBar(id);
            final IProgressIndicatorHandler handler)
    {
      progressBar.registerHandler(id, handler);
 -    // if (progressBarHandlers == null || !progressBars.contains(Long.valueOf(id)))
 +    // if (progressBarHandlers == null ||
 +    // !progressBars.contains(Long.valueOf(id)))
      // {
      // throw new
      // Error(MessageManager.getString("error.call_setprogressbar_before_registering_handler"));
@@@ -1,11 -1,5 +1,5 @@@
  package jalview.gui;
  
- import jalview.bin.Cache;
- import jalview.bin.Console;
- import jalview.util.MessageManager;
- import jalview.ws.WSDiscovererI;
- import jalview.ws.slivkaws.SlivkaWSDiscoverer;
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Component;
@@@ -22,6 -16,7 +16,7 @@@ import java.util.ArrayList
  import java.util.HashMap;
  import java.util.Map;
  import java.util.NoSuchElementException;
+ import java.util.concurrent.CancellationException;
  import java.util.concurrent.CompletableFuture;
  
  import javax.swing.BorderFactory;
@@@ -38,6 -33,11 +33,11 @@@ import javax.swing.UIManager
  import javax.swing.table.AbstractTableModel;
  import javax.swing.table.DefaultTableCellRenderer;
  
 -import jalview.bin.Cache;
++import jalview.bin.Console;
+ import jalview.util.MessageManager;
+ import jalview.ws2.client.api.WebServiceDiscovererI;
+ import jalview.ws2.client.slivka.SlivkaWSDiscoverer;
  @SuppressWarnings("serial")
  public class SlivkaPreferences extends JPanel
  {
      setPreferredSize(new Dimension(500, 450));
    }
  
-   WSDiscovererI discoverer;
+   WebServiceDiscovererI discoverer;
  
-   private final ArrayList<String> urls = new ArrayList<>();
+   private final ArrayList<URL> urls = new ArrayList<>();
  
-   private final Map<String, Integer> statuses = new HashMap<>();
+   private final Map<URL, Integer> statuses = new HashMap<>();
  
    private final AbstractTableModel urlTableModel = new AbstractTableModel()
    {
@@@ -68,9 -68,9 +68,9 @@@
        switch (columnIndex)
        {
        case 0:
-         return urls.get(rowIndex);
+         return urls.get(rowIndex).toString();
        case 1:
-         return statuses.getOrDefault(urls.get(rowIndex), WSDiscovererI.STATUS_UNKNOWN);
+         return statuses.getOrDefault(urls.get(rowIndex), WebServiceDiscovererI.STATUS_UNKNOWN);
        default:
          throw new NoSuchElementException();
        }
            hasFocus, row, column);
        switch ((Integer) value)
        {
-       case WSDiscovererI.STATUS_NO_SERVICES:
+       case WebServiceDiscovererI.STATUS_NO_SERVICES:
          setForeground(Color.ORANGE);
          break;
-       case WSDiscovererI.STATUS_OK:
+       case WebServiceDiscovererI.STATUS_OK:
          setForeground(Color.GREEN);
          break;
-       case WSDiscovererI.STATUS_INVALID:
+       case WebServiceDiscovererI.STATUS_INVALID:
          setForeground(Color.RED);
          break;
-       case WSDiscovererI.STATUS_UNKNOWN:
+       case WebServiceDiscovererI.STATUS_UNKNOWN:
        default:
          setForeground(Color.LIGHT_GRAY);
        }
    JButton moveUrlDown = new JButton(
        MessageManager.getString("action.move_down"));
  
-   private String showEditUrlDialog(String oldUrl)
+   private URL showEditUrlDialog(String oldUrl)
    {
      String input = (String) JvOptionPane
          .showInternalInputDialog(
      }
      try
      {
-       new URL(input);
+       return new URL(input);
      } catch (MalformedURLException ex)
      {
        JvOptionPane.showInternalMessageDialog(this,
            JOptionPane.WARNING_MESSAGE);
        return null;
      }
-     return input;
    }
  
    // Button Action Listeners
    private ActionListener newUrlAction = (ActionEvent e) -> {
-     final String input = showEditUrlDialog("");
+     final URL input = showEditUrlDialog("");
      if (input != null)
      {
        urls.add(input);
        reloadStatusForUrl(input);
        urlTableModel.fireTableRowsInserted(urls.size(), urls.size());
-       discoverer.setServiceUrls(urls);
+       discoverer.setUrls(urls);
      }
    };
  
      final int i = urlListTable.getSelectedRow();
      if (i >= 0)
      {
-       final String input = showEditUrlDialog(urls.get(i));
+       final URL input = showEditUrlDialog(urls.get(i).toString());
        if (input != null)
        {
          urls.set(i, input);
          statuses.remove(input);
          reloadStatusForUrl(input);
          urlTableModel.fireTableRowsUpdated(i, i);
-         discoverer.setServiceUrls(urls);
+         discoverer.setUrls(urls);
        }
      }
    };
        urls.remove(i);
        statuses.remove(i);
        urlTableModel.fireTableRowsDeleted(i, i);
-       discoverer.setServiceUrls(urls);
+       discoverer.setUrls(urls);
      }
    };
  
      if (i > 0)
      {
        moveTableRow(i, i - 1);
-       discoverer.setServiceUrls(urls);
+       discoverer.setUrls(urls);
      }
    };
  
      if (i >= 0 && i < urls.size() - 1)
      {
        moveTableRow(i, i + 1);
-       discoverer.setServiceUrls(urls);
+       discoverer.setUrls(urls);
      }
    };
  
  
    private void moveTableRow(int fromIndex, int toIndex)
    {
-     String url = urls.get(fromIndex);
+     URL url = urls.get(fromIndex);
      int status = statuses.get(fromIndex);
      urls.set(fromIndex, urls.get(toIndex));
      urls.set(toIndex, url);
    // Discoverer buttons action listeners
    private ActionListener refreshServicesAction = (ActionEvent e) -> {
      progressBar.setVisible(true);
 -    Cache.log.info("Requesting service reload");
 +    Console.info("Requesting service reload");
-     discoverer.startDiscoverer().handle((_discoverer, exception) -> {
+     discoverer.startDiscoverer().handle((services, exception) -> {
        if (exception == null)
        {
 -        Cache.log.info("Reloading done");
 +        Console.info("Reloading done");
        }
-       else
+       else if (exception instanceof CancellationException)
        {
 -        Cache.log.info("Reloading cancelled");
++        Console.info("Reloading cancelled");
+       }
+       else {
 -        Cache.log.error("Reloading failed", exception);
 +        Console.error("Reloading failed", exception);
        }
        SwingUtilities.invokeLater(() -> progressBar.setVisible(false));
        return null;
    };
  
    private ActionListener resetServicesAction = (ActionEvent e) -> {
-     discoverer.setServiceUrls(null);
+     discoverer.setUrls(null);
      urls.clear();
      statuses.clear();
-     urls.addAll(discoverer.getServiceUrls());
-     for (String url : urls)
+     urls.addAll(discoverer.getUrls());
+     for (URL url : urls)
      {
        reloadStatusForUrl(url);
      }
    {
      // Initial URLs loading
      discoverer = SlivkaWSDiscoverer.getInstance();
-     urls.addAll(discoverer.getServiceUrls());
-     for (String url : urls)
+     urls.addAll(discoverer.getUrls());
+     for (URL url : urls)
      {
        reloadStatusForUrl(url);
      }
    }
  
-   private void reloadStatusForUrl(String url)
+   private void reloadStatusForUrl(URL url)
    {
-     CompletableFuture.supplyAsync(() -> discoverer.getServerStatusFor(url))
+     CompletableFuture.supplyAsync(() -> discoverer.getStatusForUrl(url))
          .thenAccept((status) -> {
            statuses.put(url, status);
            int row = urls.indexOf(url);
  
  package jalview.gui;
  
 +import java.awt.event.ActionEvent;
 +import java.awt.event.ActionListener;
  import java.awt.event.ItemEvent;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.HashSet;
  import java.util.LinkedHashSet;
  import java.util.List;
 -import java.util.Objects;
 -import java.util.Set;
 -import java.util.Vector;
 +import java.util.Locale;
 +import java.util.concurrent.Executors;
  
  import javax.swing.JCheckBox;
  import javax.swing.JComboBox;
  import javax.swing.JLabel;
 +import javax.swing.JMenu;
 +import javax.swing.JMenuItem;
 +import javax.swing.JPopupMenu;
  import javax.swing.JTable;
  import javax.swing.SwingUtilities;
  import javax.swing.table.AbstractTableModel;
  
  import jalview.api.structures.JalviewStructureDisplayI;
  import jalview.bin.Cache;
 +import jalview.bin.Console;
  import jalview.bin.Jalview;
 -import jalview.datamodel.DBRefEntry;
 -import jalview.datamodel.DBRefSource;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.SequenceI;
  import jalview.fts.api.FTSData;
  import jalview.fts.api.FTSDataColumnI;
  import jalview.fts.api.FTSRestClientI;
 +import jalview.fts.core.FTSDataColumnPreferences;
  import jalview.fts.core.FTSRestRequest;
  import jalview.fts.core.FTSRestResponse;
  import jalview.fts.service.pdb.PDBFTSRestClient;
 +import jalview.fts.service.threedbeacons.TDB_FTSData;
 +import jalview.gui.structurechooser.PDBStructureChooserQuerySource;
 +import jalview.gui.structurechooser.StructureChooserQuerySource;
 +import jalview.gui.structurechooser.ThreeDBStructureChooserQuerySource;
  import jalview.io.DataSourceType;
 +import jalview.jbgui.FilterOption;
  import jalview.jbgui.GStructureChooser;
 +import jalview.structure.StructureMapping;
 +import jalview.structure.StructureSelectionManager;
  import jalview.util.MessageManager;
 +import jalview.ws.DBRefFetcher;
 +import jalview.ws.DBRefFetcher.FetchFinishedListenerI;
 +import jalview.ws.seqfetcher.DbSourceProxy;
 +import jalview.ws.sifts.SiftsSettings;
  
  /**
   * Provides the behaviors for the Structure chooser Panel
  public class StructureChooser extends GStructureChooser
          implements IProgressIndicator
  {
 -  static final String AUTOSUPERIMPOSE = "AUTOSUPERIMPOSE";
 -
 -  private static int MAX_QLENGTH = 7820;
 +  protected static final String AUTOSUPERIMPOSE = "AUTOSUPERIMPOSE";
  
 +  /**
 +   * warn user if need to fetch more than this many uniprot records at once
 +   */
 +  private static final int THRESHOLD_WARN_UNIPROT_FETCH_NEEDED = 20;
    private SequenceI selectedSequence;
  
    private SequenceI[] selectedSequences;
  
    private Collection<FTSData> discoveredStructuresSet;
  
 -  private FTSRestRequest lastPdbRequest;
 +  private StructureChooserQuerySource data;
  
 -  private FTSRestClientI pdbRestClient;
 +  @Override
 +  protected FTSDataColumnPreferences getFTSDocFieldPrefs()
 +  {
 +    return data.getDocFieldPrefs();
 +  }
  
    private String selectedPdbFileName;
  
  
    private boolean cachedPDBExists;
  
 -  static StructureViewer lastTargetedView = null;
 +  private Collection<FTSData> lastDiscoveredStructuresSet;
 +
 +  private boolean canQueryTDB = false;
 +
 +  private boolean notQueriedTDBYet = true;
 +
 +  List<SequenceI> seqsWithoutSourceDBRef = null;
 +
 +  protected static StructureViewer lastTargetedView = null;
  
    public StructureChooser(SequenceI[] selectedSeqs, SequenceI selectedSeq,
            AlignmentPanel ap)
    {
 +    // which FTS engine to use
 +    data = StructureChooserQuerySource.getQuerySourceFor(selectedSeqs);
 +    initDialog();
 +
      this.ap = ap;
      this.selectedSequence = selectedSeq;
      this.selectedSequences = selectedSeqs;
      this.progressIndicator = (ap == null) ? null : ap.alignFrame;
      init();
 +
    }
  
    /**
 +   * sets canQueryTDB if protein sequences without a canonical uniprot ref or at
 +   * least one structure are discovered.
 +   */
 +  private void populateSeqsWithoutSourceDBRef()
 +  {
 +    seqsWithoutSourceDBRef = new ArrayList<SequenceI>();
 +    boolean needCanonical = false;
 +    for (SequenceI seq : selectedSequences)
 +    {
 +      if (seq.isProtein())
 +      {
 +        int dbRef = ThreeDBStructureChooserQuerySource
 +                .checkUniprotRefs(seq.getDBRefs());
 +        if (dbRef < 0)
 +        {
 +          if (dbRef == -1)
 +          {
 +            // need to retrieve canonicals
 +            needCanonical = true;
 +            seqsWithoutSourceDBRef.add(seq);
 +          }
 +          else
 +          {
 +            // could be a sequence with pdb ref
 +            if (seq.getAllPDBEntries() == null
 +                    || seq.getAllPDBEntries().size() == 0)
 +            {
 +              seqsWithoutSourceDBRef.add(seq);
 +            }
 +          }
 +        }
 +      }
 +    }
 +    // retrieve database refs for protein sequences
 +    if (!seqsWithoutSourceDBRef.isEmpty())
 +    {
 +      canQueryTDB = true;
 +      if (needCanonical)
 +      {
 +        // triggers display of the 'Query TDB' button
 +        notQueriedTDBYet = true;
 +      }
 +    }
 +  };
 +
 +  /**
     * Initializes parameters used by the Structure Chooser Panel
     */
    protected void init()
      }
  
      chk_superpose.setSelected(Cache.getDefault(AUTOSUPERIMPOSE, true));
 +    btn_queryTDB.addActionListener(new ActionListener()
 +    {
 +
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        promptForTDBFetch(false);
 +      }
 +    });
 +
 +    Executors.defaultThreadFactory().newThread(new Runnable()
 +    {
 +      @Override
 +      public void run()
 +      {
 +        populateSeqsWithoutSourceDBRef();
 +        initialStructureDiscovery();
 +      }
 +
 +    }).start();
 +
 +  }
 +
 +  // called by init
 +  private void initialStructureDiscovery()
 +  {
 +    // check which FTS engine to use
 +    data = StructureChooserQuerySource.getQuerySourceFor(selectedSequences);
  
      // ensure a filter option is in force for search
      populateFilterComboBox(true, cachedPDBExists);
 -    Thread discoverPDBStructuresThread = new Thread(new Runnable()
 +
 +    // looks for any existing structures already loaded
 +    // for the sequences (the cached ones)
 +    // then queries the StructureChooserQuerySource to
 +    // discover more structures.
 +    //
 +    // Possible optimisation is to only begin querying
 +    // the structure chooser if there are no cached structures.
 +
 +    long startTime = System.currentTimeMillis();
 +    updateProgressIndicator(
 +            MessageManager.getString("status.loading_cached_pdb_entries"),
 +            startTime);
 +    loadLocalCachedPDBEntries();
 +    updateProgressIndicator(null, startTime);
 +    updateProgressIndicator(
 +            MessageManager.getString("status.searching_for_pdb_structures"),
 +            startTime);
 +    fetchStructuresMetaData();
 +    // revise filter options if no results were found
 +    populateFilterComboBox(isStructuresDiscovered(), cachedPDBExists);
 +    discoverStructureViews();
 +    updateProgressIndicator(null, startTime);
 +    mainFrame.setVisible(true);
 +    updateCurrentView();
 +  }
 +
 +  /**
 +   * raises dialog for Uniprot fetch followed by 3D beacons search
 +   * 
 +   * @param ignoreGui
 +   *          - when true, don't ask, just fetch
 +   */
 +  public void promptForTDBFetch(boolean ignoreGui)
 +  {
 +    final long progressId = System.currentTimeMillis();
 +
 +    // final action after prompting and discovering db refs
 +    final Runnable strucDiscovery = new Runnable()
      {
        @Override
        public void run()
        {
 -        long startTime = System.currentTimeMillis();
 -        updateProgressIndicator(MessageManager
 -                .getString("status.loading_cached_pdb_entries"), startTime);
 -        loadLocalCachedPDBEntries();
 -        updateProgressIndicator(null, startTime);
 -        updateProgressIndicator(MessageManager.getString(
 -                "status.searching_for_pdb_structures"), startTime);
 -        fetchStructuresMetaData();
 -        // revise filter options if no results were found
 -        populateFilterComboBox(isStructuresDiscovered(), cachedPDBExists);
 -        discoverStructureViews();
 -        updateProgressIndicator(null, startTime);
 -        mainFrame.setVisible(true);
 -        updateCurrentView();
 +        mainFrame.setEnabled(false);
 +        cmb_filterOption.setEnabled(false);
 +        progressBar.setProgressBar(
 +                MessageManager.getString("status.searching_3d_beacons"),
 +                progressId);
 +        btn_queryTDB.setEnabled(false);
 +        // TODO: warn if no accessions discovered
 +        populateSeqsWithoutSourceDBRef();
 +        // redo initial discovery - this time with 3d beacons
 +        // Executors.
 +        previousWantedFields = null;
 +        lastSelected = (FilterOption) cmb_filterOption.getSelectedItem();
 +        cmb_filterOption.setSelectedItem(null);
 +        cachedPDBExists=false; // reset to initial
 +        initialStructureDiscovery();
 +        if (!isStructuresDiscovered())
 +        {
 +          progressBar.setProgressBar(MessageManager.getString(
 +                  "status.no_structures_discovered_from_3d_beacons"),
 +                  progressId);
 +          btn_queryTDB.setToolTipText(MessageManager.getString(
 +                  "status.no_structures_discovered_from_3d_beacons"));
 +          btn_queryTDB.setEnabled(false);
 +          pnl_queryTDB.setVisible(false);
 +        }
 +        else
 +        {
 +          cmb_filterOption.setSelectedIndex(0); // select 'best'
 +          btn_queryTDB.setVisible(false);
 +          pnl_queryTDB.setVisible(false);
 +          progressBar.setProgressBar(null, progressId);
 +        }
 +        mainFrame.setEnabled(true);
 +        cmb_filterOption.setEnabled(true);
        }
 -    });
 -    discoverPDBStructuresThread.start();
 +    };
 +
 +    final FetchFinishedListenerI afterDbRefFetch = new FetchFinishedListenerI()
 +    {
 +      
 +      @Override
 +      public void finished()
 +      {
 +        // filter has been selected, so we set flag to remove ourselves
 +        notQueriedTDBYet = false;
 +        // new thread to discover structures - via 3d beacons
 +        Executors.defaultThreadFactory().newThread(strucDiscovery).start();
 +        
 +      }
 +    };
 +    
 +    // fetch db refs if OK pressed
 +    final Runnable discoverCanonicalDBrefs = new Runnable() 
 +    {
 +      @Override
 +      public void run()
 +      {
 +        btn_queryTDB.setEnabled(false);
 +        populateSeqsWithoutSourceDBRef();
 +
 +        final int y = seqsWithoutSourceDBRef.size();
 +        if (y > 0)
 +        {
 +          final SequenceI[] seqWithoutSrcDBRef = seqsWithoutSourceDBRef
 +                  .toArray(new SequenceI[y]);
 +          DBRefFetcher dbRefFetcher = new DBRefFetcher(seqWithoutSrcDBRef,
 +                  progressBar, new DbSourceProxy[]
 +                  { new jalview.ws.dbsources.Uniprot() }, null, false);
 +          dbRefFetcher.addListener(afterDbRefFetch);
 +          // ideally this would also gracefully run with callbacks
 +          dbRefFetcher.fetchDBRefs(true);
 +        }
 +        else
 +        {
 +          // call finished action directly
 +          afterDbRefFetch.finished();
 +        }
 +      }
 +
 +    };
 +    final Runnable revertview = new Runnable()
 +    {
 +      @Override
 +      public void run()
 +      {
 +        if (lastSelected != null)
 +        {
 +          cmb_filterOption.setSelectedItem(lastSelected);
 +        }
 +      };
 +    };
 +    int threshold = Cache.getDefault("UNIPROT_AUTOFETCH_THRESHOLD",
 +            THRESHOLD_WARN_UNIPROT_FETCH_NEEDED);
 +    Console.debug("Using Uniprot fetch threshold of " + threshold);
 +    if (ignoreGui || seqsWithoutSourceDBRef.size() < threshold)
 +    {
 +      Executors.defaultThreadFactory().newThread(discoverCanonicalDBrefs).start();
 +      return;
 +    }
 +    // need cancel and no to result in the discoverPDB action - mocked is
 +    // 'cancel' TODO: mock should be OK
 +    StructureChooser thisSC = this;
 +    JvOptionPane.newOptionDialog(thisSC.getFrame())
 +            .setResponseHandler(JvOptionPane.OK_OPTION,
 +                    discoverCanonicalDBrefs)
 +            .setResponseHandler(JvOptionPane.CANCEL_OPTION, revertview)
 +            .setResponseHandler(JvOptionPane.NO_OPTION, revertview)
 +            .showDialog(
 +                    MessageManager.formatMessage(
 +                            "label.fetch_references_for_3dbeacons",
 +                            seqsWithoutSourceDBRef.size()),
 +                    MessageManager
 +                            .getString("label.3dbeacons"),
 +                    JvOptionPane.YES_NO_OPTION, JvOptionPane.PLAIN_MESSAGE,
 +                    null, new Object[]
 +                    { MessageManager.getString("action.ok"),
 +                        MessageManager.getString("action.cancel") },
 +                    MessageManager.getString("action.ok"), false);
    }
  
    /**
    void fetchStructuresMetaData()
    {
      long startTime = System.currentTimeMillis();
 -    pdbRestClient = PDBFTSRestClient.getInstance();
 -    Collection<FTSDataColumnI> wantedFields = pdbDocFieldPrefs
 +    Collection<FTSDataColumnI> wantedFields = data.getDocFieldPrefs()
              .getStructureSummaryFields();
  
      discoveredStructuresSet = new LinkedHashSet<>();
      HashSet<String> errors = new HashSet<>();
 +
 +    FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
 +            .getSelectedItem());
 +
      for (SequenceI seq : selectedSequences)
      {
 -      FTSRestRequest pdbRequest = new FTSRestRequest();
 -      pdbRequest.setAllowEmptySeq(false);
 -      pdbRequest.setResponseSize(500);
 -      pdbRequest.setFieldToSearchBy("(");
 -      FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
 -              .getSelectedItem());
 -      pdbRequest.setFieldToSortBy(selectedFilterOpt.getValue(),
 -              !chk_invertFilter.isSelected());
 -      pdbRequest.setWantedFields(wantedFields);
 -      pdbRequest.setSearchTerm(buildQuery(seq) + ")");
 -      pdbRequest.setAssociatedSequence(seq);
 +
        FTSRestResponse resultList;
        try
        {
 -        resultList = pdbRestClient.executeRequest(pdbRequest);
 +        resultList = data.fetchStructuresMetaData(seq, wantedFields,
 +                selectedFilterOpt, !chk_invertFilter.isSelected());
 +        // null response means the FTSengine didn't yield a query for this
 +        // consider designing a special exception if we really wanted to be
 +        // OOCrazy
 +        if (resultList == null)
 +        {
 +          continue;
 +        }
        } catch (Exception e)
        {
          e.printStackTrace();
          errors.add(e.getMessage());
          continue;
        }
 -      lastPdbRequest = pdbRequest;
        if (resultList.getSearchSummary() != null
                && !resultList.getSearchSummary().isEmpty())
        {
      if (discoveredStructuresSet != null
              && !discoveredStructuresSet.isEmpty())
      {
 -      getResultTable().setModel(FTSRestResponse
 -              .getTableModel(lastPdbRequest, discoveredStructuresSet));
 +      getResultTable()
 +              .setModel(data.getTableModel(discoveredStructuresSet));
 +
        noOfStructuresFound = discoveredStructuresSet.size();
 +      lastDiscoveredStructuresSet = discoveredStructuresSet;
        mainFrame.setTitle(MessageManager.formatMessage(
                "label.structure_chooser_no_of_structures",
                noOfStructuresFound, totalTime));
    }
  
    /**
 -   * Builds a query string for a given sequences using its DBRef entries
 -   * 
 -   * @param seq
 -   *          the sequences to build a query for
 -   * @return the built query string
 -   */
 -
 -  static String buildQuery(SequenceI seq)
 -  {
 -    boolean isPDBRefsFound = false;
 -    boolean isUniProtRefsFound = false;
 -    StringBuilder queryBuilder = new StringBuilder();
 -    Set<String> seqRefs = new LinkedHashSet<>();
 -
 -    /*
 -     * note PDBs as DBRefEntry so they are not duplicated in query
 -     */
 -    Set<String> pdbids = new HashSet<>();
 -
 -    if (seq.getAllPDBEntries() != null
 -            && queryBuilder.length() < MAX_QLENGTH)
 -    {
 -      for (PDBEntry entry : seq.getAllPDBEntries())
 -      {
 -        if (isValidSeqName(entry.getId()))
 -        {
 -          String id = entry.getId().toLowerCase();
 -          queryBuilder.append("pdb_id:").append(id).append(" OR ");
 -          isPDBRefsFound = true;
 -          pdbids.add(id);
 -        }
 -      }
 -    }
 -
 -    List<DBRefEntry> refs = seq.getDBRefs();
 -    if (refs != null && refs.size() != 0)
 -    {
 -      for (int ib = 0, nb = refs.size(); ib < nb; ib++)
 -      {
 -        DBRefEntry dbRef = refs.get(ib);
 -        if (isValidSeqName(getDBRefId(dbRef))
 -                && queryBuilder.length() < MAX_QLENGTH)
 -        {
 -          if (dbRef.getSource().equalsIgnoreCase(DBRefSource.UNIPROT))
 -          {
 -            queryBuilder.append("uniprot_accession:")
 -                    .append(getDBRefId(dbRef)).append(" OR ");
 -            queryBuilder.append("uniprot_id:").append(getDBRefId(dbRef))
 -                    .append(" OR ");
 -            isUniProtRefsFound = true;
 -          }
 -          else if (dbRef.getSource().equalsIgnoreCase(DBRefSource.PDB))
 -          {
 -
 -            String id = getDBRefId(dbRef).toLowerCase();
 -            if (!pdbids.contains(id))
 -            {
 -              queryBuilder.append("pdb_id:").append(id).append(" OR ");
 -              isPDBRefsFound = true;
 -              pdbids.add(id);
 -            }
 -          }
 -          else
 -          {
 -            seqRefs.add(getDBRefId(dbRef));
 -          }
 -        }
 -      }
 -    }
 -
 -    if (!isPDBRefsFound && !isUniProtRefsFound)
 -    {
 -      String seqName = seq.getName();
 -      seqName = sanitizeSeqName(seqName);
 -      String[] names = seqName.toLowerCase().split("\\|");
 -      for (String name : names)
 -      {
 -        // System.out.println("Found name : " + name);
 -        name.trim();
 -        if (isValidSeqName(name))
 -        {
 -          seqRefs.add(name);
 -        }
 -      }
 -
 -      for (String seqRef : seqRefs)
 -      {
 -        queryBuilder.append("text:").append(seqRef).append(" OR ");
 -      }
 -    }
 -
 -    int endIndex = queryBuilder.lastIndexOf(" OR ");
 -    if (queryBuilder.toString().length() < 6)
 -    {
 -      return null;
 -    }
 -    String query = queryBuilder.toString().substring(0, endIndex);
 -    return query;
 -  }
 -
 -  /**
 -   * Remove the following special characters from input string +, -, &, !, (, ),
 -   * {, }, [, ], ^, ", ~, *, ?, :, \
 -   * 
 -   * @param seqName
 -   * @return
 -   */
 -  static String sanitizeSeqName(String seqName)
 -  {
 -    Objects.requireNonNull(seqName);
 -    return seqName.replaceAll("\\[\\d*\\]", "")
 -            .replaceAll("[^\\dA-Za-z|_]", "").replaceAll("\\s+", "+");
 -  }
 -
 -  /**
 -   * Ensures sequence ref names are not less than 3 characters and does not
 -   * contain a database name
 -   * 
 -   * @param seqName
 -   * @return
 -   */
 -  static boolean isValidSeqName(String seqName)
 -  {
 -    // System.out.println("seqName : " + seqName);
 -    String ignoreList = "pdb,uniprot,swiss-prot";
 -    if (seqName.length() < 3)
 -    {
 -      return false;
 -    }
 -    if (seqName.contains(":"))
 -    {
 -      return false;
 -    }
 -    seqName = seqName.toLowerCase();
 -    for (String ignoredEntry : ignoreList.split(","))
 -    {
 -      if (seqName.contains(ignoredEntry))
 -      {
 -        return false;
 -      }
 -    }
 -    return true;
 -  }
 -
 -  static String getDBRefId(DBRefEntry dbRef)
 -  {
 -    String ref = dbRef.getAccessionId().replaceAll("GO:", "");
 -    return ref;
 -  }
 -
 -  /**
     * Filters a given list of discovered structures based on supplied argument
     * 
     * @param fieldToFilterBy
    {
      Thread filterThread = new Thread(new Runnable()
      {
 +
        @Override
        public void run()
        {
          long startTime = System.currentTimeMillis();
 -        pdbRestClient = PDBFTSRestClient.getInstance();
          lbl_loading.setVisible(true);
 -        Collection<FTSDataColumnI> wantedFields = pdbDocFieldPrefs
 +        Collection<FTSDataColumnI> wantedFields = data.getDocFieldPrefs()
                  .getStructureSummaryFields();
          Collection<FTSData> filteredResponse = new HashSet<>();
          HashSet<String> errors = new HashSet<>();
  
          for (SequenceI seq : selectedSequences)
          {
 -          FTSRestRequest pdbRequest = new FTSRestRequest();
 -          if (fieldToFilterBy.equalsIgnoreCase("uniprot_coverage"))
 -          {
 -            pdbRequest.setAllowEmptySeq(false);
 -            pdbRequest.setResponseSize(1);
 -            pdbRequest.setFieldToSearchBy("(");
 -            pdbRequest.setSearchTerm(buildQuery(seq) + ")");
 -            pdbRequest.setWantedFields(wantedFields);
 -            pdbRequest.setAssociatedSequence(seq);
 -            pdbRequest.setFacet(true);
 -            pdbRequest.setFacetPivot(fieldToFilterBy + ",entry_entity");
 -            pdbRequest.setFacetPivotMinCount(1);
 -          }
 -          else
 -          {
 -            pdbRequest.setAllowEmptySeq(false);
 -            pdbRequest.setResponseSize(1);
 -            pdbRequest.setFieldToSearchBy("(");
 -            pdbRequest.setFieldToSortBy(fieldToFilterBy,
 -                    !chk_invertFilter.isSelected());
 -            pdbRequest.setSearchTerm(buildQuery(seq) + ")");
 -            pdbRequest.setWantedFields(wantedFields);
 -            pdbRequest.setAssociatedSequence(seq);
 -          }
 +
            FTSRestResponse resultList;
            try
            {
 -            resultList = pdbRestClient.executeRequest(pdbRequest);
 +            resultList = data.selectFirstRankedQuery(seq,
 +                    discoveredStructuresSet, wantedFields, fieldToFilterBy,
 +                    !chk_invertFilter.isSelected());
 +
            } catch (Exception e)
            {
              e.printStackTrace();
              errors.add(e.getMessage());
              continue;
            }
 -          lastPdbRequest = pdbRequest;
            if (resultList.getSearchSummary() != null
                    && !resultList.getSearchSummary().isEmpty())
            {
            Collection<FTSData> reorderedStructuresSet = new LinkedHashSet<>();
            reorderedStructuresSet.addAll(filteredResponse);
            reorderedStructuresSet.addAll(discoveredStructuresSet);
 -          getResultTable().setModel(FTSRestResponse
 -                  .getTableModel(lastPdbRequest, reorderedStructuresSet));
 +          getResultTable()
 +                  .setModel(data.getTableModel(reorderedStructuresSet));
  
            FTSRestResponse.configureTableColumn(getResultTable(),
                    wantedFields, tempUserPrefs);
      // StructureChooser
      // works
      jalview.io.JalviewFileChooser chooser = new jalview.io.JalviewFileChooser(
 -            jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
 +            Cache.getProperty("LAST_DIRECTORY"));
      chooser.setFileView(new jalview.io.JalviewFileView());
      chooser.setDialogTitle(
              MessageManager.formatMessage("label.select_pdb_file_for",
      if (value == jalview.io.JalviewFileChooser.APPROVE_OPTION)
      {
        selectedPdbFileName = chooser.getSelectedFile().getPath();
 -      jalview.bin.Cache.setProperty("LAST_DIRECTORY", selectedPdbFileName);
 +      Cache.setProperty("LAST_DIRECTORY", selectedPdbFileName);
        validateSelections();
      }
    }
    protected void populateFilterComboBox(boolean haveData,
            boolean cachedPDBExist)
    {
 +    populateFilterComboBox(haveData, cachedPDBExist, null);
 +  }
 +
 +  /**
 +   * Populates the filter combo-box options dynamically depending on discovered
 +   * structures
 +   */
 +  protected void populateFilterComboBox(boolean haveData,
 +          boolean cachedPDBExist, FilterOption lastSel)
 +  {
 +
      /*
       * temporarily suspend the change listener behaviour
       */
      cmb_filterOption.removeItemListener(this);
 -
 +    int selSet = -1;
      cmb_filterOption.removeAllItems();
      if (haveData)
      {
 -      cmb_filterOption.addItem(new FilterOption(
 -              MessageManager.getString("label.best_quality"),
 -              "overall_quality", VIEWS_FILTER, false));
 -      cmb_filterOption.addItem(new FilterOption(
 -              MessageManager.getString("label.best_resolution"),
 -              "resolution", VIEWS_FILTER, false));
 -      cmb_filterOption.addItem(new FilterOption(
 -              MessageManager.getString("label.most_protein_chain"),
 -              "number_of_protein_chains", VIEWS_FILTER, false));
 -      cmb_filterOption.addItem(new FilterOption(
 -              MessageManager.getString("label.most_bound_molecules"),
 -              "number_of_bound_molecules", VIEWS_FILTER, false));
 -      cmb_filterOption.addItem(new FilterOption(
 -              MessageManager.getString("label.most_polymer_residues"),
 -              "number_of_polymer_residues", VIEWS_FILTER, true));
 +      List<FilterOption> filters = data
 +              .getAvailableFilterOptions(VIEWS_FILTER);
 +      data.updateAvailableFilterOptions(VIEWS_FILTER, filters,
 +              lastDiscoveredStructuresSet);
 +      int p = 0;
 +      for (FilterOption filter : filters)
 +      {
 +        if (lastSel != null && filter.equals(lastSel))
 +        {
 +          selSet = p;
 +        }
 +        p++;
 +        cmb_filterOption.addItem(filter);
 +      }
      }
 +
      cmb_filterOption.addItem(
              new FilterOption(MessageManager.getString("label.enter_pdb_id"),
 -                    "-", VIEWS_ENTER_ID, false));
 +                    "-", VIEWS_ENTER_ID, false, null));
      cmb_filterOption.addItem(
              new FilterOption(MessageManager.getString("label.from_file"),
 -                    "-", VIEWS_FROM_FILE, false));
 +                    "-", VIEWS_FROM_FILE, false, null));
 +    if (canQueryTDB && notQueriedTDBYet)
 +    {
 +      btn_queryTDB.setVisible(true);
 +      pnl_queryTDB.setVisible(true);
 +    }
  
      if (cachedPDBExist)
      {
        FilterOption cachedOption = new FilterOption(
                MessageManager.getString("label.cached_structures"), "-",
 -              VIEWS_LOCAL_PDB, false);
 +              VIEWS_LOCAL_PDB, false, null);
        cmb_filterOption.addItem(cachedOption);
 -      cmb_filterOption.setSelectedItem(cachedOption);
 +      if (selSet == -1)
 +      {
 +        cmb_filterOption.setSelectedItem(cachedOption);
 +      }
 +    }
 +    if (selSet > -1)
 +    {
 +      cmb_filterOption.setSelectedIndex(selSet);
      }
 -
      cmb_filterOption.addItemListener(this);
    }
  
    {
      FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
              .getSelectedItem());
 +    
 +    if (lastSelected == selectedFilterOpt)
 +    {
 +      // don't need to do anything, probably
 +      return;
 +    }
 +    // otherwise, record selection
 +    // and update the layout and dialog accordingly
 +    lastSelected = selectedFilterOpt;
 +
      layout_switchableViews.show(pnl_switchableViews,
              selectedFilterOpt.getView());
      String filterTitle = mainFrame.getTitle();
      mainFrame.setTitle(frameTitle);
      chk_invertFilter.setVisible(false);
 +    
      if (selectedFilterOpt.getView() == VIEWS_FILTER)
      {
        mainFrame.setTitle(filterTitle);
 -      chk_invertFilter.setVisible(true);
 -      filterResultSet(selectedFilterOpt.getValue());
 +      // TDB Query has no invert as yet
 +      chk_invertFilter.setVisible(selectedFilterOpt
 +              .getQuerySource() instanceof PDBStructureChooserQuerySource);
 +
 +      if (data != selectedFilterOpt.getQuerySource()
 +              || data.needsRefetch(selectedFilterOpt))
 +      {
 +        data = selectedFilterOpt.getQuerySource();
 +        // rebuild the views completely, since prefs will also change
 +        tabRefresh();
 +        return;
 +      }
 +      else
 +      {
 +        filterResultSet(selectedFilterOpt.getValue());
 +      }
      }
      else if (selectedFilterOpt.getView() == VIEWS_ENTER_ID
              || selectedFilterOpt.getView() == VIEWS_FROM_FILE)
              .setEnabled(selectedCount > 1 || targetView.getItemCount() > 0);
    }
  
 +  @Override
 +  protected boolean showPopupFor(int selectedRow, int x, int y)
 +  {
 +    FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
 +            .getSelectedItem());
 +    String currentView = selectedFilterOpt.getView();
 +    if (currentView == VIEWS_FILTER
 +            && data instanceof ThreeDBStructureChooserQuerySource)
 +    {
 +      TDB_FTSData row = ((ThreeDBStructureChooserQuerySource) data)
 +              .getFTSDataFor(getResultTable(), selectedRow,
 +                      discoveredStructuresSet);
 +      String pageUrl = row.getModelViewUrl();
 +      JPopupMenu popup = new JPopupMenu("3D Beacons");
 +      JMenuItem viewUrl = new JMenuItem("View model web page");
 +      viewUrl.addActionListener(new ActionListener()
 +      {
 +        @Override
 +        public void actionPerformed(ActionEvent e)
 +        {
 +          Desktop.showUrl(pageUrl);
 +        }
 +      });
 +      popup.add(viewUrl);
 +      SwingUtilities.invokeLater(new Runnable()
 +      {
 +        @Override
 +        public void run()
 +        {
 +          popup.show(getResultTable(), x, y);
 +        }
 +      });
 +      return true;
 +    }
 +    // event not handled by us
 +    return false;
 +  }
    /**
     * Validates inputs from the Manual PDB entry panel
     */
    {
      validateSelections();
    }
 -
 +  private FilterOption lastSelected=null;
    /**
     * Handles the state change event for the 'filter' combo-box and 'invert'
     * check-box
    public void showStructures(boolean waitUntilFinished)
    {
  
 +    final StructureSelectionManager ssm = ap.getStructureSelectionManager();
 +    final StructureViewer theViewer = getTargetedStructureViewer(ssm);
 +    boolean superimpose = chk_superpose.isSelected(); 
      final int preferredHeight = pnl_filter.getHeight();
  
 -    final StructureViewer theViewer = getTargetedStructureViewer();
 -    boolean superimpose = chk_superpose.isSelected();
 -
      Runnable viewStruc = new Runnable()
      {
        @Override
  
          if (currentView == VIEWS_FILTER)
          {
 -          int pdbIdColIndex = restable.getColumn("PDB Id").getModelIndex();
 -          int refSeqColIndex = restable.getColumn("Ref Sequence")
 -                  .getModelIndex();
            int[] selectedRows = restable.getSelectedRows();
            PDBEntry[] pdbEntriesToView = new PDBEntry[selectedRows.length];
 -          int count = 0;
            List<SequenceI> selectedSeqsToView = new ArrayList<>();
 -          for (int row : selectedRows)
 -          {
 -            String pdbIdStr = restable.getValueAt(row, pdbIdColIndex)
 -                    .toString();
 -            SequenceI selectedSeq = (SequenceI) restable.getValueAt(row,
 -                    refSeqColIndex);
 -            selectedSeqsToView.add(selectedSeq);
 -            PDBEntry pdbEntry = selectedSeq.getPDBEntry(pdbIdStr);
 -            if (pdbEntry == null)
 -            {
 -              pdbEntry = getFindEntry(pdbIdStr,
 -                      selectedSeq.getAllPDBEntries());
 -            }
 +          pdbEntriesToView = data.collectSelectedRows(restable,
 +                  selectedRows, selectedSeqsToView);
  
 -            if (pdbEntry == null)
 -            {
 -              pdbEntry = new PDBEntry(pdbIdStr, null, "pdb");
 -              selectedSeq.getDatasetSequence().addPDBId(pdbEntry);
 -            }
 -            pdbEntriesToView[count++] = pdbEntry;
 -          }
            SequenceI[] selectedSeqs = selectedSeqsToView
                    .toArray(new SequenceI[selectedSeqsToView.size()]);
            sViewer = StructureViewer.launchStructureViewer(ap, pdbEntriesToView,
            List<SequenceI> selectedSeqsToView = new ArrayList<>();
            for (int row : selectedRows)
            {
 -            PDBEntry pdbEntry = (PDBEntry) tbl_local_pdb.getValueAt(row,
 -                    pdbIdColIndex);
 +            PDBEntry pdbEntry = ((PDBEntryTableModel) tbl_local_pdb
 +                    .getModel()).getPDBEntryAt(row).getPdbEntry();
 +
              pdbEntriesToView[count++] = pdbEntry;
              SequenceI selectedSeq = (SequenceI) tbl_local_pdb
                      .getValueAt(row, refSeqColIndex);
              if (pdbIdStr.split(":").length > 1)
              {
                pdbEntry.setId(pdbIdStr.split(":")[0]);
 -              pdbEntry.setChainCode(pdbIdStr.split(":")[1].toUpperCase());
 +              pdbEntry.setChainCode(pdbIdStr.split(":")[1].toUpperCase(Locale.ROOT));
              }
              else
              {
  
            PDBEntry[] pdbEntriesToView = new PDBEntry[] { pdbEntry };
            sViewer = StructureViewer.launchStructureViewer(ap, pdbEntriesToView,
 -                  new SequenceI[]
 +                   new SequenceI[]
                    { selectedSequence }, superimpose, theViewer,
                    progressBar);
          }
            {
              selectedSequence = userSelectedSeq;
            }
 -          PDBEntry fileEntry = AssociatePdbFileWithSeq.associatePdbWithSeq(selectedPdbFileName,
 +          PDBEntry fileEntry =  AssociatePdbFileWithSeq.associatePdbWithSeq(selectedPdbFileName,
                            DataSourceType.FILE, selectedSequence, true);
 -          sViewer = StructureViewer.launchStructureViewer(ap, new PDBEntry[] { fileEntry },
 -                  new SequenceI[]
 -                  { selectedSequence }, superimpose, theViewer,
 +
 +          sViewer = StructureViewer.launchStructureViewer(ap,  new PDBEntry[] { fileEntry },
 +                new SequenceI[] { selectedSequence }, superimpose, theViewer,
                    progressBar);
          }
          SwingUtilities.invokeLater(new Runnable()
      }
    }
  
 -  private PDBEntry getFindEntry(String id, Vector<PDBEntry> pdbEntries)
 -  {
 -    Objects.requireNonNull(id);
 -    Objects.requireNonNull(pdbEntries);
 -    PDBEntry foundEntry = null;
 -    for (PDBEntry entry : pdbEntries)
 -    {
 -      if (entry.getId().equalsIgnoreCase(id))
 -      {
 -        return entry;
 -      }
 -    }
 -    return foundEntry;
 -  }
 -
    /**
     * Answers a structure viewer (new or existing) configured to superimpose
     * added structures or not according to the user's choice
     * @param ssm
     * @return
     */
 -  StructureViewer getTargetedStructureViewer()
 +  StructureViewer getTargetedStructureViewer(StructureSelectionManager ssm)
    {
 -    return (StructureViewer) targetView.getSelectedItem();
 +    Object sv = targetView.getSelectedItem();
 +
 +    return sv == null ? new StructureViewer(ssm) : (StructureViewer) sv;
    }
  
    /**
            isValidPBDEntry = false;
            if (text.length() > 0)
            {
 -            String searchTerm = text.toLowerCase();
 +            // TODO move this pdb id search into the PDB specific
 +            // FTSSearchEngine
 +            // for moment, it will work fine as is because it is self-contained
 +            String searchTerm = text.toLowerCase(Locale.ROOT);
              searchTerm = searchTerm.split(":")[0];
              // System.out.println(">>>>> search term : " + searchTerm);
              List<FTSDataColumnI> wantedFields = new ArrayList<>();
              pdbRequest.setWantedFields(wantedFields);
              pdbRequest.setSearchTerm(searchTerm + ")");
              pdbRequest.setAssociatedSequence(selectedSequence);
 -            pdbRestClient = PDBFTSRestClient.getInstance();
 +            FTSRestClientI pdbRestClient = PDBFTSRestClient.getInstance();
              wantedFields.add(pdbRestClient.getPrimaryKeyColumn());
              FTSRestResponse resultList;
              try
    {
      if (selectedSequences != null)
      {
 +      lbl_loading.setVisible(true);
        Thread refreshThread = new Thread(new Runnable()
        {
          @Override
          public void run()
          {
            fetchStructuresMetaData();
 +          // populateFilterComboBox(true, cachedPDBExists);
 +
            filterResultSet(
                    ((FilterOption) cmb_filterOption.getSelectedItem())
                            .getValue());
 +          lbl_loading.setVisible(false);
          }
        });
        refreshThread.start();
          value = entry.getSequence();
          break;
        case 1:
 -        value = entry.getPdbEntry();
 +        value = entry.getQualifiedId();
          break;
        case 2:
          value = entry.getPdbEntry().getChainCode() == null ? "_"
        this.pdbEntry = pdbEntry;
      }
  
 +    public String getQualifiedId()
 +    {
 +      if (pdbEntry.hasProvider())
 +      {
 +        return pdbEntry.getProvider() + ":" + pdbEntry.getId();
 +      }
 +      return pdbEntry.toString();
 +    }
 +
      public SequenceI getSequence()
      {
        return sequence;
    }
    
    @Override
+   public void addProgressBar(long id, String message)
+   {
+     progressBar.addProgressBar(id, message);
+   }
+   @Override
    public void removeProgressBar(long id)
    {
      progressBar.removeProgressBar(id);
      return sViewer == null ? null : sViewer.sview;
    }
  
 +  @Override
 +  protected void setFTSDocFieldPrefs(FTSDataColumnPreferences newPrefs)
 +  {
 +    data.setDocFieldPrefs(newPrefs);
 +
 +  }
 +
 +  /**
 +   * 
 +   * @return true when all initialisation threads have finished and dialog is
 +   *         visible
 +   */
 +  public boolean isDialogVisible()
 +  {
 +    return mainFrame != null && data != null && cmb_filterOption != null
 +            && mainFrame.isVisible()
 +            && cmb_filterOption.getSelectedItem() != null;
 +  }
 +  /**
 +   * 
 +   * @return true if the 3D-Beacons query button will/has been displayed
 +   */
 +  public boolean isCanQueryTDB()
 +  {
 +    return canQueryTDB;
 +  }
 +
 +  public boolean isNotQueriedTDBYet()
 +  {
 +    return notQueriedTDBYet;
 +  }
  }
@@@ -23,7 -23,6 +23,7 @@@ package jalview.gui
  import jalview.jbgui.GWebserviceInfo;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
 +import jalview.util.ChannelProperties;
  import jalview.ws.WSClientI;
  
  import java.awt.BorderLayout;
@@@ -37,7 -36,6 +37,7 @@@ import java.awt.MediaTracker
  import java.awt.RenderingHints;
  import java.awt.event.ActionEvent;
  import java.awt.image.BufferedImage;
 +import java.util.Locale;
  import java.util.Vector;
  
  import javax.swing.JComponent;
@@@ -334,7 -332,9 +334,7 @@@ public class WebserviceInfo extends GWe
      this.title = title;
      setInfoText(info);
  
 -    java.net.URL url = getClass()
 -            .getResource("/images/Jalview_Logo_small_with_border.png");
 -    image = java.awt.Toolkit.getDefaultToolkit().createImage(url);
 +    image = ChannelProperties.getImage("rotatable_logo.48");
  
      MediaTracker mt = new MediaTracker(this);
      mt.addImage(image, 0);
      titlePanel.add(ap, BorderLayout.WEST);
      titlePanel.add(titleText, BorderLayout.CENTER);
      setStatus(currentStatus);
 +
      if (!Platform.isJS())
      {
        // No animation for the moment//
      {
        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");
      }
  
      @Override
 -      public void paintComponent(Graphics g1)
 +    public void paintComponent(Graphics g1)
      {
        drawPanel();
  
    }
  
    @Override
 -public void hyperlinkUpdate(HyperlinkEvent e)
 +  public void hyperlinkUpdate(HyperlinkEvent e)
    {
      Desktop.hyperlinkUpdate(e);
    }
    }
    
    @Override
+   public void addProgressBar(long id, String message)
+   {
+     progressBar.addProgressBar(id, message);
+   }
+   @Override
    public void removeProgressBar(long id)
    {
      progressBar.removeProgressBar(id);
@@@ -20,7 -20,6 +20,7 @@@
   */
  package jalview.gui;
  
 +import jalview.bin.Console;
  import jalview.gui.OptsAndParamsPage.OptionBox;
  import jalview.gui.OptsAndParamsPage.ParamBox;
  import jalview.util.MessageManager;
@@@ -30,6 -29,7 +30,6 @@@ import jalview.ws.params.OptionI
  import jalview.ws.params.ParamDatastoreI;
  import jalview.ws.params.ParameterI;
  import jalview.ws.params.WsParamSetI;
 -
  import java.awt.BorderLayout;
  import java.awt.Component;
  import java.awt.Dimension;
@@@ -173,7 -173,7 +173,7 @@@ public class WsJobParameters extends JP
      jbInit();
      this.paramStore = store;
      this.service = null;
-     init(preset, args);
+     initForService(preset, args);
      validate();
    }
  
            WsParamSetI preset, List<ArgumentI> jobArgset)
    {
      super();
 -
      // parameters dialog in 'expanded' format (help text boxes)
      opanp = new OptsAndParamsPage(this, false);
 -
      jbInit();
      this.paramStore = paramStorei;
      if (paramStore == null && service != null)
      validate();
    }
  
 +
    /**
     * Shows a modal dialog containing the parameters and Start or Cancel options.
     * Answers true if the job is started, false if cancelled.
     */
    public CompletionStage<Boolean> showRunDialog()
    {
 +
 +    // Should JFrame hahve a parent of getDesktop ?
      frame = new JFrame();
      frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
      if (service != null)
      });
      
      frame.setVisible(true);
 +
      return completionStage;
    }
  
      paramPane.getViewport().setView(jp);
      paramPane.setBorder(null);
      setLayout(new BorderLayout());
 -
      JPanel jobPanel = new JPanel();
      jobPanel.setPreferredSize(null);
      jobPanel.setLayout(new BorderLayout());
        exnames.put(SVC_DEF, SVC_DEF);
        servicePresets.put(SVC_DEF, SVC_DEF);
      }
 -
      // String curname = (p == null ? "" : p.getName());
      for (WsParamSetI pr : paramStore.getPresets())
      {
        }
      }
      settingDialog = false;
 +
    }
  
    private void updateTable(WsParamSetI p, List<ArgumentI> jobArgset)
      return modifiedElements.size() > 0;
    }
  
 +
    /**
     * reset gui and modification state settings
     */
      paramPane.revalidate();
      revalidate();
    }
 +
 +
    public boolean isServiceDefaults()
    {
      return (!isModified()
      return opanp.getCurrentSettings();
    }
  
 +
    /*
     * Hashtable<String, Object[]> editedParams = new Hashtable<String,
     * Object[]>();
              && e.getStateChange() == ItemEvent.SELECTED)
      {
        final String setname = (String) setName.getSelectedItem();
 -      // System.out.println("Item state changed for " + setname
 -      // + " (handling ? " + !settingDialog + ")");
 +      if (Console.isDebugEnabled())
 +      {
 +        Console.debug("Item state changed for " + setname + " (handling ? "
 +                + !settingDialog + ")");
 +      }
        if (settingDialog)
        {
          // ignore event
@@@ -74,6 -74,9 +74,9 @@@ import java.util.Hashtable
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
+ import java.util.concurrent.Executors;
+ import java.util.concurrent.ScheduledExecutorService;
+ import java.util.concurrent.ScheduledThreadPoolExecutor;
  
  /**
   * base class holding visualization and analysis attributes and common logic for
@@@ -87,6 -90,7 +90,6 @@@ public abstract class AlignmentViewpor
  {
    public static final String PROPERTY_ALIGNMENT = "alignment";
    public static final String PROPERTY_SEQUENCE = "sequence";
 -
    protected ViewportRanges ranges;
  
    protected ViewStyleI viewStyle = new ViewStyle();
     * alignment
     */
    protected boolean isDataset = false;
 +
    
    public void setDataset(boolean b)
    {
    {
      autoCalculateStrucConsensus = b;
    }
    protected boolean ignoreGapsInConsensusCalculation = false;
  
    protected ResidueShaderI residueShading = new ResidueShader();
 +
    
    @Override
    public void setGlobalColourScheme(ColourSchemeI cs)
    {
      return residueShading;
    }
 +
    
    protected AlignmentAnnotation consensus;
  
    protected Hashtable<String, Object>[] hStrucConsensus = null;
  
    protected Conservation hconservation = null;
 +
    
    @Override
    public void setConservation(Conservation cons)
        calculator.registerWorker(new InformationThread(this, ap));
      }
    }
 -
    // --------START Structure Conservation
    public void updateStrucConsensus(final AlignmentViewPanel ap)
    {
      return false;
    }
  
+   private ScheduledExecutorService serviceExecutor = Executors.newSingleThreadScheduledExecutor();
+   /**
+    * Get a default scheduled executor service which can be used by
+    * services and calculators to run parallel jobs associated with this
+    * viewport.
+    * 
+    * @return default service executor of that viewport
+    */
+   public ScheduledExecutorService getServiceExecutor()
+   {
+     return serviceExecutor;
+   }
    public void setAlignment(AlignmentI align)
    {
      this.alignment = align;
      gapcounts = null;
      calculator.shutdown();
      calculator = null;
+     serviceExecutor.shutdown();
+     serviceExecutor = null;
      residueShading = null; // may hold a reference to Consensus
      changeSupport = null;
      ranges = null;
      currentTree = null;
      selectionGroup = null;
 +    colSel = null;
      setAlignment(null);
    }
  
      }
      this.hmmShowSequenceLogo = showHMMSequenceLogo;
    }
 -
    /**
     * @param showConsensusHistogram
     *          the showConsensusHistogram to set
    {
      return infoLetterHeight;
    }
 -
    // property change stuff
    // JBPNote Prolly only need this in the applet version.
    private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
      }
    }
  
 +
    // common hide/show column stuff
  
    public void hideSelectedColumns()
      }
    }
  
 -
    public void showSequence(int index)
    {
      int startSeq = ranges.getStartSeq();
        }
  
        ranges.setStartEndSeq(startSeq, endSeq + tmp.size());
 +
        notifyAlignment();
        sendSelection();
      }
                MessageManager.getString("label.consensus_descr"),
                new Annotation[1], 0f, 100f, AlignmentAnnotation.BAR_GRAPH);
        initConsensus(consensus);
 -
        initGapCounts();
  
        initComplementConsensus();
      }
      return false;
    }
 -
    @Override
    public void setCurrentTree(TreeModel tree)
    {
        codingComplement.setUpdateStructures(needToUpdateStructureViews);
      }
    }
 +
 +  @Override
 +  public Iterator<int[]> getViewAsVisibleContigs(boolean selectedRegionOnly)
 +  {
 +    int start = 0;
 +    int end = 0;
 +    if (selectedRegionOnly && selectionGroup != null)
 +    {
 +      start = selectionGroup.getStartRes();
 +      end = selectionGroup.getEndRes() + 1;
 +    }
 +    else
 +    {
 +      end = alignment.getWidth();
 +    }
 +    return (alignment.getHiddenColumns().getVisContigsIterator(start, end,
 +            false));
 +  }
    /**
     * Filters out sequences with an eValue higher than the specified value. The
     * filtered sequences are hidden or deleted. Sequences with no eValues are also
@@@ -23,7 -23,6 +23,7 @@@ import jalview.api.AlignCalcManagerI2
  import jalview.api.AlignCalcWorkerI;
  import jalview.api.PollableAlignCalcWorkerI;
  import jalview.bin.Cache;
 +import jalview.bin.Console;
  import jalview.datamodel.AlignmentAnnotation;
  
  public class AlignCalcManager2 implements AlignCalcManagerI2
          throw new IllegalStateException(
                  "Cannot submit new task if the prevoius one is still running");
        }
 -      Cache.log.debug(
 -              format("Worker %s queued", getWorker().getClass().getName()));
 +      Console.debug(
-               format("Worker %s queued", getWorker().getClass().getName()));
++              format("Worker %s queued", getWorker()));
        task = executor.submit(() -> {
          try
          {
-           Console.debug(format("Worker %s started",
-                   getWorker().getClass().getName()));
 -          Cache.log.debug(format("Worker %s started", getWorker()));
++          Console.debug(format("Worker %s started", getWorker()));
            getWorker().run();
-           Console.debug(format("Worker %s finished",
-                   getWorker().getClass().getName()));
 -          Cache.log.debug(format("Worker %s finished", getWorker()));
++          Console.debug(format("Worker %s finished", getWorker()));
          } catch (InterruptedException e)
          {
-           Console.debug(format("Worker %s interrupted",
-                   getWorker().getClass().getName()));
 -          Cache.log.debug(format("Worker %s interrupted", getWorker()));
++          Console.debug(format("Worker %s interrupted", getWorker()));
          } catch (Throwable th)
          {
-           Console.debug(format("Worker %s failed",
-                   getWorker().getClass().getName()), th);
 -          Cache.log.debug(format("Worker %s failed", getWorker()), th);
++          Console.debug(format("Worker %s failed", getWorker()), th);
          } finally
          {
            if (!isRegistered())
        {
          return;
        }
-       Console.debug(format("Cancelling worker %s",
-               getWorker().getClass().getName()));
 -      Cache.log.debug(format("Cancelling worker %s", getWorker()));
++      Console.debug(format("Cancelling worker %s", getWorker()));
        task.cancel(true);
      }
    }
        if (task != null && !(task.isDone() || task.isCancelled()))
        {
          throw new IllegalStateException(
--                "Cannot submit new task if the prevoius one is still running");
++                "Cannot submit new task if the previous one is still running");
        }
-       Console.debug(
-               format("Worker %s queued", getWorker().getClass().getName()));
 -      Cache.log.debug(
 -              format("Worker %s queued", getWorker()));
++      Console.debug( format("Worker %s queued", getWorker()));
        final var runnable = new Runnable()
        {
          private boolean started = false;
            {
              if (!started)
              {
-               Console.debug(format("Worker %s started",
-                       getWorker().getClass().getName()));
 -              Cache.log.debug(format("Worker %s started", getWorker()));
++              Console.debug(format("Worker %s started", getWorker()));
                getWorker().startUp();
                started = true;
              }
              else if (!completed)
              {
-               Console.debug(format("Polling worker %s",
-                       getWorker().getClass().getName()));
 -              Cache.log.debug(format("Polling worker %s", getWorker()));
++              Console.debug(format("Polling worker %s", getWorker()));
                if (getWorker().poll())
                {
-                 Console.debug(format("Worker %s finished",
-                         getWorker().getClass().getName()));
 -                Cache.log.debug(format("Worker %s finished", getWorker()));
++                Console.debug(format("Worker %s finished", getWorker()));
                  completed = true;
                }
              }
            } catch (Throwable th)
            {
-             Console.debug(format("Worker %s failed",
-                     getWorker().getClass().getName()), th);
 -            Cache.log.debug(format("Worker %s failed", getWorker()), th);
++            Console.debug(format("Worker %s failed", getWorker()), th);
              completed = true;
            }
            if (completed)
              final var worker = getWorker();
              if (!isRegistered())
                PollableWorkerManager.super.worker = null;
-             Console.debug(format("Finalizing completed worker %s",
-                     worker.getClass().getName()));
 -            Cache.log.debug(format("Finalizing completed worker %s", worker));
++            Console.debug(format("Finalizing completed worker %s", worker));
              worker.done();
              // almost impossible, but the future may be null at this point
              // let it throw NPE to cancel forcefully
        {
          return;
        }
-       Console.debug(format("Cancelling worker %s",
-               getWorker().getClass().getName()));
 -      Cache.log.debug(format("Cancelling worker %s", getWorker()));
++      Console.debug(format("Cancelling worker %s", getWorker()));
        task.cancel(false);
        executor.submit(() -> {
          final var worker = getWorker();
          if (worker != null)
          {
            worker.cancel();
-           Console.debug(format("Finalizing cancelled worker %s",
-                   worker.getClass().getName()));
 -          Cache.log.debug(format("Finalizing cancelled worker %s", worker));
++          Console.debug(format("Finalizing cancelled worker %s", worker));
            worker.done();
          }
        });
      var manager = registered.get(worker);
      if (manager == null)
      {
 -      Cache.log.warn("Starting unregistered worker " + worker);
 +      Console.warn("Starting unregistered worker " + worker);
        manager = createManager(worker);
        oneshot.put(worker, manager);
      }