Merge branch 'releases/Release_2_11_3_Branch'
[jalview.git] / src / jalview / gui / CalculationChooser.java
index d3b7cfc..af41e58 100644 (file)
  */
 package jalview.gui;
 
-import jalview.analysis.TreeBuilder;
-import jalview.analysis.scoremodels.ScoreModels;
-import jalview.analysis.scoremodels.SimilarityParams;
-import jalview.api.analysis.ScoreModelI;
-import jalview.api.analysis.SimilarityParamsI;
-import jalview.util.MessageManager;
-
+import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Component;
+import java.awt.Dimension;
 import java.awt.FlowLayout;
 import java.awt.Font;
 import java.awt.GridLayout;
+import java.awt.Insets;
 import java.awt.event.ActionEvent;
-import java.awt.event.ItemEvent;
-import java.awt.event.ItemListener;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 import java.beans.PropertyVetoException;
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.swing.BorderFactory;
 import javax.swing.ButtonGroup;
+import javax.swing.DefaultComboBoxModel;
 import javax.swing.JButton;
 import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
@@ -50,6 +49,17 @@ import javax.swing.JLabel;
 import javax.swing.JLayeredPane;
 import javax.swing.JPanel;
 import javax.swing.JRadioButton;
+import javax.swing.event.InternalFrameAdapter;
+import javax.swing.event.InternalFrameEvent;
+
+import jalview.analysis.TreeBuilder;
+import jalview.analysis.scoremodels.ScoreModels;
+import jalview.analysis.scoremodels.SimilarityParams;
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.bin.Cache;
+import jalview.datamodel.SequenceGroup;
+import jalview.util.MessageManager;
 
 /**
  * A dialog where a user can choose and action Tree or PCA calculation options
@@ -66,21 +76,23 @@ public class CalculationChooser extends JPanel
 
   private static final Font VERDANA_11PT = new Font("Verdana", 0, 11);
 
+  private static final int MIN_TREE_SELECTION = 3;
+
+  private static final int MIN_PCA_SELECTION = 4;
+
   AlignFrame af;
 
   JRadioButton pca;
 
-  JRadioButton tree;
-
   JRadioButton neighbourJoining;
 
   JRadioButton averageDistance;
 
   JComboBox<String> modelNames;
 
-  private JInternalFrame frame;
+  JButton calculate;
 
-  private ButtonGroup treeTypes;
+  private JInternalFrame frame;
 
   private JCheckBox includeGaps;
 
@@ -90,6 +102,15 @@ public class CalculationChooser extends JPanel
 
   private JCheckBox shorterSequence;
 
+  final ComboBoxTooltipRenderer renderer = new ComboBoxTooltipRenderer();
+
+  List<String> tips = new ArrayList<>();
+
+  /*
+   * the most recently opened PCA results panel
+   */
+  private PCAPanel pcaPanel;
+
   /**
    * Constructor
    * 
@@ -99,6 +120,7 @@ public class CalculationChooser extends JPanel
   {
     this.af = alignFrame;
     init();
+    af.alignPanel.setCalculationDialog(this);
   }
 
   /**
@@ -106,65 +128,95 @@ public class CalculationChooser extends JPanel
    */
   void init()
   {
+    setLayout(new BorderLayout());
     frame = new JInternalFrame();
+    frame.setFrameIcon(null);
     frame.setContentPane(this);
     this.setBackground(Color.white);
+    frame.addFocusListener(new FocusListener()
+    {
 
+      @Override
+      public void focusLost(FocusEvent e)
+      {
+      }
+
+      @Override
+      public void focusGained(FocusEvent e)
+      {
+        validateCalcTypes();
+      }
+    });
     /*
-     * Layout consists of 4 or 5 panels:
-     * - first with choice of Tree or PCA
-     * - second with choice of tree method NJ or AV
-     * - third with choice of score model
-     * - fourth with score model parameter options [suppressed]
-     * - fifth with OK and Cancel
+     * Layout consists of 3 or 4 panels:
+     * - first with choice of PCA or tree method NJ or AV
+     * - second with choice of score model
+     * - third with score model parameter options [suppressed]
+     * - fourth with OK and Cancel
      */
-    tree = new JRadioButton(MessageManager.getString("label.tree"));
-    tree.setOpaque(false);
     pca = new JRadioButton(
             MessageManager.getString("label.principal_component_analysis"));
     pca.setOpaque(false);
+
     neighbourJoining = new JRadioButton(
             MessageManager.getString("label.tree_calc_nj"));
+    neighbourJoining.setSelected(true);
+    neighbourJoining.setOpaque(false);
+
     averageDistance = new JRadioButton(
             MessageManager.getString("label.tree_calc_av"));
-    ItemListener listener = new ItemListener()
+    averageDistance.setOpaque(false);
+
+    JPanel calcChoicePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    calcChoicePanel.setOpaque(false);
+
+    // first create the Tree calculation's border panel
+    JPanel treePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    treePanel.setOpaque(false);
+
+    JvSwingUtils.createTitledBorder(treePanel,
+            MessageManager.getString("label.tree"), true);
+
+    // then copy the inset dimensions for the border-less PCA panel
+    JPanel pcaBorderless = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    Insets b = treePanel.getBorder().getBorderInsets(treePanel);
+    pcaBorderless.setBorder(
+            BorderFactory.createEmptyBorder(2, b.left, 2, b.right));
+    pcaBorderless.setOpaque(false);
+
+    pcaBorderless.add(pca, FlowLayout.LEFT);
+    calcChoicePanel.add(pcaBorderless, FlowLayout.LEFT);
+
+    treePanel.add(neighbourJoining);
+    treePanel.add(averageDistance);
+
+    calcChoicePanel.add(treePanel);
+
+    ButtonGroup calcTypes = new ButtonGroup();
+    calcTypes.add(pca);
+    calcTypes.add(neighbourJoining);
+    calcTypes.add(averageDistance);
+
+    ActionListener calcChanged = new ActionListener()
     {
       @Override
-      public void itemStateChanged(ItemEvent e)
+      public void actionPerformed(ActionEvent e)
       {
-        neighbourJoining.setEnabled(tree.isSelected());
-        averageDistance.setEnabled(tree.isSelected());
+        validateCalcTypes();
       }
     };
-    pca.addItemListener(listener);
-    tree.addItemListener(listener);
-    ButtonGroup calcTypes = new ButtonGroup();
-    calcTypes.add(pca);
-    calcTypes.add(tree);
-    JPanel calcChoicePanel = new JPanel();
-    calcChoicePanel.setOpaque(false);
-    tree.setSelected(true);
-    calcChoicePanel.add(tree);
-    calcChoicePanel.add(pca);
-
-    neighbourJoining.setOpaque(false);
-    treeTypes = new ButtonGroup();
-    treeTypes.add(neighbourJoining);
-    treeTypes.add(averageDistance);
-    neighbourJoining.setSelected(true);
-    JPanel treeChoicePanel = new JPanel();
-    treeChoicePanel.setOpaque(false);
-    treeChoicePanel.add(neighbourJoining);
-    treeChoicePanel.add(averageDistance);
+    pca.addActionListener(calcChanged);
+    neighbourJoining.addActionListener(calcChanged);
+    averageDistance.addActionListener(calcChanged);
 
     /*
      * score models drop-down - with added tooltips!
      */
     modelNames = buildModelOptionsList();
 
-    JPanel scoreModelPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    JPanel scoreModelPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
     scoreModelPanel.setOpaque(false);
-    scoreModelPanel.add(modelNames, FlowLayout.LEFT);
+    scoreModelPanel.add(modelNames);
 
     /*
      * score model parameters
@@ -184,51 +236,144 @@ public class CalculationChooser extends JPanel
     /*
      * OK / Cancel buttons
      */
-    JButton ok = new JButton(MessageManager.getString("action.ok"));
-    ok.setFont(VERDANA_11PT);
-    ok.addActionListener(new java.awt.event.ActionListener()
+    calculate = new JButton(MessageManager.getString("action.calculate"));
+    calculate.setFont(VERDANA_11PT);
+    calculate.addActionListener(new java.awt.event.ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        ok_actionPerformed();
+        calculate_actionPerformed();
       }
     });
