JAL-2808 add attribute filter options to graduate colour dialog
[jalview.git] / src / jalview / gui / FeatureColourChooser.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.FeatureColourI;
24 import jalview.datamodel.GraphLine;
25 import jalview.datamodel.features.FeatureAttributes;
26 import jalview.schemes.FeatureColour;
27 import jalview.util.MessageManager;
28 import jalview.util.matcher.Condition;
29 import jalview.util.matcher.KeyedMatcher;
30 import jalview.util.matcher.KeyedMatcherI;
31 import jalview.util.matcher.Matcher;
32 import jalview.util.matcher.MatcherI;
33
34 import java.awt.BorderLayout;
35 import java.awt.Color;
36 import java.awt.Dimension;
37 import java.awt.FlowLayout;
38 import java.awt.GridLayout;
39 import java.awt.event.ActionEvent;
40 import java.awt.event.ActionListener;
41 import java.awt.event.FocusAdapter;
42 import java.awt.event.FocusEvent;
43 import java.awt.event.MouseAdapter;
44 import java.awt.event.MouseEvent;
45 import java.util.Iterator;
46
47 import javax.swing.BorderFactory;
48 import javax.swing.JCheckBox;
49 import javax.swing.JColorChooser;
50 import javax.swing.JComboBox;
51 import javax.swing.JLabel;
52 import javax.swing.JPanel;
53 import javax.swing.JSlider;
54 import javax.swing.JTextField;
55 import javax.swing.border.LineBorder;
56 import javax.swing.event.ChangeEvent;
57 import javax.swing.event.ChangeListener;
58
59 public class FeatureColourChooser extends JalviewDialog
60 {
61   // FeatureSettings fs;
62   private FeatureRenderer fr;
63
64   private FeatureColourI cs;
65
66   private FeatureColourI oldcs;
67
68   private AlignmentPanel ap;
69
70   private boolean adjusting = false;
71
72   final private float min;
73
74   final private float max;
75
76   final private float scaleFactor;
77
78   private String type = null;
79
80   private JPanel minColour = new JPanel();
81
82   private JPanel maxColour = new JPanel();
83
84   private JComboBox<String> threshold = new JComboBox<>();
85
86   private JSlider slider = new JSlider();
87
88   private JTextField thresholdValue = new JTextField(20);
89
90   // TODO implement GUI for tolower flag
91   // JCheckBox toLower = new JCheckBox();
92
93   private JCheckBox thresholdIsMin = new JCheckBox();
94
95   private JCheckBox colourByLabel = new JCheckBox();
96
97   private GraphLine threshline;
98
99   private Color oldmaxColour;
100
101   private Color oldminColour;
102
103   private ActionListener colourEditor = null;
104
105   private JComboBox<String> filterAttribute;
106
107   private JComboBox<Condition> filterCondition;
108
109   private JTextField filterValue;
110
111   private JComboBox<String> filterAttribute2;
112
113   private JComboBox<Condition> filterCondition2;
114
115   private JTextField filterValue2;
116
117   /**
118    * Constructor
119    * 
120    * @param frender
121    * @param theType
122    */
123   public FeatureColourChooser(FeatureRenderer frender, String theType)
124   {
125     this(frender, false, theType);
126   }
127
128   /**
129    * Constructor, with option to make a blocking dialog (has to complete in the
130    * AWT event queue thread). Currently this option is always set to false.
131    * 
132    * @param frender
133    * @param blocking
134    * @param theType
135    */
136   FeatureColourChooser(FeatureRenderer frender, boolean blocking,
137           String theType)
138   {
139     this.fr = frender;
140     this.type = theType;
141     ap = fr.ap;
142     String title = MessageManager
143             .formatMessage("label.graduated_color_for_params", new String[]
144             { theType });
145     initDialogFrame(this, true, blocking, title, 480, 185);
146
147     slider.addChangeListener(new ChangeListener()
148     {
149       @Override
150       public void stateChanged(ChangeEvent evt)
151       {
152         if (!adjusting)
153         {
154           thresholdValue.setText((slider.getValue() / scaleFactor) + "");
155           sliderValueChanged();
156         }
157       }
158     });
159     slider.addMouseListener(new MouseAdapter()
160     {
161       @Override
162       public void mouseReleased(MouseEvent evt)
163       {
164         /*
165          * only update Overview and/or structure colouring
166          * when threshold slider drag ends (mouse up)
167          */
168         if (ap != null)
169         {
170           ap.paintAlignment(true, true);
171         }
172       }
173     });
174
175     float mm[] = fr.getMinMax().get(theType)[0];
176     min = mm[0];
177     max = mm[1];
178
179     /*
180      * ensure scale factor allows a scaled range with
181      * 10 integer divisions ('ticks'); if we have got here,
182      * we should expect that max != min
183      */
184     scaleFactor = (max == min) ? 1f : 100f / (max - min);
185
186     oldcs = fr.getFeatureColours().get(theType);
187     if (!oldcs.isSimpleColour())
188     {
189       if (oldcs.isAutoScaled())
190       {
191         // update the scale
192         cs = new FeatureColour((FeatureColour) oldcs, min, max);
193       }
194       else
195       {
196         cs = new FeatureColour((FeatureColour) oldcs);
197       }
198     }
199     else
200     {
201       // promote original color to a graduated color
202       Color bl = oldcs.getColour();
203       if (bl == null)
204       {
205         bl = Color.BLACK;
206       }
207       // original colour becomes the maximum colour
208       cs = new FeatureColour(Color.white, bl, mm[0], mm[1]);
209       cs.setColourByLabel(false);
210     }
211     minColour.setBackground(oldminColour = cs.getMinColour());
212     maxColour.setBackground(oldmaxColour = cs.getMaxColour());
213     adjusting = true;
214
215     try
216     {
217       jbInit();
218     } catch (Exception ex)
219     {
220     }
221     // update the gui from threshold state
222     thresholdIsMin.setSelected(!cs.isAutoScaled());
223     colourByLabel.setSelected(cs.isColourByLabel());
224     if (cs.hasThreshold())
225     {
226       // initialise threshold slider and selector
227       threshold.setSelectedIndex(cs.isAboveThreshold() ? 1 : 2);
228       slider.setEnabled(true);
229       slider.setValue((int) (cs.getThreshold() * scaleFactor));
230       thresholdValue.setEnabled(true);
231       threshline = new GraphLine((max - min) / 2f, "Threshold",
232               Color.black);
233       threshline.value = cs.getThreshold();
234     }
235
236     setInitialFilters(cs.getAttributeFilters());
237
238     adjusting = false;
239
240     changeColour(false);
241     waitForInput();
242   }
243
244   /**
245    * Populates the attribute filter fields for the initial display
246    * 
247    * @param attributeFilters
248    */
249   void setInitialFilters(KeyedMatcherI attributeFilters)
250   {
251     // todo generalise to populate N conditions
252     
253     if (attributeFilters != null)
254     {
255       filterAttribute.setSelectedItem(attributeFilters.getKey());
256       filterCondition.setSelectedItem(attributeFilters.getMatcher()
257               .getCondition());
258       filterValue.setText(attributeFilters.getMatcher().getPattern());
259       
260       KeyedMatcherI second = attributeFilters.getSecondMatcher();
261       if (second != null)
262       {
263         // todo add OR/AND condition to gui
264         filterAttribute2.setSelectedItem(second.getKey());
265         filterCondition2
266                 .setSelectedItem(second.getMatcher().getCondition());
267         filterValue2.setText(second.getMatcher().getPattern());
268       }
269     }
270   }
271
272   private void jbInit() throws Exception
273   {
274     this.setLayout(new GridLayout(4, 1));
275
276     JPanel colourByPanel = initColoursPanel();
277
278     JPanel thresholdPanel = initThresholdPanel();
279
280     JPanel okCancelPanel = initOkCancelPanel();
281
282     this.add(colourByPanel);
283     this.add(thresholdPanel);
284
285     /*
286      * add filter by attributes options only if we know any attributes
287      */
288     Iterator<String> attributes = FeatureAttributes.getInstance()
289             .getAttributes(type).iterator();
290     if (attributes.hasNext())
291     {
292       JPanel filtersPanel = initFiltersPanel(attributes);
293       this.add(filtersPanel);
294     }
295
296     this.add(okCancelPanel);
297   }
298
299   /**
300    * Lay out fields for threshold options
301    * 
302    * @return
303    */
304   protected JPanel initThresholdPanel()
305   {
306     JPanel thresholdPanel = new JPanel();
307     thresholdPanel.setLayout(new FlowLayout());
308     threshold.addActionListener(new ActionListener()
309     {
310       @Override
311       public void actionPerformed(ActionEvent e)
312       {
313         changeColour(true);
314       }
315     });
316     threshold.setToolTipText(MessageManager
317             .getString("label.threshold_feature_display_by_score"));
318     threshold.addItem(MessageManager
319             .getString("label.threshold_feature_no_threshold")); // index 0
320     threshold.addItem(MessageManager
321             .getString("label.threshold_feature_above_threshold")); // index 1
322     threshold.addItem(MessageManager
323             .getString("label.threshold_feature_below_threshold")); // index 2
324
325     thresholdValue.addActionListener(new ActionListener()
326     {
327       @Override
328       public void actionPerformed(ActionEvent e)
329       {
330         thresholdValue_actionPerformed();
331       }
332     });
333     thresholdValue.addFocusListener(new FocusAdapter()
334     {
335       @Override
336       public void focusLost(FocusEvent e)
337       {
338         thresholdValue_actionPerformed();
339       }
340     });
341     slider.setPaintLabels(false);
342     slider.setPaintTicks(true);
343     slider.setBackground(Color.white);
344     slider.setEnabled(false);
345     slider.setOpaque(false);
346     slider.setPreferredSize(new Dimension(100, 32));
347     slider.setToolTipText(
348             MessageManager.getString("label.adjust_threshold"));
349     thresholdValue.setEnabled(false);
350     thresholdValue.setColumns(7);
351     thresholdPanel.setBackground(Color.white);
352     thresholdIsMin.setBackground(Color.white);
353     thresholdIsMin
354             .setText(MessageManager.getString("label.threshold_minmax"));
355     thresholdIsMin.setToolTipText(MessageManager
356             .getString("label.toggle_absolute_relative_display_threshold"));
357     thresholdIsMin.addActionListener(new ActionListener()
358     {
359       @Override
360       public void actionPerformed(ActionEvent actionEvent)
361       {
362         changeColour(true);
363       }
364     });
365     thresholdPanel.add(threshold);
366     thresholdPanel.add(slider);
367     thresholdPanel.add(thresholdValue);
368     thresholdPanel.add(thresholdIsMin);
369     return thresholdPanel;
370   }
371
372   /**
373    * Lay out OK and Cancel buttons
374    * 
375    * @return
376    */
377   protected JPanel initOkCancelPanel()
378   {
379     JPanel okCancelPanel = new JPanel();
380     okCancelPanel.setBackground(Color.white);
381     okCancelPanel.add(ok);
382     okCancelPanel.add(cancel);
383     return okCancelPanel;
384   }
385
386   /**
387    * Lay out Colour by Label and min/max colour widgets
388    * 
389    * @return
390    */
391   protected JPanel initColoursPanel()
392   {
393     JPanel colourByPanel = new JPanel();
394     colourByPanel.setLayout(new FlowLayout());
395     colourByPanel.setBackground(Color.white);
396     minColour.setFont(JvSwingUtils.getLabelFont());
397     minColour.setBorder(BorderFactory.createLineBorder(Color.black));
398     minColour.setPreferredSize(new Dimension(40, 20));
399     minColour.setToolTipText(MessageManager.getString("label.min_colour"));
400     minColour.addMouseListener(new MouseAdapter()
401     {
402       @Override
403       public void mousePressed(MouseEvent e)
404       {
405         if (minColour.isEnabled())
406         {
407           minColour_actionPerformed();
408         }
409       }
410     });
411     maxColour.setFont(JvSwingUtils.getLabelFont());
412     maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
413     maxColour.setPreferredSize(new Dimension(40, 20));
414     maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
415     maxColour.addMouseListener(new MouseAdapter()
416     {
417       @Override
418       public void mousePressed(MouseEvent e)
419       {
420         if (maxColour.isEnabled())
421         {
422           maxColour_actionPerformed();
423         }
424       }
425     });
426     maxColour.setBorder(new LineBorder(Color.black));
427     JLabel minText = new JLabel(MessageManager.getString("label.min"));
428     minText.setFont(JvSwingUtils.getLabelFont());
429     JLabel maxText = new JLabel(MessageManager.getString("label.max"));
430     maxText.setFont(JvSwingUtils.getLabelFont());
431
432     JPanel colourPanel = new JPanel();
433     colourPanel.setBackground(Color.white);
434     colourPanel.add(minText);
435     colourPanel.add(minColour);
436     colourPanel.add(maxText);
437     colourPanel.add(maxColour);
438     colourByPanel.add(colourByLabel, BorderLayout.WEST);
439     colourByPanel.add(colourPanel, BorderLayout.EAST);
440
441     colourByLabel.setBackground(Color.white);
442     colourByLabel
443             .setText(MessageManager.getString("label.colour_by_label"));
444     colourByLabel
445             .setToolTipText(MessageManager
446                     .getString("label.display_features_same_type_different_label_using_different_colour"));
447     colourByLabel.addActionListener(new ActionListener()
448     {
449       @Override
450       public void actionPerformed(ActionEvent actionEvent)
451       {
452         changeColour(true);
453       }
454     });
455
456     return colourByPanel;
457   }
458
459   /**
460    * Action on clicking the 'minimum colour' - open a colour chooser dialog, and
461    * set the selected colour (if the user does not cancel out of the dialog)
462    */
463   protected void minColour_actionPerformed()
464   {
465     Color col = JColorChooser.showDialog(this,
466             MessageManager.getString("label.select_colour_minimum_value"),
467             minColour.getBackground());
468     if (col != null)
469     {
470       minColour.setBackground(col);
471       minColour.setForeground(col);
472     }
473     minColour.repaint();
474     changeColour(true);
475   }
476
477   /**
478    * Action on clicking the 'maximum colour' - open a colour chooser dialog, and
479    * set the selected colour (if the user does not cancel out of the dialog)
480    */
481   protected void maxColour_actionPerformed()
482   {
483     Color col = JColorChooser.showDialog(this,
484             MessageManager.getString("label.select_colour_maximum_value"),
485             maxColour.getBackground());
486     if (col != null)
487     {
488       maxColour.setBackground(col);
489       maxColour.setForeground(col);
490     }
491     maxColour.repaint();
492     changeColour(true);
493   }
494
495   /**
496    * Constructs and sets the selected colour options as the colour for the
497    * feature type, and repaints the alignment, and optionally the Overview
498    * and/or structure viewer if open
499    * 
500    * @param updateStructsAndOverview
501    */
502   void changeColour(boolean updateStructsAndOverview)
503   {
504     // Check if combobox is still adjusting
505     if (adjusting)
506     {
507       return;
508     }
509
510     if (!validateInputs())
511     {
512       return;
513     }
514
515     boolean aboveThreshold = false;
516     boolean belowThreshold = false;
517     if (threshold.getSelectedIndex() == 1)
518     {
519       aboveThreshold = true;
520     }
521     else if (threshold.getSelectedIndex() == 2)
522     {
523       belowThreshold = true;
524     }
525     boolean hasThreshold = aboveThreshold || belowThreshold;
526
527     slider.setEnabled(true);
528     thresholdValue.setEnabled(true);
529
530     FeatureColourI acg;
531     if (cs.isColourByLabel())
532     {
533       acg = new FeatureColour(oldminColour, oldmaxColour, min, max);
534     }
535     else
536     {
537       acg = new FeatureColour(oldminColour = minColour.getBackground(),
538               oldmaxColour = maxColour.getBackground(), min, max);
539     }
540
541     if (!hasThreshold)
542     {
543       slider.setEnabled(false);
544       thresholdValue.setEnabled(false);
545       thresholdValue.setText("");
546       thresholdIsMin.setEnabled(false);
547     }
548     else if (threshline == null)
549     {
550       /*
551        * todo not yet implemented: visual indication of feature threshold
552        */
553       threshline = new GraphLine((max - min) / 2f, "Threshold",
554               Color.black);
555     }
556
557     if (hasThreshold)
558     {
559       adjusting = true;
560       acg.setThreshold(threshline.value);
561
562       float range = (max - min) * scaleFactor;
563
564       slider.setMinimum((int) (min * scaleFactor));
565       slider.setMaximum((int) (max * scaleFactor));
566       // slider.setValue((int) (threshline.value * scaleFactor));
567       slider.setValue(Math.round(threshline.value * scaleFactor));
568       thresholdValue.setText(threshline.value + "");
569       slider.setMajorTickSpacing((int) (range / 10f));
570       slider.setEnabled(true);
571       thresholdValue.setEnabled(true);
572       thresholdIsMin.setEnabled(!colourByLabel.isSelected());
573       adjusting = false;
574     }
575
576     acg.setAboveThreshold(aboveThreshold);
577     acg.setBelowThreshold(belowThreshold);
578     if (thresholdIsMin.isSelected() && hasThreshold)
579     {
580       acg.setAutoScaled(false);
581       if (aboveThreshold)
582       {
583         acg = new FeatureColour((FeatureColour) acg, threshline.value, max);
584       }
585       else
586       {
587         acg = new FeatureColour((FeatureColour) acg, min, threshline.value);
588       }
589     }
590     else
591     {
592       acg.setAutoScaled(true);
593     }
594     acg.setColourByLabel(colourByLabel.isSelected());
595     if (acg.isColourByLabel())
596     {
597       maxColour.setEnabled(false);
598       minColour.setEnabled(false);
599       maxColour.setBackground(this.getBackground());
600       maxColour.setForeground(this.getBackground());
601       minColour.setBackground(this.getBackground());
602       minColour.setForeground(this.getBackground());
603
604     }
605     else
606     {
607       maxColour.setEnabled(true);
608       minColour.setEnabled(true);
609       maxColour.setBackground(oldmaxColour);
610       minColour.setBackground(oldminColour);
611       maxColour.setForeground(oldmaxColour);
612       minColour.setForeground(oldminColour);
613     }
614
615     /*
616      * add attribute filters if entered
617      */
618     if (filterAttribute != null)
619     {
620       setAttributeFilters(acg);
621     }
622
623     fr.setColour(type, acg);
624     cs = acg;
625     ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
626   }
627
628   /**
629    * Checks inputs are valid, and answers true if they are, else false. Also
630    * sets the colour of invalid inputs to red.
631    * 
632    * @return
633    */
634   boolean validateInputs()
635   {
636     // todo generalise to N filters
637     return isValidFilter(filterValue, filterCondition)
638             || isValidFilter(filterValue2, filterCondition2);
639   }
640
641   /**
642    * Answers true unless a numeric condition has been selected with a
643    * non-numeric value
644    * 
645    * @param value
646    * @param condition
647    */
648   protected boolean isValidFilter(JTextField value, JComboBox<Condition> condition)
649   {
650     if (value == null || condition == null)
651     {
652       return true; // fields not populated
653     }
654
655     value.setBackground(Color.white);
656     String v1 = value.getText().trim();
657     if (v1.length() > 0)
658     {
659       Condition c1 = (Condition) condition.getSelectedItem();
660       if (c1.isNumeric())
661       {
662         try
663         {
664           Float.valueOf(v1);
665         } catch (NumberFormatException e)
666         {
667           value.setBackground(Color.red);
668           return false;
669         }
670       }
671     }
672
673     return true;
674   }
675
676   /**
677    * Sets any attribute value filters entered in the dialog as filters on the
678    * colour scheme
679    * 
680    * @param acg
681    */
682   protected void setAttributeFilters(FeatureColourI acg)
683   {
684     String attribute = (String) filterAttribute.getSelectedItem();
685     Condition cond = (Condition) filterCondition.getSelectedItem();
686     String pattern = filterValue.getText().trim();
687     if (pattern.length() > 1)
688     {
689       MatcherI filter = new Matcher(cond, pattern);
690       KeyedMatcherI km = new KeyedMatcher(attribute, filter);
691
692       /*
693        * is there a second condition?
694        * todo: generalise to N conditions
695        */
696       pattern = filterValue2.getText().trim();
697       if (pattern.length() > 1)
698       {
699         attribute = (String) filterAttribute2.getSelectedItem();
700         cond = (Condition) filterCondition2.getSelectedItem();
701         filter = new Matcher(cond, pattern);
702         km = km.and(attribute, filter);
703       }
704       acg.setAttributeFilters(km);
705     }
706   }
707
708   @Override
709   protected void raiseClosed()
710   {
711     if (this.colourEditor != null)
712     {
713       colourEditor.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
714     }
715   }
716
717   @Override
718   public void okPressed()
719   {
720     changeColour(false);
721   }
722
723   @Override
724   public void cancelPressed()
725   {
726     reset();
727   }
728
729   /**
730    * Action when the user cancels the dialog. All previous settings should be
731    * restored and rendered on the alignment, and any linked Overview window or
732    * structure.
733    */
734   void reset()
735   {
736     fr.setColour(type, oldcs);
737     ap.paintAlignment(true, true);
738     cs = null;
739   }
740
741   /**
742    * Action on text entry of a threshold value
743    */
744   protected void thresholdValue_actionPerformed()
745   {
746     try
747     {
748       float f = Float.parseFloat(thresholdValue.getText());
749       slider.setValue((int) (f * scaleFactor));
750       threshline.value = f;
751
752       /*
753        * force repaint of any Overview window or structure
754        */
755       ap.paintAlignment(true, true);
756     } catch (NumberFormatException ex)
757     {
758     }
759   }
760
761   /**
762    * Action on change of threshold slider value. This may be done interactively
763    * (by moving the slider), or programmatically (to update the slider after
764    * manual input of a threshold value).
765    */
766   protected void sliderValueChanged()
767   {
768     /*
769      * squash rounding errors by forcing min/max of slider to 
770      * actual min/max of feature score range
771      */
772     int value = slider.getValue();
773     threshline.value = value == slider.getMaximum() ? max
774             : (value == slider.getMinimum() ? min : value / scaleFactor);
775     cs.setThreshold(threshline.value);
776
777     /*
778      * repaint alignment, but not Overview or structure,
779      * to avoid overload while dragging the slider
780      */
781     changeColour(false);
782   }
783
784   void addActionListener(ActionListener graduatedColorEditor)
785   {
786     if (colourEditor != null)
787     {
788       System.err.println(
789               "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
790     }
791     colourEditor = graduatedColorEditor;
792   }
793
794   /**
795    * Answers the last colour setting selected by user - either oldcs (which may
796    * be a java.awt.Color) or the new GraduatedColor
797    * 
798    * @return
799    */
800   FeatureColourI getLastColour()
801   {
802     if (cs == null)
803     {
804       return oldcs;
805     }
806     return cs;
807   }
808
809   /**
810    * Lay out fields for attribute value filters
811    * 
812    * @param attNames
813    * 
814    * @return
815    */
816   protected JPanel initFiltersPanel(Iterator<String> attNames)
817   {
818     JPanel filtersPanel = new JPanel();
819     filtersPanel.setLayout(new GridLayout(2, 3));
820     filtersPanel.setBackground(Color.white);
821
822     /*
823      * drop-down choice of attribute
824      */
825     filterAttribute = new JComboBox<>();
826     filterAttribute2 = new JComboBox<>();
827     while (attNames.hasNext())
828     {
829       String attName = attNames.next();
830       filterAttribute.addItem(attName);
831       filterAttribute2.addItem(attName);
832     }
833     filterAttribute.addActionListener(new ActionListener()
834     {
835       @Override
836       public void actionPerformed(ActionEvent e)
837       {
838         changeColour(true);
839       }
840     });
841
842     /*
843      * drop-down choice of test condition
844      */
845     filterCondition = new JComboBox<>();
846     for (Condition cond : Condition.values())
847     {
848       filterCondition.addItem(cond);
849     }
850     filterCondition.addActionListener(new ActionListener()
851     {
852       @Override
853       public void actionPerformed(ActionEvent e)
854       {
855         changeColour(true);
856       }
857     });
858
859     filterValue = new JTextField(12);
860     filterValue.addActionListener(new ActionListener()
861     {
862       @Override
863       public void actionPerformed(ActionEvent e)
864       {
865         changeColour(true);
866       }
867     });
868     filterValue.addFocusListener(new FocusAdapter()
869     {
870       @Override
871       public void focusLost(FocusEvent e)
872       {
873         changeColour(true);
874       }
875     });
876
877     /*
878      * repeat for a second filter
879      * todo: generalise to N filters
880      */
881     filterAttribute2.addActionListener(new ActionListener()
882     {
883       @Override
884       public void actionPerformed(ActionEvent e)
885       {
886         changeColour(true);
887       }
888     });
889
890     /*
891      * drop-down choice of test condition
892      */
893     filterCondition2 = new JComboBox<>();
894     for (Condition cond : Condition.values())
895     {
896       filterCondition2.addItem(cond);
897     }
898     filterCondition2.addActionListener(new ActionListener()
899     {
900       @Override
901       public void actionPerformed(ActionEvent e)
902       {
903         changeColour(true);
904       }
905     });
906
907     filterValue2 = new JTextField(12);
908     filterValue2.addActionListener(new ActionListener()
909     {
910       @Override
911       public void actionPerformed(ActionEvent e)
912       {
913         changeColour(true);
914       }
915     });
916     filterValue2.addFocusListener(new FocusAdapter()
917     {
918       @Override
919       public void focusLost(FocusEvent e)
920       {
921         changeColour(true);
922       }
923     });
924
925     filtersPanel.add(filterAttribute);
926     filtersPanel.add(filterCondition);
927     filtersPanel.add(filterValue);
928     filtersPanel.add(filterAttribute2);
929     filtersPanel.add(filterCondition2);
930     filtersPanel.add(filterValue2);
931
932     return filtersPanel;
933   }
934
935 }