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