-    JButton cancel = new JButton(MessageManager.getString("action.cancel"));
-    cancel.setFont(VERDANA_11PT);
-    cancel.addActionListener(new java.awt.event.ActionListener()
+    JButton close = new JButton(MessageManager.getString("action.close"));
+    close.setFont(VERDANA_11PT);
+    close.addActionListener(new java.awt.event.ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        cancel_actionPerformed(e);
+        close_actionPerformed();
       }
     });
     JPanel actionPanel = new JPanel();
     actionPanel.setOpaque(false);
-    actionPanel.add(ok);
-    actionPanel.add(cancel);
+    actionPanel.add(calculate);
+    actionPanel.add(close);
 
     boolean includeParams = false;
-    this.add(calcChoicePanel);
-    this.add(treeChoicePanel);
-    this.add(scoreModelPanel);
+    this.add(calcChoicePanel, BorderLayout.CENTER);
+    calcChoicePanel.add(scoreModelPanel);
     if (includeParams)
     {
-      this.add(paramsPanel);
+      scoreModelPanel.add(paramsPanel);
     }
-    this.add(actionPanel);
+    this.add(actionPanel, BorderLayout.SOUTH);
 
     int width = 350;
-    int height = includeParams ? 400 : 220;
-    Desktop.addInternalFrame(frame,
-            MessageManager.getString("label.choose_calculation"), width,
-            height, false);
+    int height = includeParams ? 420 : 240;
+
+    setMinimumSize(new Dimension(325, height - 10));
+    String title = MessageManager.getString("label.choose_calculation");
+    if (af.getViewport().getViewName() != null)
+    {
+      title = title + " (" + af.getViewport().getViewName() + ")";
+    }
 
