Clover icon

Coverage Report

  1. Project Clover database Thu Aug 13 2020 12:04:21 BST
  2. Package jalview.gui

File FeatureTypeSettings.java

 

Coverage histogram

../../img/srcFileCovDistChart0.png
56% of files have more coverage

Code metrics

144
541
51
1
1,800
1,135
147
0.27
10.61
51
2.88

Classes

Class Line # Actions
FeatureTypeSettings 81 541 147
0.00%
 

Contributing tests

No tests hitting this source file were found.

Source view

1    /*
2    * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3    * Copyright (C) $$Year-Rel$$ The Jalview Authors
4    *
5    * This file is part of Jalview.
6    *
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.
11    *
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.
16    *
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.
20    */
21    package jalview.gui;
22   
23    import jalview.api.AlignViewportI;
24    import jalview.api.AlignmentViewPanel;
25    import jalview.api.FeatureColourI;
26    import jalview.bin.Cache;
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;
39   
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;
58   
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;
73   
74    /**
75    * A dialog where the user can configure colour scheme, and any filters, for one
76    * feature type
77    * <p>
78    * (Was FeatureColourChooser prior to Jalview 1.11, renamed with the addition of
79    * filter options)
80    */
 
81    public class FeatureTypeSettings extends JalviewDialog
82    {
83    private final static MathContext FOUR_SIG_FIG = new MathContext(4);
84   
85    private final static String LABEL_18N = MessageManager
86    .getString("label.label");
87   
88    private final static String SCORE_18N = MessageManager
89    .getString("label.score");
90   
91    private static final int RADIO_WIDTH = 130;
92   
93    private static final String COLON = ":";
94   
95    private static final int MAX_TOOLTIP_LENGTH = 50;
96   
97    private static final int NO_COLOUR_OPTION = 0;
98   
99    private static final int MIN_COLOUR_OPTION = 1;
100   
101    private static final int MAX_COLOUR_OPTION = 2;
102   
103    private static final int ABOVE_THRESHOLD_OPTION = 1;
104   
105    private static final int BELOW_THRESHOLD_OPTION = 2;
106   
107    private static final DecimalFormat DECFMT_2_2 = new DecimalFormat(
108    "##.##");
109   
110    /*
111    * FeatureRenderer holds colour scheme and filters for feature types
112    */
113    private final FeatureRenderer fr; // todo refactor to allow interface type
114    // here
115   
116    /*
117    * the view panel to update when settings change
118    */
119    final AlignmentViewPanel ap;
120   
121    final String featureType;
122   
123    /*
124    * the colour and filters to reset to on Cancel
125    */
126    private final FeatureColourI originalColour;
127   
128    private final FeatureMatcherSetI originalFilter;
129   
130    /*
131    * set flag to true when setting values programmatically,
132    * to avoid invocation of action handlers
133    */
134    boolean adjusting = false;
135   
136    /*
137    * minimum of the value range for graduated colour
138    * (may be for feature score or for a numeric attribute)
139    */
140    private float min;
141   
142    /*
143    * maximum of the value range for graduated colour
144    */
145    private float max;
146   
147    /*
148    * radio button group, to select what to colour by:
149    * simple colour, by category (text), or graduated
150    */
151    JRadioButton simpleColour = new JRadioButton();
152   
153    JRadioButton byCategory = new JRadioButton();
154   
155    JRadioButton graduatedColour = new JRadioButton();
156   
157    JPanel coloursPanel;
158   
159    JPanel filtersPanel;
160   
161    JPanel singleColour = new JPanel();
162   
163    JPanel minColour = new JPanel();
164   
165    JPanel maxColour = new JPanel();
166   
167    private JComboBox<Object> threshold = new JComboBox<>();
168   
169    private Slider slider;
170   
171    JTextField thresholdValue = new JTextField(20);
172   
173    private JCheckBox thresholdIsMin = new JCheckBox();
174   
175    private GraphLine threshline;
176   
177    private ActionListener featureSettings = null;
178   
179    private ActionListener changeColourAction;
180   
181    /*
182    * choice of option for 'colour for no value'
183    */
184    private JComboBox<Object> noValueCombo;
185   
186    /*
187    * choice of what to colour by text (Label or attribute)
188    */
189    private JComboBox<Object> colourByTextCombo;
190   
191    /*
192    * choice of what to colour by range (Score or attribute)
193    */
194    private JComboBox<Object> colourByRangeCombo;
195   
196    private JRadioButton andFilters;
197   
198    private JRadioButton orFilters;
199   
200    /*
201    * filters for the currently selected feature type
202    */
203    List<FeatureMatcherI> filters;
204   
205    private JPanel chooseFiltersPanel;
206   
207    /**
208    * Constructor
209    *
210    * @param frender
211    * @param theType
212    */
 
213  0 toggle public FeatureTypeSettings(FeatureRenderer frender, String theType)
214    {
215  0 this.fr = frender;
216  0 this.featureType = theType;
217  0 ap = fr.ap;
218  0 originalFilter = fr.getFeatureFilter(theType);
219  0 originalColour = fr.getFeatureColours().get(theType);
220   
221  0 adjusting = true;
222   
223  0 try
224    {
225  0 initialise();
226    } catch (Exception ex)
227    {
228  0 ex.printStackTrace();
229  0 return;
230    }
231   
232  0 updateColoursPanel();
233   
234  0 updateFiltersPanel();
235   
236  0 adjusting = false;
237   
238  0 colourChanged(false);
239   
240  0 String title = MessageManager
241    .formatMessage("label.display_settings_for", new String[]
242    { theType });
243  0 initDialogFrame(this, true, false, title, 580, 500);
244  0 waitForInput();
245    }
246   
247    /**
248    * Configures the widgets on the Colours panel according to the current feature
249    * colour scheme
250    */
 
251  0 toggle private void updateColoursPanel()
252    {
253  0 FeatureColourI fc = fr.getFeatureColours().get(featureType);
254   
255    /*
256    * suppress action handling while updating values programmatically
257    */
258  0 adjusting = true;
259  0 try
260    {
261    /*
262    * single colour
263    */
264  0 if (fc.isSimpleColour())
265    {
266  0 singleColour.setBackground(fc.getColour());
267  0 singleColour.setForeground(fc.getColour());
268  0 simpleColour.setSelected(true);
269    }
270   
271    /*
272    * colour by text (Label or attribute text)
273    */
274  0 if (fc.isColourByLabel())
275    {
276  0 byCategory.setSelected(true);
277  0 colourByTextCombo.setEnabled(colourByTextCombo.getItemCount() > 1);
278  0 if (fc.isColourByAttribute())
279    {
280  0 String[] attributeName = fc.getAttributeName();
281  0 colourByTextCombo.setSelectedItem(
282    FeatureMatcher.toAttributeDisplayName(attributeName));
283    }
284    else
285    {
286  0 colourByTextCombo.setSelectedItem(LABEL_18N);
287    }
288    }
289    else
290    {
291  0 colourByTextCombo.setEnabled(false);
292    }
293   
294  0 if (!fc.isGraduatedColour())
295    {
296  0 colourByRangeCombo.setEnabled(false);
297  0 minColour.setEnabled(false);
298  0 maxColour.setEnabled(false);
299  0 noValueCombo.setEnabled(false);
300  0 threshold.setEnabled(false);
301  0 slider.setEnabled(false);
302  0 thresholdValue.setEnabled(false);
303  0 thresholdIsMin.setEnabled(false);
304  0 return;
305    }
306   
307    /*
308    * Graduated colour, by score or attribute value range
309    */
310  0 graduatedColour.setSelected(true);
311  0 updateColourMinMax(); // ensure min, max are set
312  0 colourByRangeCombo.setEnabled(colourByRangeCombo.getItemCount() > 1);
313  0 minColour.setEnabled(true);
314  0 maxColour.setEnabled(true);
315  0 noValueCombo.setEnabled(true);
316  0 threshold.setEnabled(true);
317  0 minColour.setBackground(fc.getMinColour());
318  0 maxColour.setBackground(fc.getMaxColour());
319   
320  0 if (fc.isColourByAttribute())
321    {
322  0 String[] attributeName = fc.getAttributeName();
323  0 colourByRangeCombo.setSelectedItem(
324    FeatureMatcher.toAttributeDisplayName(attributeName));
325    }
326    else
327    {
328  0 colourByRangeCombo.setSelectedItem(SCORE_18N);
329    }
330  0 Color noColour = fc.getNoColour();
331  0 if (noColour == null)
332    {
333  0 noValueCombo.setSelectedIndex(NO_COLOUR_OPTION);
334    }
335  0 else if (noColour.equals(fc.getMinColour()))
336    {
337  0 noValueCombo.setSelectedIndex(MIN_COLOUR_OPTION);
338    }
339  0 else if (noColour.equals(fc.getMaxColour()))
340    {
341  0 noValueCombo.setSelectedIndex(MAX_COLOUR_OPTION);
342    }
343   
344    /*
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)
349    */
350  0 slider.setSliderModel(min, max, min);
351  0 slider.setMajorTickSpacing(
352    (int) ((slider.getMaximum() - slider.getMinimum()) / 10f));
353   
354  0 threshline = new GraphLine((max - min) / 2f, "Threshold",
355    Color.black);
356  0 threshline.value = fc.getThreshold();
357   
358  0 if (fc.hasThreshold())
359    {
360  0 threshold.setSelectedIndex(
361  0 fc.isAboveThreshold() ? ABOVE_THRESHOLD_OPTION
362    : BELOW_THRESHOLD_OPTION);
363  0 slider.setEnabled(true);
364  0 slider.setSliderValue(fc.getThreshold());
365  0 setThresholdValueText(fc.getThreshold());
366  0 thresholdValue.setEnabled(true);
367  0 thresholdIsMin.setEnabled(true);
368    }
369    else
370    {
371  0 slider.setEnabled(false);
372  0 thresholdValue.setEnabled(false);
373  0 thresholdIsMin.setEnabled(false);
374    }
375  0 thresholdIsMin.setSelected(!fc.isAutoScaled());
376    } finally
377    {
378  0 adjusting = false;
379    }
380    }
381   
382    /**
383    * Configures the initial layout
384    */
 
385  0 toggle private void initialise()
386    {
387  0 this.setLayout(new BorderLayout());
388   
389    /*
390    * an ActionListener that applies colour changes
391    */
392  0 changeColourAction = new ActionListener()
393    {
 
394  0 toggle @Override
395    public void actionPerformed(ActionEvent e)
396    {
397  0 colourChanged(true);
398    }
399    };
400   
401    /*
402    * first panel: colour options
403    */
404  0 JPanel coloursPanel = initialiseColoursPanel();
405  0 this.add(coloursPanel, BorderLayout.NORTH);
406   
407    /*
408    * second panel: filter options
409    */
410  0 JPanel filtersPanel = initialiseFiltersPanel();
411  0 this.add(filtersPanel, BorderLayout.CENTER);
412   
413  0 JPanel okCancelPanel = initialiseOkCancelPanel();
414   
415  0 this.add(okCancelPanel, BorderLayout.SOUTH);
416    }
417   
418    /**
419    * Updates the min-max range if Colour By selected item is Score, or an
420    * attribute, with a min-max range
421    */
 
422  0 toggle protected void updateColourMinMax()
423    {
424  0 if (!graduatedColour.isSelected())
425    {
426  0 return;
427    }
428   
429  0 String colourBy = (String) colourByRangeCombo.getSelectedItem();
430  0 float[] minMax = getMinMax(colourBy);
431   
432  0 if (minMax != null)
433    {
434  0 min = minMax[0];
435  0 max = minMax[1];
436    }
437    }
438   
439    /**
440    * Retrieves the min-max range:
441    * <ul>
442    * <li>of feature score, if colour or filter is by Score</li>
443    * <li>else of the selected attribute</li>
444    * </ul>
445    *
446    * @param attName
447    * @return
448    */
 
449  0 toggle private float[] getMinMax(String attName)
450    {
451  0 float[] minMax = null;
452  0 if (SCORE_18N.equals(attName))
453    {
454  0 minMax = fr.getMinMax().get(featureType)[0];
455    }
456    else
457    {
458    // colour by attribute range
459  0 minMax = FeatureAttributes.getInstance().getMinMax(featureType,
460    FeatureMatcher.fromAttributeDisplayName(attName));
461    }
462  0 return minMax;
463    }
464   
465    /**
466    * Lay out fields for graduated colour (by score or attribute value)
467    *
468    * @return
469    */
 
470  0 toggle private JPanel initialiseGraduatedColourPanel()
471    {
472  0 JPanel graduatedColourPanel = new JPanel();
473  0 graduatedColourPanel.setLayout(
474    new BoxLayout(graduatedColourPanel, BoxLayout.Y_AXIS));
475  0 JvSwingUtils.createTitledBorder(graduatedColourPanel,
476    MessageManager.getString("label.graduated_colour"), true);
477  0 graduatedColourPanel.setBackground(Color.white);
478   
479    /*
480    * first row: graduated colour radio button, score/attribute drop-down
481    */
482  0 JPanel graduatedChoicePanel = new JPanel(
483    new FlowLayout(FlowLayout.LEFT));
484  0 graduatedChoicePanel.setBackground(Color.white);
485  0 graduatedColour = new JRadioButton(
486    MessageManager.getString("label.by_range_of") + COLON);
487  0 graduatedColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
488  0 graduatedColour.setOpaque(false);
489  0 graduatedColour.addItemListener(new ItemListener()
490    {
 
491  0 toggle @Override
492    public void itemStateChanged(ItemEvent e)
493    {
494  0 if (graduatedColour.isSelected())
495    {
496  0 colourChanged(true);
497    }
498    }
499    });
500  0 graduatedChoicePanel.add(graduatedColour);
501   
502  0 List<String[]> attNames = FeatureAttributes.getInstance()
503    .getAttributes(featureType);
504  0 colourByRangeCombo = populateAttributesDropdown(attNames, true, false);
505  0 colourByRangeCombo.addItemListener(new ItemListener()
506    {
 
507  0 toggle @Override
508    public void itemStateChanged(ItemEvent e)
509    {
510  0 colourChanged(true);
511    }
512    });
513   
514    /*
515    * disable graduated colour option if no range found
516    */
517  0 graduatedColour.setEnabled(colourByRangeCombo.getItemCount() > 0);
518   
519  0 graduatedChoicePanel.add(colourByRangeCombo);
520  0 graduatedColourPanel.add(graduatedChoicePanel);
521   
522    /*
523    * second row - min/max/no colours
524    */
525  0 JPanel colourRangePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
526  0 colourRangePanel.setBackground(Color.white);
527  0 graduatedColourPanel.add(colourRangePanel);
528   
529  0 minColour.setFont(JvSwingUtils.getLabelFont());
530  0 minColour.setBorder(BorderFactory.createLineBorder(Color.black));
531  0 minColour.setPreferredSize(new Dimension(40, 20));
532  0 minColour.setToolTipText(MessageManager.getString("label.min_colour"));
533  0 minColour.addMouseListener(new MouseAdapter()
534    {
 
535  0 toggle @Override
536    public void mousePressed(MouseEvent e)
537    {
538  0 if (minColour.isEnabled())
539    {
540  0 String ttl = MessageManager.getString("label.select_colour_minimum_value");
541  0 showColourChooser(minColour, ttl);
542    }
543    }
544    });
545   
546  0 maxColour.setFont(JvSwingUtils.getLabelFont());
547  0 maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
548  0 maxColour.setPreferredSize(new Dimension(40, 20));
549  0 maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
550  0 maxColour.addMouseListener(new MouseAdapter()
551    {
 
552  0 toggle @Override
553    public void mousePressed(MouseEvent e)
554    {
555  0 if (maxColour.isEnabled())
556    {
557  0 String ttl = MessageManager.getString("label.select_colour_maximum_value");
558  0 showColourChooser(maxColour, ttl);
559    }
560    }
561    });
562  0 maxColour.setBorder(new LineBorder(Color.black));
563   
564    /*
565    * if not set, default max colour to last plain colour,
566    * and make min colour a pale version of max colour
567    */
568  0 Color max = originalColour.getMaxColour();
569  0 if (max == null)
570    {
571  0 max = originalColour.getColour();
572  0 minColour.setBackground(ColorUtils.bleachColour(max, 0.9f));
573    }
574    else
575    {
576  0 maxColour.setBackground(max);
577  0 minColour.setBackground(originalColour.getMinColour());
578    }
579   
580  0 noValueCombo = new JComboBox<>();
581  0 noValueCombo.addItem(MessageManager.getString("label.no_colour"));
582  0 noValueCombo.addItem(MessageManager.getString("label.min_colour"));
583  0 noValueCombo.addItem(MessageManager.getString("label.max_colour"));
584  0 noValueCombo.addItemListener(new ItemListener()
585    {
 
586  0 toggle @Override
587    public void itemStateChanged(ItemEvent e)
588    {
589  0 colourChanged(true);
590    }
591    });
592   
593  0 JLabel minText = new JLabel(
594    MessageManager.getString("label.min_value") + COLON);
595  0 minText.setFont(JvSwingUtils.getLabelFont());
596  0 JLabel maxText = new JLabel(
597    MessageManager.getString("label.max_value") + COLON);
598  0 maxText.setFont(JvSwingUtils.getLabelFont());
599  0 JLabel noText = new JLabel(
600    MessageManager.getString("label.no_value") + COLON);
601  0 noText.setFont(JvSwingUtils.getLabelFont());
602   
603  0 colourRangePanel.add(minText);
604  0 colourRangePanel.add(minColour);
605  0 colourRangePanel.add(maxText);
606  0 colourRangePanel.add(maxColour);
607  0 colourRangePanel.add(noText);
608  0 colourRangePanel.add(noValueCombo);
609   
610    /*
611    * third row - threshold options and value
612    */
613  0 JPanel thresholdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
614  0 thresholdPanel.setBackground(Color.white);
615  0 graduatedColourPanel.add(thresholdPanel);
616   
617  0 threshold.addActionListener(changeColourAction);
618  0 threshold.setToolTipText(MessageManager
619    .getString("label.threshold_feature_display_by_score"));
620  0 threshold.addItem(MessageManager
621    .getString("label.threshold_feature_no_threshold")); // index 0
622  0 threshold.addItem(MessageManager
623    .getString("label.threshold_feature_above_threshold")); // index 1
624  0 threshold.addItem(MessageManager
625    .getString("label.threshold_feature_below_threshold")); // index 2
626   
627  0 thresholdValue.addActionListener(new ActionListener()
628    {
 
629  0 toggle @Override
630    public void actionPerformed(ActionEvent e)
631    {
632  0 thresholdValue_actionPerformed();
633    }
634    });
635  0 thresholdValue.addFocusListener(new FocusAdapter()
636    {
 
637  0 toggle @Override
638    public void focusLost(FocusEvent e)
639    {
640  0 thresholdValue_actionPerformed();
641    }
642    });
643  0 slider = new Slider(0f, 100f, 50f);
644  0 slider.setPaintLabels(false);
645  0 slider.setPaintTicks(true);
646  0 slider.setBackground(Color.white);
647  0 slider.setEnabled(false);
648  0 slider.setOpaque(false);
649  0 slider.setPreferredSize(new Dimension(100, 32));
650  0 slider.setToolTipText(
651    MessageManager.getString("label.adjust_threshold"));
652   
653  0 slider.addChangeListener(new ChangeListener()
654    {
 
655  0 toggle @Override
656    public void stateChanged(ChangeEvent evt)
657    {
658  0 if (!adjusting)
659    {
660  0 setThresholdValueText(slider.getSliderValue());
661  0 thresholdValue.setBackground(Color.white); // to reset red for invalid
662  0 sliderValueChanged();
663    }
664    }
665    });
666  0 slider.addMouseListener(new MouseAdapter()
667    {
 
668  0 toggle @Override
669    public void mouseReleased(MouseEvent evt)
670    {
671    /*
672    * only update Overview and/or structure colouring
673    * when threshold slider drag ends (mouse up)
674    */
675  0 if (ap != null)
676    {
677  0 refreshDisplay(true);
678    }
679    }
680    });
681   
682  0 thresholdValue.setEnabled(false);
683  0 thresholdValue.setColumns(7);
684   
685  0 thresholdPanel.add(threshold);
686  0 thresholdPanel.add(slider);
687  0 thresholdPanel.add(thresholdValue);
688   
689  0 thresholdIsMin.setBackground(Color.white);
690  0 thresholdIsMin
691    .setText(MessageManager.getString("label.threshold_minmax"));
692  0 thresholdIsMin.setToolTipText(MessageManager
693    .getString("label.toggle_absolute_relative_display_threshold"));
694  0 thresholdIsMin.addActionListener(changeColourAction);
695  0 thresholdPanel.add(thresholdIsMin);
696   
697  0 return graduatedColourPanel;
698    }
699   
700    /**
701    * Lay out OK and Cancel buttons
702    *
703    * @return
704    */
 
705  0 toggle private JPanel initialiseOkCancelPanel()
706    {
707  0 JPanel okCancelPanel = new JPanel();
708    // okCancelPanel.setBackground(Color.white);
709  0 okCancelPanel.add(ok);
710  0 okCancelPanel.add(cancel);
711  0 return okCancelPanel;
712    }
713   
714    /**
715    * Lay out Colour options panel, containing
716    * <ul>
717    * <li>plain colour, with colour picker</li>
718    * <li>colour by text, with choice of Label or other attribute</li>
719    * <li>colour by range, of score or other attribute, when available</li>
720    * </ul>
721    *
722    * @return
723    */
 
724  0 toggle private JPanel initialiseColoursPanel()
725    {
726  0 JPanel colourByPanel = new JPanel();
727  0 colourByPanel.setBackground(Color.white);
728  0 colourByPanel.setLayout(new BoxLayout(colourByPanel, BoxLayout.Y_AXIS));
729  0 JvSwingUtils.createTitledBorder(colourByPanel,
730    MessageManager.getString("action.colour"), true);
731   
732    /*
733    * simple colour radio button and colour picker
734    */
735  0 JPanel simpleColourPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
736  0 simpleColourPanel.setBackground(Color.white);
737  0 colourByPanel.add(simpleColourPanel);
738   
739  0 simpleColour = new JRadioButton(
740    MessageManager.getString("label.simple_colour"));
741  0 simpleColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
742  0 simpleColour.setOpaque(false);
743  0 simpleColour.addItemListener(new ItemListener()
744    {
 
745  0 toggle @Override
746    public void itemStateChanged(ItemEvent e)
747    {
748  0 if (simpleColour.isSelected() && !adjusting)
749    {
750  0 colourChanged(true);
751    }
752    }
753    });
754   
755  0 singleColour.setFont(JvSwingUtils.getLabelFont());
756  0 singleColour.setBorder(BorderFactory.createLineBorder(Color.black));
757  0 singleColour.setPreferredSize(new Dimension(40, 20));
758    // if (originalColour.isGraduatedColour())
759    // {
760    // singleColour.setBackground(originalColour.getMaxColour());
761    // singleColour.setForeground(originalColour.getMaxColour());
762    // }
763    // else
764    // {
765  0 singleColour.setBackground(originalColour.getColour());
766  0 singleColour.setForeground(originalColour.getColour());
767    // }
768  0 singleColour.addMouseListener(new MouseAdapter()
769    {
 
770  0 toggle @Override
771    public void mousePressed(MouseEvent e)
772    {
773  0 if (simpleColour.isSelected())
774    {
775  0 String ttl = MessageManager.formatMessage("label.select_colour_for", featureType);
776  0 showColourChooser(singleColour, ttl);
777    }
778    }
779    });
780  0 simpleColourPanel.add(simpleColour); // radio button
781  0 simpleColourPanel.add(singleColour); // colour picker button
782   
783    /*
784    * colour by text (category) radio button and drop-down choice list
785    */
786  0 JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
787  0 byTextPanel.setBackground(Color.white);
788  0 JvSwingUtils.createTitledBorder(byTextPanel,
789    MessageManager.getString("label.colour_by_text"), true);
790  0 colourByPanel.add(byTextPanel);
791  0 byCategory = new JRadioButton(
792    MessageManager.getString("label.by_text_of") + COLON);
793  0 byCategory.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
794  0 byCategory.setOpaque(false);
795  0 byCategory.addItemListener(new ItemListener()
796    {
 
797  0 toggle @Override
798    public void itemStateChanged(ItemEvent e)
799    {
800  0 if (byCategory.isSelected())
801    {
802  0 colourChanged(true);
803    }
804    }
805    });
806  0 byTextPanel.add(byCategory);
807   
808  0 List<String[]> attNames = FeatureAttributes.getInstance()
809    .getAttributes(featureType);
810  0 colourByTextCombo = populateAttributesDropdown(attNames, false, true);
811  0 colourByTextCombo.addItemListener(new ItemListener()
812    {
 
813  0 toggle @Override
814    public void itemStateChanged(ItemEvent e)
815    {
816  0 colourChanged(true);
817    }
818    });
819  0 byTextPanel.add(colourByTextCombo);
820   
821    /*
822    * graduated colour panel
823    */
824  0 JPanel graduatedColourPanel = initialiseGraduatedColourPanel();
825  0 colourByPanel.add(graduatedColourPanel);
826   
827    /*
828    * 3 radio buttons select between simple colour,
829    * by category (text), or graduated
830    */
831  0 ButtonGroup bg = new ButtonGroup();
832  0 bg.add(simpleColour);
833  0 bg.add(byCategory);
834  0 bg.add(graduatedColour);
835   
836  0 return colourByPanel;
837    }
838   
839    /**
840    * Shows a colour chooser dialog, and if a selection is made, updates the
841    * colour of the given panel
842    *
843    * @param colourPanel
844    * the panel whose background colour is being picked
845    * @param title
846    */
 
847  0 toggle void showColourChooser(JPanel colourPanel, String title)
848    {
849  0 ColourChooserListener listener = new ColourChooserListener()
850    {
 
851  0 toggle @Override
852    public void colourSelected(Color col)
853    {
854  0 colourPanel.setBackground(col);
855  0 colourPanel.setForeground(col);
856  0 colourPanel.repaint();
857  0 colourChanged(true);
858    }
859    };
860  0 JalviewColourChooser.showColourChooser(this, title,
861    colourPanel.getBackground(), listener);
862    }
863   
864    /**
865    * Constructs and sets the selected colour options as the colour for the
866    * feature type, and repaints the alignment, and optionally the Overview
867    * and/or structure viewer if open
868    *
869    * @param updateStructsAndOverview
870    */
 
871  0 toggle void colourChanged(boolean updateStructsAndOverview)
872    {
873  0 if (adjusting)
874    {
875    /*
876    * ignore action handlers while setting values programmatically
877    */
878  0 return;
879    }
880   
881    /*
882    * ensure min-max range is for the latest choice of
883    * 'graduated colour by'
884    */
885  0 updateColourMinMax();
886   
887  0 FeatureColourI acg = makeColourFromInputs();
888   
889    /*
890    * save the colour, and repaint stuff
891    */
892  0 fr.setColour(featureType, acg);
893  0 refreshDisplay(updateStructsAndOverview);
894   
895  0 updateColoursPanel();
896    }
897   
898    /**
899    * Converts the input values into an instance of FeatureColour
900    *
901    * @return
902    */
 
903  0 toggle private FeatureColourI makeColourFromInputs()
904    {
905    /*
906    * min-max range is to (or from) threshold value if
907    * 'threshold is min/max' is selected
908    */
909   
910  0 float thresh = 0f;
911  0 try
912    {
913  0 thresh = Float.valueOf(thresholdValue.getText());
914    } catch (NumberFormatException e)
915    {
916    // invalid inputs are already handled on entry
917    }
918  0 float minValue = min;
919  0 float maxValue = max;
920  0 int thresholdOption = threshold.getSelectedIndex();
921  0 if (thresholdIsMin.isSelected()
922    && thresholdOption == ABOVE_THRESHOLD_OPTION)
923    {
924  0 minValue = thresh;
925    }
926  0 if (thresholdIsMin.isSelected()
927    && thresholdOption == BELOW_THRESHOLD_OPTION)
928    {
929  0 maxValue = thresh;
930    }
931  0 Color noColour = null;
932  0 if (noValueCombo.getSelectedIndex() == MIN_COLOUR_OPTION)
933    {
934  0 noColour = minColour.getBackground();
935    }
936  0 else if (noValueCombo.getSelectedIndex() == MAX_COLOUR_OPTION)
937    {
938  0 noColour = maxColour.getBackground();
939    }
940   
941    /*
942    * construct a colour that 'remembers' all the options, including
943    * those not currently selected
944    */
945  0 FeatureColourI fc = new FeatureColour(singleColour.getBackground(),
946    minColour.getBackground(), maxColour.getBackground(), noColour,
947    minValue, maxValue);
948   
949    /*
950    * easiest case - a single colour
951    */
952  0 if (simpleColour.isSelected())
953    {
954  0 ((FeatureColour) fc).setGraduatedColour(false);
955  0 return fc;
956    }
957   
958    /*
959    * next easiest case - colour by Label, or attribute text
960    */
961  0 if (byCategory.isSelected())
962    {
963  0 fc.setColourByLabel(true);
964  0 String byWhat = (String) colourByTextCombo.getSelectedItem();
965  0 if (!LABEL_18N.equals(byWhat))
966    {
967  0 fc.setAttributeName(
968    FeatureMatcher.fromAttributeDisplayName(byWhat));
969    }
970  0 return fc;
971    }
972   
973    /*
974    * remaining case - graduated colour by score, or attribute value;
975    * set attribute to colour by if selected
976    */
977  0 String byWhat = (String) colourByRangeCombo.getSelectedItem();
978  0 if (!SCORE_18N.equals(byWhat))
979    {
980  0 fc.setAttributeName(FeatureMatcher.fromAttributeDisplayName(byWhat));
981    }
982   
983    /*
984    * set threshold options and 'autoscaled' which is
985    * false if 'threshold is min/max' is selected
986    * else true (colour range is on actual range of values)
987    */
988  0 fc.setThreshold(thresh);
989  0 fc.setAutoScaled(!thresholdIsMin.isSelected());
990  0 fc.setAboveThreshold(thresholdOption == ABOVE_THRESHOLD_OPTION);
991  0 fc.setBelowThreshold(thresholdOption == BELOW_THRESHOLD_OPTION);
992   
993  0 if (threshline == null)
994    {
995    /*
996    * todo not yet implemented: visual indication of feature threshold
997    */
998  0 threshline = new GraphLine((max - min) / 2f, "Threshold",
999    Color.black);
1000    }
1001   
1002  0 return fc;
1003    }
1004   
 
1005  0 toggle @Override
1006    protected void raiseClosed()
1007    {
1008  0 if (this.featureSettings != null)
1009    {
1010  0 featureSettings.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
1011    }
1012    }
1013   
1014    /**
1015    * Action on OK is just to dismiss the dialog - any changes have already been
1016    * applied
1017    */
 
1018  0 toggle @Override
1019    public void okPressed()
1020    {
1021    }
1022   
1023    /**
1024    * Action on Cancel is to restore colour scheme and filters as they were when
1025    * the dialog was opened
1026    */
 
1027  0 toggle @Override
1028    public void cancelPressed()
1029    {
1030  0 fr.setColour(featureType, originalColour);
1031  0 fr.setFeatureFilter(featureType, originalFilter);
1032  0 refreshDisplay(true);
1033    }
1034   
1035    /**
1036    * Action on text entry of a threshold value
1037    */
 
1038  0 toggle protected void thresholdValue_actionPerformed()
1039    {
1040  0 try
1041    {
1042    /*
1043    * set 'adjusting' flag while moving the slider, so it
1044    * doesn't then in turn change the value (with rounding)
1045    */
1046  0 adjusting = true;
1047  0 float f = Float.parseFloat(thresholdValue.getText());
1048  0 f = Float.max(f, this.min);
1049  0 f = Float.min(f, this.max);
1050  0 setThresholdValueText(f);
1051  0 slider.setSliderValue(f);
1052  0 threshline.value = f;
1053  0 thresholdValue.setBackground(Color.white); // ok
1054  0 adjusting = false;
1055  0 colourChanged(true);
1056    } catch (NumberFormatException ex)
1057    {
1058  0 thresholdValue.setBackground(Color.red); // not ok
1059  0 adjusting = false;
1060    }
1061    }
1062   
1063    /**
1064    * Sets the text field for threshold value, rounded to four significant figures
1065    *
1066    * @param f
1067    */
 
1068  0 toggle void setThresholdValueText(float f)
1069    {
1070  0 BigDecimal formatted = new BigDecimal(f).round(FOUR_SIG_FIG)
1071    .stripTrailingZeros();
1072  0 thresholdValue.setText(formatted.toPlainString());
1073    }
1074   
1075    /**
1076    * Action on change of threshold slider value. This may be done interactively
1077    * (by moving the slider), or programmatically (to update the slider after
1078    * manual input of a threshold value).
1079    */
 
1080  0 toggle protected void sliderValueChanged()
1081    {
1082  0 threshline.value = slider.getSliderValue();
1083   
1084    /*
1085    * repaint alignment, but not Overview or structure,
1086    * to avoid overload while dragging the slider
1087    */
1088  0 colourChanged(false);
1089    }
1090   
 
1091  0 toggle void addActionListener(ActionListener listener)
1092    {
1093  0 if (featureSettings != null)
1094    {
1095  0 System.err.println(
1096    "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
1097    }
1098  0 featureSettings = listener;
1099    }
1100   
1101    /**
1102    * A helper method to build the drop-down choice of attributes for a feature.
1103    * If 'withRange' is true, then Score, and any attributes with a min-max
1104    * range, are added. If 'withText' is true, Label and any known attributes are
1105    * added. This allows 'categorical numerical' attributes e.g. codon position
1106    * to be coloured by text.
1107    * <p>
1108    * Where metadata is available with a description for an attribute, that is
1109    * added as a tooltip.
1110    * <p>
1111    * Attribute names may be 'simple' e.g. "AC" or 'compound' e.g. {"CSQ",
1112    * "Allele"}. Compound names are rendered for display as (e.g.) CSQ:Allele.
1113    * <p>
1114    * This method does not add any ActionListener to the JComboBox.
1115    *
1116    * @param attNames
1117    * @param withRange
1118    * @param withText
1119    */
 
1120  0 toggle protected JComboBox<Object> populateAttributesDropdown(
1121    List<String[]> attNames, boolean withRange, boolean withText)
1122    {
1123  0 List<String> displayAtts = new ArrayList<>();
1124  0 List<String> tooltips = new ArrayList<>();
1125   
1126  0 if (withText)
1127    {
1128  0 displayAtts.add(LABEL_18N);
1129  0 tooltips.add(MessageManager.getString("label.description"));
1130    }
1131  0 if (withRange)
1132    {
1133  0 float[][] minMax = fr.getMinMax().get(featureType);
1134  0 if (minMax != null && minMax[0][0] != minMax[0][1])
1135    {
1136  0 displayAtts.add(SCORE_18N);
1137  0 tooltips.add(SCORE_18N);
1138    }
1139    }
1140   
1141  0 FeatureAttributes fa = FeatureAttributes.getInstance();
1142  0 for (String[] attName : attNames)
1143    {
1144  0 float[] minMax = fa.getMinMax(featureType, attName);
1145  0 boolean hasRange = minMax != null && minMax[0] != minMax[1];
1146  0 if (!withText && !hasRange)
1147    {
1148  0 continue;
1149    }
1150  0 displayAtts.add(FeatureMatcher.toAttributeDisplayName(attName));
1151  0 String desc = fa.getDescription(featureType, attName);
1152  0 if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
1153    {
1154  0 desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
1155    }
1156  0 tooltips.add(desc == null ? "" : desc);
1157    }
1158   
1159    // now convert String List to Object List for buildComboWithTooltips
1160  0 List<Object> displayAttsObjects = new ArrayList<>(displayAtts);
1161  0 JComboBox<Object> attCombo = JvSwingUtils
1162    .buildComboWithTooltips(displayAttsObjects, tooltips);
1163   
1164  0 return attCombo;
1165    }
1166   
1167    /**
1168    * Populates initial layout of the feature attribute filters panel
1169    */
 
1170  0 toggle private JPanel initialiseFiltersPanel()
1171    {
1172  0 filters = new ArrayList<>();
1173   
1174  0 JPanel filtersPanel = new JPanel();
1175  0 filtersPanel.setLayout(new BoxLayout(filtersPanel, BoxLayout.Y_AXIS));
1176  0 filtersPanel.setBackground(Color.white);
1177  0 JvSwingUtils.createTitledBorder(filtersPanel,
1178    MessageManager.getString("label.filters"), true);
1179   
1180  0 JPanel andOrPanel = initialiseAndOrPanel();
1181  0 filtersPanel.add(andOrPanel);
1182   
1183    /*
1184    * panel with filters - populated by refreshFiltersDisplay,
1185    * which also sets the layout manager
1186    */
1187  0 chooseFiltersPanel = new JPanel();
1188  0 chooseFiltersPanel.setBackground(Color.white);
1189  0 filtersPanel.add(chooseFiltersPanel);
1190   
1191  0 return filtersPanel;
1192    }
1193   
1194    /**
1195    * Lays out the panel with radio buttons to AND or OR filter conditions
1196    *
1197    * @return
1198    */
 
1199  0 toggle private JPanel initialiseAndOrPanel()
1200    {
1201  0 JPanel andOrPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
1202  0 andOrPanel.setBackground(Color.white);
1203  0 andFilters = new JRadioButton(MessageManager.getString("label.and"));
1204  0 orFilters = new JRadioButton(MessageManager.getString("label.or"));
1205  0 andFilters.setOpaque(false);
1206  0 orFilters.setOpaque(false);
1207  0 ActionListener actionListener = new ActionListener()
1208    {
 
1209  0 toggle @Override
1210    public void actionPerformed(ActionEvent e)
1211    {
1212  0 filtersChanged();
1213    }
1214    };
1215  0 andFilters.addActionListener(actionListener);
1216  0 orFilters.addActionListener(actionListener);
1217  0 ButtonGroup andOr = new ButtonGroup();
1218  0 andOr.add(andFilters);
1219  0 andOr.add(orFilters);
1220  0 andFilters.setSelected(true);
1221  0 andOrPanel.add(
1222    new JLabel(MessageManager.getString("label.join_conditions")));
1223  0 andOrPanel.add(andFilters);
1224  0 andOrPanel.add(orFilters);
1225  0 return andOrPanel;
1226    }
1227   
1228    /**
1229    * Refreshes the display to show any filters currently configured for the
1230    * selected feature type (editable, with 'remove' option), plus one extra row
1231    * for adding a condition. This should be called after a filter has been
1232    * removed, added or amended.
1233    */
 
1234  0 toggle private void updateFiltersPanel()
1235    {
1236    /*
1237    * clear the panel and list of filter conditions
1238    */
1239  0 chooseFiltersPanel.removeAll();
1240  0 filters.clear();
1241   
1242    /*
1243    * look up attributes known for feature type
1244    */
1245  0 List<String[]> attNames = FeatureAttributes.getInstance()
1246    .getAttributes(featureType);
1247   
1248    /*
1249    * if this feature type has filters set, load them first
1250    */
1251  0 FeatureMatcherSetI featureFilters = fr.getFeatureFilter(featureType);
1252  0 if (featureFilters != null)
1253    {
1254  0 if (!featureFilters.isAnded())
1255    {
1256  0 orFilters.setSelected(true);
1257    }
1258    // avoid use of lambda expression to keep SwingJS happy
1259    // featureFilters.getMatchers().forEach(item -> filters.add(item));
1260  0 for (FeatureMatcherI matcher : featureFilters.getMatchers())
1261    {
1262  0 filters.add(matcher);
1263    }
1264    }
1265   
1266    /*
1267    * and an empty filter for the user to populate (add)
1268    */
1269  0 filters.add(FeatureMatcher.NULL_MATCHER);
1270   
1271    /*
1272    * use GridLayout to 'justify' rows to the top of the panel, until
1273    * there are too many to fit in, then fall back on BoxLayout
1274    */
1275  0 if (filters.size() <= 5)
1276    {
1277  0 chooseFiltersPanel.setLayout(new GridLayout(5, 1));
1278    }
1279    else
1280    {
1281  0 chooseFiltersPanel.setLayout(
1282    new BoxLayout(chooseFiltersPanel, BoxLayout.Y_AXIS));
1283    }
1284   
1285    /*
1286    * render the conditions in rows, each in its own JPanel
1287    */
1288  0 int filterIndex = 0;
1289  0 for (FeatureMatcherI filter : filters)
1290    {
1291  0 JPanel row = addFilter(filter, attNames, filterIndex);
1292  0 chooseFiltersPanel.add(row);
1293  0 filterIndex++;
1294    }
1295   
1296  0 this.validate();
1297  0 this.repaint();
1298    }
1299   
1300    /**
1301    * A helper method that constructs a row (panel) with one filter condition:
1302    * <ul>
1303    * <li>a drop-down list of Label, Score and attribute names to choose
1304    * from</li>
1305    * <li>a drop-down list of conditions to choose from</li>
1306    * <li>a text field for input of a match pattern</li>
1307    * <li>optionally, a 'remove' button</li>
1308    * </ul>
1309    * The filter values are set as defaults for the input fields. The 'remove'
1310    * button is added unless the pattern is empty (incomplete filter condition).
1311    * <p>
1312    * Action handlers on these fields provide for
1313    * <ul>
1314    * <li>validate pattern field - should be numeric if condition is numeric</li>
1315    * <li>save filters and refresh display on any (valid) change</li>
1316    * <li>remove filter and refresh on 'Remove'</li>
1317    * <li>update conditions list on change of Label/Score/Attribute</li>
1318    * <li>refresh value field tooltip with min-max range on change of
1319    * attribute</li>
1320    * </ul>
1321    *
1322    * @param filter
1323    * @param attNames
1324    * @param filterIndex
1325    * @return
1326    */
 
1327  0 toggle protected JPanel addFilter(FeatureMatcherI filter,
1328    List<String[]> attNames, int filterIndex)
1329    {
1330  0 String[] attName = filter.getAttribute();
1331  0 Condition cond = filter.getMatcher().getCondition();
1332  0 String pattern = filter.getMatcher().getPattern();
1333   
1334  0 JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
1335  0 filterRow.setBackground(Color.white);
1336   
1337    /*
1338    * drop-down choice of attribute, with description as a tooltip
1339    * if we can obtain it
1340    */
1341  0 final JComboBox<Object> attCombo = populateAttributesDropdown(attNames,
1342    true, true);
1343  0 String filterBy = setSelectedAttribute(attCombo, filter);
1344   
1345  0 JComboBox<Condition> condCombo = new JComboBox<>();
1346   
1347  0 JTextField patternField = new JTextField(8);
1348  0 patternField.setText(pattern);
1349   
1350    /*
1351    * action handlers that validate and (if valid) apply changes
1352    */
1353  0 ActionListener actionListener = new ActionListener()
1354    {
 
1355  0 toggle @Override
1356    public void actionPerformed(ActionEvent e)
1357    {
1358  0 if (validateFilter(patternField, condCombo))
1359    {
1360  0 if (updateFilter(attCombo, condCombo, patternField, filterIndex))
1361    {
1362  0 filtersChanged();
1363    }
1364    }
1365    }
1366    };
1367  0 ItemListener itemListener = new ItemListener()
1368    {
 
1369  0 toggle @Override
1370    public void itemStateChanged(ItemEvent e)
1371    {
1372  0 actionListener.actionPerformed(null);
1373    }
1374    };
1375   
1376  0 if (filter == FeatureMatcher.NULL_MATCHER) // the 'add a condition' row
1377    {
1378  0 attCombo.setSelectedIndex(0);
1379    }
1380    else
1381    {
1382  0 attCombo.setSelectedItem(
1383    FeatureMatcher.toAttributeDisplayName(attName));
1384    }
1385  0 attCombo.addItemListener(new ItemListener()
1386    {
 
1387  0 toggle @Override
1388    public void itemStateChanged(ItemEvent e)
1389    {
1390    /*
1391    * on change of attribute, refresh the conditions list to
1392    * ensure it is appropriate for the attribute datatype
1393    */
1394  0 populateConditions((String) attCombo.getSelectedItem(),
1395    (Condition) condCombo.getSelectedItem(), condCombo,
1396    patternField);
1397  0 actionListener.actionPerformed(null);
1398    }
1399    });
1400   
1401  0 filterRow.add(attCombo);
1402   
1403    /*
1404    * drop-down choice of test condition
1405    */
1406  0 populateConditions(filterBy, cond, condCombo, patternField);
1407  0 condCombo.setPreferredSize(new Dimension(150, 20));
1408  0 condCombo.addItemListener(itemListener);
1409  0 filterRow.add(condCombo);
1410   
1411    /*
1412    * pattern to match against
1413    */
1414  0 patternField.addActionListener(actionListener);
1415  0 patternField.addFocusListener(new FocusAdapter()
1416    {
 
1417  0 toggle @Override
1418    public void focusLost(FocusEvent e)
1419    {
1420  0 actionListener.actionPerformed(null);
1421    }
1422    });
1423  0 filterRow.add(patternField);
1424   
1425    /*
1426    * disable pattern field for condition 'Present / NotPresent'
1427    */
1428  0 Condition selectedCondition = (Condition) condCombo.getSelectedItem();
1429  0 patternField.setEnabled(selectedCondition.needsAPattern());
1430   
1431    /*
1432    * if a numeric condition is selected, show the value range
1433    * as a tooltip on the value input field
1434    */
1435  0 setNumericHints(filterBy, selectedCondition, patternField);
1436   
1437    /*
1438    * add remove button if filter is populated (non-empty pattern)
1439    */
1440  0 if (!patternField.isEnabled()
1441    || (pattern != null && pattern.trim().length() > 0))
1442    {
1443  0 JButton removeCondition = new JButton("\u2717");
1444    // Dingbats cursive x
1445  0 removeCondition.setBorder(new EmptyBorder(0, 0, 0, 0));
1446  0 removeCondition.setBackground(Color.WHITE);
1447  0 removeCondition.setPreferredSize(new Dimension(23, 17));
1448  0 removeCondition.setToolTipText(
1449    MessageManager.getString("label.delete_condition"));
1450  0 removeCondition.addActionListener(new ActionListener()
1451    {
 
1452  0 toggle @Override
1453    public void actionPerformed(ActionEvent e)
1454    {
1455  0 filters.remove(filterIndex);
1456  0 filtersChanged();
1457    }
1458    });
1459  0 filterRow.add(removeCondition);
1460    }
1461   
1462  0 return filterRow;
1463    }
1464   
1465    /**
1466    * Sets the selected item in the Label/Score/Attribute drop-down to match the
1467    * filter
1468    *
1469    * @param attCombo
1470    * @param filter
1471    */
 
1472  0 toggle private String setSelectedAttribute(JComboBox<Object> attCombo,
1473    FeatureMatcherI filter)
1474    {
1475  0 String item = null;
1476  0 if (filter.isByScore())
1477    {
1478  0 item = SCORE_18N;
1479    }
1480  0 else if (filter.isByLabel())
1481    {
1482  0 item = LABEL_18N;
1483    }
1484    else
1485    {
1486  0 item = FeatureMatcher.toAttributeDisplayName(filter.getAttribute());
1487    }
1488  0 attCombo.setSelectedItem(item);
1489  0 return item;
1490    }
1491   
1492    /**
1493    * If a numeric comparison condition is selected, retrieves the min-max range
1494    * for the value (score or attribute), and sets it as a tooltip on the value
1495    * field. If the field is currently empty, then pre-populates it with
1496    * <ul>
1497    * <li>the minimum value, if condition is > or >=</li>
1498    * <li>the maximum value, if condition is < or <=</li>
1499    * </ul>
1500    *
1501    * @param attName
1502    * @param selectedCondition
1503    * @param patternField
1504    */
 
1505  0 toggle private void setNumericHints(String attName, Condition selectedCondition,
1506    JTextField patternField)
1507    {
1508  0 patternField.setToolTipText("");
1509   
1510  0 if (selectedCondition.isNumeric())
1511    {
1512  0 float[] minMax = getMinMax(attName);
1513  0 if (minMax != null)
1514    {
1515  0 String minFormatted = DECFMT_2_2.format(minMax[0]);
1516  0 String maxFormatted = DECFMT_2_2.format(minMax[1]);
1517  0 String tip = String.format("(%s - %s)", minFormatted, maxFormatted);
1518  0 patternField.setToolTipText(tip);
1519  0 if (patternField.getText().isEmpty())
1520    {
1521  0 if (selectedCondition == Condition.GE
1522    || selectedCondition == Condition.GT)
1523    {
1524  0 patternField.setText(minFormatted);
1525    }
1526    else
1527    {
1528  0 if (selectedCondition == Condition.LE
1529    || selectedCondition == Condition.LT)
1530    {
1531  0 patternField.setText(maxFormatted);
1532    }
1533    }
1534    }
1535    }
1536    }
1537    }
1538   
1539    /**
1540    * Populates the drop-down list of comparison conditions for the given
1541    * attribute name. The conditions added depend on the datatype of the
1542    * attribute values. The supplied condition is set as the selected item in the
1543    * list, provided it is in the list. If the pattern is now invalid
1544    * (non-numeric pattern for a numeric condition), it is cleared.
1545    *
1546    * @param attName
1547    * @param cond
1548    * @param condCombo
1549    * @param patternField
1550    */
 
1551  0 toggle void populateConditions(String attName, Condition cond,
1552    JComboBox<Condition> condCombo, JTextField patternField)
1553    {
1554  0 Datatype type = FeatureAttributes.getInstance().getDatatype(featureType,
1555    FeatureMatcher.fromAttributeDisplayName(attName));
1556  0 if (LABEL_18N.equals(attName))
1557    {
1558  0 type = Datatype.Character;
1559    }
1560  0 else if (SCORE_18N.equals(attName))
1561    {
1562  0 type = Datatype.Number;
1563    }
1564   
1565    /*
1566    * remove itemListener before starting
1567    */
1568  0 ItemListener listener = condCombo.getItemListeners()[0];
1569  0 condCombo.removeItemListener(listener);
1570  0 boolean condIsValid = false;
1571   
1572  0 condCombo.removeAllItems();
1573  0 for (Condition c : Condition.values())
1574    {
1575  0 if ((c.isNumeric() && type == Datatype.Number)
1576    || (!c.isNumeric() && type != Datatype.Number))
1577    {
1578  0 condCombo.addItem(c);
1579  0 if (c == cond)
1580    {
1581  0 condIsValid = true;
1582    }
1583    }
1584    }
1585   
1586    /*
1587    * set the selected condition (does nothing if not in the list)
1588    */
1589  0 if (condIsValid)
1590    {
1591  0 condCombo.setSelectedItem(cond);
1592    }
1593    else
1594    {
1595  0 condCombo.setSelectedIndex(0);
1596    }
1597   
1598    /*
1599    * clear pattern if it is now invalid for condition
1600    */
1601  0 if (((Condition) condCombo.getSelectedItem()).isNumeric())
1602    {
1603  0 try
1604    {
1605  0 String pattern = patternField.getText().trim();
1606  0 if (pattern.length() > 0)
1607    {
1608  0 Float.valueOf(pattern);
1609    }
1610    } catch (NumberFormatException e)
1611    {
1612  0 patternField.setText("");
1613    }
1614    }
1615   
1616    /*
1617    * restore the listener
1618    */
1619  0 condCombo.addItemListener(listener);
1620    }
1621   
1622    /**
1623    * Answers true unless a numeric condition has been selected with a
1624    * non-numeric value. Sets the value field to RED with a tooltip if in error.
1625    * <p>
1626    * If the pattern is expected but is empty, this method returns false, but
1627    * does not mark the field as invalid. This supports selecting an attribute
1628    * for a new condition before a match pattern has been entered.
1629    *
1630    * @param value
1631    * @param condCombo
1632    */
 
1633  0 toggle protected boolean validateFilter(JTextField value,
1634    JComboBox<Condition> condCombo)
1635    {
1636  0 if (value == null || condCombo == null)
1637    {
1638  0 return true; // fields not populated
1639    }
1640   
1641  0 Condition cond = (Condition) condCombo.getSelectedItem();
1642  0 if (!cond.needsAPattern())
1643    {
1644  0 return true;
1645    }
1646   
1647  0 value.setBackground(Color.white);
1648  0 value.setToolTipText("");
1649  0 String v1 = value.getText().trim();
1650  0 if (v1.length() == 0)
1651    {
1652    // return false;
1653    }
1654   
1655  0 if (cond.isNumeric() && v1.length() > 0)
1656    {
1657  0 try
1658    {
1659  0 Float.valueOf(v1);
1660    } catch (NumberFormatException e)
1661    {
1662  0 value.setBackground(Color.red);
1663  0 value.setToolTipText(
1664    MessageManager.getString("label.numeric_required"));
1665  0 return false;
1666    }
1667    }
1668   
1669  0 return true;
1670    }
1671   
1672    /**
1673    * Constructs a filter condition from the given input fields, and replaces the
1674    * condition at filterIndex with the new one. Does nothing if the pattern
1675    * field is blank (unless the match condition is one that doesn't require a
1676    * pattern, e.g. 'Is present'). Answers true if the filter was updated, else
1677    * false.
1678    * <p>
1679    * This method may update the tooltip on the filter value field to show the
1680    * value range, if a numeric condition is selected. This ensures the tooltip
1681    * is updated when a numeric valued attribute is chosen on the last 'add a
1682    * filter' row.
1683    *
1684    * @param attCombo
1685    * @param condCombo
1686    * @param valueField
1687    * @param filterIndex
1688    */
 
1689  0 toggle protected boolean updateFilter(JComboBox<Object> attCombo,
1690    JComboBox<Condition> condCombo, JTextField valueField,
1691    int filterIndex)
1692    {
1693  0 String attName;
1694  0 try
1695    {
1696  0 attName = (String) attCombo.getSelectedItem();
1697    } catch (Exception e)
1698    {
1699  0 Cache.log.error("Problem casting Combo box entry to String");
1700  0 attName = attCombo.getSelectedItem().toString();
1701    }
1702  0 Condition cond = (Condition) condCombo.getSelectedItem();
1703  0 String pattern = valueField.getText().trim();
1704   
1705  0 setNumericHints(attName, cond, valueField);
1706   
1707  0 if (pattern.length() == 0 && cond.needsAPattern())
1708    {
1709  0 valueField.setEnabled(true); // ensure pattern field is enabled!
1710  0 return false;
1711    }
1712   
1713    /*
1714    * Construct a matcher that operates on Label, Score,
1715    * or named attribute
1716    */
1717  0 FeatureMatcherI km = null;
1718  0 if (LABEL_18N.equals(attName))
1719    {
1720  0 km = FeatureMatcher.byLabel(cond, pattern);
1721    }
1722  0 else if (SCORE_18N.equals(attName))
1723    {
1724  0 km = FeatureMatcher.byScore(cond, pattern);
1725    }
1726    else
1727    {
1728  0 km = FeatureMatcher.byAttribute(cond, pattern,
1729    FeatureMatcher.fromAttributeDisplayName(attName));
1730    }
1731   
1732  0 filters.set(filterIndex, km);
1733   
1734  0 return true;
1735    }
1736   
1737    /**
1738    * Action on any change to feature filtering, namely
1739    * <ul>
1740    * <li>change of selected attribute</li>
1741    * <li>change of selected condition</li>
1742    * <li>change of match pattern</li>
1743    * <li>removal of a condition</li>
1744    * </ul>
1745    * The inputs are parsed into a combined filter and this is set for the
1746    * feature type, and the alignment redrawn.
1747    */
 
1748  0 toggle protected void filtersChanged()
1749    {
1750    /*
1751    * update the filter conditions for the feature type
1752    */
1753  0 boolean anded = andFilters.isSelected();
1754  0 FeatureMatcherSetI combined = new FeatureMatcherSet();
1755   
1756  0 for (FeatureMatcherI filter : filters)
1757    {
1758  0 String pattern = filter.getMatcher().getPattern();
1759  0 Condition condition = filter.getMatcher().getCondition();
1760  0 if (pattern.trim().length() > 0 || !condition.needsAPattern())
1761    {
1762  0 if (anded)
1763    {
1764  0 combined.and(filter);
1765    }
1766    else
1767    {
1768  0 combined.or(filter);
1769    }
1770    }
1771    }
1772   
1773    /*
1774    * save the filter conditions in the FeatureRenderer
1775    * (note this might now be an empty filter with no conditions)
1776    */
1777  0 fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined);
1778  0 refreshDisplay(true);
1779   
1780  0 updateFiltersPanel();
1781    }
1782   
1783    /**
1784    * Repaints alignment, structure and overview (if shown). If there is a
1785    * complementary view which is showing this view's features, then also
1786    * repaints that.
1787    *
1788    * @param updateStructsAndOverview
1789    */
 
1790  0 toggle void refreshDisplay(boolean updateStructsAndOverview)
1791    {
1792  0 ap.paintAlignment(true, updateStructsAndOverview);
1793  0 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
1794  0 if (complement != null && complement.isShowComplementFeatures())
1795    {
1796  0 AlignFrame af2 = Desktop.getAlignFrameFor(complement);
1797  0 af2.alignPanel.paintAlignment(true, updateStructsAndOverview);
1798    }
1799    }
1800    }