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