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