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.AlignmentViewPanel;
24 import jalview.api.FeatureColourI;
25 import jalview.bin.Cache;
26 import jalview.datamodel.GraphLine;
27 import jalview.datamodel.features.FeatureAttributes;
28 import jalview.datamodel.features.FeatureAttributes.Datatype;
29 import jalview.datamodel.features.FeatureMatcher;
30 import jalview.datamodel.features.FeatureMatcherI;
31 import jalview.datamodel.features.FeatureMatcherSet;
32 import jalview.datamodel.features.FeatureMatcherSetI;
33 import jalview.io.gff.SequenceOntologyFactory;
34 import jalview.io.gff.SequenceOntologyI;
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.text.DecimalFormat;
54 import java.util.ArrayList;
55 import java.util.Collections;
56 import java.util.HashMap;
57 import java.util.List;
59 import java.util.Map.Entry;
61 import javax.swing.BorderFactory;
62 import javax.swing.BoxLayout;
63 import javax.swing.ButtonGroup;
64 import javax.swing.JButton;
65 import javax.swing.JCheckBox;
66 import javax.swing.JColorChooser;
67 import javax.swing.JComboBox;
68 import javax.swing.JLabel;
69 import javax.swing.JPanel;
70 import javax.swing.JRadioButton;
71 import javax.swing.JSlider;
72 import javax.swing.JTextField;
73 import javax.swing.SwingConstants;
74 import javax.swing.border.LineBorder;
75 import javax.swing.event.ChangeEvent;
76 import javax.swing.event.ChangeListener;
77 import javax.swing.plaf.basic.BasicArrowButton;
80 * A dialog where the user can configure colour scheme, and any filters, for one
83 * (Was FeatureColourChooser prior to Jalview 1.11, renamed with the addition of
86 public class FeatureTypeSettings extends JalviewDialog
89 * 'top level' Sequence Ontology terms
91 private final static String SO_ROOTS = "sequence_variant,sequence_attribute,sequence_collection,sequence_feature";
93 private final static String LABEL_18N = MessageManager
94 .getString("label.label");
96 private final static String SCORE_18N = MessageManager
97 .getString("label.score");
99 private static final int RADIO_WIDTH = 130;
101 private static final String COLON = ":";
103 private static final int MAX_TOOLTIP_LENGTH = 50;
105 private static final int NO_COLOUR_OPTION = 0;
107 private static final int MIN_COLOUR_OPTION = 1;
109 private static final int MAX_COLOUR_OPTION = 2;
111 private static final int ABOVE_THRESHOLD_OPTION = 1;
113 private static final int BELOW_THRESHOLD_OPTION = 2;
115 private static final DecimalFormat DECFMT_2_2 = new DecimalFormat(
119 * FeatureRenderer holds colour scheme and filters for feature types
121 private final FeatureRenderer fr; // todo refactor to allow interface type
125 * the view panel to update when settings change
127 private final AlignmentViewPanel ap;
129 private final String featureType;
132 * the colour and filters to reset to on Cancel
133 * (including feature sub-types if modified)
135 private Map<String, FeatureColourI> originalColours;
137 private Map<String, FeatureMatcherSetI> originalFilters;
140 * set flag to true when setting values programmatically,
141 * to avoid invocation of action handlers
143 private boolean adjusting = false;
146 * minimum of the value range for graduated colour
147 * (may be for feature score or for a numeric attribute)
152 * maximum of the value range for graduated colour
157 * scale factor for conversion between absolute min-max and slider
159 private float scaleFactor;
162 * radio button group, to select what to colour by:
163 * simple colour, by category (text), or graduated
165 private JRadioButton simpleColour = new JRadioButton();
167 private JRadioButton byCategory = new JRadioButton();
169 private JRadioButton graduatedColour = new JRadioButton();
172 * colours and filters are shown in tabbed view or single content pane
174 JPanel coloursPanel, filtersPanel;
176 JPanel singleColour = new JPanel();
178 private JPanel minColour = new JPanel();
180 private JPanel maxColour = new JPanel();
182 private JComboBox<String> threshold = new JComboBox<>();
184 private JSlider slider = new JSlider();
186 private JTextField thresholdValue = new JTextField(20);
188 private JCheckBox thresholdIsMin = new JCheckBox();
190 private GraphLine threshline;
192 private ActionListener featureSettings = null;
194 private ActionListener changeColourAction;
197 * choice of option for 'colour for no value'
199 private JComboBox<String> noValueCombo;
202 * choice of what to colour by text (Label or attribute)
204 private JComboBox<String> colourByTextCombo;
207 * choice of what to colour by range (Score or attribute)
209 private JComboBox<String> colourByRangeCombo;
211 private JRadioButton andFilters;
213 private JRadioButton orFilters;
216 * filters for the currently selected feature type
218 private List<FeatureMatcherI> filters;
220 private JPanel chooseFiltersPanel;
223 * the root Sequence Ontology terms (if any) that is a parent of
224 * the current feature type
226 private String rootSOTerm;
229 * feature types present in Feature Renderer which have the same Sequence
230 * Ontology root parent as the one this editor is acting on
232 private final List<String> peerSoTerms;
235 * if true, filter or colour settings are also applied to
236 * any sub-types of rootSOTerm in the Sequence Ontology
238 private boolean applyFiltersToSubtypes;
240 private boolean applyColourToSubtypes;
248 public FeatureTypeSettings(FeatureRenderer frender, String theType)
251 this.featureType = theType;
254 peerSoTerms = findSequenceOntologyPeers(this.featureType);
257 * save original colours and filters for this feature type
258 * and any sub-types, to restore on Cancel
260 originalFilters = new HashMap<>();
261 originalFilters.put(theType, fr.getFeatureFilter(theType));
262 originalColours = new HashMap<>();
263 originalColours.put(theType, fr.getFeatureColours().get(theType));
264 for (String child : peerSoTerms)
266 originalFilters.put(child, fr.getFeatureFilter(child));
267 originalColours.put(child, fr.getFeatureColours().get(child));
275 } catch (Exception ex)
277 ex.printStackTrace();
287 colourChanged(false);
289 String title = MessageManager
290 .formatMessage("label.display_settings_for", new String[]
292 initDialogFrame(this, true, false, title, 580, 500);
297 * Answers a (possibly empty) list of feature types known to the Feature
298 * Renderer which share a top level Sequence Ontology parent with the current
299 * feature type. The current type is not included.
303 protected List<String> findSequenceOntologyPeers(String featureType)
305 List<String> peers = new ArrayList<>();
308 * first find the SO term (if any) that is the root
309 * parent of the current type
311 SequenceOntologyI so = SequenceOntologyFactory.getInstance();
312 String[] roots = Cache.getDefault("SO_ROOTS", SO_ROOTS).split(",");
314 for (String root : roots)
316 if (so.isA(featureType, root.trim()))
322 if (rootSOTerm == null)
325 * feature type is not an SO term
330 List<String> types = fr.getRenderOrder();
331 for (String type : types)
333 if (!type.equals(featureType) && so.isA(type, rootSOTerm))
339 Collections.sort(peers); // sort for ease of reading in tooltip
344 * Configures the widgets on the Colours tab according to the current feature
347 private void updateColoursTab()
349 FeatureColourI fc = fr.getFeatureColours().get(featureType);
352 * suppress action handling while updating values programmatically
360 if (fc.isSimpleColour())
362 singleColour.setBackground(fc.getColour());
363 singleColour.setForeground(fc.getColour());
364 simpleColour.setSelected(true);
368 * colour by text (Label or attribute text)
370 if (fc.isColourByLabel())
372 byCategory.setSelected(true);
373 colourByTextCombo.setEnabled(colourByTextCombo.getItemCount() > 1);
374 if (fc.isColourByAttribute())
376 String[] attributeName = fc.getAttributeName();
377 colourByTextCombo.setSelectedItem(
378 FeatureMatcher.toAttributeDisplayName(attributeName));
382 colourByTextCombo.setSelectedItem(LABEL_18N);
387 colourByTextCombo.setEnabled(false);
390 if (!fc.isGraduatedColour())
392 colourByRangeCombo.setEnabled(false);
393 minColour.setEnabled(false);
394 maxColour.setEnabled(false);
395 noValueCombo.setEnabled(false);
396 threshold.setEnabled(false);
397 slider.setEnabled(false);
398 thresholdValue.setEnabled(false);
399 thresholdIsMin.setEnabled(false);
404 * Graduated colour, by score or attribute value range
406 graduatedColour.setSelected(true);
407 updateColourMinMax(); // ensure min, max are set
408 colourByRangeCombo.setEnabled(colourByRangeCombo.getItemCount() > 1);
409 minColour.setEnabled(true);
410 maxColour.setEnabled(true);
411 noValueCombo.setEnabled(true);
412 threshold.setEnabled(true);
413 minColour.setBackground(fc.getMinColour());
414 maxColour.setBackground(fc.getMaxColour());
416 if (fc.isColourByAttribute())
418 String[] attributeName = fc.getAttributeName();
419 colourByRangeCombo.setSelectedItem(
420 FeatureMatcher.toAttributeDisplayName(attributeName));
424 colourByRangeCombo.setSelectedItem(SCORE_18N);
426 Color noColour = fc.getNoColour();
427 if (noColour == null)
429 noValueCombo.setSelectedIndex(NO_COLOUR_OPTION);
431 else if (noColour.equals(fc.getMinColour()))
433 noValueCombo.setSelectedIndex(MIN_COLOUR_OPTION);
435 else if (noColour.equals(fc.getMaxColour()))
437 noValueCombo.setSelectedIndex(MAX_COLOUR_OPTION);
441 * update min-max scaling if there is a range to work with,
442 * else disable the widgets (this shouldn't happen if only
443 * valid options are offered in the combo box)
445 scaleFactor = (max == min) ? 1f : 100f / (max - min);
446 float range = (max - min) * scaleFactor;
447 slider.setMinimum((int) (min * scaleFactor));
448 slider.setMaximum((int) (max * scaleFactor));
449 slider.setMajorTickSpacing((int) (range / 10f));
451 threshline = new GraphLine((max - min) / 2f, "Threshold",
453 threshline.value = fc.getThreshold();
455 if (fc.hasThreshold())
457 threshold.setSelectedIndex(
458 fc.isAboveThreshold() ? ABOVE_THRESHOLD_OPTION
459 : BELOW_THRESHOLD_OPTION);
460 slider.setEnabled(true);
461 slider.setValue((int) (fc.getThreshold() * scaleFactor));
462 thresholdValue.setText(String.valueOf(fc.getThreshold()));
463 thresholdValue.setEnabled(true);
464 thresholdIsMin.setEnabled(true);
468 slider.setEnabled(false);
469 thresholdValue.setEnabled(false);
470 thresholdIsMin.setEnabled(false);
472 thresholdIsMin.setSelected(!fc.isAutoScaled());
480 * Configures the initial layout
482 private void initialise()
484 this.setLayout(new BorderLayout());
487 * an ActionListener that applies colour changes
489 changeColourAction = new ActionListener()
492 public void actionPerformed(ActionEvent e)
499 * first panel/tab: colour options
501 JPanel coloursPanel = initialiseColoursPanel();
502 this.add(coloursPanel, BorderLayout.NORTH);
505 * second panel/tab: filter options
507 JPanel filtersPanel = initialiseFiltersPanel();
508 this.add(filtersPanel, BorderLayout.CENTER);
510 JPanel okCancelPanel = initialiseOkCancelPanel();
512 this.add(okCancelPanel, BorderLayout.SOUTH);
516 * Updates the min-max range if Colour By selected item is Score, or an
517 * attribute, with a min-max range
519 protected void updateColourMinMax()
521 if (!graduatedColour.isSelected())
526 String colourBy = (String) colourByRangeCombo.getSelectedItem();
527 float[] minMax = getMinMax(colourBy);
537 * Retrieves the min-max range:
539 * <li>of feature score, if colour or filter is by Score</li>
540 * <li>else of the selected attribute</li>
546 private float[] getMinMax(String attName)
548 float[] minMax = null;
549 if (SCORE_18N.equals(attName))
551 minMax = fr.getMinMax().get(featureType)[0];
555 // colour by attribute range
556 minMax = FeatureAttributes.getInstance().getMinMax(featureType,
557 FeatureMatcher.fromAttributeDisplayName(attName));
563 * Lay out fields for graduated colour (by score or attribute value)
567 private JPanel initialiseGraduatedColourPanel()
569 JPanel graduatedColourPanel = new JPanel();
570 graduatedColourPanel.setLayout(
571 new BoxLayout(graduatedColourPanel, BoxLayout.Y_AXIS));
572 JvSwingUtils.createTitledBorder(graduatedColourPanel,
573 MessageManager.getString("label.graduated_colour"), true);
574 graduatedColourPanel.setBackground(Color.white);
577 * first row: graduated colour radio button, score/attribute drop-down
579 JPanel graduatedChoicePanel = new JPanel(
580 new FlowLayout(FlowLayout.LEFT));
581 graduatedChoicePanel.setBackground(Color.white);
582 graduatedColour = new JRadioButton(
583 MessageManager.getString("label.by_range_of") + COLON);
584 graduatedColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
585 graduatedColour.addItemListener(new ItemListener()
588 public void itemStateChanged(ItemEvent e)
590 if (graduatedColour.isSelected())
596 graduatedChoicePanel.add(graduatedColour);
598 List<String[]> attNames = FeatureAttributes.getInstance()
599 .getAttributes(featureType);
600 colourByRangeCombo = populateAttributesDropdown(attNames, true, false);
601 colourByRangeCombo.addItemListener(new ItemListener()
604 public void itemStateChanged(ItemEvent e)
611 * disable graduated colour option if no range found
613 graduatedColour.setEnabled(colourByRangeCombo.getItemCount() > 0);
615 graduatedChoicePanel.add(colourByRangeCombo);
616 graduatedColourPanel.add(graduatedChoicePanel);
619 * second row - min/max/no colours
621 JPanel colourRangePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
622 colourRangePanel.setBackground(Color.white);
623 graduatedColourPanel.add(colourRangePanel);
625 minColour.setFont(JvSwingUtils.getLabelFont());
626 minColour.setBorder(BorderFactory.createLineBorder(Color.black));
627 minColour.setPreferredSize(new Dimension(40, 20));
628 minColour.setToolTipText(MessageManager.getString("label.min_colour"));
629 minColour.addMouseListener(new MouseAdapter()
632 public void mousePressed(MouseEvent e)
634 if (minColour.isEnabled())
636 showColourChooser(minColour, "label.select_colour_minimum_value");
641 maxColour.setFont(JvSwingUtils.getLabelFont());
642 maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
643 maxColour.setPreferredSize(new Dimension(40, 20));
644 maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
645 maxColour.addMouseListener(new MouseAdapter()
648 public void mousePressed(MouseEvent e)
650 if (maxColour.isEnabled())
652 showColourChooser(maxColour, "label.select_colour_maximum_value");
656 maxColour.setBorder(new LineBorder(Color.black));
659 * if not set, default max colour to last plain colour,
660 * and make min colour a pale version of max colour
662 FeatureColourI originalColour = originalColours.get(featureType);
663 Color max = originalColour.getMaxColour();
666 max = originalColour.getColour();
667 minColour.setBackground(ColorUtils.bleachColour(max, 0.9f));
671 maxColour.setBackground(max);
672 minColour.setBackground(originalColour.getMinColour());
675 noValueCombo = new JComboBox<>();
676 noValueCombo.addItem(MessageManager.getString("label.no_colour"));
677 noValueCombo.addItem(MessageManager.getString("label.min_colour"));
678 noValueCombo.addItem(MessageManager.getString("label.max_colour"));
679 noValueCombo.addItemListener(new ItemListener()
682 public void itemStateChanged(ItemEvent e)
688 JLabel minText = new JLabel(
689 MessageManager.getString("label.min_value") + COLON);
690 minText.setFont(JvSwingUtils.getLabelFont());
691 JLabel maxText = new JLabel(
692 MessageManager.getString("label.max_value") + COLON);
693 maxText.setFont(JvSwingUtils.getLabelFont());
694 JLabel noText = new JLabel(
695 MessageManager.getString("label.no_value") + COLON);
696 noText.setFont(JvSwingUtils.getLabelFont());
698 colourRangePanel.add(minText);
699 colourRangePanel.add(minColour);
700 colourRangePanel.add(maxText);
701 colourRangePanel.add(maxColour);
702 colourRangePanel.add(noText);
703 colourRangePanel.add(noValueCombo);
706 * third row - threshold options and value
708 JPanel thresholdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
709 thresholdPanel.setBackground(Color.white);
710 graduatedColourPanel.add(thresholdPanel);
712 threshold.addActionListener(changeColourAction);
713 threshold.setToolTipText(MessageManager
714 .getString("label.threshold_feature_display_by_score"));
715 threshold.addItem(MessageManager
716 .getString("label.threshold_feature_no_threshold")); // index 0
717 threshold.addItem(MessageManager
718 .getString("label.threshold_feature_above_threshold")); // index 1
719 threshold.addItem(MessageManager
720 .getString("label.threshold_feature_below_threshold")); // index 2
722 thresholdValue.addActionListener(new ActionListener()
725 public void actionPerformed(ActionEvent e)
727 thresholdValue_actionPerformed();
730 thresholdValue.addFocusListener(new FocusAdapter()
733 public void focusLost(FocusEvent e)
735 thresholdValue_actionPerformed();
738 slider.setPaintLabels(false);
739 slider.setPaintTicks(true);
740 slider.setBackground(Color.white);
741 slider.setEnabled(false);
742 slider.setOpaque(false);
743 slider.setPreferredSize(new Dimension(100, 32));
744 slider.setToolTipText(
745 MessageManager.getString("label.adjust_threshold"));
747 slider.addChangeListener(new ChangeListener()
750 public void stateChanged(ChangeEvent evt)
755 .setText(String.valueOf(slider.getValue() / scaleFactor));
756 thresholdValue.setBackground(Color.white); // to reset red for invalid
757 sliderValueChanged();
761 slider.addMouseListener(new MouseAdapter()
764 public void mouseReleased(MouseEvent evt)
767 * only update Overview and/or structure colouring
768 * when threshold slider drag ends (mouse up)
772 ap.paintAlignment(true, true);
777 thresholdValue.setEnabled(false);
778 thresholdValue.setColumns(7);
780 thresholdPanel.add(threshold);
781 thresholdPanel.add(slider);
782 thresholdPanel.add(thresholdValue);
784 thresholdIsMin.setBackground(Color.white);
786 .setText(MessageManager.getString("label.threshold_minmax"));
787 thresholdIsMin.setToolTipText(MessageManager
788 .getString("label.toggle_absolute_relative_display_threshold"));
789 thresholdIsMin.addActionListener(changeColourAction);
790 thresholdPanel.add(thresholdIsMin);
792 return graduatedColourPanel;
796 * Lay out OK and Cancel buttons
800 private JPanel initialiseOkCancelPanel()
802 JPanel okCancelPanel = new JPanel();
803 // okCancelPanel.setBackground(Color.white);
804 okCancelPanel.add(ok);
805 okCancelPanel.add(cancel);
806 return okCancelPanel;
810 * Lay out Colour options panel, containing
812 * <li>plain colour, with colour picker</li>
813 * <li>colour by text, with choice of Label or other attribute</li>
814 * <li>colour by range, of score or other attribute, when available</li>
819 private JPanel initialiseColoursPanel()
821 JPanel colourByPanel = new JPanel();
822 colourByPanel.setBackground(Color.white);
823 colourByPanel.setLayout(new BoxLayout(colourByPanel, BoxLayout.Y_AXIS));
824 JvSwingUtils.createTitledBorder(colourByPanel,
825 MessageManager.getString("action.colour"), true);
828 * option to apply colour to peer types as well (if there are any)
830 if (!peerSoTerms.isEmpty())
832 applyColourToSubtypes = false;
833 colourByPanel.add(initSubtypesPanel(false));
837 * simple colour radio button and colour picker
839 JPanel simpleColourPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
840 simpleColourPanel.setBackground(Color.white);
841 colourByPanel.add(simpleColourPanel);
843 simpleColour = new JRadioButton(
844 MessageManager.getString("label.simple_colour"));
845 simpleColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
846 simpleColour.addItemListener(new ItemListener()
849 public void itemStateChanged(ItemEvent e)
851 if (simpleColour.isSelected() && !adjusting)
858 singleColour.setFont(JvSwingUtils.getLabelFont());
859 singleColour.setBorder(BorderFactory.createLineBorder(Color.black));
860 singleColour.setPreferredSize(new Dimension(40, 20));
861 FeatureColourI originalColour = originalColours.get(featureType);
862 singleColour.setBackground(originalColour.getColour());
863 singleColour.setForeground(originalColour.getColour());
865 singleColour.addMouseListener(new MouseAdapter()
868 public void mousePressed(MouseEvent e)
870 if (simpleColour.isSelected())
872 showColourChooser(singleColour, "label.select_colour");
876 simpleColourPanel.add(simpleColour); // radio button
877 simpleColourPanel.add(singleColour); // colour picker button
880 * colour by text (category) radio button and drop-down choice list
882 JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
883 byTextPanel.setBackground(Color.white);
884 JvSwingUtils.createTitledBorder(byTextPanel,
885 MessageManager.getString("label.colour_by_text"), true);
886 colourByPanel.add(byTextPanel);
887 byCategory = new JRadioButton(
888 MessageManager.getString("label.by_text_of") + COLON);
889 byCategory.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
890 byCategory.addItemListener(new ItemListener()
893 public void itemStateChanged(ItemEvent e)
895 if (byCategory.isSelected())
901 byTextPanel.add(byCategory);
903 List<String[]> attNames = FeatureAttributes.getInstance()
904 .getAttributes(featureType);
905 colourByTextCombo = populateAttributesDropdown(attNames, false, true);
906 colourByTextCombo.addItemListener(new ItemListener()
909 public void itemStateChanged(ItemEvent e)
914 byTextPanel.add(colourByTextCombo);
917 * graduated colour panel
919 JPanel graduatedColourPanel = initialiseGraduatedColourPanel();
920 colourByPanel.add(graduatedColourPanel);
923 * 3 radio buttons select between simple colour,
924 * by category (text), or graduated
926 ButtonGroup bg = new ButtonGroup();
927 bg.add(simpleColour);
929 bg.add(graduatedColour);
931 return colourByPanel;
935 * Constructs and returns a panel with a checkbox for the option to apply any
936 * changes also to sub-types of the feature type
940 protected JPanel initSubtypesPanel(final boolean forFilters)
942 JPanel toSubtypes = new JPanel(new FlowLayout(FlowLayout.LEFT));
943 toSubtypes.setBackground(Color.WHITE);
944 JCheckBox applyToSubtypesCB = new JCheckBox(MessageManager
945 .formatMessage("label.apply_to_subtypes", rootSOTerm));
946 applyToSubtypesCB.setToolTipText(getSubtypesTooltip());
947 applyToSubtypesCB.addActionListener(new ActionListener()
950 * reset and reapply settings on toggle of checkbox
953 public void actionPerformed(ActionEvent e)
957 applyFiltersToSubtypes = applyToSubtypesCB.isSelected();
958 restoreOriginalFilters();
963 applyColourToSubtypes = applyToSubtypesCB.isSelected();
964 restoreOriginalColours();
969 toSubtypes.add(applyToSubtypesCB);
974 private void showColourChooser(JPanel colourPanel, String key)
976 Color col = JColorChooser.showDialog(this,
977 MessageManager.getString(key), colourPanel.getBackground());
980 colourPanel.setBackground(col);
981 colourPanel.setForeground(col);
983 colourPanel.repaint();
988 * Constructs and sets the selected colour options as the colour for the
989 * feature type, and repaints the alignment, and optionally the Overview
990 * and/or structure viewer if open
992 * @param updateStructsAndOverview
994 void colourChanged(boolean updateStructsAndOverview)
999 * ignore action handlers while setting values programmatically
1005 * ensure min-max range is for the latest choice of
1006 * 'graduated colour by'
1008 updateColourMinMax();
1010 FeatureColourI acg = makeColourFromInputs();
1013 * save the colour, and set on subtypes if selected
1015 fr.setColour(featureType, acg);
1016 if (applyColourToSubtypes)
1018 for (String child : peerSoTerms)
1020 fr.setColour(child, acg);
1023 refreshFeatureSettings();
1024 ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
1030 * Converts the input values into an instance of FeatureColour
1034 private FeatureColourI makeColourFromInputs()
1037 * min-max range is to (or from) threshold value if
1038 * 'threshold is min/max' is selected
1044 thresh = Float.valueOf(thresholdValue.getText());
1045 } catch (NumberFormatException e)
1047 // invalid inputs are already handled on entry
1049 float minValue = min;
1050 float maxValue = max;
1051 final int thresholdOption = threshold.getSelectedIndex();
1052 if (thresholdIsMin.isSelected()
1053 && thresholdOption == ABOVE_THRESHOLD_OPTION)
1057 if (thresholdIsMin.isSelected()
1058 && thresholdOption == BELOW_THRESHOLD_OPTION)
1062 Color noColour = null;
1063 if (noValueCombo.getSelectedIndex() == MIN_COLOUR_OPTION)
1065 noColour = minColour.getBackground();
1067 else if (noValueCombo.getSelectedIndex() == MAX_COLOUR_OPTION)
1069 noColour = maxColour.getBackground();
1073 * construct a colour that 'remembers' all the options, including
1074 * those not currently selected
1076 FeatureColourI fc = new FeatureColour(singleColour.getBackground(),
1077 minColour.getBackground(), maxColour.getBackground(), noColour,
1078 minValue, maxValue);
1081 * easiest case - a single colour
1083 if (simpleColour.isSelected())
1085 ((FeatureColour) fc).setGraduatedColour(false);
1090 * next easiest case - colour by Label, or attribute text
1092 if (byCategory.isSelected())
1094 fc.setColourByLabel(true);
1095 String byWhat = (String) colourByTextCombo.getSelectedItem();
1096 if (!LABEL_18N.equals(byWhat))
1098 fc.setAttributeName(
1099 FeatureMatcher.fromAttributeDisplayName(byWhat));
1105 * remaining case - graduated colour by score, or attribute value;
1106 * set attribute to colour by if selected
1108 String byWhat = (String) colourByRangeCombo.getSelectedItem();
1109 if (!SCORE_18N.equals(byWhat))
1111 fc.setAttributeName(FeatureMatcher.fromAttributeDisplayName(byWhat));
1115 * set threshold options and 'autoscaled' which is
1116 * false if 'threshold is min/max' is selected
1117 * else true (colour range is on actual range of values)
1119 fc.setThreshold(thresh);
1120 fc.setAutoScaled(!thresholdIsMin.isSelected());
1121 fc.setAboveThreshold(thresholdOption == ABOVE_THRESHOLD_OPTION);
1122 fc.setBelowThreshold(thresholdOption == BELOW_THRESHOLD_OPTION);
1124 if (threshline == null)
1127 * todo not yet implemented: visual indication of feature threshold
1129 threshline = new GraphLine((max - min) / 2f, "Threshold",
1137 protected void raiseClosed()
1139 refreshFeatureSettings();
1142 protected void refreshFeatureSettings()
1144 if (this.featureSettings != null)
1146 featureSettings.actionPerformed(new ActionEvent(this, 0, "REFRESH"));
1151 * Action on OK is just to dismiss the dialog - any changes have already been
1155 public void okPressed()
1160 * Action on Cancel is to restore colour scheme and filters as they were when
1161 * the dialog was opened (including any feature sub-types that may have been
1165 public void cancelPressed()
1167 restoreOriginalColours();
1168 restoreOriginalFilters();
1169 ap.paintAlignment(true, true);
1172 protected void restoreOriginalFilters()
1174 for (Entry<String, FeatureMatcherSetI> entry : originalFilters
1177 fr.setFeatureFilter(entry.getKey(), entry.getValue());
1181 protected void restoreOriginalColours()
1183 for (Entry<String, FeatureColourI> entry : originalColours.entrySet())
1185 fr.setColour(entry.getKey(), entry.getValue());
1190 * Action on text entry of a threshold value
1192 protected void thresholdValue_actionPerformed()
1197 * set 'adjusting' flag while moving the slider, so it
1198 * doesn't then in turn change the value (with rounding)
1201 float f = Float.parseFloat(thresholdValue.getText());
1202 f = Float.max(f, this.min);
1203 f = Float.min(f, this.max);
1204 thresholdValue.setText(String.valueOf(f));
1205 slider.setValue((int) (f * scaleFactor));
1206 threshline.value = f;
1207 thresholdValue.setBackground(Color.white); // ok
1209 colourChanged(true);
1210 } catch (NumberFormatException ex)
1212 thresholdValue.setBackground(Color.red); // not ok
1218 * Action on change of threshold slider value. This may be done interactively
1219 * (by moving the slider), or programmatically (to update the slider after
1220 * manual input of a threshold value).
1222 protected void sliderValueChanged()
1224 threshline.value = getRoundedSliderValue();
1227 * repaint alignment, but not Overview or structure,
1228 * to avoid overload while dragging the slider
1230 colourChanged(false);
1234 * Converts the slider value to its absolute value by dividing by the
1235 * scaleFactor. Rounding errors are squashed by forcing min/max of slider
1236 * range to the actual min/max of feature score range
1240 private float getRoundedSliderValue()
1242 int value = slider.getValue();
1243 float f = value == slider.getMaximum() ? max
1244 : (value == slider.getMinimum() ? min : value / scaleFactor);
1248 void addActionListener(ActionListener listener)
1250 if (featureSettings != null)
1253 "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
1255 featureSettings = listener;
1259 * A helper method to build the drop-down choice of attributes for a feature.
1260 * If 'withRange' is true, then Score, and any attributes with a min-max
1261 * range, are added. If 'withText' is true, Label and any known attributes are
1262 * added. This allows 'categorical numerical' attributes e.g. codon position
1263 * to be coloured by text.
1265 * Where metadata is available with a description for an attribute, that is
1266 * added as a tooltip.
1268 * Attribute names may be 'simple' e.g. "AC" or 'compound' e.g. {"CSQ",
1269 * "Allele"}. Compound names are rendered for display as (e.g.) CSQ:Allele.
1271 * This method does not add any ActionListener to the JComboBox.
1277 protected JComboBox<String> populateAttributesDropdown(
1278 List<String[]> attNames, boolean withRange, boolean withText)
1280 List<String> displayAtts = new ArrayList<>();
1281 List<String> tooltips = new ArrayList<>();
1285 displayAtts.add(LABEL_18N);
1286 tooltips.add(MessageManager.getString("label.description"));
1290 float[][] minMax = fr.getMinMax().get(featureType);
1291 if (minMax != null && minMax[0][0] != minMax[0][1])
1293 displayAtts.add(SCORE_18N);
1294 tooltips.add(SCORE_18N);
1298 FeatureAttributes fa = FeatureAttributes.getInstance();
1299 for (String[] attName : attNames)
1301 float[] minMax = fa.getMinMax(featureType, attName);
1302 boolean hasRange = minMax != null && minMax[0] != minMax[1];
1303 if (!withText && !hasRange)
1307 displayAtts.add(FeatureMatcher.toAttributeDisplayName(attName));
1308 String desc = fa.getDescription(featureType, attName);
1309 if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
1311 desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
1313 tooltips.add(desc == null ? "" : desc);
1316 JComboBox<String> attCombo = JvSwingUtils
1317 .buildComboWithTooltips(displayAtts, tooltips);
1323 * Populates initial layout of the feature attribute filters panel
1325 private JPanel initialiseFiltersPanel()
1327 filters = new ArrayList<>();
1329 JPanel outerPanel = new JPanel();
1330 outerPanel.setLayout(new BoxLayout(outerPanel, BoxLayout.Y_AXIS));
1331 outerPanel.setBackground(Color.white);
1334 * option to apply colour to peer types as well (if there are any)
1336 if (!peerSoTerms.isEmpty())
1338 applyFiltersToSubtypes = false;
1339 outerPanel.add(initSubtypesPanel(true));
1342 JPanel filtersPanel = new JPanel();
1343 filtersPanel.setLayout(new BoxLayout(filtersPanel, BoxLayout.Y_AXIS));
1344 filtersPanel.setBackground(Color.white);
1345 JvSwingUtils.createTitledBorder(filtersPanel,
1346 MessageManager.getString("label.filters"), true);
1347 outerPanel.add(filtersPanel);
1349 JPanel andOrPanel = initialiseAndOrPanel();
1350 filtersPanel.add(andOrPanel);
1353 * panel with filters - populated by refreshFiltersDisplay,
1354 * which also sets the layout manager
1356 chooseFiltersPanel = new JPanel();
1357 chooseFiltersPanel.setBackground(Color.white);
1358 filtersPanel.add(chooseFiltersPanel);
1364 * Lays out the panel with radio buttons to AND or OR filter conditions
1368 private JPanel initialiseAndOrPanel()
1370 JPanel andOrPanel = new JPanel(new BorderLayout());
1371 andOrPanel.setBackground(Color.white);
1373 andFilters = new JRadioButton(MessageManager.getString("label.and"));
1374 orFilters = new JRadioButton(MessageManager.getString("label.or"));
1375 ActionListener actionListener = new ActionListener()
1378 public void actionPerformed(ActionEvent e)
1383 andFilters.addActionListener(actionListener);
1384 orFilters.addActionListener(actionListener);
1385 ButtonGroup andOr = new ButtonGroup();
1386 andOr.add(andFilters);
1387 andOr.add(orFilters);
1388 andFilters.setSelected(true);
1390 new JLabel(MessageManager.getString("label.join_conditions")));
1391 andOrPanel.add(andFilters);
1392 andOrPanel.add(orFilters);
1398 * Builds a tooltip for the 'Apply to subtypes' checkbox with a list of
1399 * subtypes of this feature type
1403 protected String getSubtypesTooltip()
1405 StringBuilder sb = new StringBuilder(20 * peerSoTerms.size());
1406 sb.append(MessageManager.getString("label.apply_also_to"));
1407 for (String child : peerSoTerms)
1409 sb.append("<br>").append(child);
1411 String tooltip = JvSwingUtils.wrapTooltip(true, sb.toString());
1416 * Refreshes the display to show any filters currently configured for the
1417 * selected feature type (editable, with 'remove' option), plus one extra row
1418 * for adding a condition. This should be called after a filter has been
1419 * removed, added or amended.
1421 private void updateFiltersTab()
1424 * clear the panel and list of filter conditions
1426 chooseFiltersPanel.removeAll();
1430 * look up attributes known for feature type
1432 List<String[]> attNames = FeatureAttributes.getInstance()
1433 .getAttributes(featureType);
1436 * if this feature type has filters set, load them first
1438 FeatureMatcherSetI featureFilters = fr.getFeatureFilter(featureType);
1439 if (featureFilters != null)
1441 if (!featureFilters.isAnded())
1443 orFilters.setSelected(true);
1445 featureFilters.getMatchers().forEach(matcher -> filters.add(matcher));
1449 * and an empty filter for the user to populate (add)
1451 filters.add(FeatureMatcher.NULL_MATCHER);
1454 * use GridLayout to 'justify' rows to the top of the panel, until
1455 * there are too many to fit in, then fall back on BoxLayout
1457 if (filters.size() <= 5)
1459 chooseFiltersPanel.setLayout(new GridLayout(5, 1));
1463 chooseFiltersPanel.setLayout(
1464 new BoxLayout(chooseFiltersPanel, BoxLayout.Y_AXIS));
1468 * render the conditions in rows, each in its own JPanel
1470 int filterIndex = 0;
1471 for (FeatureMatcherI filter : filters)
1473 JPanel row = addFilter(filter, attNames, filterIndex);
1474 chooseFiltersPanel.add(row);
1483 * A helper method that constructs a row (panel) with one filter condition:
1485 * <li>a drop-down list of Label, Score and attribute names to choose
1487 * <li>a drop-down list of conditions to choose from</li>
1488 * <li>a text field for input of a match pattern</li>
1489 * <li>optionally, a 'remove' button</li>
1491 * The filter values are set as defaults for the input fields. The 'remove'
1492 * button is added unless the pattern is empty (incomplete filter condition).
1494 * Action handlers on these fields provide for
1496 * <li>validate pattern field - should be numeric if condition is numeric</li>
1497 * <li>save filters and refresh display on any (valid) change</li>
1498 * <li>remove filter and refresh on 'Remove'</li>
1499 * <li>update conditions list on change of Label/Score/Attribute</li>
1500 * <li>refresh value field tooltip with min-max range on change of
1506 * @param filterIndex
1509 protected JPanel addFilter(FeatureMatcherI filter,
1510 List<String[]> attNames, int filterIndex)
1512 String[] attName = filter.getAttribute();
1513 Condition cond = filter.getMatcher().getCondition();
1514 String pattern = filter.getMatcher().getPattern();
1516 JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
1517 filterRow.setBackground(Color.white);
1520 * drop-down choice of attribute, with description as a tooltip
1521 * if we can obtain it
1523 final JComboBox<String> attCombo = populateAttributesDropdown(attNames,
1525 String filterBy = setSelectedAttribute(attCombo, filter);
1527 JComboBox<Condition> condCombo = new JComboBox<>();
1529 JTextField patternField = new JTextField(8);
1530 patternField.setText(pattern);
1533 * action handlers that validate and (if valid) apply changes
1535 ActionListener actionListener = new ActionListener()
1538 public void actionPerformed(ActionEvent e)
1540 if (validateFilter(patternField, condCombo))
1542 if (updateFilter(attCombo, condCombo, patternField, filterIndex))
1549 ItemListener itemListener = new ItemListener()
1552 public void itemStateChanged(ItemEvent e)
1554 actionListener.actionPerformed(null);
1558 if (filter == FeatureMatcher.NULL_MATCHER) // the 'add a condition' row
1560 attCombo.setSelectedIndex(0);
1564 attCombo.setSelectedItem(
1565 FeatureMatcher.toAttributeDisplayName(attName));
1567 attCombo.addItemListener(new ItemListener()
1570 public void itemStateChanged(ItemEvent e)
1573 * on change of attribute, refresh the conditions list to
1574 * ensure it is appropriate for the attribute datatype
1576 populateConditions((String) attCombo.getSelectedItem(),
1577 (Condition) condCombo.getSelectedItem(), condCombo,
1579 actionListener.actionPerformed(null);
1583 filterRow.add(attCombo);
1586 * drop-down choice of test condition
1588 populateConditions(filterBy, cond, condCombo, patternField);
1589 condCombo.setPreferredSize(new Dimension(150, 20));
1590 condCombo.addItemListener(itemListener);
1591 filterRow.add(condCombo);
1594 * pattern to match against
1596 patternField.addActionListener(actionListener);
1597 patternField.addFocusListener(new FocusAdapter()
1600 public void focusLost(FocusEvent e)
1602 actionListener.actionPerformed(null);
1605 filterRow.add(patternField);
1608 * disable pattern field for condition 'Present / NotPresent'
1610 Condition selectedCondition = (Condition) condCombo.getSelectedItem();
1611 patternField.setEnabled(selectedCondition.needsAPattern());
1614 * if a numeric condition is selected, show the value range
1615 * as a tooltip on the value input field
1617 setNumericHints(filterBy, selectedCondition, patternField);
1620 * add remove button if filter is populated (non-empty pattern)
1622 if (!patternField.isEnabled()
1623 || (pattern != null && pattern.trim().length() > 0))
1625 // todo: gif for button drawing '-' or 'x'
1626 JButton removeCondition = new BasicArrowButton(SwingConstants.WEST);
1628 .setToolTipText(MessageManager.getString("label.delete_row"));
1629 removeCondition.addActionListener(new ActionListener()
1632 public void actionPerformed(ActionEvent e)
1634 filters.remove(filterIndex);
1638 filterRow.add(removeCondition);
1645 * Sets the selected item in the Label/Score/Attribute drop-down to match the
1651 private String setSelectedAttribute(JComboBox<String> attCombo,
1652 FeatureMatcherI filter)
1655 if (filter.isByScore())
1659 else if (filter.isByLabel())
1665 item = FeatureMatcher.toAttributeDisplayName(filter.getAttribute());
1667 attCombo.setSelectedItem(item);
1672 * If a numeric comparison condition is selected, retrieves the min-max range
1673 * for the value (score or attribute), and sets it as a tooltip on the value
1674 * field. If the field is currently empty, then pre-populates it with
1676 * <li>the minimum value, if condition is > or >=</li>
1677 * <li>the maximum value, if condition is < or <=</li>
1681 * @param selectedCondition
1682 * @param patternField
1684 private void setNumericHints(String attName, Condition selectedCondition,
1685 JTextField patternField)
1687 patternField.setToolTipText("");
1689 if (selectedCondition.isNumeric())
1691 float[] minMax = getMinMax(attName);
1694 String minFormatted = DECFMT_2_2.format(minMax[0]);
1695 String maxFormatted = DECFMT_2_2.format(minMax[1]);
1696 String tip = String.format("(%s - %s)", minFormatted, maxFormatted);
1697 patternField.setToolTipText(tip);
1698 if (patternField.getText().isEmpty())
1700 if (selectedCondition == Condition.GE
1701 || selectedCondition == Condition.GT)
1703 patternField.setText(minFormatted);
1707 if (selectedCondition == Condition.LE
1708 || selectedCondition == Condition.LT)
1710 patternField.setText(maxFormatted);
1719 * Populates the drop-down list of comparison conditions for the given
1720 * attribute name. The conditions added depend on the datatype of the
1721 * attribute values. The supplied condition is set as the selected item in the
1722 * list, provided it is in the list. If the pattern is now invalid
1723 * (non-numeric pattern for a numeric condition), it is cleared.
1728 * @param patternField
1730 private void populateConditions(String attName, Condition cond,
1731 JComboBox<Condition> condCombo, JTextField patternField)
1733 Datatype type = FeatureAttributes.getInstance().getDatatype(featureType,
1734 FeatureMatcher.fromAttributeDisplayName(attName));
1735 if (LABEL_18N.equals(attName))
1737 type = Datatype.Character;
1739 else if (SCORE_18N.equals(attName))
1741 type = Datatype.Number;
1745 * remove itemListener before starting
1747 ItemListener listener = condCombo.getItemListeners()[0];
1748 condCombo.removeItemListener(listener);
1749 boolean condIsValid = false;
1751 condCombo.removeAllItems();
1752 for (Condition c : Condition.values())
1754 if ((c.isNumeric() && type == Datatype.Number)
1755 || (!c.isNumeric() && type != Datatype.Number))
1757 condCombo.addItem(c);
1766 * set the selected condition (does nothing if not in the list)
1770 condCombo.setSelectedItem(cond);
1774 condCombo.setSelectedIndex(0);
1778 * clear pattern if it is now invalid for condition
1780 if (((Condition) condCombo.getSelectedItem()).isNumeric())
1784 String pattern = patternField.getText().trim();
1785 if (pattern.length() > 0)
1787 Float.valueOf(pattern);
1789 } catch (NumberFormatException e)
1791 patternField.setText("");
1796 * restore the listener
1798 condCombo.addItemListener(listener);
1802 * Answers true unless a numeric condition has been selected with a
1803 * non-numeric value. Sets the value field to RED with a tooltip if in error.
1805 * If the pattern is expected but is empty, this method returns false, but
1806 * does not mark the field as invalid. This supports selecting an attribute
1807 * for a new condition before a match pattern has been entered.
1812 protected boolean validateFilter(JTextField value,
1813 JComboBox<Condition> condCombo)
1815 if (value == null || condCombo == null)
1817 return true; // fields not populated
1820 Condition cond = (Condition) condCombo.getSelectedItem();
1821 if (!cond.needsAPattern())
1826 value.setBackground(Color.white);
1827 value.setToolTipText("");
1828 String v1 = value.getText().trim();
1829 if (v1.length() == 0)
1834 if (cond.isNumeric() && v1.length() > 0)
1839 } catch (NumberFormatException e)
1841 value.setBackground(Color.red);
1842 value.setToolTipText(
1843 MessageManager.getString("label.numeric_required"));
1852 * Constructs a filter condition from the given input fields, and replaces the
1853 * condition at filterIndex with the new one. Does nothing if the pattern
1854 * field is blank (unless the match condition is one that doesn't require a
1855 * pattern, e.g. 'Is present'). Answers true if the filter was updated, else
1858 * This method may update the tooltip on the filter value field to show the
1859 * value range, if a numeric condition is selected. This ensures the tooltip
1860 * is updated when a numeric valued attribute is chosen on the last 'add a
1866 * @param filterIndex
1868 protected boolean updateFilter(JComboBox<String> attCombo,
1869 JComboBox<Condition> condCombo, JTextField valueField,
1872 String attName = (String) attCombo.getSelectedItem();
1873 Condition cond = (Condition) condCombo.getSelectedItem();
1874 String pattern = valueField.getText().trim();
1876 setNumericHints(attName, cond, valueField);
1878 if (pattern.length() == 0 && cond.needsAPattern())
1880 valueField.setEnabled(true); // ensure pattern field is enabled!
1885 * Construct a matcher that operates on Label, Score,
1886 * or named attribute
1888 FeatureMatcherI km = null;
1889 if (LABEL_18N.equals(attName))
1891 km = FeatureMatcher.byLabel(cond, pattern);
1893 else if (SCORE_18N.equals(attName))
1895 km = FeatureMatcher.byScore(cond, pattern);
1899 km = FeatureMatcher.byAttribute(cond, pattern,
1900 FeatureMatcher.fromAttributeDisplayName(attName));
1903 filters.set(filterIndex, km);
1909 * Action on any change to feature filtering, namely
1911 * <li>change of selected attribute</li>
1912 * <li>change of selected condition</li>
1913 * <li>change of match pattern</li>
1914 * <li>removal of a condition</li>
1916 * The inputs are parsed into a combined filter and this is set for the
1917 * feature type, and the alignment redrawn.
1919 protected void filtersChanged()
1922 * update the filter conditions for the feature type
1924 boolean anded = andFilters.isSelected();
1925 FeatureMatcherSetI combined = new FeatureMatcherSet();
1927 for (FeatureMatcherI filter : filters)
1929 String pattern = filter.getMatcher().getPattern();
1930 Condition condition = filter.getMatcher().getCondition();
1931 if (pattern.trim().length() > 0 || !condition.needsAPattern())
1935 combined.and(filter);
1939 combined.or(filter);
1945 * save the filter conditions in the FeatureRenderer
1946 * (note this might now be an empty filter with no conditions)
1948 fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined);
1949 if (applyFiltersToSubtypes)
1951 for (String child : peerSoTerms)
1953 fr.setFeatureFilter(child, combined.isEmpty() ? null : combined);
1957 refreshFeatureSettings();
1958 ap.paintAlignment(true, true);