/* * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * Jalview is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ package jalview.gui; import jalview.api.FeatureColourI; import jalview.datamodel.GraphLine; import jalview.datamodel.features.FeatureAttributes; import jalview.schemes.FeatureColour; import jalview.util.MessageManager; import java.awt.Color; import java.awt.Dimension; import java.awt.FlowLayout; 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.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.List; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.JCheckBox; import javax.swing.JColorChooser; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JSlider; import javax.swing.JTextField; import javax.swing.border.LineBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; public class FeatureColourChooser extends JalviewDialog { private static final String COLON = ":"; private static final int MAX_TOOLTIP_LENGTH = 50; private static int NO_COLOUR_OPTION = 0; private static int MIN_COLOUR_OPTION = 1; private static int MAX_COLOUR_OPTION = 2; private FeatureRenderer fr; private FeatureColourI cs; private FeatureColourI oldcs; private AlignmentPanel ap; private boolean adjusting = false; private float min; private float max; private float scaleFactor; private String type = null; private JPanel minColour = new JPanel(); private JPanel maxColour = new JPanel(); private Color noColour; private JComboBox threshold = new JComboBox<>(); private JSlider slider = new JSlider(); private JTextField thresholdValue = new JTextField(20); private JCheckBox thresholdIsMin = new JCheckBox(); private GraphLine threshline; private Color oldmaxColour; private Color oldminColour; private Color oldNoColour; private ActionListener colourEditor = null; /* * radio buttons to select what to colour by * label, attribute text, score, attribute value */ private JRadioButton byDescription = new JRadioButton(); private JRadioButton byAttributeText = new JRadioButton(); private JRadioButton byScore = new JRadioButton(); private JRadioButton byAttributeValue = new JRadioButton(); private ActionListener changeColourAction; private ActionListener changeMinMaxAction; /* * choice of option for 'colour for no value' */ private JComboBox noValueCombo; /* * choice of attribute (if any) for 'colour by text' */ private JComboBox textAttributeCombo; /* * choice of attribute (if any) for 'colour by value' */ private JComboBox valueAttributeCombo; /** * Constructor * * @param frender * @param theType */ public FeatureColourChooser(FeatureRenderer frender, String theType) { this(frender, false, theType); } /** * Constructor, with option to make a blocking dialog (has to complete in the * AWT event queue thread). Currently this option is always set to false. * * @param frender * @param blocking * @param theType */ FeatureColourChooser(FeatureRenderer frender, boolean blocking, String theType) { this.fr = frender; this.type = theType; ap = fr.ap; String title = MessageManager.formatMessage("label.variable_color_for", new String[] { theType }); initDialogFrame(this, true, blocking, title, 470, 300); slider.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent evt) { if (!adjusting) { thresholdValue.setText((slider.getValue() / scaleFactor) + ""); sliderValueChanged(); } } }); slider.addMouseListener(new MouseAdapter() { @Override public void mouseReleased(MouseEvent evt) { /* * only update Overview and/or structure colouring * when threshold slider drag ends (mouse up) */ if (ap != null) { ap.paintAlignment(true, true); } } }); // todo move all threshold setup inside a method float mm[] = fr.getMinMax().get(theType)[0]; min = mm[0]; max = mm[1]; /* * ensure scale factor allows a scaled range with * 10 integer divisions ('ticks'); if we have got here, * we should expect that max != min */ scaleFactor = (max == min) ? 1f : 100f / (max - min); oldcs = fr.getFeatureColours().get(theType); if (!oldcs.isSimpleColour()) { if (oldcs.isAutoScaled()) { // update the scale cs = new FeatureColour((FeatureColour) oldcs, min, max); } else { cs = new FeatureColour((FeatureColour) oldcs); } } else { /* * promote original simple color to a graduated color * - by score if there is a score range, else by label */ Color bl = oldcs.getColour(); if (bl == null) { bl = Color.BLACK; } // original colour becomes the maximum colour cs = new FeatureColour(Color.white, bl, mm[0], mm[1]); cs.setColourByLabel(mm[0] == mm[1]); } minColour.setBackground(oldminColour = cs.getMinColour()); maxColour.setBackground(oldmaxColour = cs.getMaxColour()); noColour = cs.getNoColour(); adjusting = true; try { jbInit(); } catch (Exception ex) { ex.printStackTrace(); return; } /* * set the initial state of options on screen */ if (cs.isColourByLabel()) { if (cs.isColourByAttribute()) { byAttributeText.setSelected(true); textAttributeCombo.setEnabled(true); String[] attributeName = cs.getAttributeName(); textAttributeCombo .setSelectedItem(String.join(COLON, attributeName)); } else { byDescription.setSelected(true); textAttributeCombo.setEnabled(false); } } else { if (cs.isColourByAttribute()) { byAttributeValue.setSelected(true); String[] attributeName = cs.getAttributeName(); valueAttributeCombo .setSelectedItem(String.join(COLON, attributeName)); valueAttributeCombo.setEnabled(true); updateMinMax(); } else { byScore.setSelected(true); valueAttributeCombo.setEnabled(false); } } if (noColour == null) { noValueCombo.setSelectedIndex(NO_COLOUR_OPTION); } else if (noColour.equals(oldminColour)) { noValueCombo.setSelectedIndex(MIN_COLOUR_OPTION); } else if (noColour.equals(oldmaxColour)) { noValueCombo.setSelectedIndex(MAX_COLOUR_OPTION); } threshline = new GraphLine((max - min) / 2f, "Threshold", Color.black); threshline.value = cs.getThreshold(); if (cs.hasThreshold()) { // initialise threshold slider and selector threshold.setSelectedIndex(cs.isAboveThreshold() ? 1 : 2); slider.setEnabled(true); slider.setValue((int) (cs.getThreshold() * scaleFactor)); thresholdValue.setEnabled(true); } adjusting = false; changeColour(false); waitForInput(); } /** * Configures the initial layout */ private void jbInit() { this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); this.setBackground(Color.white); changeColourAction = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { changeColour(true); } }; changeMinMaxAction = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { updateMinMax(); changeColour(true); } }; /* * this panel * detailsPanel * colourByTextPanel * colourByScorePanel * okCancelPanel */ JPanel detailsPanel = new JPanel(); detailsPanel.setLayout(new BoxLayout(detailsPanel, BoxLayout.Y_AXIS)); JPanel colourByTextPanel = initColourByTextPanel(); detailsPanel.add(colourByTextPanel); JPanel colourByValuePanel = initColourByValuePanel(); detailsPanel.add(colourByValuePanel); /* * 4 radio buttons select between colour by description, by * attribute text, by score, or by attribute value */ ButtonGroup bg = new ButtonGroup(); bg.add(byDescription); bg.add(byAttributeText); bg.add(byScore); bg.add(byAttributeValue); JPanel okCancelPanel = initOkCancelPanel(); this.add(detailsPanel); this.add(okCancelPanel); } /** * Updates the min-max range for a change in choice of Colour by Score, or * Colour by Attribute (value) */ protected void updateMinMax() { float[] minMax = null; if (byScore.isSelected()) { minMax = fr.getMinMax().get(type)[0]; } else if (byAttributeValue.isSelected()) { String attName = (String) valueAttributeCombo.getSelectedItem(); String[] attNames = attName.split(COLON); minMax = FeatureAttributes.getInstance().getMinMax(type, attNames); } if (minMax != null) { min = minMax[0]; max = minMax[1]; scaleFactor = (max == min) ? 1f : 100f / (max - min); slider.setValue((int) (min * scaleFactor)); } } /** * Lay out fields for graduated colour by value * * @return */ protected JPanel initColourByValuePanel() { JPanel byValuePanel = new JPanel(); byValuePanel.setLayout(new BoxLayout(byValuePanel, BoxLayout.Y_AXIS)); JvSwingUtils.createItalicTitledBorder(byValuePanel, MessageManager.getString("label.colour_by_value"), true); byValuePanel.setBackground(Color.white); /* * first row - choose colour by score or by attribute, choose attribute */ JPanel byWhatPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); byWhatPanel.setBackground(Color.white); byValuePanel.add(byWhatPanel); byScore.setText(MessageManager.getString("label.score")); byWhatPanel.add(byScore); byScore.addActionListener(changeMinMaxAction); byAttributeValue.setText(MessageManager.getString("label.attribute")); byAttributeValue.addActionListener(changeMinMaxAction); byWhatPanel.add(byAttributeValue); List attNames = FeatureAttributes.getInstance() .getAttributes(type); valueAttributeCombo = populateAttributesDropdown(type, attNames, true); /* * if no numeric atttibutes found, disable colour by attribute value */ if (valueAttributeCombo.getItemCount() == 0) { byAttributeValue.setEnabled(false); } byWhatPanel.add(valueAttributeCombo); /* * second row - min/max/no colours */ JPanel colourRangePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); colourRangePanel.setBackground(Color.white); byValuePanel.add(colourRangePanel); minColour.setFont(JvSwingUtils.getLabelFont()); minColour.setBorder(BorderFactory.createLineBorder(Color.black)); minColour.setPreferredSize(new Dimension(40, 20)); minColour.setToolTipText(MessageManager.getString("label.min_colour")); minColour.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (minColour.isEnabled()) { minColour_actionPerformed(); } } }); maxColour.setFont(JvSwingUtils.getLabelFont()); maxColour.setBorder(BorderFactory.createLineBorder(Color.black)); maxColour.setPreferredSize(new Dimension(40, 20)); maxColour.setToolTipText(MessageManager.getString("label.max_colour")); maxColour.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (maxColour.isEnabled()) { maxColour_actionPerformed(); } } }); maxColour.setBorder(new LineBorder(Color.black)); noValueCombo = new JComboBox<>(); noValueCombo.addItem(MessageManager.getString("label.no_colour")); noValueCombo.addItem(MessageManager.getString("label.min_colour")); noValueCombo.addItem(MessageManager.getString("label.max_colour")); noValueCombo.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { setNoValueColour(); } }); JLabel minText = new JLabel(MessageManager.getString("label.min_value")); minText.setFont(JvSwingUtils.getLabelFont()); JLabel maxText = new JLabel(MessageManager.getString("label.max_value")); maxText.setFont(JvSwingUtils.getLabelFont()); JLabel noText = new JLabel(MessageManager.getString("label.no_value")); noText.setFont(JvSwingUtils.getLabelFont()); colourRangePanel.add(minText); colourRangePanel.add(minColour); colourRangePanel.add(maxText); colourRangePanel.add(maxColour); colourRangePanel.add(noText); colourRangePanel.add(noValueCombo); /* * third row - threshold options and value */ JPanel thresholdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); thresholdPanel.setBackground(Color.white); byValuePanel.add(thresholdPanel); threshold.addActionListener(changeColourAction); threshold.setToolTipText(MessageManager .getString("label.threshold_feature_display_by_score")); threshold.addItem(MessageManager .getString("label.threshold_feature_no_threshold")); // index 0 threshold.addItem(MessageManager .getString("label.threshold_feature_above_threshold")); // index 1 threshold.addItem(MessageManager .getString("label.threshold_feature_below_threshold")); // index 2 thresholdValue.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { thresholdValue_actionPerformed(); } }); thresholdValue.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { thresholdValue_actionPerformed(); } }); slider.setPaintLabels(false); slider.setPaintTicks(true); slider.setBackground(Color.white); slider.setEnabled(false); slider.setOpaque(false); slider.setPreferredSize(new Dimension(100, 32)); slider.setToolTipText(MessageManager .getString("label.adjust_threshold")); thresholdValue.setEnabled(false); thresholdValue.setColumns(7); thresholdPanel.add(threshold); thresholdPanel.add(slider); thresholdPanel.add(thresholdValue); /* * 4th row - threshold is min / max */ JPanel isMinMaxPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); isMinMaxPanel.setBackground(Color.white); byValuePanel.add(isMinMaxPanel); thresholdIsMin.setBackground(Color.white); thresholdIsMin.setText(MessageManager .getString("label.threshold_minmax")); thresholdIsMin.setToolTipText(MessageManager .getString("label.toggle_absolute_relative_display_threshold")); thresholdIsMin.addActionListener(changeColourAction); isMinMaxPanel.add(thresholdIsMin); return byValuePanel; } /** * Action on user choice of no / min / max colour to use when there is no * value to colour by */ protected void setNoValueColour() { int i = noValueCombo.getSelectedIndex(); if (i == NO_COLOUR_OPTION) { noColour = null; } else if (i == MIN_COLOUR_OPTION) { noColour = minColour.getBackground(); } else if (i == MAX_COLOUR_OPTION) { noColour = maxColour.getBackground(); } changeColour(true); } /** * Lay out OK and Cancel buttons * * @return */ protected JPanel initOkCancelPanel() { JPanel okCancelPanel = new JPanel(); okCancelPanel.setBackground(Color.white); okCancelPanel.add(ok); okCancelPanel.add(cancel); return okCancelPanel; } /** * Lay out Colour by Label and attribute choice elements * * @return */ protected JPanel initColourByTextPanel() { JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); byTextPanel.setBackground(Color.white); JvSwingUtils.createItalicTitledBorder(byTextPanel, MessageManager.getString("label.colour_by_text"), true); byDescription.setText(MessageManager.getString("label.label")); byDescription.setToolTipText(MessageManager .getString("label.colour_by_label_tip")); byDescription.addActionListener(changeColourAction); byTextPanel.add(byDescription); byAttributeText.setText(MessageManager.getString("label.attribute")); byAttributeText.addActionListener(changeColourAction); byTextPanel.add(byAttributeText); List attNames = FeatureAttributes.getInstance() .getAttributes(type); textAttributeCombo = populateAttributesDropdown(type, attNames, false); byTextPanel.add(textAttributeCombo); /* * disable colour by attribute if no attributes */ if (attNames.isEmpty()) { byAttributeText.setEnabled(false); } return byTextPanel; } /** * Action on clicking the 'minimum colour' - open a colour chooser dialog, and * set the selected colour (if the user does not cancel out of the dialog) */ protected void minColour_actionPerformed() { Color col = JColorChooser.showDialog(this, MessageManager.getString("label.select_colour_minimum_value"), minColour.getBackground()); if (col != null) { minColour.setBackground(col); minColour.setForeground(col); } minColour.repaint(); changeColour(true); } /** * Action on clicking the 'maximum colour' - open a colour chooser dialog, and * set the selected colour (if the user does not cancel out of the dialog) */ protected void maxColour_actionPerformed() { Color col = JColorChooser.showDialog(this, MessageManager.getString("label.select_colour_maximum_value"), maxColour.getBackground()); if (col != null) { maxColour.setBackground(col); maxColour.setForeground(col); } maxColour.repaint(); changeColour(true); } /** * Constructs and sets the selected colour options as the colour for the * feature type, and repaints the alignment, and optionally the Overview * and/or structure viewer if open * * @param updateStructsAndOverview */ void changeColour(boolean updateStructsAndOverview) { // Check if combobox is still adjusting if (adjusting) { return; } boolean aboveThreshold = false; boolean belowThreshold = false; if (threshold.getSelectedIndex() == 1) { aboveThreshold = true; } else if (threshold.getSelectedIndex() == 2) { belowThreshold = true; } boolean hasThreshold = aboveThreshold || belowThreshold; slider.setEnabled(true); thresholdValue.setEnabled(true); /* * make the feature colour */ FeatureColourI acg; if (cs.isColourByLabel()) { acg = new FeatureColour(oldminColour, oldmaxColour, min, max); } else { acg = new FeatureColour(oldminColour = minColour.getBackground(), oldmaxColour = maxColour.getBackground(), oldNoColour = noColour, min, max); } String attribute = null; textAttributeCombo.setEnabled(false); valueAttributeCombo.setEnabled(false); if (byAttributeText.isSelected()) { attribute = (String) textAttributeCombo.getSelectedItem(); textAttributeCombo.setEnabled(true); acg.setAttributeName(attribute.split(COLON)); } else if (byAttributeValue.isSelected()) { attribute = (String) valueAttributeCombo.getSelectedItem(); valueAttributeCombo.setEnabled(true); acg.setAttributeName(attribute.split(COLON)); } else { acg.setAttributeName((String) null); } if (!hasThreshold) { slider.setEnabled(false); thresholdValue.setEnabled(false); thresholdValue.setText(""); thresholdIsMin.setEnabled(false); } else if (threshline == null) { /* * todo not yet implemented: visual indication of feature threshold */ threshline = new GraphLine((max - min) / 2f, "Threshold", Color.black); } if (hasThreshold) { adjusting = true; acg.setThreshold(threshline.value); float range = (max - min) * scaleFactor; slider.setMinimum((int) (min * scaleFactor)); slider.setMaximum((int) (max * scaleFactor)); // slider.setValue((int) (threshline.value * scaleFactor)); slider.setValue(Math.round(threshline.value * scaleFactor)); thresholdValue.setText(threshline.value + ""); slider.setMajorTickSpacing((int) (range / 10f)); slider.setEnabled(true); thresholdValue.setEnabled(true); thresholdIsMin.setEnabled(!byDescription.isSelected()); adjusting = false; } acg.setAboveThreshold(aboveThreshold); acg.setBelowThreshold(belowThreshold); if (thresholdIsMin.isSelected() && hasThreshold) { acg.setAutoScaled(false); if (aboveThreshold) { acg = new FeatureColour((FeatureColour) acg, threshline.value, max); } else { acg = new FeatureColour((FeatureColour) acg, min, threshline.value); } } else { acg.setAutoScaled(true); } acg.setColourByLabel(byDescription.isSelected() || byAttributeText.isSelected()); if (acg.isColourByLabel()) { maxColour.setEnabled(false); minColour.setEnabled(false); noValueCombo.setEnabled(false); maxColour.setBackground(this.getBackground()); maxColour.setForeground(this.getBackground()); minColour.setBackground(this.getBackground()); minColour.setForeground(this.getBackground()); } else { maxColour.setEnabled(true); minColour.setEnabled(true); noValueCombo.setEnabled(true); maxColour.setBackground(oldmaxColour); maxColour.setForeground(oldmaxColour); minColour.setBackground(oldminColour); minColour.setForeground(oldminColour); noColour = oldNoColour; } /* * save the colour, and repaint stuff */ fr.setColour(type, acg); cs = acg; ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview); } @Override protected void raiseClosed() { if (this.colourEditor != null) { colourEditor.actionPerformed(new ActionEvent(this, 0, "CLOSED")); } } @Override public void okPressed() { changeColour(false); } @Override public void cancelPressed() { reset(); } /** * Action when the user cancels the dialog. All previous settings should be * restored and rendered on the alignment, and any linked Overview window or * structure. */ void reset() { fr.setColour(type, oldcs); ap.paintAlignment(true, true); cs = null; } /** * Action on text entry of a threshold value */ protected void thresholdValue_actionPerformed() { try { float f = Float.parseFloat(thresholdValue.getText()); slider.setValue((int) (f * scaleFactor)); threshline.value = f; /* * force repaint of any Overview window or structure */ ap.paintAlignment(true, true); } catch (NumberFormatException ex) { } } /** * Action on change of threshold slider value. This may be done interactively * (by moving the slider), or programmatically (to update the slider after * manual input of a threshold value). */ protected void sliderValueChanged() { /* * squash rounding errors by forcing min/max of slider to * actual min/max of feature score range */ int value = slider.getValue(); threshline.value = value == slider.getMaximum() ? max : (value == slider.getMinimum() ? min : value / scaleFactor); cs.setThreshold(threshline.value); /* * repaint alignment, but not Overview or structure, * to avoid overload while dragging the slider */ changeColour(false); } void addActionListener(ActionListener graduatedColorEditor) { if (colourEditor != null) { System.err.println( "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser"); } colourEditor = graduatedColorEditor; } /** * Answers the last colour setting selected by user - either oldcs (which may * be a java.awt.Color) or the new GraduatedColor * * @return */ FeatureColourI getLastColour() { if (cs == null) { return oldcs; } return cs; } /** * A helper method to build the drop-down choice of attributes for a feature. * Where metadata is available with a description for an attribute, that is * added as a tooltip. The list may optionally be restricted to attributes for * which we hold a range of numerical values (so suitable candidates for a * graduated colour scheme). *

* Attribute names may be 'simple' e.g. "AC" or 'compound' e.g. {"CSQ", * "Allele"}. Compound names are rendered for display as (e.g.) CSQ:Allele. * * @param featureType * @param attNames * @param withNumericRange */ protected JComboBox populateAttributesDropdown( String featureType, List attNames, boolean withNumericRange) { List validAtts = new ArrayList<>(); List tooltips = new ArrayList<>(); FeatureAttributes fa = FeatureAttributes.getInstance(); for (String[] attName : attNames) { if (withNumericRange) { float[] minMax = fa.getMinMax(featureType, attName); if (minMax == null) { continue; } } validAtts.add(String.join(COLON, attName)); String desc = fa.getDescription(featureType, attName); if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH) { desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "..."; } tooltips.add(desc == null ? "" : desc); } JComboBox attCombo = JvSwingUtils.buildComboWithTooltips( validAtts, tooltips); attCombo.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { changeMinMaxAction.actionPerformed(null); } }); if (validAtts.isEmpty()) { attCombo.setToolTipText(MessageManager .getString(withNumericRange ? "label.no_numeric_attributes" : "label.no_attributes")); } return attCombo; } }