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