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