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