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