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