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