+    Desktop.addInternalFrame(frame, title, width, height, false);
+    calcChoicePanel.doLayout();
+    revalidate();
+    /*
+     * null the AlignmentPanel's reference to the dialog when it is closed
+     */
+    frame.addInternalFrameListener(new InternalFrameAdapter()
+    {
+      @Override
+      public void internalFrameClosed(InternalFrameEvent evt)
+      {
+        af.alignPanel.setCalculationDialog(null);
+      };
+    });
+
+    validateCalcTypes();
     frame.setLayer(JLayeredPane.PALETTE_LAYER);
   }
 
   /**
+   * enable calculations applicable for the current alignment or selection.
+   */
+  protected void validateCalcTypes()
+  {
+    int size = af.getViewport().getAlignment().getHeight();
+    if (af.getViewport().getSelectionGroup() != null)
+    {
+      size = af.getViewport().getSelectionGroup().getSize();
+    }
+
+    /*
+     * disable calc options for which there is insufficient input data
+     * return value of true means enabled and selected
+     */
+    boolean checkPca = checkEnabled(pca, size, MIN_PCA_SELECTION);
+    boolean checkNeighbourJoining = checkEnabled(neighbourJoining, size,
+            MIN_TREE_SELECTION);
+    boolean checkAverageDistance = checkEnabled(averageDistance, size,
+            MIN_TREE_SELECTION);
+
+    if (checkPca || checkNeighbourJoining || checkAverageDistance)
+    {
+      calculate.setToolTipText(null);
+      calculate.setEnabled(true);
+    }
+    else
+    {
+      calculate.setEnabled(false);
+    }
+    updateScoreModels(modelNames, tips);
+  }
+
+  /**
+   * Check the input and disable a calculation's radio button if necessary. A
+   * tooltip is shown for disabled calculations.
+   * 
+   * @param calc
+   *          - radio button for the calculation being validated
+   * @param size
+   *          - size of input to calculation
+   * @param minsize
+   *          - minimum size for calculation
+   * @return true if size >= minsize and calc.isSelected
+   */
+  private boolean checkEnabled(JRadioButton calc, int size, int minsize)
+  {
+    String ttip = MessageManager
+            .formatMessage("label.you_need_at_least_n_sequences", minsize);
+
+    calc.setEnabled(size >= minsize);
+    if (!calc.isEnabled())
+    {
+      calc.setToolTipText(ttip);
+    }
+    else
+    {
+      calc.setToolTipText(null);
+    }
+    if (calc.isSelected())
+    {
+      modelNames.setEnabled(calc.isEnabled());
+      if (calc.isEnabled())
+      {
+        return true;
+      }
+      else
+      {
+        calculate.setToolTipText(ttip);
+      }
+    }
+    return false;
+  }
+
+  /**
    * A rather elaborate helper method (blame Swing, not me) that builds a
    * drop-down list of score models (by name) with descriptions as tooltips.
    * There is also a tooltip shown for the currently selected item when hovering
@@ -236,86 +381,150 @@ public class CalculationChooser extends JPanel
    */
   protected JComboBox<String> buildModelOptionsList()
   {
-    final JComboBox<String> comboBox = new JComboBox<String>();
-    ComboBoxTooltipRenderer renderer = new ComboBoxTooltipRenderer();
-    comboBox.setRenderer(renderer);
-    final List<String> tips = new ArrayList<String>();
+    final JComboBox<String> scoreModelsCombo = new JComboBox<>();
+    scoreModelsCombo.setRenderer(renderer);
 
     /*
      * show tooltip on mouse over the combobox
      * note the listener has to be on the components that make up
      * the combobox, doesn't work if just on the combobox
      */
-    MouseAdapter mouseListener = new MouseAdapter()
+    final MouseAdapter mouseListener = new MouseAdapter()
     {
       @Override
       public void mouseEntered(MouseEvent e)
       {
-        comboBox.setToolTipText(tips.get(comboBox.getSelectedIndex()));
+        scoreModelsCombo.setToolTipText(
+                tips.get(scoreModelsCombo.getSelectedIndex()));
       }
 
       @Override
       public void mouseExited(MouseEvent e)
       {
-        comboBox.setToolTipText(null);
+        scoreModelsCombo.setToolTipText(null);
       }
     };
-    for (Component c : comboBox.getComponents())
+    for (Component c : scoreModelsCombo.getComponents())
     {
       c.addMouseListener(mouseListener);
     }
 
+    updateScoreModels(scoreModelsCombo, tips);
+
+    /*
+     * set the list of tooltips on the combobox's renderer
+     */
+    renderer.setTooltips(tips);
+
+    return scoreModelsCombo;
+  }
+
+  private void updateScoreModels(JComboBox<String> comboBox,
+          List<String> toolTips)
+  {
+    Object curSel = comboBox.getSelectedItem();
+    toolTips.clear();
+    DefaultComboBoxModel<String> model = new DefaultComboBoxModel<>();
+
+    /*
+     * select the score models applicable to the alignment type
+     */
+    boolean nucleotide = af.getViewport().getAlignment().isNucleotide();
+    List<ScoreModelI> models = getApplicableScoreModels(nucleotide,
+            pca.isSelected());
+
     /*
      * now we can actually add entries to the combobox,
      * remembering their descriptions for tooltips
      */
-    ScoreModels scoreModels = ScoreModels.getInstance();
-    for (ScoreModelI sm : scoreModels.getModels())
+    boolean selectedIsPresent = false;
+    for (ScoreModelI sm : models)
     {
-      boolean nucleotide = af.getViewport().getAlignment().isNucleotide();
-      if (sm.isDNA() && nucleotide || sm.isProtein() && !nucleotide)
+      if (curSel != null && sm.getName().equals(curSel))
       {
-        comboBox.addItem(sm.getName());
-
-        /*
-         * tooltip is description if provided, else text lookup with
-         * fallback on the model name
-         */
-        String tooltip = sm.getDescription();
-        if (tooltip == null)
-        {
-          tooltip = MessageManager.getStringOrReturn("label.score_model_",
-                  sm.getName());
-        }
-        tips.add(tooltip);
+        selectedIsPresent = true;
+        curSel = sm.getName();
       }
+      model.addElement(sm.getName());
 
       /*
-       * set the list of tooltips on the combobox's renderer
+       * tooltip is description if provided, else text lookup with
+       * fallback on the model name
        */
-      renderer.setTooltips(tips);
+      String tooltip = sm.getDescription();
+      if (tooltip == null)
+      {
+        tooltip = MessageManager.getStringOrReturn("label.score_model_",
+                sm.getName());
+      }
+      toolTips.add(tooltip);
     }
 
-    return comboBox;
+    if (selectedIsPresent)
+    {
+      model.setSelectedItem(curSel);
+    }
+    // finally, update the model
+    comboBox.setModel(model);
   }
 
   /**
-   * Open and calculate the selected tree on 'OK'
+   * Builds a list of score models which are applicable for the alignment and
+   * calculation type (peptide or generic models for protein, nucleotide or
+   * generic models for nucleotide).
+   * <p>
+   * As a special case, includes BLOSUM62 as an extra option for nucleotide PCA.
+   * This is for backwards compatibility with Jalview prior to 2.8 when BLOSUM62
+   * was the only score matrix supported. This is included if property
+   * BLOSUM62_PCA_FOR_NUCLEOTIDE is set to true in the Jalview properties file.
+   * 
+   * @param nucleotide
+   * @param forPca
+   * @return
+   */
+  protected static List<ScoreModelI> getApplicableScoreModels(
+          boolean nucleotide, boolean forPca)
+  {
+    List<ScoreModelI> filtered = new ArrayList<>();
+
+    ScoreModels scoreModels = ScoreModels.getInstance();
+    for (ScoreModelI sm : scoreModels.getModels())
+    {
+      if (!nucleotide && sm.isProtein() || nucleotide && sm.isDNA())
+      {
+        filtered.add(sm);
+      }
+    }
+
+    /*
+     * special case: add BLOSUM62 as last option for nucleotide PCA, 
+     * for backwards compatibility with Jalview < 2.8 (JAL-2962)
+     */
+    if (nucleotide && forPca
+            && Cache.getDefault("BLOSUM62_PCA_FOR_NUCLEOTIDE", false))
+    {
+      filtered.add(scoreModels.getBlosum62());
+    }
+
+    return filtered;
+  }
+
+  /**
+   * Open and calculate the selected tree or PCA on 'OK'
    */
