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