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.AlignViewportI;
24 import jalview.api.AlignmentViewPanel;
25 import jalview.api.FeatureColourI;
26 import jalview.bin.Console;
27 import jalview.datamodel.GraphLine;
28 import jalview.datamodel.features.FeatureAttributes;
29 import jalview.datamodel.features.FeatureAttributes.Datatype;
30 import jalview.datamodel.features.FeatureMatcher;
31 import jalview.datamodel.features.FeatureMatcherI;
32 import jalview.datamodel.features.FeatureMatcherSet;
33 import jalview.datamodel.features.FeatureMatcherSetI;
34 import jalview.gui.JalviewColourChooser.ColourChooserListener;
35 import jalview.schemes.FeatureColour;
36 import jalview.util.ColorUtils;
37 import jalview.util.MessageManager;
38 import jalview.util.matcher.Condition;
40 import java.awt.BorderLayout;
41 import java.awt.Color;
42 import java.awt.Dimension;
43 import java.awt.FlowLayout;
44 import java.awt.GridLayout;
45 import java.awt.event.ActionEvent;
46 import java.awt.event.ActionListener;
47 import java.awt.event.FocusAdapter;
48 import java.awt.event.FocusEvent;
49 import java.awt.event.ItemEvent;
50 import java.awt.event.ItemListener;
51 import java.awt.event.MouseAdapter;
52 import java.awt.event.MouseEvent;
53 import java.math.BigDecimal;
54 import java.math.MathContext;
55 import java.text.DecimalFormat;
56 import java.util.ArrayList;
57 import java.util.List;
59 import javax.swing.BorderFactory;
60 import javax.swing.BoxLayout;
61 import javax.swing.ButtonGroup;
62 import javax.swing.JButton;
63 import javax.swing.JCheckBox;
64 import javax.swing.JComboBox;
65 import javax.swing.JLabel;
66 import javax.swing.JPanel;
67 import javax.swing.JRadioButton;
68 import javax.swing.JTextField;
69 import javax.swing.border.EmptyBorder;
70 import javax.swing.border.LineBorder;
71 import javax.swing.event.ChangeEvent;
72 import javax.swing.event.ChangeListener;
75 * A dialog where the user can configure colour scheme, and any filters, for one
78 * (Was FeatureColourChooser prior to Jalview 1.11, renamed with the addition of
81 public class FeatureTypeSettings extends JalviewDialog
83 private final static MathContext FOUR_SIG_FIG = new MathContext(4);
85 private final static String LABEL_18N = MessageManager
86 .getString("label.label");
88 private final static String SCORE_18N = MessageManager
89 .getString("label.score");
91 private static final int RADIO_WIDTH = 130;
93 private static final String COLON = ":";
95 private static final int MAX_TOOLTIP_LENGTH = 50;
97 private static final int NO_COLOUR_OPTION = 0;
99 private static final int MIN_COLOUR_OPTION = 1;
101 private static final int MAX_COLOUR_OPTION = 2;
103 private static final int ABOVE_THRESHOLD_OPTION = 1;
105 private static final int BELOW_THRESHOLD_OPTION = 2;
107 private static final DecimalFormat DECFMT_2_2 = new DecimalFormat(
111 * FeatureRenderer holds colour scheme and filters for feature types
113 private final FeatureRenderer fr; // todo refactor to allow interface type
117 * the view panel to update when settings change
119 final AlignmentViewPanel ap;
121 final String featureType;
124 * the colour and filters to reset to on Cancel
126 private final FeatureColourI originalColour;
128 private final FeatureMatcherSetI originalFilter;
131 * set flag to true when setting values programmatically,
132 * to avoid invocation of action handlers
134 boolean adjusting = false;
137 * minimum of the value range for graduated colour
138 * (may be for feature score or for a numeric attribute)
143 * maximum of the value range for graduated colour
148 * radio button group, to select what to colour by:
149 * simple colour, by category (text), or graduated
151 JRadioButton simpleColour = new JRadioButton();
153 JRadioButton byCategory = new JRadioButton();
155 JRadioButton graduatedColour = new JRadioButton();
161 JPanel singleColour = new JPanel();
163 JPanel minColour = new JPanel();
165 JPanel maxColour = new JPanel();
167 private JComboBox<Object> threshold = new JComboBox<>();
169 private Slider slider;
171 JTextField thresholdValue = new JTextField(20);
173 private JCheckBox thresholdIsMin = new JCheckBox();
175 private GraphLine threshline;
177 private ActionListener featureSettings = null;
179 private ActionListener changeColourAction;
182 * choice of option for 'colour for no value'
184 private JComboBox<Object> noValueCombo;
187 * choice of what to colour by text (Label or attribute)
189 private JComboBox<Object> colourByTextCombo;
192 * choice of what to colour by range (Score or attribute)
194 private JComboBox<Object> colourByRangeCombo;
196 private JRadioButton andFilters;
198 private JRadioButton orFilters;
201 * filters for the currently selected feature type
203 List<FeatureMatcherI> filters;
205 private JPanel chooseFiltersPanel;
213 public FeatureTypeSettings(FeatureRenderer frender, String theType)
216 this.featureType = theType;
218 originalFilter = fr.getFeatureFilter(theType);
219 originalColour = fr.getFeatureColours().get(theType);
226 } catch (Exception ex)
228 ex.printStackTrace();
232 updateColoursPanel();
234 updateFiltersPanel();
238 colourChanged(false);
240 String title = MessageManager
241 .formatMessage("label.display_settings_for", new String[]
243 initDialogFrame(this, true, false, title, 580, 500);
248 * Configures the widgets on the Colours panel according to the current
249 * feature colour scheme
251 private void updateColoursPanel()
253 FeatureColourI fc = fr.getFeatureColours().get(featureType);
256 * suppress action handling while updating values programmatically
264 if (fc.isSimpleColour())
266 singleColour.setBackground(fc.getColour());
267 singleColour.setForeground(fc.getColour());
268 simpleColour.setSelected(true);
272 * colour by text (Label or attribute text)
274 if (fc.isColourByLabel())
276 byCategory.setSelected(true);
277 colourByTextCombo.setEnabled(colourByTextCombo.getItemCount() > 1);
278 if (fc.isColourByAttribute())
280 String[] attributeName = fc.getAttributeName();
281 colourByTextCombo.setSelectedItem(
282 FeatureMatcher.toAttributeDisplayName(attributeName));
286 colourByTextCombo.setSelectedItem(LABEL_18N);
291 colourByTextCombo.setEnabled(false);
294 if (!fc.isGraduatedColour())
296 colourByRangeCombo.setEnabled(false);
297 minColour.setEnabled(false);
298 maxColour.setEnabled(false);
299 noValueCombo.setEnabled(false);
300 threshold.setEnabled(false);
301 slider.setEnabled(false);
302 thresholdValue.setEnabled(false);
303 thresholdIsMin.setEnabled(false);
308 * Graduated colour, by score or attribute value range
310 graduatedColour.setSelected(true);
311 updateColourMinMax(); // ensure min, max are set
312 colourByRangeCombo.setEnabled(colourByRangeCombo.getItemCount() > 1);
313 minColour.setEnabled(true);
314 maxColour.setEnabled(true);
315 noValueCombo.setEnabled(true);
316 threshold.setEnabled(true);
317 minColour.setBackground(fc.getMinColour());
318 maxColour.setBackground(fc.getMaxColour());
320 if (fc.isColourByAttribute())
322 String[] attributeName = fc.getAttributeName();
323 colourByRangeCombo.setSelectedItem(
324 FeatureMatcher.toAttributeDisplayName(attributeName));
328 colourByRangeCombo.setSelectedItem(SCORE_18N);
330 Color noColour = fc.getNoColour();
331 if (noColour == null)
333 noValueCombo.setSelectedIndex(NO_COLOUR_OPTION);
335 else if (noColour.equals(fc.getMinColour()))
337 noValueCombo.setSelectedIndex(MIN_COLOUR_OPTION);
339 else if (noColour.equals(fc.getMaxColour()))
341 noValueCombo.setSelectedIndex(MAX_COLOUR_OPTION);
345 * update min-max scaling if there is a range to work with,
346 * else disable the widgets (this shouldn't happen if only
347 * valid options are offered in the combo box)
348 * offset slider to have only non-negative values if necessary (JAL-2983)
350 slider.setSliderModel(min, max, min);
351 slider.setMajorTickSpacing(
352 (int) ((slider.getMaximum() - slider.getMinimum()) / 10f));
354 threshline = new GraphLine((max - min) / 2f, "Threshold",
356 threshline.value = fc.getThreshold();
358 if (fc.hasThreshold())
360 threshold.setSelectedIndex(
361 fc.isAboveThreshold() ? ABOVE_THRESHOLD_OPTION
362 : BELOW_THRESHOLD_OPTION);
363 slider.setEnabled(true);
364 slider.setSliderValue(fc.getThreshold());
365 setThresholdValueText(fc.getThreshold());
366 thresholdValue.setEnabled(true);
367 thresholdIsMin.setEnabled(true);
371 slider.setEnabled(false);
372 thresholdValue.setEnabled(false);
373 thresholdIsMin.setEnabled(false);
375 thresholdIsMin.setSelected(!fc.isAutoScaled());
383 * Configures the initial layout
385 private void initialise()
387 this.setLayout(new BorderLayout());
390 * an ActionListener that applies colour changes
392 changeColourAction = new ActionListener()
395 public void actionPerformed(ActionEvent e)
402 * first panel: colour options
404 JPanel coloursPanel = initialiseColoursPanel();
405 this.add(coloursPanel, BorderLayout.NORTH);
408 * second panel: filter options
410 JPanel filtersPanel = initialiseFiltersPanel();
411 this.add(filtersPanel, BorderLayout.CENTER);
413 JPanel okCancelPanel = initialiseOkCancelPanel();
415 this.add(okCancelPanel, BorderLayout.SOUTH);
419 * Updates the min-max range if Colour By selected item is Score, or an
420 * attribute, with a min-max range
422 protected void updateColourMinMax()
424 if (!graduatedColour.isSelected())
429 String colourBy = (String) colourByRangeCombo.getSelectedItem();
430 float[] minMax = getMinMax(colourBy);
440 * Retrieves the min-max range:
442 * <li>of feature score, if colour or filter is by Score</li>
443 * <li>else of the selected attribute</li>
449 private float[] getMinMax(String attName)
451 float[] minMax = null;
452 if (SCORE_18N.equals(attName))
454 minMax = fr.getMinMax().get(featureType)[0];
458 // colour by attribute range
459 minMax = FeatureAttributes.getInstance().getMinMax(featureType,
460 FeatureMatcher.fromAttributeDisplayName(attName));
466 * Lay out fields for graduated colour (by score or attribute value)
470 private JPanel initialiseGraduatedColourPanel()
472 JPanel graduatedColourPanel = new JPanel();
473 graduatedColourPanel.setLayout(
474 new BoxLayout(graduatedColourPanel, BoxLayout.Y_AXIS));
475 JvSwingUtils.createTitledBorder(graduatedColourPanel,
476 MessageManager.getString("label.graduated_colour"), true);
477 graduatedColourPanel.setBackground(Color.white);
480 * first row: graduated colour radio button, score/attribute drop-down
482 JPanel graduatedChoicePanel = new JPanel(
483 new FlowLayout(FlowLayout.LEFT));
484 graduatedChoicePanel.setBackground(Color.white);
485 graduatedColour = new JRadioButton(
486 MessageManager.getString("label.by_range_of") + COLON);
487 graduatedColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
488 graduatedColour.setOpaque(false);
489 graduatedColour.addItemListener(new ItemListener()
492 public void itemStateChanged(ItemEvent e)
494 if (graduatedColour.isSelected())
500 graduatedChoicePanel.add(graduatedColour);
502 List<String[]> attNames = FeatureAttributes.getInstance()
503 .getAttributes(featureType);
504 colourByRangeCombo = populateAttributesDropdown(attNames, true, false);
505 colourByRangeCombo.addItemListener(new ItemListener()
508 public void itemStateChanged(ItemEvent e)
515 * disable graduated colour option if no range found
517 graduatedColour.setEnabled(colourByRangeCombo.getItemCount() > 0);
519 graduatedChoicePanel.add(colourByRangeCombo);
520 graduatedColourPanel.add(graduatedChoicePanel);
523 * second row - min/max/no colours
525 JPanel colourRangePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
526 colourRangePanel.setBackground(Color.white);
527 graduatedColourPanel.add(colourRangePanel);
529 minColour.setFont(JvSwingUtils.getLabelFont());
530 minColour.setBorder(BorderFactory.createLineBorder(Color.black));
531 minColour.setPreferredSize(new Dimension(40, 20));
532 minColour.setToolTipText(MessageManager.getString("label.min_colour"));
533 minColour.addMouseListener(new MouseAdapter()
536 public void mousePressed(MouseEvent e)
538 if (minColour.isEnabled())
540 String ttl = MessageManager
541 .getString("label.select_colour_minimum_value");
542 showColourChooser(minColour, ttl);
547 maxColour.setFont(JvSwingUtils.getLabelFont());
548 maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
549 maxColour.setPreferredSize(new Dimension(40, 20));
550 maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
551 maxColour.addMouseListener(new MouseAdapter()
554 public void mousePressed(MouseEvent e)
556 if (maxColour.isEnabled())
558 String ttl = MessageManager
559 .getString("label.select_colour_maximum_value");
560 showColourChooser(maxColour, ttl);
564 maxColour.setBorder(new LineBorder(Color.black));
567 * if not set, default max colour to last plain colour,
568 * and make min colour a pale version of max colour
570 Color max = originalColour.getMaxColour();
573 max = originalColour.getColour();
574 minColour.setBackground(ColorUtils.bleachColour(max, 0.9f));
578 maxColour.setBackground(max);
579 minColour.setBackground(originalColour.getMinColour());
582 noValueCombo = new JComboBox<>();
583 noValueCombo.addItem(MessageManager.getString("label.no_colour"));
584 noValueCombo.addItem(MessageManager.getString("label.min_colour"));
585 noValueCombo.addItem(MessageManager.getString("label.max_colour"));
586 noValueCombo.addItemListener(new ItemListener()
589 public void itemStateChanged(ItemEvent e)
595 JLabel minText = new JLabel(
596 MessageManager.getString("label.min_value") + COLON);
597 minText.setFont(JvSwingUtils.getLabelFont());
598 JLabel maxText = new JLabel(
599 MessageManager.getString("label.max_value") + COLON);
600 maxText.setFont(JvSwingUtils.getLabelFont());
601 JLabel noText = new JLabel(
602 MessageManager.getString("label.no_value") + COLON);
603 noText.setFont(JvSwingUtils.getLabelFont());
605 colourRangePanel.add(minText);
606 colourRangePanel.add(minColour);
607 colourRangePanel.add(maxText);
608 colourRangePanel.add(maxColour);
609 colourRangePanel.add(noText);
610 colourRangePanel.add(noValueCombo);
613 * third row - threshold options and value
615 JPanel thresholdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
616 thresholdPanel.setBackground(Color.white);
617 graduatedColourPanel.add(thresholdPanel);
619 threshold.addActionListener(changeColourAction);
620 threshold.setToolTipText(MessageManager
621 .getString("label.threshold_feature_display_by_score"));
622 threshold.addItem(MessageManager
623 .getString("label.threshold_feature_no_threshold")); // index 0
624 threshold.addItem(MessageManager
625 .getString("label.threshold_feature_above_threshold")); // index 1
626 threshold.addItem(MessageManager
627 .getString("label.threshold_feature_below_threshold")); // index 2
629 thresholdValue.addActionListener(new ActionListener()
632 public void actionPerformed(ActionEvent e)
634 thresholdValue_actionPerformed();
637 thresholdValue.addFocusListener(new FocusAdapter()
640 public void focusLost(FocusEvent e)
642 thresholdValue_actionPerformed();
645 slider = new Slider(0f, 100f, 50f);
646 slider.setPaintLabels(false);
647 slider.setPaintTicks(true);
648 slider.setBackground(Color.white);
649 slider.setEnabled(false);
650 slider.setOpaque(false);
651 slider.setPreferredSize(new Dimension(100, 32));
652 slider.setToolTipText(
653 MessageManager.getString("label.adjust_threshold"));
655 slider.addChangeListener(new ChangeListener()
658 public void stateChanged(ChangeEvent evt)
662 setThresholdValueText(slider.getSliderValue());
663 thresholdValue.setBackground(Color.white); // to reset red for invalid
664 sliderValueChanged();
668 slider.addMouseListener(new MouseAdapter()
671 public void mouseReleased(MouseEvent evt)
674 * only update Overview and/or structure colouring
675 * when threshold slider drag ends (mouse up)
679 refreshDisplay(true);
684 thresholdValue.setEnabled(false);
685 thresholdValue.setColumns(7);
687 thresholdPanel.add(threshold);
688 thresholdPanel.add(slider);
689 thresholdPanel.add(thresholdValue);
691 thresholdIsMin.setBackground(Color.white);
693 .setText(MessageManager.getString("label.threshold_minmax"));
694 thresholdIsMin.setToolTipText(MessageManager
695 .getString("label.toggle_absolute_relative_display_threshold"));
696 thresholdIsMin.addActionListener(changeColourAction);
697 thresholdPanel.add(thresholdIsMin);
699 return graduatedColourPanel;
703 * Lay out OK and Cancel buttons
707 private JPanel initialiseOkCancelPanel()
709 JPanel okCancelPanel = new JPanel();
710 // okCancelPanel.setBackground(Color.white);
711 okCancelPanel.add(ok);
712 okCancelPanel.add(cancel);
713 return okCancelPanel;
717 * Lay out Colour options panel, containing
719 * <li>plain colour, with colour picker</li>
720 * <li>colour by text, with choice of Label or other attribute</li>
721 * <li>colour by range, of score or other attribute, when available</li>
726 private JPanel initialiseColoursPanel()
728 JPanel colourByPanel = new JPanel();
729 colourByPanel.setBackground(Color.white);
730 colourByPanel.setLayout(new BoxLayout(colourByPanel, BoxLayout.Y_AXIS));
731 JvSwingUtils.createTitledBorder(colourByPanel,
732 MessageManager.getString("action.colour"), true);
735 * simple colour radio button and colour picker
737 JPanel simpleColourPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
738 simpleColourPanel.setBackground(Color.white);
739 colourByPanel.add(simpleColourPanel);
741 simpleColour = new JRadioButton(
742 MessageManager.getString("label.simple_colour"));
743 simpleColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
744 simpleColour.setOpaque(false);
745 simpleColour.addItemListener(new ItemListener()
748 public void itemStateChanged(ItemEvent e)
750 if (simpleColour.isSelected() && !adjusting)
757 singleColour.setFont(JvSwingUtils.getLabelFont());
758 singleColour.setBorder(BorderFactory.createLineBorder(Color.black));
759 singleColour.setPreferredSize(new Dimension(40, 20));
760 // if (originalColour.isGraduatedColour())
762 // singleColour.setBackground(originalColour.getMaxColour());
763 // singleColour.setForeground(originalColour.getMaxColour());
767 singleColour.setBackground(originalColour.getColour());
768 singleColour.setForeground(originalColour.getColour());
770 singleColour.addMouseListener(new MouseAdapter()
773 public void mousePressed(MouseEvent e)
775 if (simpleColour.isSelected())
777 String ttl = MessageManager
778 .formatMessage("label.select_colour_for", featureType);
779 showColourChooser(singleColour, ttl);
783 simpleColourPanel.add(simpleColour); // radio button
784 simpleColourPanel.add(singleColour); // colour picker button
787 * colour by text (category) radio button and drop-down choice list
789 JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
790 byTextPanel.setBackground(Color.white);
791 JvSwingUtils.createTitledBorder(byTextPanel,
792 MessageManager.getString("label.colour_by_text"), true);
793 colourByPanel.add(byTextPanel);
794 byCategory = new JRadioButton(
795 MessageManager.getString("label.by_text_of") + COLON);
796 byCategory.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
797 byCategory.setOpaque(false);
798 byCategory.addItemListener(new ItemListener()
801 public void itemStateChanged(ItemEvent e)
803 if (byCategory.isSelected())
809 byTextPanel.add(byCategory);
811 List<String[]> attNames = FeatureAttributes.getInstance()
812 .getAttributes(featureType);
813 colourByTextCombo = populateAttributesDropdown(attNames, false, true);
814 colourByTextCombo.addItemListener(new ItemListener()
817 public void itemStateChanged(ItemEvent e)
822 byTextPanel.add(colourByTextCombo);
825 * graduated colour panel
827 JPanel graduatedColourPanel = initialiseGraduatedColourPanel();
828 colourByPanel.add(graduatedColourPanel);
831 * 3 radio buttons select between simple colour,
832 * by category (text), or graduated
834 ButtonGroup bg = new ButtonGroup();
835 bg.add(simpleColour);
837 bg.add(graduatedColour);
839 return colourByPanel;
843 * Shows a colour chooser dialog, and if a selection is made, updates the
844 * colour of the given panel
847 * the panel whose background colour is being picked
850 void showColourChooser(JPanel colourPanel, String title)
852 ColourChooserListener listener = new ColourChooserListener()
855 public void colourSelected(Color col)
857 colourPanel.setBackground(col);
858 colourPanel.setForeground(col);
859 colourPanel.repaint();
863 JalviewColourChooser.showColourChooser(this, title,
864 colourPanel.getBackground(), listener);
868 * Constructs and sets the selected colour options as the colour for the
869 * feature type, and repaints the alignment, and optionally the Overview
870 * and/or structure viewer if open
872 * @param updateStructsAndOverview
874 void colourChanged(boolean updateStructsAndOverview)
879 * ignore action handlers while setting values programmatically
885 * ensure min-max range is for the latest choice of
886 * 'graduated colour by'
888 updateColourMinMax();
890 FeatureColourI acg = makeColourFromInputs();
893 * save the colour, and repaint stuff
895 fr.setColour(featureType, acg);
896 refreshDisplay(updateStructsAndOverview);
898 updateColoursPanel();
902 * Converts the input values into an instance of FeatureColour
906 private FeatureColourI makeColourFromInputs()
909 * min-max range is to (or from) threshold value if
910 * 'threshold is min/max' is selected
916 thresh = Float.valueOf(thresholdValue.getText());
917 } catch (NumberFormatException e)
919 // invalid inputs are already handled on entry
921 float minValue = min;
922 float maxValue = max;
923 int thresholdOption = threshold.getSelectedIndex();
924 if (thresholdIsMin.isSelected()
925 && thresholdOption == ABOVE_THRESHOLD_OPTION)
929 if (thresholdIsMin.isSelected()
930 && thresholdOption == BELOW_THRESHOLD_OPTION)
934 Color noColour = null;
935 if (noValueCombo.getSelectedIndex() == MIN_COLOUR_OPTION)
937 noColour = minColour.getBackground();
939 else if (noValueCombo.getSelectedIndex() == MAX_COLOUR_OPTION)
941 noColour = maxColour.getBackground();
945 * construct a colour that 'remembers' all the options, including
946 * those not currently selected
948 FeatureColourI fc = new FeatureColour(singleColour.getBackground(),
949 minColour.getBackground(), maxColour.getBackground(), noColour,
953 * easiest case - a single colour
955 if (simpleColour.isSelected())
957 ((FeatureColour) fc).setGraduatedColour(false);
962 * next easiest case - colour by Label, or attribute text
964 if (byCategory.isSelected())
966 fc.setColourByLabel(true);
967 String byWhat = (String) colourByTextCombo.getSelectedItem();
968 if (!LABEL_18N.equals(byWhat))
971 FeatureMatcher.fromAttributeDisplayName(byWhat));
977 * remaining case - graduated colour by score, or attribute value;
978 * set attribute to colour by if selected
980 String byWhat = (String) colourByRangeCombo.getSelectedItem();
981 if (!SCORE_18N.equals(byWhat))
983 fc.setAttributeName(FeatureMatcher.fromAttributeDisplayName(byWhat));
987 * set threshold options and 'autoscaled' which is
988 * false if 'threshold is min/max' is selected
989 * else true (colour range is on actual range of values)
991 fc.setThreshold(thresh);
992 fc.setAutoScaled(!thresholdIsMin.isSelected());
993 fc.setAboveThreshold(thresholdOption == ABOVE_THRESHOLD_OPTION);
994 fc.setBelowThreshold(thresholdOption == BELOW_THRESHOLD_OPTION);
996 if (threshline == null)
999 * todo not yet implemented: visual indication of feature threshold
1001 threshline = new GraphLine((max - min) / 2f, "Threshold",
1009 protected void raiseClosed()
1011 if (this.featureSettings != null)
1013 featureSettings.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
1018 * Action on OK is just to dismiss the dialog - any changes have already been
1022 public void okPressed()
1027 * Action on Cancel is to restore colour scheme and filters as they were when
1028 * the dialog was opened
1031 public void cancelPressed()
1033 fr.setColour(featureType, originalColour);
1034 fr.setFeatureFilter(featureType, originalFilter);
1035 refreshDisplay(true);
1039 * Action on text entry of a threshold value
1041 protected void thresholdValue_actionPerformed()
1046 * set 'adjusting' flag while moving the slider, so it
1047 * doesn't then in turn change the value (with rounding)
1050 float f = Float.parseFloat(thresholdValue.getText());
1051 f = Float.max(f, this.min);
1052 f = Float.min(f, this.max);
1053 setThresholdValueText(f);
1054 slider.setSliderValue(f);
1055 threshline.value = f;
1056 thresholdValue.setBackground(Color.white); // ok
1058 colourChanged(true);
1059 } catch (NumberFormatException ex)
1061 thresholdValue.setBackground(Color.red); // not ok
1067 * Sets the text field for threshold value, rounded to four significant
1072 void setThresholdValueText(float f)
1074 BigDecimal formatted = new BigDecimal(f).round(FOUR_SIG_FIG)
1075 .stripTrailingZeros();
1076 thresholdValue.setText(formatted.toPlainString());
1080 * Action on change of threshold slider value. This may be done interactively
1081 * (by moving the slider), or programmatically (to update the slider after
1082 * manual input of a threshold value).
1084 protected void sliderValueChanged()
1086 threshline.value = slider.getSliderValue();
1089 * repaint alignment, but not Overview or structure,
1090 * to avoid overload while dragging the slider
1092 colourChanged(false);
1095 void addActionListener(ActionListener listener)
1097 if (featureSettings != null)
1099 jalview.bin.Console.errPrintln(
1100 "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
1102 featureSettings = listener;
1106 * A helper method to build the drop-down choice of attributes for a feature.
1107 * If 'withRange' is true, then Score, and any attributes with a min-max
1108 * range, are added. If 'withText' is true, Label and any known attributes are
1109 * added. This allows 'categorical numerical' attributes e.g. codon position
1110 * to be coloured by text.
1112 * Where metadata is available with a description for an attribute, that is
1113 * added as a tooltip.
1115 * Attribute names may be 'simple' e.g. "AC" or 'compound' e.g. {"CSQ",
1116 * "Allele"}. Compound names are rendered for display as (e.g.) CSQ:Allele.
1118 * This method does not add any ActionListener to the JComboBox.
1124 protected JComboBox<Object> populateAttributesDropdown(
1125 List<String[]> attNames, boolean withRange, boolean withText)
1127 List<String> displayAtts = new ArrayList<>();
1128 List<String> tooltips = new ArrayList<>();
1132 displayAtts.add(LABEL_18N);
1133 tooltips.add(MessageManager.getString("label.description"));
1137 float[][] minMax = fr.getMinMax().get(featureType);
1138 if (minMax != null && minMax[0][0] != minMax[0][1])
1140 displayAtts.add(SCORE_18N);
1141 tooltips.add(SCORE_18N);
1145 FeatureAttributes fa = FeatureAttributes.getInstance();
1146 for (String[] attName : attNames)
1148 float[] minMax = fa.getMinMax(featureType, attName);
1149 boolean hasRange = minMax != null && minMax[0] != minMax[1];
1150 if (!withText && !hasRange)
1154 displayAtts.add(FeatureMatcher.toAttributeDisplayName(attName));
1155 String desc = fa.getDescription(featureType, attName);
1156 if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
1158 desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
1160 tooltips.add(desc == null ? "" : desc);
1163 // now convert String List to Object List for buildComboWithTooltips
1164 List<Object> displayAttsObjects = new ArrayList<>(displayAtts);
1165 JComboBox<Object> attCombo = JvSwingUtils
1166 .buildComboWithTooltips(displayAttsObjects, tooltips);
1172 * Populates initial layout of the feature attribute filters panel
1174 private JPanel initialiseFiltersPanel()
1176 filters = new ArrayList<>();
1178 JPanel filtersPanel = new JPanel();
1179 filtersPanel.setLayout(new BoxLayout(filtersPanel, BoxLayout.Y_AXIS));
1180 filtersPanel.setBackground(Color.white);
1181 JvSwingUtils.createTitledBorder(filtersPanel,
1182 MessageManager.getString("label.filters"), true);
1184 JPanel andOrPanel = initialiseAndOrPanel();
1185 filtersPanel.add(andOrPanel);
1188 * panel with filters - populated by refreshFiltersDisplay,
1189 * which also sets the layout manager
1191 chooseFiltersPanel = new JPanel();
1192 chooseFiltersPanel.setBackground(Color.white);
1193 filtersPanel.add(chooseFiltersPanel);
1195 return filtersPanel;
1199 * Lays out the panel with radio buttons to AND or OR filter conditions
1203 private JPanel initialiseAndOrPanel()
1205 JPanel andOrPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
1206 andOrPanel.setBackground(Color.white);
1207 andFilters = new JRadioButton(MessageManager.getString("label.and"));
1208 orFilters = new JRadioButton(MessageManager.getString("label.or"));
1209 andFilters.setOpaque(false);
1210 orFilters.setOpaque(false);
1211 ActionListener actionListener = new ActionListener()
1214 public void actionPerformed(ActionEvent e)
1219 andFilters.addActionListener(actionListener);
1220 orFilters.addActionListener(actionListener);
1221 ButtonGroup andOr = new ButtonGroup();
1222 andOr.add(andFilters);
1223 andOr.add(orFilters);
1224 andFilters.setSelected(true);
1226 new JLabel(MessageManager.getString("label.join_conditions")));
1227 andOrPanel.add(andFilters);
1228 andOrPanel.add(orFilters);
1233 * Refreshes the display to show any filters currently configured for the
1234 * selected feature type (editable, with 'remove' option), plus one extra row
1235 * for adding a condition. This should be called after a filter has been
1236 * removed, added or amended.
1238 private void updateFiltersPanel()
1241 * clear the panel and list of filter conditions
1243 chooseFiltersPanel.removeAll();
1247 * look up attributes known for feature type
1249 List<String[]> attNames = FeatureAttributes.getInstance()
1250 .getAttributes(featureType);
1253 * if this feature type has filters set, load them first
1255 FeatureMatcherSetI featureFilters = fr.getFeatureFilter(featureType);
1256 if (featureFilters != null)
1258 if (!featureFilters.isAnded())
1260 orFilters.setSelected(true);
1262 // avoid use of lambda expression to keep SwingJS happy
1263 // featureFilters.getMatchers().forEach(item -> filters.add(item));
1264 for (FeatureMatcherI matcher : featureFilters.getMatchers())
1266 filters.add(matcher);
1271 * and an empty filter for the user to populate (add)
1273 filters.add(FeatureMatcher.NULL_MATCHER);
1276 * use GridLayout to 'justify' rows to the top of the panel, until
1277 * there are too many to fit in, then fall back on BoxLayout
1279 if (filters.size() <= 5)
1281 chooseFiltersPanel.setLayout(new GridLayout(5, 1));
1285 chooseFiltersPanel.setLayout(
1286 new BoxLayout(chooseFiltersPanel, BoxLayout.Y_AXIS));
1290 * render the conditions in rows, each in its own JPanel
1292 int filterIndex = 0;
1293 for (FeatureMatcherI filter : filters)
1295 JPanel row = addFilter(filter, attNames, filterIndex);
1296 chooseFiltersPanel.add(row);
1305 * A helper method that constructs a row (panel) with one filter condition:
1307 * <li>a drop-down list of Label, Score and attribute names to choose
1309 * <li>a drop-down list of conditions to choose from</li>
1310 * <li>a text field for input of a match pattern</li>
1311 * <li>optionally, a 'remove' button</li>
1313 * The filter values are set as defaults for the input fields. The 'remove'
1314 * button is added unless the pattern is empty (incomplete filter condition).
1316 * Action handlers on these fields provide for
1318 * <li>validate pattern field - should be numeric if condition is numeric</li>
1319 * <li>save filters and refresh display on any (valid) change</li>
1320 * <li>remove filter and refresh on 'Remove'</li>
1321 * <li>update conditions list on change of Label/Score/Attribute</li>
1322 * <li>refresh value field tooltip with min-max range on change of
1328 * @param filterIndex
1331 protected JPanel addFilter(FeatureMatcherI filter,
1332 List<String[]> attNames, int filterIndex)
1334 String[] attName = filter.getAttribute();
1335 Condition cond = filter.getMatcher().getCondition();
1336 String pattern = filter.getMatcher().getPattern();
1338 JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
1339 filterRow.setBackground(Color.white);
1342 * drop-down choice of attribute, with description as a tooltip
1343 * if we can obtain it
1345 final JComboBox<Object> attCombo = populateAttributesDropdown(attNames,
1347 String filterBy = setSelectedAttribute(attCombo, filter);
1349 JComboBox<Condition> condCombo = new JComboBox<>();
1351 JTextField patternField = new JTextField(8);
1352 patternField.setText(pattern);
1355 * action handlers that validate and (if valid) apply changes
1357 ActionListener actionListener = new ActionListener()
1360 public void actionPerformed(ActionEvent e)
1362 if (validateFilter(patternField, condCombo))
1364 if (updateFilter(attCombo, condCombo, patternField, filterIndex))
1371 ItemListener itemListener = new ItemListener()
1374 public void itemStateChanged(ItemEvent e)
1376 actionListener.actionPerformed(null);
1380 if (filter == FeatureMatcher.NULL_MATCHER) // the 'add a condition' row
1382 attCombo.setSelectedIndex(0);
1386 attCombo.setSelectedItem(
1387 FeatureMatcher.toAttributeDisplayName(attName));
1389 attCombo.addItemListener(new ItemListener()
1392 public void itemStateChanged(ItemEvent e)
1395 * on change of attribute, refresh the conditions list to
1396 * ensure it is appropriate for the attribute datatype
1398 populateConditions((String) attCombo.getSelectedItem(),
1399 (Condition) condCombo.getSelectedItem(), condCombo,
1401 actionListener.actionPerformed(null);
1405 filterRow.add(attCombo);
1408 * drop-down choice of test condition
1410 populateConditions(filterBy, cond, condCombo, patternField);
1411 condCombo.setPreferredSize(new Dimension(150, 20));
1412 condCombo.addItemListener(itemListener);
1413 filterRow.add(condCombo);
1416 * pattern to match against
1418 patternField.addActionListener(actionListener);
1419 patternField.addFocusListener(new FocusAdapter()
1422 public void focusLost(FocusEvent e)
1424 actionListener.actionPerformed(null);
1427 filterRow.add(patternField);
1430 * disable pattern field for condition 'Present / NotPresent'
1432 Condition selectedCondition = (Condition) condCombo.getSelectedItem();
1433 patternField.setEnabled(selectedCondition.needsAPattern());
1436 * if a numeric condition is selected, show the value range
1437 * as a tooltip on the value input field
1439 setNumericHints(filterBy, selectedCondition, patternField);
1442 * add remove button if filter is populated (non-empty pattern)
1444 if (!patternField.isEnabled()
1445 || (pattern != null && pattern.trim().length() > 0))
1447 JButton removeCondition = new JButton("\u2717");
1448 // Dingbats cursive x
1449 removeCondition.setBorder(new EmptyBorder(0, 0, 0, 0));
1450 removeCondition.setBackground(Color.WHITE);
1451 removeCondition.setPreferredSize(new Dimension(23, 17));
1452 removeCondition.setToolTipText(
1453 MessageManager.getString("label.delete_condition"));
1454 removeCondition.addActionListener(new ActionListener()
1457 public void actionPerformed(ActionEvent e)
1459 filters.remove(filterIndex);
1463 filterRow.add(removeCondition);
1470 * Sets the selected item in the Label/Score/Attribute drop-down to match the
1476 private String setSelectedAttribute(JComboBox<Object> attCombo,
1477 FeatureMatcherI filter)
1480 if (filter.isByScore())
1484 else if (filter.isByLabel())
1490 item = FeatureMatcher.toAttributeDisplayName(filter.getAttribute());
1492 attCombo.setSelectedItem(item);
1497 * If a numeric comparison condition is selected, retrieves the min-max range
1498 * for the value (score or attribute), and sets it as a tooltip on the value
1499 * field. If the field is currently empty, then pre-populates it with
1501 * <li>the minimum value, if condition is > or >=</li>
1502 * <li>the maximum value, if condition is < or <=</li>
1506 * @param selectedCondition
1507 * @param patternField
1509 private void setNumericHints(String attName, Condition selectedCondition,
1510 JTextField patternField)
1512 patternField.setToolTipText("");
1514 if (selectedCondition.isNumeric())
1516 float[] minMax = getMinMax(attName);
1519 String minFormatted = DECFMT_2_2.format(minMax[0]);
1520 String maxFormatted = DECFMT_2_2.format(minMax[1]);
1521 String tip = String.format("(%s - %s)", minFormatted, maxFormatted);
1522 patternField.setToolTipText(tip);
1523 if (patternField.getText().isEmpty())
1525 if (selectedCondition == Condition.GE
1526 || selectedCondition == Condition.GT)
1528 patternField.setText(minFormatted);
1532 if (selectedCondition == Condition.LE
1533 || selectedCondition == Condition.LT)
1535 patternField.setText(maxFormatted);
1544 * Populates the drop-down list of comparison conditions for the given
1545 * attribute name. The conditions added depend on the datatype of the
1546 * attribute values. The supplied condition is set as the selected item in the
1547 * list, provided it is in the list. If the pattern is now invalid
1548 * (non-numeric pattern for a numeric condition), it is cleared.
1553 * @param patternField
1555 void populateConditions(String attName, Condition cond,
1556 JComboBox<Condition> condCombo, JTextField patternField)
1558 Datatype type = FeatureAttributes.getInstance().getDatatype(featureType,
1559 FeatureMatcher.fromAttributeDisplayName(attName));
1560 if (LABEL_18N.equals(attName))
1562 type = Datatype.Character;
1564 else if (SCORE_18N.equals(attName))
1566 type = Datatype.Number;
1570 * remove itemListener before starting
1572 ItemListener listener = condCombo.getItemListeners()[0];
1573 condCombo.removeItemListener(listener);
1574 boolean condIsValid = false;
1576 condCombo.removeAllItems();
1577 for (Condition c : Condition.values())
1579 if ((c.isNumeric() && type == Datatype.Number)
1580 || (!c.isNumeric() && type != Datatype.Number))
1582 condCombo.addItem(c);
1591 * set the selected condition (does nothing if not in the list)
1595 condCombo.setSelectedItem(cond);
1599 condCombo.setSelectedIndex(0);
1603 * clear pattern if it is now invalid for condition
1605 if (((Condition) condCombo.getSelectedItem()).isNumeric())
1609 String pattern = patternField.getText().trim();
1610 if (pattern.length() > 0)
1612 Float.valueOf(pattern);
1614 } catch (NumberFormatException e)
1616 patternField.setText("");
1621 * restore the listener
1623 condCombo.addItemListener(listener);
1627 * Answers true unless a numeric condition has been selected with a
1628 * non-numeric value. Sets the value field to RED with a tooltip if in error.
1630 * If the pattern is expected but is empty, this method returns false, but
1631 * does not mark the field as invalid. This supports selecting an attribute
1632 * for a new condition before a match pattern has been entered.
1637 protected boolean validateFilter(JTextField value,
1638 JComboBox<Condition> condCombo)
1640 if (value == null || condCombo == null)
1642 return true; // fields not populated
1645 Condition cond = (Condition) condCombo.getSelectedItem();
1646 if (!cond.needsAPattern())
1651 value.setBackground(Color.white);
1652 value.setToolTipText("");
1653 String v1 = value.getText().trim();
1654 if (v1.length() == 0)
1659 if (cond.isNumeric() && v1.length() > 0)
1664 } catch (NumberFormatException e)
1666 value.setBackground(Color.red);
1667 value.setToolTipText(
1668 MessageManager.getString("label.numeric_required"));
1677 * Constructs a filter condition from the given input fields, and replaces the
1678 * condition at filterIndex with the new one. Does nothing if the pattern
1679 * field is blank (unless the match condition is one that doesn't require a
1680 * pattern, e.g. 'Is present'). Answers true if the filter was updated, else
1683 * This method may update the tooltip on the filter value field to show the
1684 * value range, if a numeric condition is selected. This ensures the tooltip
1685 * is updated when a numeric valued attribute is chosen on the last 'add a
1691 * @param filterIndex
1693 protected boolean updateFilter(JComboBox<Object> attCombo,
1694 JComboBox<Condition> condCombo, JTextField valueField,
1700 attName = (String) attCombo.getSelectedItem();
1701 } catch (Exception e)
1703 Console.error("Problem casting Combo box entry to String");
1704 attName = attCombo.getSelectedItem().toString();
1706 Condition cond = (Condition) condCombo.getSelectedItem();
1707 String pattern = valueField.getText().trim();
1709 setNumericHints(attName, cond, valueField);
1711 if (pattern.length() == 0 && cond.needsAPattern())
1713 valueField.setEnabled(true); // ensure pattern field is enabled!
1718 * Construct a matcher that operates on Label, Score,
1719 * or named attribute
1721 FeatureMatcherI km = null;
1722 if (LABEL_18N.equals(attName))
1724 km = FeatureMatcher.byLabel(cond, pattern);
1726 else if (SCORE_18N.equals(attName))
1728 km = FeatureMatcher.byScore(cond, pattern);
1732 km = FeatureMatcher.byAttribute(cond, pattern,
1733 FeatureMatcher.fromAttributeDisplayName(attName));
1736 filters.set(filterIndex, km);
1742 * Action on any change to feature filtering, namely
1744 * <li>change of selected attribute</li>
1745 * <li>change of selected condition</li>
1746 * <li>change of match pattern</li>
1747 * <li>removal of a condition</li>
1749 * The inputs are parsed into a combined filter and this is set for the
1750 * feature type, and the alignment redrawn.
1752 protected void filtersChanged()
1755 * update the filter conditions for the feature type
1757 boolean anded = andFilters.isSelected();
1758 FeatureMatcherSetI combined = new FeatureMatcherSet();
1760 for (FeatureMatcherI filter : filters)
1762 String pattern = filter.getMatcher().getPattern();
1763 Condition condition = filter.getMatcher().getCondition();
1764 if (pattern.trim().length() > 0 || !condition.needsAPattern())
1768 combined.and(filter);
1772 combined.or(filter);
1778 * save the filter conditions in the FeatureRenderer
1779 * (note this might now be an empty filter with no conditions)
1781 fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined);
1782 refreshDisplay(true);
1784 updateFiltersPanel();
1788 * Repaints alignment, structure and overview (if shown). If there is a
1789 * complementary view which is showing this view's features, then also
1792 * @param updateStructsAndOverview
1794 void refreshDisplay(boolean updateStructsAndOverview)
1796 ap.paintAlignment(true, updateStructsAndOverview);
1797 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
1798 if (complement != null && complement.isShowComplementFeatures())
1800 AlignFrame af2 = Desktop.getAlignFrameFor(complement);
1801 af2.alignPanel.paintAlignment(true, updateStructsAndOverview);