-  protected void ok_actionPerformed()
+  protected void calculate_actionPerformed()
   {
     boolean doPCA = pca.isSelected();
-    ScoreModelI sm = ScoreModels.getInstance().forName(
-            modelNames.getSelectedItem().toString());
+    String modelName = modelNames.getSelectedItem().toString();
     SimilarityParamsI params = getSimilarityParameters(doPCA);
 
     if (doPCA)
     {
-      openPcaPanel(sm, params);
+      openPcaPanel(modelName, params);
     }
     else
     {
-      openTreePanel(sm, params);
+      openTreePanel(modelName, params);
     }
 
     // closeFrame();
@@ -324,41 +533,69 @@ public class CalculationChooser extends JPanel
   /**
    * Open a new Tree panel on the desktop
    * 
-   * @param sm
+   * @param modelName
    * @param params
    */
-  protected void openTreePanel(ScoreModelI sm, SimilarityParamsI params)
+  protected void openTreePanel(String modelName, SimilarityParamsI params)
   {
-    String treeType = neighbourJoining.isSelected() ? TreeBuilder.NEIGHBOUR_JOINING
+    /*
+     * gui validation shouldn't allow insufficient sequences here, but leave
+     * this check in in case this method gets exposed programmatically in future
+     */
+    AlignViewport viewport = af.getViewport();
+    SequenceGroup sg = viewport.getSelectionGroup();
+    if (sg != null && sg.getSize() < MIN_TREE_SELECTION)
+    {
+      JvOptionPane.showMessageDialog(Desktop.desktop,
+              MessageManager.formatMessage(
+                      "label.you_need_at_least_n_sequences",
+                      MIN_TREE_SELECTION),
+              MessageManager.getString("label.not_enough_sequences"),
+              JvOptionPane.WARNING_MESSAGE);
+      return;
+    }
+
+    String treeType = neighbourJoining.isSelected()
+            ? TreeBuilder.NEIGHBOUR_JOINING
             : TreeBuilder.AVERAGE_DISTANCE;
-    af.newTreePanel(treeType, sm, params);
+    af.newTreePanel(treeType, modelName, params);
   }
 
   /**
    * Open a new PCA panel on the desktop
    * 
-   * @param sm
+   * @param modelName
    * @param params
    */
-  protected void openPcaPanel(ScoreModelI sm, SimilarityParamsI params)
+  protected void openPcaPanel(String modelName, SimilarityParamsI params)
   {
     AlignViewport viewport = af.getViewport();
+
+    /*
+     * gui validation shouldn't allow insufficient sequences here, but leave
+     * this check in in case this method gets exposed programmatically in future
+     */
     if (((viewport.getSelectionGroup() != null)
-            && (viewport.getSelectionGroup().getSize() < 4) && (viewport
-            .getSelectionGroup().getSize() > 0))
-            || (viewport.getAlignment().getHeight() < 4))
+            && (viewport.getSelectionGroup().getSize() < MIN_PCA_SELECTION)
+            && (viewport.getSelectionGroup().getSize() > 0))
+            || (viewport.getAlignment().getHeight() < MIN_PCA_SELECTION))
     {
-      JvOptionPane
-              .showInternalMessageDialog(
-                      this,
-                      MessageManager
-                              .getString("label.principal_component_analysis_must_take_least_four_input_sequences"),
-                      MessageManager
-                              .getString("label.sequence_selection_insufficient"),
-                      JvOptionPane.WARNING_MESSAGE);
+      JvOptionPane.showInternalMessageDialog(this,
+              MessageManager.formatMessage(
+                      "label.you_need_at_least_n_sequences",
+                      MIN_PCA_SELECTION),
+              MessageManager
+                      .getString("label.sequence_selection_insufficient"),
+              JvOptionPane.WARNING_MESSAGE);
       return;
     }
-    new PCAPanel(af.alignPanel, sm, params);
+
+    /*
+     * construct the panel and kick off its calculation thread
+     */
+    pcaPanel = new PCAPanel(af.alignPanel, modelName, params);
+    new Thread(pcaPanel).start();
+
   }
 
   /**
@@ -400,15 +637,14 @@ public class CalculationChooser extends JPanel
      */
     boolean matchGap = doPCA ? false : treeMatchGaps;
 
-    return new SimilarityParams(includeGapGap, matchGap, includeGapResidue, matchOnShortestLength);
+    return new SimilarityParams(includeGapGap, matchGap, includeGapResidue,
+            matchOnShortestLength);
   }
 
   /**
-   * Closes dialog on cancel
-   * 
-   * @param e
+   * Closes dialog on Close button press
    */
-  protected void cancel_actionPerformed(ActionEvent e)
+  protected void close_actionPerformed()
   {
     try
     {
@@ -417,4 +653,9 @@ public class CalculationChooser extends JPanel
     {
     }
   }
+
+  public PCAPanel getPcaPanel()
+  {
+    return pcaPanel;
+  }
 }