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