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