2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
23 import jalview.api.FeatureColourI;
24 import jalview.datamodel.GraphLine;
25 import jalview.datamodel.features.FeatureAttributes;
26 import jalview.schemes.FeatureColour;
27 import jalview.util.MessageManager;
29 import java.awt.Color;
30 import java.awt.Dimension;
31 import java.awt.FlowLayout;
32 import java.awt.event.ActionEvent;
33 import java.awt.event.ActionListener;
34 import java.awt.event.FocusAdapter;
35 import java.awt.event.FocusEvent;
36 import java.awt.event.ItemEvent;
37 import java.awt.event.ItemListener;
38 import java.awt.event.MouseAdapter;
39 import java.awt.event.MouseEvent;
40 import java.util.ArrayList;
41 import java.util.List;
43 import javax.swing.BorderFactory;
44 import javax.swing.BoxLayout;
45 import javax.swing.ButtonGroup;
46 import javax.swing.JCheckBox;
47 import javax.swing.JColorChooser;
48 import javax.swing.JComboBox;
49 import javax.swing.JLabel;
50 import javax.swing.JPanel;
51 import javax.swing.JRadioButton;
52 import javax.swing.JSlider;
53 import javax.swing.JTextField;
54 import javax.swing.border.LineBorder;
55 import javax.swing.event.ChangeEvent;
56 import javax.swing.event.ChangeListener;
58 public class FeatureColourChooser extends JalviewDialog
60 private static final int MAX_TOOLTIP_LENGTH = 50;
62 private FeatureRenderer fr;
64 private FeatureColourI cs;
66 private FeatureColourI oldcs;
68 private AlignmentPanel ap;
70 private boolean adjusting = false;
76 private float scaleFactor;
78 private String type = null;
80 private JPanel minColour = new JPanel();
82 private JPanel maxColour = new JPanel();
84 private Color noColour;
86 private JComboBox<String> threshold = new JComboBox<>();
88 private JSlider slider = new JSlider();
90 private JTextField thresholdValue = new JTextField(20);
92 private JCheckBox thresholdIsMin = new JCheckBox();
94 private GraphLine threshline;
96 private Color oldmaxColour;
98 private Color oldminColour;
100 private Color oldNoColour;
102 private ActionListener colourEditor = null;
105 * radio buttons to select what to colour by
106 * label, attribute text, score, attribute value
108 private JRadioButton byDescription = new JRadioButton();
110 private JRadioButton byAttributeText = new JRadioButton();
112 private JRadioButton byScore = new JRadioButton();
114 private JRadioButton byAttributeValue = new JRadioButton();
116 private ActionListener changeColourAction;
118 private ActionListener changeMinMaxAction;
121 * choice of option for 'colour for no value'
123 private JComboBox<String> noValueCombo;
126 * choice of attribute (if any) for 'colour by text'
128 private JComboBox<String> textAttributeCombo;
131 * choice of attribute (if any) for 'colour by value'
133 private JComboBox<String> valueAttributeCombo;
141 public FeatureColourChooser(FeatureRenderer frender, String theType)
143 this(frender, false, theType);
147 * Constructor, with option to make a blocking dialog (has to complete in the
148 * AWT event queue thread). Currently this option is always set to false.
154 FeatureColourChooser(FeatureRenderer frender, boolean blocking,
160 String title = MessageManager
161 .formatMessage("label.graduated_color_for_params", new String[]
163 initDialogFrame(this, true, blocking, title, 470, 300);
165 slider.addChangeListener(new ChangeListener()
168 public void stateChanged(ChangeEvent evt)
172 thresholdValue.setText((slider.getValue() / scaleFactor) + "");
173 sliderValueChanged();
177 slider.addMouseListener(new MouseAdapter()
180 public void mouseReleased(MouseEvent evt)
183 * only update Overview and/or structure colouring
184 * when threshold slider drag ends (mouse up)
188 ap.paintAlignment(true, true);
193 float mm[] = fr.getMinMax().get(theType)[0];
198 * ensure scale factor allows a scaled range with
199 * 10 integer divisions ('ticks'); if we have got here,
200 * we should expect that max != min
202 scaleFactor = (max == min) ? 1f : 100f / (max - min);
204 oldcs = fr.getFeatureColours().get(theType);
205 if (!oldcs.isSimpleColour())
207 if (oldcs.isAutoScaled())
210 cs = new FeatureColour((FeatureColour) oldcs, min, max);
214 cs = new FeatureColour((FeatureColour) oldcs);
220 * promote original simple color to a graduated color
221 * - by score if there is a score range, else by label
223 Color bl = oldcs.getColour();
228 // original colour becomes the maximum colour
229 cs = new FeatureColour(Color.white, bl, mm[0], mm[1]);
230 cs.setColourByLabel(mm[0] == mm[1]);
232 minColour.setBackground(oldminColour = cs.getMinColour());
233 maxColour.setBackground(oldmaxColour = cs.getMaxColour());
239 } catch (Exception ex)
241 ex.printStackTrace();
246 * set the initial state of options on screen
248 thresholdIsMin.setSelected(!cs.isAutoScaled());
250 if (cs.isColourByLabel())
252 if (cs.isColourByAttribute())
254 byAttributeText.setSelected(true);
255 textAttributeCombo.setEnabled(true);
256 textAttributeCombo.setSelectedItem(cs.getAttributeName());
260 byDescription.setSelected(true);
261 textAttributeCombo.setEnabled(false);
266 if (cs.isColourByAttribute())
268 byAttributeValue.setSelected(true);
269 String attributeName = cs.getAttributeName();
270 valueAttributeCombo.setSelectedItem(attributeName);
271 valueAttributeCombo.setEnabled(true);
276 byScore.setSelected(true);
277 valueAttributeCombo.setEnabled(false);
281 if (cs.hasThreshold())
283 // initialise threshold slider and selector
284 threshold.setSelectedIndex(cs.isAboveThreshold() ? 1 : 2);
285 slider.setEnabled(true);
286 slider.setValue((int) (cs.getThreshold() * scaleFactor));
287 thresholdValue.setEnabled(true);
288 threshline = new GraphLine((max - min) / 2f, "Threshold",
290 threshline.value = cs.getThreshold();
300 * Configures the initial layout
302 private void jbInit()
304 this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
305 this.setBackground(Color.white);
307 changeColourAction = new ActionListener()
310 public void actionPerformed(ActionEvent e)
316 changeMinMaxAction = new ActionListener()
319 public void actionPerformed(ActionEvent e)
333 JPanel detailsPanel = new JPanel();
334 detailsPanel.setLayout(new BoxLayout(detailsPanel, BoxLayout.Y_AXIS));
336 JPanel colourByTextPanel = initColourByTextPanel();
337 detailsPanel.add(colourByTextPanel);
339 JPanel colourByValuePanel = initColourByValuePanel();
340 detailsPanel.add(colourByValuePanel);
343 * 4 radio buttons select between colour by description, by
344 * attribute text, by score, or by attribute value
346 ButtonGroup bg = new ButtonGroup();
347 bg.add(byDescription);
348 bg.add(byAttributeText);
350 bg.add(byAttributeValue);
352 JPanel okCancelPanel = initOkCancelPanel();
354 this.add(detailsPanel);
355 this.add(okCancelPanel);
359 * Updates the min-max range for a change in choice of Colour by Score, or
360 * Colour by Attribute (value)
362 protected void updateMinMax()
364 float[] minMax = null;
365 if (byScore.isSelected())
367 minMax = fr.getMinMax().get(type)[0];
369 else if (byAttributeValue.isSelected())
371 String attName = (String) valueAttributeCombo.getSelectedItem();
372 minMax = FeatureAttributes.getInstance().getMinMax(type, attName);
378 scaleFactor = (max == min) ? 1f : 100f / (max - min);
379 slider.setValue((int) (min * scaleFactor));
384 * Lay out fields for graduated colour by value
388 protected JPanel initColourByValuePanel()
390 JPanel byValuePanel = new JPanel();
391 byValuePanel.setLayout(new BoxLayout(byValuePanel, BoxLayout.Y_AXIS));
392 byValuePanel.setBorder(BorderFactory.createTitledBorder(MessageManager
393 .getString("label.colour_by_value")));
394 byValuePanel.setBackground(Color.white);
397 * first row - choose colour by score or by attribute, choose attribute
399 JPanel byWhatPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
400 byWhatPanel.setBackground(Color.white);
401 byValuePanel.add(byWhatPanel);
403 byScore.setText(MessageManager.getString("label.score"));
404 byWhatPanel.add(byScore);
405 byScore.addActionListener(changeMinMaxAction);
407 byAttributeValue.setText(MessageManager.getString("label.attribute"));
408 byAttributeValue.addActionListener(changeMinMaxAction);
409 byWhatPanel.add(byAttributeValue);
411 List<String> attNames = FeatureAttributes.getInstance().getAttributes(
413 valueAttributeCombo = populateAttributesDropdown(type, attNames, true);
416 * if no numeric atttibutes found, disable colour by attribute value
418 if (valueAttributeCombo.getItemCount() == 0)
420 byAttributeValue.setEnabled(false);
423 byWhatPanel.add(valueAttributeCombo);
426 * second row - min/max/no colours
428 JPanel colourRangePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
429 colourRangePanel.setBackground(Color.white);
430 byValuePanel.add(colourRangePanel);
432 minColour.setFont(JvSwingUtils.getLabelFont());
433 minColour.setBorder(BorderFactory.createLineBorder(Color.black));
434 minColour.setPreferredSize(new Dimension(40, 20));
435 minColour.setToolTipText(MessageManager.getString("label.min_colour"));
436 minColour.addMouseListener(new MouseAdapter()
439 public void mousePressed(MouseEvent e)
441 if (minColour.isEnabled())
443 minColour_actionPerformed();
448 maxColour.setFont(JvSwingUtils.getLabelFont());
449 maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
450 maxColour.setPreferredSize(new Dimension(40, 20));
451 maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
452 maxColour.addMouseListener(new MouseAdapter()
455 public void mousePressed(MouseEvent e)
457 if (maxColour.isEnabled())
459 maxColour_actionPerformed();
463 maxColour.setBorder(new LineBorder(Color.black));
465 noValueCombo = new JComboBox<>();
466 noValueCombo.addItem(MessageManager.getString("label.no_colour"));
467 noValueCombo.addItem(MessageManager.getString("label.min_colour"));
468 noValueCombo.addItem(MessageManager.getString("label.max_colour"));
469 noValueCombo.addItemListener(new ItemListener()
472 public void itemStateChanged(ItemEvent e)
478 JLabel minText = new JLabel(MessageManager.getString("label.min_value"));
479 minText.setFont(JvSwingUtils.getLabelFont());
480 JLabel maxText = new JLabel(MessageManager.getString("label.max_value"));
481 maxText.setFont(JvSwingUtils.getLabelFont());
482 JLabel noText = new JLabel(MessageManager.getString("label.no_value"));
483 noText.setFont(JvSwingUtils.getLabelFont());
485 colourRangePanel.add(minText);
486 colourRangePanel.add(minColour);
487 colourRangePanel.add(maxText);
488 colourRangePanel.add(maxColour);
489 colourRangePanel.add(noText);
490 colourRangePanel.add(noValueCombo);
493 * third row - threshold options and value
495 JPanel thresholdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
496 thresholdPanel.setBackground(Color.white);
497 byValuePanel.add(thresholdPanel);
499 threshold.addActionListener(changeColourAction);
500 threshold.setToolTipText(MessageManager
501 .getString("label.threshold_feature_display_by_score"));
502 threshold.addItem(MessageManager
503 .getString("label.threshold_feature_no_threshold")); // index 0
504 threshold.addItem(MessageManager
505 .getString("label.threshold_feature_above_threshold")); // index 1
506 threshold.addItem(MessageManager
507 .getString("label.threshold_feature_below_threshold")); // index 2
509 thresholdValue.addActionListener(new ActionListener()
512 public void actionPerformed(ActionEvent e)
514 thresholdValue_actionPerformed();
517 thresholdValue.addFocusListener(new FocusAdapter()
520 public void focusLost(FocusEvent e)
522 thresholdValue_actionPerformed();
525 slider.setPaintLabels(false);
526 slider.setPaintTicks(true);
527 slider.setBackground(Color.white);
528 slider.setEnabled(false);
529 slider.setOpaque(false);
530 slider.setPreferredSize(new Dimension(100, 32));
531 slider.setToolTipText(MessageManager
532 .getString("label.adjust_threshold"));
533 thresholdValue.setEnabled(false);
534 thresholdValue.setColumns(7);
536 thresholdPanel.add(threshold);
537 thresholdPanel.add(slider);
538 thresholdPanel.add(thresholdValue);
541 * 4th row - threshold is min / max
543 JPanel isMinMaxPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
544 isMinMaxPanel.setBackground(Color.white);
545 byValuePanel.add(isMinMaxPanel);
546 thresholdIsMin.setBackground(Color.white);
547 thresholdIsMin.setText(MessageManager
548 .getString("label.threshold_minmax"));
549 thresholdIsMin.setToolTipText(MessageManager
550 .getString("label.toggle_absolute_relative_display_threshold"));
551 thresholdIsMin.addActionListener(changeColourAction);
552 isMinMaxPanel.add(thresholdIsMin);
558 * Action on user choice of no / min / max colour when there is no value to
561 protected void setNoValueColour()
563 int i = noValueCombo.getSelectedIndex();
570 noColour = minColour.getBackground();
574 noColour = maxColour.getBackground();
580 * Lay out OK and Cancel buttons
584 protected JPanel initOkCancelPanel()
586 JPanel okCancelPanel = new JPanel();
587 okCancelPanel.setBackground(Color.white);
588 okCancelPanel.add(ok);
589 okCancelPanel.add(cancel);
590 return okCancelPanel;
594 * Lay out Colour by Label and attribute choice elements
598 protected JPanel initColourByTextPanel()
600 JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
601 byTextPanel.setBackground(Color.white);
602 byTextPanel.setBorder(BorderFactory.createTitledBorder(MessageManager
603 .getString("label.colour_by_text")));
605 byDescription.setText(MessageManager.getString("label.label"));
606 byDescription.setToolTipText(MessageManager
607 .getString("label.colour_by_label_tip"));
608 byDescription.addActionListener(changeColourAction);
609 byTextPanel.add(byDescription);
611 byAttributeText.setText(MessageManager.getString("label.attribute"));
612 byAttributeText.addActionListener(changeColourAction);
613 byTextPanel.add(byAttributeText);
615 List<String> attNames = FeatureAttributes.getInstance().getAttributes(
617 textAttributeCombo = populateAttributesDropdown(type, attNames, false);
618 byTextPanel.add(textAttributeCombo);
621 * disable colour by attribute if no attributes
623 if (attNames.isEmpty())
625 byAttributeText.setEnabled(false);
632 * Action on clicking the 'minimum colour' - open a colour chooser dialog, and
633 * set the selected colour (if the user does not cancel out of the dialog)
635 protected void minColour_actionPerformed()
637 Color col = JColorChooser.showDialog(this,
638 MessageManager.getString("label.select_colour_minimum_value"),
639 minColour.getBackground());
642 minColour.setBackground(col);
643 minColour.setForeground(col);
650 * Action on clicking the 'maximum colour' - open a colour chooser dialog, and
651 * set the selected colour (if the user does not cancel out of the dialog)
653 protected void maxColour_actionPerformed()
655 Color col = JColorChooser.showDialog(this,
656 MessageManager.getString("label.select_colour_maximum_value"),
657 maxColour.getBackground());
660 maxColour.setBackground(col);
661 maxColour.setForeground(col);
668 * Constructs and sets the selected colour options as the colour for the
669 * feature type, and repaints the alignment, and optionally the Overview
670 * and/or structure viewer if open
672 * @param updateStructsAndOverview
674 void changeColour(boolean updateStructsAndOverview)
676 // Check if combobox is still adjusting
682 boolean aboveThreshold = false;
683 boolean belowThreshold = false;
684 if (threshold.getSelectedIndex() == 1)
686 aboveThreshold = true;
688 else if (threshold.getSelectedIndex() == 2)
690 belowThreshold = true;
692 boolean hasThreshold = aboveThreshold || belowThreshold;
694 slider.setEnabled(true);
695 thresholdValue.setEnabled(true);
698 * make the feature colour
701 if (cs.isColourByLabel())
703 acg = new FeatureColour(oldminColour, oldmaxColour, min, max);
707 acg = new FeatureColour(oldminColour = minColour.getBackground(),
708 oldmaxColour = maxColour.getBackground(),
709 oldNoColour = noColour, min, max);
711 String attribute = null;
712 textAttributeCombo.setEnabled(false);
713 valueAttributeCombo.setEnabled(false);
714 if (byAttributeText.isSelected())
716 attribute = (String) textAttributeCombo.getSelectedItem();
717 textAttributeCombo.setEnabled(true);
719 else if (byAttributeValue.isSelected())
721 attribute = (String) valueAttributeCombo.getSelectedItem();
722 valueAttributeCombo.setEnabled(true);
724 acg.setAttributeName(attribute);
728 slider.setEnabled(false);
729 thresholdValue.setEnabled(false);
730 thresholdValue.setText("");
731 thresholdIsMin.setEnabled(false);
733 else if (threshline == null)
736 * todo not yet implemented: visual indication of feature threshold
738 threshline = new GraphLine((max - min) / 2f, "Threshold",
745 acg.setThreshold(threshline.value);
747 float range = (max - min) * scaleFactor;
749 slider.setMinimum((int) (min * scaleFactor));
750 slider.setMaximum((int) (max * scaleFactor));
751 // slider.setValue((int) (threshline.value * scaleFactor));
752 slider.setValue(Math.round(threshline.value * scaleFactor));
753 thresholdValue.setText(threshline.value + "");
754 slider.setMajorTickSpacing((int) (range / 10f));
755 slider.setEnabled(true);
756 thresholdValue.setEnabled(true);
757 thresholdIsMin.setEnabled(!byDescription.isSelected());
761 acg.setAboveThreshold(aboveThreshold);
762 acg.setBelowThreshold(belowThreshold);
763 if (thresholdIsMin.isSelected() && hasThreshold)
765 acg.setAutoScaled(false);
768 acg = new FeatureColour((FeatureColour) acg, threshline.value, max);
772 acg = new FeatureColour((FeatureColour) acg, min, threshline.value);
777 acg.setAutoScaled(true);
779 acg.setColourByLabel(byDescription.isSelected()
780 || byAttributeText.isSelected());
782 if (acg.isColourByLabel())
784 maxColour.setEnabled(false);
785 minColour.setEnabled(false);
786 noValueCombo.setEnabled(false);
787 maxColour.setBackground(this.getBackground());
788 maxColour.setForeground(this.getBackground());
789 minColour.setBackground(this.getBackground());
790 minColour.setForeground(this.getBackground());
794 maxColour.setEnabled(true);
795 minColour.setEnabled(true);
796 noValueCombo.setEnabled(true);
797 maxColour.setBackground(oldmaxColour);
798 maxColour.setForeground(oldmaxColour);
799 minColour.setBackground(oldminColour);
800 minColour.setForeground(oldminColour);
804 * save the colour, and repaint stuff
806 fr.setColour(type, acg);
808 ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
812 protected void raiseClosed()
814 if (this.colourEditor != null)
816 colourEditor.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
821 public void okPressed()
827 public void cancelPressed()
833 * Action when the user cancels the dialog. All previous settings should be
834 * restored and rendered on the alignment, and any linked Overview window or
839 fr.setColour(type, oldcs);
840 ap.paintAlignment(true, true);
845 * Action on text entry of a threshold value
847 protected void thresholdValue_actionPerformed()
851 float f = Float.parseFloat(thresholdValue.getText());
852 slider.setValue((int) (f * scaleFactor));
853 threshline.value = f;
856 * force repaint of any Overview window or structure
858 ap.paintAlignment(true, true);
859 } catch (NumberFormatException ex)
865 * Action on change of threshold slider value. This may be done interactively
866 * (by moving the slider), or programmatically (to update the slider after
867 * manual input of a threshold value).
869 protected void sliderValueChanged()
872 * squash rounding errors by forcing min/max of slider to
873 * actual min/max of feature score range
875 int value = slider.getValue();
876 threshline.value = value == slider.getMaximum() ? max
877 : (value == slider.getMinimum() ? min : value / scaleFactor);
878 cs.setThreshold(threshline.value);
881 * repaint alignment, but not Overview or structure,
882 * to avoid overload while dragging the slider
887 void addActionListener(ActionListener graduatedColorEditor)
889 if (colourEditor != null)
892 "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
894 colourEditor = graduatedColorEditor;
898 * Answers the last colour setting selected by user - either oldcs (which may
899 * be a java.awt.Color) or the new GraduatedColor
903 FeatureColourI getLastColour()
913 * A helper method to build the drop-down choice of attributes for a feature.
914 * Where metadata is available with a description for an attribute, that is
915 * added as a tooltip. The list may be restricted to attributes for which we
916 * hold a range of numerical values (so suitable candidates for a graduated
921 * @param withNumericRange
923 protected JComboBox<String> populateAttributesDropdown(
924 String featureType, List<String> attNames,
925 boolean withNumericRange)
927 List<String> validAtts = new ArrayList<>();
928 List<String> tooltips = new ArrayList<>();
930 FeatureAttributes fa = FeatureAttributes.getInstance();
931 for (String attName : attNames)
933 if (withNumericRange)
935 float[] minMax = fa.getMinMax(featureType, attName);
941 validAtts.add(attName);
942 String desc = fa.getDescription(featureType, attName);
943 if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
945 desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
947 tooltips.add(desc == null ? "" : desc);
950 JComboBox<String> attCombo = JvSwingUtils.buildComboWithTooltips(
951 validAtts, tooltips);
953 attCombo.addItemListener(new ItemListener()
956 public void itemStateChanged(ItemEvent e)
958 changeMinMaxAction.actionPerformed(null);
962 if (validAtts.isEmpty())
964 attCombo.setToolTipText(MessageManager
965 .getString(withNumericRange ? "label.no_numeric_attributes"
966 : "label.no_attributes"));