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