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