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