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