JAL-2069 update spike branch with latest
[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.JMenuItem;
51 import javax.swing.JPanel;
52 import javax.swing.JPopupMenu;
53 import javax.swing.JRadioButton;
54 import javax.swing.JSlider;
55 import javax.swing.JTextField;
56 import javax.swing.border.LineBorder;
57 import javax.swing.event.ChangeEvent;
58 import javax.swing.event.ChangeListener;
59
60 public class FeatureColourChooser extends JalviewDialog
61 {
62   private static final int MAX_TOOLTIP_LENGTH = 50;
63
64   private FeatureRenderer fr;
65
66   private FeatureColourI cs;
67
68   private FeatureColourI oldcs;
69
70   private AlignmentPanel ap;
71
72   private boolean adjusting = false;
73
74   private float min;
75
76   private float max;
77
78   private float scaleFactor;
79
80   private String type = null;
81
82   private JPanel minColour = new JPanel();
83
84   private JPanel maxColour = new JPanel();
85
86   private JPanel noColour = new JPanel();
87
88   private JComboBox<String> threshold = new JComboBox<>();
89
90   private JSlider slider = new JSlider();
91
92   private JTextField thresholdValue = new JTextField(20);
93
94   private JCheckBox thresholdIsMin = new JCheckBox();
95
96   private GraphLine threshline;
97
98   private Color oldmaxColour;
99
100   private Color oldminColour;
101
102   private Color oldNoColour;
103
104   private ActionListener colourEditor = null;
105
106   /*
107    * radio buttons to select what to colour by
108    * label, attribute text, score, attribute value
109    */
110   private JRadioButton byDescription = new JRadioButton();
111
112   private JRadioButton byAttributeText = new JRadioButton();
113
114   private JRadioButton byScore = new JRadioButton();
115
116   private JRadioButton byAttributeValue = new JRadioButton();
117
118   private ActionListener changeColourAction;
119
120   /*
121    * choice of attribute (if any) for 'colour by text'
122    */
123   private JComboBox<String> textAttributeCombo;
124
125   /*
126    * choice of attribute (if any) for 'colour by value'
127    */
128   private JComboBox<String> valueAttributeCombo;
129
130   /**
131    * Constructor
132    * 
133    * @param frender
134    * @param theType
135    */
136   public FeatureColourChooser(FeatureRenderer frender, String theType)
137   {
138     this(frender, false, theType);
139   }
140
141   /**
142    * Constructor, with option to make a blocking dialog (has to complete in the
143    * AWT event queue thread). Currently this option is always set to false.
144    * 
145    * @param frender
146    * @param blocking
147    * @param theType
148    */
149   FeatureColourChooser(FeatureRenderer frender, boolean blocking,
150           String theType)
151   {
152     this.fr = frender;
153     this.type = theType;
154     ap = fr.ap;
155     String title = MessageManager
156             .formatMessage("label.graduated_color_for_params", new String[]
157             { theType });
158     initDialogFrame(this, true, blocking, title, 450, 300);
159
160     slider.addChangeListener(new ChangeListener()
161     {
162       @Override
163       public void stateChanged(ChangeEvent evt)
164       {
165         if (!adjusting)
166         {
167           thresholdValue.setText((slider.getValue() / scaleFactor) + "");
168           sliderValueChanged();
169         }
170       }
171     });
172     slider.addMouseListener(new MouseAdapter()
173     {
174       @Override
175       public void mouseReleased(MouseEvent evt)
176       {
177         /*
178          * only update Overview and/or structure colouring
179          * when threshold slider drag ends (mouse up)
180          */
181         if (ap != null)
182         {
183           ap.paintAlignment(true, true);
184         }
185       }
186     });
187
188     float mm[] = fr.getMinMax().get(theType)[0];
189     min = mm[0];
190     max = mm[1];
191
192     /*
193      * ensure scale factor allows a scaled range with
194      * 10 integer divisions ('ticks'); if we have got here,
195      * we should expect that max != min
196      */
197     scaleFactor = (max == min) ? 1f : 100f / (max - min);
198
199     oldcs = fr.getFeatureColours().get(theType);
200     if (!oldcs.isSimpleColour())
201     {
202       if (oldcs.isAutoScaled())
203       {
204         // update the scale
205         cs = new FeatureColour((FeatureColour) oldcs, min, max);
206       }
207       else
208       {
209         cs = new FeatureColour((FeatureColour) oldcs);
210       }
211     }
212     else
213     {
214       /*
215        * promote original simple color to a graduated color
216        * - by score if there is a score range, else by label
217        */
218       Color bl = oldcs.getColour();
219       if (bl == null)
220       {
221         bl = Color.BLACK;
222       }
223       // original colour becomes the maximum colour
224       cs = new FeatureColour(Color.white, bl, mm[0], mm[1]);
225       cs.setColourByLabel(mm[0] == mm[1]);
226     }
227     minColour.setBackground(oldminColour = cs.getMinColour());
228     maxColour.setBackground(oldmaxColour = cs.getMaxColour());
229     noColour.setBackground(oldNoColour = cs.getNoColour());
230     adjusting = true;
231
232     try
233     {
234       jbInit();
235     } catch (Exception ex)
236     {
237       ex.printStackTrace();
238       return;
239     }
240
241     /*
242      * set the initial state of options on screen
243      */
244     thresholdIsMin.setSelected(!cs.isAutoScaled());
245
246     if (cs.isColourByLabel())
247     {
248       if (cs.isColourByAttribute())
249       {
250         byAttributeText.setSelected(true);
251         textAttributeCombo.setEnabled(true);
252         textAttributeCombo.setSelectedItem(cs.getAttributeName());
253       }
254       else
255       {
256         byDescription.setSelected(true);
257         textAttributeCombo.setEnabled(false);
258       }
259     }
260     else
261     {
262       if (cs.isColourByAttribute())
263       {
264         byAttributeValue.setSelected(true);
265         String attributeName = cs.getAttributeName();
266         valueAttributeCombo.setSelectedItem(attributeName);
267         valueAttributeCombo.setEnabled(true);
268         setAttributeMinMax(attributeName);
269       }
270       else
271       {
272         byScore.setSelected(true);
273         valueAttributeCombo.setEnabled(false);
274       }
275     }
276
277     if (cs.hasThreshold())
278     {
279       // initialise threshold slider and selector
280       threshold.setSelectedIndex(cs.isAboveThreshold() ? 1 : 2);
281       slider.setEnabled(true);
282       slider.setValue((int) (cs.getThreshold() * scaleFactor));
283       thresholdValue.setEnabled(true);
284       threshline = new GraphLine((max - min) / 2f, "Threshold",
285               Color.black);
286       threshline.value = cs.getThreshold();
287     }
288
289     adjusting = false;
290
291     changeColour(false);
292     waitForInput();
293   }
294
295   /**
296    * Configures the initial layout
297    */
298   private void jbInit()
299   {
300     this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
301     this.setBackground(Color.white);
302
303     changeColourAction = new ActionListener() {
304       @Override
305       public void actionPerformed(ActionEvent e)
306       {
307         changeColour(true);
308       }
309     };
310
311     /*
312      * this panel
313      *     detailsPanel
314      *         colourByTextPanel
315      *         colourByScorePanel
316      *     okCancelPanel
317      */
318     JPanel detailsPanel = new JPanel();
319     detailsPanel.setLayout(new BoxLayout(detailsPanel, BoxLayout.Y_AXIS));
320
321     JPanel colourByTextPanel = initColourByTextPanel();
322     detailsPanel.add(colourByTextPanel);
323
324     JPanel colourByValuePanel = initColourByValuePanel();
325     detailsPanel.add(colourByValuePanel);
326
327     /*
328      * 4 radio buttons select between colour by description, by
329      * attribute text, by score, or by attribute value
330      */
331     ButtonGroup bg = new ButtonGroup();
332     bg.add(byDescription);
333     bg.add(byAttributeText);
334     bg.add(byScore);
335     bg.add(byAttributeValue);
336
337     JPanel okCancelPanel = initOkCancelPanel();
338
339     this.add(detailsPanel);
340     this.add(okCancelPanel);
341   }
342
343   /**
344    * Lay out fields for graduated colour by value
345    * 
346    * @return
347    */
348   protected JPanel initColourByValuePanel()
349   {
350     JPanel byValuePanel = new JPanel();
351     byValuePanel.setLayout(new BoxLayout(byValuePanel, BoxLayout.Y_AXIS));
352     byValuePanel.setBorder(BorderFactory.createTitledBorder(MessageManager
353             .getString("label.colour_by_value")));
354     byValuePanel.setBackground(Color.white);
355
356     /*
357      * first row - choose colour by score or by attribute, choose attribute
358      */
359     JPanel byWhatPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
360     byWhatPanel.setBackground(Color.white);
361     byValuePanel.add(byWhatPanel);
362
363     byScore.setText(MessageManager.getString("label.score"));
364     byWhatPanel.add(byScore);
365     byScore.addActionListener(changeColourAction);
366
367     byAttributeValue.setText(MessageManager
368 .getString("label.attribute"));
369     byAttributeValue.addActionListener(changeColourAction);
370     byWhatPanel.add(byAttributeValue);
371
372     List<String> attNames = FeatureAttributes.getInstance().getAttributes(
373             type);
374     valueAttributeCombo = populateAttributesDropdown(type, attNames,
375             true);
376
377     /*
378      * if no numeric atttibutes found, disable colour by attribute value
379      */
380     if (valueAttributeCombo.getItemCount() == 0)
381     {
382       byAttributeValue.setEnabled(false);
383     }
384
385     byWhatPanel.add(valueAttributeCombo);
386
387     /*
388      * second row - min/max/no colours
389      */
390     JPanel colourRangePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
391     colourRangePanel.setBackground(Color.white);
392     byValuePanel.add(colourRangePanel);
393
394     minColour.setFont(JvSwingUtils.getLabelFont());
395     minColour.setBorder(BorderFactory.createLineBorder(Color.black));
396     minColour.setPreferredSize(new Dimension(40, 20));
397     minColour.setToolTipText(MessageManager.getString("label.min_colour"));
398     minColour.addMouseListener(new MouseAdapter()
399     {
400       @Override
401       public void mousePressed(MouseEvent e)
402       {
403         if (minColour.isEnabled())
404         {
405           minColour_actionPerformed();
406         }
407       }
408     });
409
410     maxColour.setFont(JvSwingUtils.getLabelFont());
411     maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
412     maxColour.setPreferredSize(new Dimension(40, 20));
413     maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
414     maxColour.addMouseListener(new MouseAdapter()
415     {
416       @Override
417       public void mousePressed(MouseEvent e)
418       {
419         if (maxColour.isEnabled())
420         {
421           maxColour_actionPerformed();
422         }
423       }
424     });
425     maxColour.setBorder(new LineBorder(Color.black));
426
427     noColour.setFont(JvSwingUtils.getLabelFont());
428     noColour.setBorder(BorderFactory.createLineBorder(Color.black));
429     noColour.setPreferredSize(new Dimension(40, 20));
430     noColour.setToolTipText("Colour if feature has no attribute value");
431     noColour.addMouseListener(new MouseAdapter()
432     {
433       @Override
434       public void mousePressed(MouseEvent e)
435       {
436         if (e.isPopupTrigger()) // Mac: mouseReleased
437         {
438           showNoColourPopup(e);
439           return;
440         }
441         if (noColour.isEnabled())
442         {
443           noColour_actionPerformed();
444         }
445       }
446
447       @Override
448       public void mouseReleased(MouseEvent e)
449       {
450         if (e.isPopupTrigger()) // Windows: mouseReleased
451         {
452           showNoColourPopup(e);
453           e.consume();
454           return;
455         }
456       }
457     });
458     noColour.setBorder(new LineBorder(Color.black));
459
460     JLabel minText = new JLabel(MessageManager.getString("label.min"));
461     minText.setFont(JvSwingUtils.getLabelFont());
462     JLabel maxText = new JLabel(MessageManager.getString("label.max"));
463     maxText.setFont(JvSwingUtils.getLabelFont());
464     JLabel noText = new JLabel(MessageManager.getString("label.no_colour"));
465     noText.setFont(JvSwingUtils.getLabelFont());
466
467     colourRangePanel.add(minText);
468     colourRangePanel.add(minColour);
469     colourRangePanel.add(maxText);
470     colourRangePanel.add(maxColour);
471     colourRangePanel.add(noText);
472     colourRangePanel.add(noColour);
473
474     /*
475      * third row - threshold options and value
476      */
477     JPanel thresholdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
478     thresholdPanel.setBackground(Color.white);
479     byValuePanel.add(thresholdPanel);
480
481     threshold.addActionListener(changeColourAction);
482     threshold.setToolTipText(MessageManager
483             .getString("label.threshold_feature_display_by_score"));
484     threshold.addItem(MessageManager
485             .getString("label.threshold_feature_no_threshold")); // index 0
486     threshold.addItem(MessageManager
487             .getString("label.threshold_feature_above_threshold")); // index 1
488     threshold.addItem(MessageManager
489             .getString("label.threshold_feature_below_threshold")); // index 2
490
491     thresholdValue.addActionListener(new ActionListener()
492     {
493       @Override
494       public void actionPerformed(ActionEvent e)
495       {
496         thresholdValue_actionPerformed();
497       }
498     });
499     thresholdValue.addFocusListener(new FocusAdapter()
500     {
501       @Override
502       public void focusLost(FocusEvent e)
503       {
504         thresholdValue_actionPerformed();
505       }
506     });
507     slider.setPaintLabels(false);
508     slider.setPaintTicks(true);
509     slider.setBackground(Color.white);
510     slider.setEnabled(false);
511     slider.setOpaque(false);
512     slider.setPreferredSize(new Dimension(100, 32));
513     slider.setToolTipText(
514             MessageManager.getString("label.adjust_threshold"));
515     thresholdValue.setEnabled(false);
516     thresholdValue.setColumns(7);
517
518     thresholdPanel.add(threshold);
519     thresholdPanel.add(slider);
520     thresholdPanel.add(thresholdValue);
521
522     /*
523      * 4th row - threshold is min / max
524      */
525     JPanel isMinMaxPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
526     isMinMaxPanel.setBackground(Color.white);
527     byValuePanel.add(isMinMaxPanel);
528     thresholdIsMin.setBackground(Color.white);
529     thresholdIsMin
530             .setText(MessageManager.getString("label.threshold_minmax"));
531     thresholdIsMin.setToolTipText(MessageManager
532             .getString("label.toggle_absolute_relative_display_threshold"));
533     thresholdIsMin.addActionListener(changeColourAction);
534     isMinMaxPanel.add(thresholdIsMin);
535
536     return byValuePanel;
537   }
538
539   /**
540    * Show a popup menu with options to make 'no value colour' the same as Min
541    * Colour or Max Colour
542    * 
543    * @param evt
544    */
545   protected void showNoColourPopup(MouseEvent evt)
546   {
547     JPopupMenu pop = new JPopupMenu();
548
549     JMenuItem copyMin = new JMenuItem(
550             MessageManager.getString("label.min_colour"));
551     copyMin.addActionListener((new ActionListener()
552     {
553       @Override
554       public void actionPerformed(ActionEvent e)
555       {
556         noColour.setBackground(minColour.getBackground());
557         changeColour(true);
558       }
559     }));
560     pop.add(copyMin);
561
562     JMenuItem copyMax = new JMenuItem(
563             MessageManager.getString("label.max_colour"));
564     copyMax.addActionListener((new ActionListener()
565     {
566       @Override
567       public void actionPerformed(ActionEvent e)
568       {
569         noColour.setBackground(maxColour.getBackground());
570         changeColour(true);
571       }
572     }));
573     pop.add(copyMax);
574
575     pop.show(noColour, evt.getX(), evt.getY());
576   }
577
578   /**
579    * Lay out OK and Cancel buttons
580    * 
581    * @return
582    */
583   protected JPanel initOkCancelPanel()
584   {
585     JPanel okCancelPanel = new JPanel();
586     okCancelPanel.setBackground(Color.white);
587     okCancelPanel.add(ok);
588     okCancelPanel.add(cancel);
589     return okCancelPanel;
590   }
591
592   /**
593    * Lay out Colour by Label and attribute choice elements
594    * 
595    * @return
596    */
597   protected JPanel initColourByTextPanel()
598   {
599     JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
600     byTextPanel.setBackground(Color.white);
601     byTextPanel.setBorder(BorderFactory.createTitledBorder(MessageManager
602             .getString("label.colour_by_text")));
603
604     byDescription.setText(MessageManager.getString("label.label"));
605     byDescription.setToolTipText(MessageManager
606             .getString("label.colour_by_label_tip"));
607     byDescription.addActionListener(changeColourAction);
608     byTextPanel.add(byDescription);
609
610     byAttributeText.setText(MessageManager.getString("label.attribute"));
611     byAttributeText.addActionListener(changeColourAction);
612     byTextPanel.add(byAttributeText);
613
614     List<String> attNames = FeatureAttributes.getInstance().getAttributes(
615             type);
616     textAttributeCombo = populateAttributesDropdown(type, attNames, false);
617     byTextPanel.add(textAttributeCombo);
618
619     /*
620      * disable colour by attribute if no attributes
621      */
622     if (attNames.isEmpty())
623     {
624       byAttributeText.setEnabled(false);
625     }
626
627     return byTextPanel;
628   }
629
630   /**
631    * Action on clicking the 'minimum colour' - open a colour chooser dialog, and
632    * set the selected colour (if the user does not cancel out of the dialog)
633    */
634   protected void minColour_actionPerformed()
635   {
636     Color col = JColorChooser.showDialog(this,
637             MessageManager.getString("label.select_colour_minimum_value"),
638             minColour.getBackground());
639     if (col != null)
640     {
641       minColour.setBackground(col);
642       minColour.setForeground(col);
643     }
644     minColour.repaint();
645     changeColour(true);
646   }
647
648   /**
649    * Action on clicking the 'maximum colour' - open a colour chooser dialog, and
650    * set the selected colour (if the user does not cancel out of the dialog)
651    */
652   protected void maxColour_actionPerformed()
653   {
654     Color col = JColorChooser.showDialog(this,
655             MessageManager.getString("label.select_colour_maximum_value"),
656             maxColour.getBackground());
657     if (col != null)
658     {
659       maxColour.setBackground(col);
660       maxColour.setForeground(col);
661     }
662     maxColour.repaint();
663     changeColour(true);
664   }
665
666   /**
667    * Action on clicking the 'no colour' - open a colour chooser dialog, and set
668    * the selected colour (if the user does not cancel out of the dialog)
669    */
670   protected void noColour_actionPerformed()
671   {
672     Color col = JColorChooser.showDialog(this,
673             MessageManager.getString("label.select_no_value_colour"),
674             noColour.getBackground());
675     if (col != null)
676     {
677       noColour.setBackground(col);
678       noColour.setForeground(col);
679     }
680     noColour.repaint();
681     changeColour(true);
682   }
683
684   /**
685    * Constructs and sets the selected colour options as the colour for the
686    * feature type, and repaints the alignment, and optionally the Overview
687    * and/or structure viewer if open
688    * 
689    * @param updateStructsAndOverview
690    */
691   void changeColour(boolean updateStructsAndOverview)
692   {
693     // Check if combobox is still adjusting
694     if (adjusting)
695     {
696       return;
697     }
698
699     boolean aboveThreshold = false;
700     boolean belowThreshold = false;
701     if (threshold.getSelectedIndex() == 1)
702     {
703       aboveThreshold = true;
704     }
705     else if (threshold.getSelectedIndex() == 2)
706     {
707       belowThreshold = true;
708     }
709     boolean hasThreshold = aboveThreshold || belowThreshold;
710
711     slider.setEnabled(true);
712     thresholdValue.setEnabled(true);
713
714     /*
715      * make the feature colour
716      */
717     FeatureColourI acg;
718     if (cs.isColourByLabel())
719     {
720       acg = new FeatureColour(oldminColour, oldmaxColour, min, max);
721     }
722     else
723     {
724       acg = new FeatureColour(oldminColour = minColour.getBackground(),
725               oldmaxColour = maxColour.getBackground(),
726               oldNoColour = noColour.getBackground(), min, max);
727     }
728     String attribute = null;
729     textAttributeCombo.setEnabled(false);
730     valueAttributeCombo.setEnabled(false);
731     if (byAttributeText.isSelected())
732     {
733       attribute = (String) textAttributeCombo.getSelectedItem();
734       textAttributeCombo.setEnabled(true);
735     }
736     else if (byAttributeValue.isSelected())
737     {
738       attribute = (String) valueAttributeCombo.getSelectedItem();
739       valueAttributeCombo.setEnabled(true);
740     }
741     acg.setAttributeName(attribute);
742
743     if (!hasThreshold)
744     {
745       slider.setEnabled(false);
746       thresholdValue.setEnabled(false);
747       thresholdValue.setText("");
748       thresholdIsMin.setEnabled(false);
749     }
750     else if (threshline == null)
751     {
752       /*
753        * todo not yet implemented: visual indication of feature threshold
754        */
755       threshline = new GraphLine((max - min) / 2f, "Threshold",
756               Color.black);
757     }
758
759     if (hasThreshold)
760     {
761       adjusting = true;
762       acg.setThreshold(threshline.value);
763
764       float range = (max - min) * scaleFactor;
765
766       slider.setMinimum((int) (min * scaleFactor));
767       slider.setMaximum((int) (max * scaleFactor));
768       // slider.setValue((int) (threshline.value * scaleFactor));
769       slider.setValue(Math.round(threshline.value * scaleFactor));
770       thresholdValue.setText(threshline.value + "");
771       slider.setMajorTickSpacing((int) (range / 10f));
772       slider.setEnabled(true);
773       thresholdValue.setEnabled(true);
774       thresholdIsMin.setEnabled(!byDescription.isSelected());
775       adjusting = false;
776     }
777
778     acg.setAboveThreshold(aboveThreshold);
779     acg.setBelowThreshold(belowThreshold);
780     if (thresholdIsMin.isSelected() && hasThreshold)
781     {
782       acg.setAutoScaled(false);
783       if (aboveThreshold)
784       {
785         acg = new FeatureColour((FeatureColour) acg, threshline.value, max);
786       }
787       else
788       {
789         acg = new FeatureColour((FeatureColour) acg, min, threshline.value);
790       }
791     }
792     else
793     {
794       acg.setAutoScaled(true);
795     }
796     acg.setColourByLabel(byDescription.isSelected()
797             || byAttributeText.isSelected());
798
799     if (acg.isColourByLabel())
800     {
801       maxColour.setEnabled(false);
802       minColour.setEnabled(false);
803       noColour.setEnabled(false);
804       maxColour.setBackground(this.getBackground());
805       maxColour.setForeground(this.getBackground());
806       minColour.setBackground(this.getBackground());
807       minColour.setForeground(this.getBackground());
808       noColour.setBackground(this.getBackground());
809       noColour.setForeground(this.getBackground());
810     }
811     else
812     {
813       maxColour.setEnabled(true);
814       minColour.setEnabled(true);
815       noColour.setEnabled(true);
816       maxColour.setBackground(oldmaxColour);
817       maxColour.setForeground(oldmaxColour);
818       minColour.setBackground(oldminColour);
819       minColour.setForeground(oldminColour);
820       noColour.setBackground(oldNoColour);
821       noColour.setForeground(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         setAttributeMinMax(attCombo.getSelectedItem().toString());
980         changeColour(true);
981       }
982     });
983
984     if (validAtts.isEmpty())
985     {
986       attCombo.setToolTipText(MessageManager
987               .getString(withNumericRange ? "label.no_numeric_attributes"
988                       : "label.no_attributes"));
989     }
990
991     return attCombo;
992   }
993
994   /**
995    * Updates the min-max range and scale to be that for the given attribute name
996    * 
997    * @param attributeName
998    */
999   protected void setAttributeMinMax(String attributeName)
1000   {
1001     float[] minMax = FeatureAttributes.getInstance().getMinMax(type,
1002             attributeName);
1003     if (minMax != null)
1004     {
1005       min = minMax[0];
1006       max = minMax[1];
1007       scaleFactor = (max == min) ? 1f : 100f / (max - min);
1008     }
1009   }
1010
1011 }