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