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