X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;ds=sidebyside;f=src%2Fjalview%2Fgui%2FCalculationChooser.java;h=524830670e5cf9528a67b0f7e23add700f7bcdbf;hb=b87512d6e28a2a93ea2f08dcfbee320856c5c8de;hp=faaf86b31e2ae37c7d9485088692c0301f8f318c;hpb=050133c52e350a0e76b94ab0c1243aea7613b042;p=jalview.git diff --git a/src/jalview/gui/CalculationChooser.java b/src/jalview/gui/CalculationChooser.java index faaf86b..5248306 100644 --- a/src/jalview/gui/CalculationChooser.java +++ b/src/jalview/gui/CalculationChooser.java @@ -25,18 +25,31 @@ 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; +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; @@ -45,10 +58,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; /** - * A dialog where a user can choose and action Tree or PCA calculation options + * A dialog where a user can choose and action Tree or PCA calculation options. + * + * Allows also for dialog-free static methods openPCAPanel(...) and + * openTreePanel(...) for scripted use. + * */ +@SuppressWarnings("serial") public class CalculationChooser extends JPanel { /* @@ -59,23 +79,25 @@ public class CalculationChooser extends JPanel */ private static boolean treeMatchGaps = true; - private static final Font VERDANA_11PT = new Font("Verdana", 0, 11); + private static Font VERDANA_11PT; + + 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 modelNames; - private JInternalFrame frame; + JButton calculate; - private ButtonGroup treeTypes; + private JInternalFrame frame; private JCheckBox includeGaps; @@ -85,6 +107,46 @@ public class CalculationChooser extends JPanel private JCheckBox shorterSequence; + private static ComboBoxTooltipRenderer renderer; // BH was not static + + List tips = new ArrayList<>(); + + /* + * the most recently opened PCA results panel + */ + private PCAPanel pcaPanel; + + /** + * Open a new Tree panel on the desktop statically. Params are standard (not + * set by Groovy). No dialog is opened. + * + * @param af + * @param treeType + * @param modelName + * @return null if successful; the string + * "label.you_need_at_least_n_sequences" if number of sequences + * selected is inappropriate + */ + public static Object openTreePanel(AlignFrame af, String treeType, + String modelName) + { + return openTreePanel(af, treeType, modelName, null); + } + + /** + * public static method for JalviewJS API to open a PCAPanel without + * necessarily using a dialog. + * + * @param af + * @param modelName + * @return the PCAPanel, or the string "label.you_need_at_least_n_sequences" + * if number of sequences selected is inappropriate + */ + public static Object openPcaPanel(AlignFrame af, String modelName) + { + return openPcaPanel(af, modelName, null); + } + /** * Constructor * @@ -94,6 +156,7 @@ public class CalculationChooser extends JPanel { this.af = alignFrame; init(); + af.alignPanel.setCalculationDialog(this); } /** @@ -101,74 +164,94 @@ public class CalculationChooser extends JPanel */ void init() { + setLayout(new BorderLayout()); frame = new JInternalFrame(); 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 model drop-down + * score models drop-down - with added tooltips! */ - modelNames = new JComboBox(); - ScoreModels scoreModels = ScoreModels.getInstance(); - for (ScoreModelI sm : scoreModels.getModels()) - { - boolean nucleotide = af.getViewport().getAlignment().isNucleotide(); - if (sm.isDNA() && nucleotide || sm.isProtein() && !nucleotide) - { - modelNames.addItem(sm.getName()); - } - } + 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 @@ -185,70 +268,307 @@ public class CalculationChooser extends JPanel paramsPanel.add(includeGappedColumns); paramsPanel.add(shorterSequence); + if (VERDANA_11PT == null) + { + VERDANA_11PT = new Font("Verdana", 0, 11); + } /* * 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); } /** - * Open and calculate the selected tree on 'OK' + * 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 + * over it (without opening the list). */ - protected void ok_actionPerformed() + protected JComboBox buildModelOptionsList() + { + JComboBox scoreModelsCombo = new JComboBox<>(); + if (renderer == null) + { + renderer = new ComboBoxTooltipRenderer(); + } + 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 + */ + final MouseAdapter mouseListener = new MouseAdapter() + { + @Override + public void mouseEntered(MouseEvent e) + { + scoreModelsCombo.setToolTipText( + tips.get(scoreModelsCombo.getSelectedIndex())); + } + + @Override + public void mouseExited(MouseEvent e) + { + scoreModelsCombo.setToolTipText(null); + } + }; + 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 comboBox, + List toolTips) + { + Object curSel = comboBox.getSelectedItem(); + toolTips.clear(); + DefaultComboBoxModel model = new DefaultComboBoxModel<>(); + + /* + * select the score models applicable to the alignment type + */ + boolean nucleotide = af.getViewport().getAlignment().isNucleotide(); + List models = getApplicableScoreModels(nucleotide, + pca.isSelected()); + + /* + * now we can actually add entries to the combobox, + * remembering their descriptions for tooltips + */ + boolean selectedIsPresent = false; + for (ScoreModelI sm : models) + { + if (curSel != null && sm.getName().equals(curSel)) + { + selectedIsPresent = true; + curSel = sm.getName(); + } + model.addElement(sm.getName()); + + /* + * tooltip is description if provided, else text lookup with + * fallback on the model name + */ + String tooltip = sm.getDescription(); + if (tooltip == null) + { + tooltip = MessageManager.getStringOrReturn("label.score_model_", + sm.getName()); + } + toolTips.add(tooltip); + } + + if (selectedIsPresent) + { + model.setSelectedItem(curSel); + } + // finally, update the model + comboBox.setModel(model); + } + + /** + * 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). + *

+ * 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 getApplicableScoreModels( + boolean nucleotide, boolean forPca) + { + List 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(Preferences.BLOSUM62_PCA_FOR_NUCLEOTIDE, + false)) + { + filtered.add(scoreModels.getBlosum62()); + } + + return filtered; + } + + /** + * Open and calculate the selected tree or PCA on 'OK' + */ + protected void calculate_actionPerformed() { 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(); @@ -257,41 +577,128 @@ 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 - : TreeBuilder.AVERAGE_DISTANCE; - af.newTreePanel(treeType, sm, params); + Object ret = openTreePanel(af, + neighbourJoining.isSelected() ? TreeBuilder.NEIGHBOUR_JOINING + : TreeBuilder.AVERAGE_DISTANCE, + modelName, params); + if (ret instanceof String) + { + JvOptionPane.showMessageDialog(this, // was opening on Desktop? + MessageManager.formatMessage( + (String) ret, + MIN_TREE_SELECTION), + MessageManager.getString("label.not_enough_sequences"), + JvOptionPane.WARNING_MESSAGE); + + } } /** * Open a new PCA panel on the desktop * - * @param sm + * @param modelName + * @param params + */ + protected void openPcaPanel(String modelName, SimilarityParamsI params) + { + Object ret = openPcaPanel(af, modelName, params); + if (ret instanceof String) + { + JvOptionPane.showInternalMessageDialog(this, + MessageManager.formatMessage( + (String) ret, + MIN_PCA_SELECTION), + MessageManager + .getString("label.sequence_selection_insufficient"), + JvOptionPane.WARNING_MESSAGE); + } + else + { + // only used for test suite + pcaPanel = (PCAPanel) ret; + } + + } + + /** + * Open a new Tree panel on the desktop statically + * + * @param af + * @param treeType + * @param modelName + * @param params + * @return null, or the string "label.you_need_at_least_n_sequences" if number + * of sequences selected is inappropriate + */ + public static Object openTreePanel(AlignFrame af, String treeType, + String modelName, SimilarityParamsI params) + { + + /* + * 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) + { + return "label.you_need_at_least_n_sequences"; + } + + if (params == null) + { + params = getSimilarityParameters(false); + } + + af.newTreePanel(treeType, modelName, params); + return null; + } + + /** + * public static method for JalviewJS API + * + * @param af + * @param modelName * @param params + * @return the PCAPanel, or null if number of sequences selected is + * inappropriate */ - protected void openPcaPanel(ScoreModelI sm, SimilarityParamsI params) + public static Object openPcaPanel(AlignFrame af, 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)) - { - JvOptionPane - .showInternalMessageDialog( - this, - MessageManager - .getString("label.principal_component_analysis_must_take_least_four_input_sequences"), - MessageManager - .getString("label.sequence_selection_insufficient"), - JvOptionPane.WARNING_MESSAGE); - return; + && (viewport.getSelectionGroup().getSize() < MIN_PCA_SELECTION) + && (viewport.getSelectionGroup().getSize() > 0)) + || (viewport.getAlignment().getHeight() < MIN_PCA_SELECTION)) + { + return "label.you_need_at_least_n_sequences"; } - new PCAPanel(af.alignPanel, sm, params); + + if (params == null) + { + params = getSimilarityParameters(true); + } + + /* + * construct the panel and kick off its calculation thread + */ + PCAPanel pcap = new PCAPanel(af.alignPanel, modelName, params); + new Thread(pcap).start(); + return pcap; } /** @@ -307,6 +714,7 @@ public class CalculationChooser extends JPanel } } + /** * Returns a data bean holding parameters for similarity (or distance) model * calculation @@ -314,7 +722,8 @@ public class CalculationChooser extends JPanel * @param doPCA * @return */ - protected SimilarityParamsI getSimilarityParameters(boolean doPCA) + public static SimilarityParamsI getSimilarityParameters( + boolean doPCA) { // commented out: parameter choices read from gui widgets // SimilarityParamsI params = new SimilarityParams( @@ -333,15 +742,15 @@ 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 { @@ -350,4 +759,9 @@ public class CalculationChooser extends JPanel { } } + + public PCAPanel getPcaPanel() + { + return pcaPanel; + } }