JAL-2069 fix minor bugs in variable setting
[jalview.git] / src / jalview / gui / FeatureColourChooser.java
index 3b39c2c..d3d9e1a 100644 (file)
 /*
- * Jalview - A Sequence Alignment Editor and Viewer (Development Version 2.4.1)
- * Copyright (C) 2009 AM Waterhouse, J Procter, G Barton, M Clamp, S Searle
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
  * 
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2
- * of the License, or (at your option) any later version.
+ * This file is part of Jalview.
  * 
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
  * 
  * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
 package jalview.gui;
 
-import java.util.*;
+import jalview.api.FeatureColourI;
+import jalview.datamodel.GraphLine;
+import jalview.datamodel.features.FeatureAttributes;
+import jalview.schemes.FeatureColour;
+import jalview.util.MessageManager;
 
-import java.awt.*;
-import java.awt.event.*;
-import javax.swing.*;
-import javax.swing.border.LineBorder;
-import javax.swing.event.*;
-
-import jalview.datamodel.*;
-import jalview.schemes.*;
+import java.awt.Color;
 import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
+import javax.swing.JCheckBox;
+import javax.swing.JColorChooser;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JSlider;
+import javax.swing.JTextField;
+import javax.swing.border.LineBorder;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
 
-public class FeatureColourChooser extends JPanel 
+public class FeatureColourChooser extends JalviewDialog
 {
-  JDialog frame;
-  
-
-//  FeatureSettings fs;
-  FeatureRenderer fr;
-  
-  
-  private GraduatedColor cs;
-  private Object oldcs;
-  /**
-   * 
-   * @return the last colour setting selected by user - either oldcs (which may be a java.awt.Color) or the new GraduatedColor
-   */
-  public Object getLastColour() {
-    if (cs==null)
-    {
-      return oldcs;
-    }
-    return cs;
-  }
-  Hashtable oldgroupColours;
-  
-  AlignmentPanel ap;
-  
+  private static final String COLON = ":";
+
+  private static final int MAX_TOOLTIP_LENGTH = 50;
+
+  private static int NO_COLOUR_OPTION = 0;
+
+  private static int MIN_COLOUR_OPTION = 1;
+
+  private static int MAX_COLOUR_OPTION = 2;
+
+  private FeatureRenderer fr;
 
-  boolean adjusting = false;
+  private FeatureColourI cs;
+
+  private FeatureColourI oldcs;
+
+  private AlignmentPanel ap;
+
+  private boolean adjusting = false;
 
   private float min;
 
   private float max;
-  String type = null;
-  public FeatureColourChooser(FeatureRenderer frender, String type)
+
+  private float scaleFactor;
+
+  private String type = null;
+
+  private JPanel minColour = new JPanel();
+
+  private JPanel maxColour = new JPanel();
+
+  private Color noColour;
+
+  private JComboBox<String> threshold = new JComboBox<>();
+
+  private JSlider slider = new JSlider();
+
+  private JTextField thresholdValue = new JTextField(20);
+
+  private JCheckBox thresholdIsMin = new JCheckBox();
+
+  private GraphLine threshline;
+
+  private Color oldmaxColour;
+
+  private Color oldminColour;
+
+  private Color oldNoColour;
+
+  private ActionListener colourEditor = null;
+
+  /*
+   * radio buttons to select what to colour by
+   * label, attribute text, score, attribute value
+   */
+  private JRadioButton byDescription = new JRadioButton();
+
+  private JRadioButton byAttributeText = new JRadioButton();
+
+  private JRadioButton byScore = new JRadioButton();
+
+  private JRadioButton byAttributeValue = new JRadioButton();
+
+  private ActionListener changeColourAction;
+
+  private ActionListener changeMinMaxAction;
+
+  /*
+   * choice of option for 'colour for no value'
+   */
+  private JComboBox<String> noValueCombo;
+
+  /*
+   * choice of attribute (if any) for 'colour by text'
+   */
+  private JComboBox<String> textAttributeCombo;
+
+  /*
+   * choice of attribute (if any) for 'colour by value'
+   */
+  private JComboBox<String> valueAttributeCombo;
+
+  /**
+   * Constructor
+   * 
+   * @param frender
+   * @param theType
+   */
+  public FeatureColourChooser(FeatureRenderer frender, String theType)
   {
-    this(frender,false,type);
+    this(frender, false, theType);
   }
-    public FeatureColourChooser(FeatureRenderer frender, boolean block, String type)
-    {
+
+  /**
+   * Constructor, with option to make a blocking dialog (has to complete in the
+   * AWT event queue thread). Currently this option is always set to false.
+   * 
+   * @param frender
+   * @param blocking
+   * @param theType
+   */
+  FeatureColourChooser(FeatureRenderer frender, boolean blocking,
+          String theType)
+  {
     this.fr = frender;
-    this.type = type;
+    this.type = theType;
     ap = fr.ap;
-    frame = new JDialog(Desktop.instance,true);
-    frame.setTitle("Graduated Feature Colour for "+type);
-    Rectangle deskr = Desktop.instance.getBounds();
-    frame.setBounds(new Rectangle((int) (deskr.getCenterX()-240),(int) (deskr.getCenterY()-92),480,185));
-    frame.setContentPane(this);
-    //frame.setLayer(JLayeredPane.PALETTE_LAYER);
-    //Desktop.addInternalFrame(frame, "Graduated Feature Colour for "+type, 480, 145);
+    String title = MessageManager.formatMessage("label.variable_color_for",
+            new String[] { theType });
+    initDialogFrame(this, true, blocking, title, 470, 300);
 
     slider.addChangeListener(new ChangeListener()
     {
+      @Override
       public void stateChanged(ChangeEvent evt)
       {
         if (!adjusting)
         {
-          thresholdValue.setText(((float) slider.getValue() / 1000f) + "");
-          valueChanged();
+          thresholdValue.setText((slider.getValue() / scaleFactor) + "");
+          sliderValueChanged();
         }
       }
     });
     slider.addMouseListener(new MouseAdapter()
     {
+      @Override
       public void mouseReleased(MouseEvent evt)
       {
-        if (ap!=null) { ap.paintAlignment(true); };
+        /*
+         * only update Overview and/or structure colouring
+         * when threshold slider drag ends (mouse up)
+         */
+        if (ap != null)
+        {
+          ap.paintAlignment(true, true);
+        }
       }
     });
 
-    float mm[] = ((float[][]) fr.minmax.get(type))[0];
+    // todo move all threshold setup inside a method
+    float mm[] = fr.getMinMax().get(theType)[0];
     min = mm[0];
     max = mm[1];
-    oldcs = fr.featureColours.get(type);
-    if (oldcs instanceof GraduatedColor)
+
+    /*
+     * ensure scale factor allows a scaled range with
+     * 10 integer divisions ('ticks'); if we have got here,
+     * we should expect that max != min
+     */
+    scaleFactor = (max == min) ? 1f : 100f / (max - min);
+
+    oldcs = fr.getFeatureColours().get(theType);
+    if (!oldcs.isSimpleColour())
     {
-      if (((GraduatedColor)oldcs).isAutoScale())
+      if (oldcs.isAutoScaled())
       {
         // update the scale
-        cs = new GraduatedColor((GraduatedColor) oldcs, min, max);
-      } else {
-        cs = new GraduatedColor((GraduatedColor) oldcs);
+        cs = new FeatureColour((FeatureColour) oldcs, min, max);
+      }
+      else
+      {
+        cs = new FeatureColour((FeatureColour) oldcs);
       }
-    } else {
-      // promote original color to a graduated color
-      Color bl = Color.black;
-      if (oldcs instanceof Color)
+    }
+    else
+    {
+      /*
+       * promote original simple color to a graduated color
+       * - by score if there is a score range, else by label
+       */
+      Color bl = oldcs.getColour();
+      if (bl == null)
       {
-        bl = (Color) oldcs;
+        bl = Color.BLACK;
       }
       // original colour becomes the maximum colour
-      cs = new GraduatedColor(Color.white,bl,mm[0],mm[1]);
-      cs.setColourByLabel(false);
+      cs = new FeatureColour(Color.white, bl, mm[0], mm[1]);
+      cs.setColourByLabel(mm[0] == mm[1]);
     }
-    minColour.setBackground(oldminColour=cs.getMinColor());
-    maxColour.setBackground(oldmaxColour=cs.getMaxColor());
+    minColour.setBackground(oldminColour = cs.getMinColour());
+    maxColour.setBackground(oldmaxColour = cs.getMaxColour());
+    noColour = cs.getNoColour();
+
     adjusting = true;
-    
+
     try
     {
       jbInit();
     } catch (Exception ex)
     {
+      ex.printStackTrace();
+      return;
+    }
+
+    /*
+     * set the initial state of options on screen
+     */
+    if (cs.isColourByLabel())
+    {
+      if (cs.isColourByAttribute())
+      {
+        byAttributeText.setSelected(true);
+        textAttributeCombo.setEnabled(true);
+        String[] attributeName = cs.getAttributeName();
+        textAttributeCombo
+                .setSelectedItem(toAttributeDisplayName(attributeName));
+      }
+      else
+      {
+        byDescription.setSelected(true);
+        textAttributeCombo.setEnabled(false);
+      }
+    }
+    else
+    {
+      if (cs.isColourByAttribute())
+      {
+        byAttributeValue.setSelected(true);
+        String[] attributeName = cs.getAttributeName();
+        valueAttributeCombo
+                .setSelectedItem(toAttributeDisplayName(attributeName));
+        valueAttributeCombo.setEnabled(true);
+        updateMinMax();
+      }
+      else
+      {
+        byScore.setSelected(true);
+        valueAttributeCombo.setEnabled(false);
+      }
+    }
+
+    if (noColour == null)
+    {
+      noValueCombo.setSelectedIndex(NO_COLOUR_OPTION);
     }
-    // update the gui from threshold state
-    thresholdIsMin.setSelected(!cs.isAutoScale());
-    colourByLabel.setSelected(cs.isColourByLabel());
-    if (cs.getThreshType()!=AnnotationColourGradient.NO_THRESHOLD)
+    else if (noColour.equals(oldminColour))
+    {
+      noValueCombo.setSelectedIndex(MIN_COLOUR_OPTION);
+    }
+    else if (noColour.equals(oldmaxColour))
+    {
+      noValueCombo.setSelectedIndex(MAX_COLOUR_OPTION);
+    }
+
+    threshline = new GraphLine((max - min) / 2f, "Threshold", Color.black);
+    threshline.value = cs.getThreshold();
+
+    if (cs.hasThreshold())
     {
       // initialise threshold slider and selector
-      threshold.setSelectedIndex(cs.getThreshType()==AnnotationColourGradient.ABOVE_THRESHOLD ? 1 : 2);
-            slider.setEnabled(true);
+      threshold.setSelectedIndex(cs.isAboveThreshold() ? 1 : 2);
+      slider.setEnabled(true);
+      slider.setValue((int) (cs.getThreshold() * scaleFactor));
       thresholdValue.setEnabled(true);
-      threshline = new jalview.datamodel.GraphLine(
-                        (max - min) / 2f,
-                        "Threshold", Color.black);
-      
-    } 
+    }
 
     adjusting = false;
 
-    changeColour();
-    if (!block)
+    changeColour(false);
+    waitForInput();
+  }
+
+  /**
+   * Configures the initial layout
+   */
+  private void jbInit()
+  {
+    this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+    this.setBackground(Color.white);
+
+    changeColourAction = new ActionListener()
     {
-      new Thread(new Runnable() {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        changeColour(true);
+      }
+    };
 
-      public void run()
+    changeMinMaxAction = new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
       {
-        frame.show();
+        updateMinMax();
+        changeColour(true);
       }
-      
-    }).start();
-    } else {
-      frame.show();
-    }
+    };
+
+    /*
+     * this panel
+     *     detailsPanel
+     *         colourByTextPanel
+     *         colourByScorePanel
+     *     okCancelPanel
+     */
+    JPanel detailsPanel = new JPanel();
+    detailsPanel.setLayout(new BoxLayout(detailsPanel, BoxLayout.Y_AXIS));
+
+    JPanel colourByTextPanel = initColourByTextPanel();
+    detailsPanel.add(colourByTextPanel);
+
+    JPanel colourByValuePanel = initColourByValuePanel();
+    detailsPanel.add(colourByValuePanel);
+
+    /*
+     * 4 radio buttons select between colour by description, by
+     * attribute text, by score, or by attribute value
+     */
+    ButtonGroup bg = new ButtonGroup();
+    bg.add(byDescription);
+    bg.add(byAttributeText);
+    bg.add(byScore);
+    bg.add(byAttributeValue);
+
+    JPanel okCancelPanel = initOkCancelPanel();
+
+    this.add(detailsPanel);
+    this.add(okCancelPanel);
   }
 
-  public FeatureColourChooser()
+  /**
+   * Updates the min-max range for a change in choice of Colour by Score, or
+   * Colour by Attribute (value)
+   */
+  protected void updateMinMax()
   {
-    try
+    float[] minMax = null;
+    if (byScore.isSelected())
     {
-      jbInit();
-    } catch (Exception ex)
+      minMax = fr.getMinMax().get(type)[0];
+    }
+    else if (byAttributeValue.isSelected())
     {
-      ex.printStackTrace();
+      String attName = (String) valueAttributeCombo.getSelectedItem();
+      String[] attNames = fromAttributeDisplayName(attName);
+      minMax = FeatureAttributes.getInstance().getMinMax(type, attNames);
+    }
+    if (minMax != null)
+    {
+      min = minMax[0];
+      max = minMax[1];
+      scaleFactor = (max == min) ? 1f : 100f / (max - min);
+      slider.setValue((int) (min * scaleFactor));
     }
   }
 
-  private void jbInit() throws Exception
+  /**
+   * Lay out fields for graduated colour by value
+   * 
+   * @return
+   */
+  protected JPanel initColourByValuePanel()
   {
-    
-    minColour.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
+    JPanel byValuePanel = new JPanel();
+    byValuePanel.setLayout(new BoxLayout(byValuePanel, BoxLayout.Y_AXIS));
+    JvSwingUtils.createItalicTitledBorder(byValuePanel,
+            MessageManager.getString("label.colour_by_value"), true);
+    byValuePanel.setBackground(Color.white);
+
+    /*
+     * first row - choose colour by score or by attribute, choose attribute
+     */
+    JPanel byWhatPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    byWhatPanel.setBackground(Color.white);
+    byValuePanel.add(byWhatPanel);
+
+    byScore.setText(MessageManager.getString("label.score"));
+    byWhatPanel.add(byScore);
+    byScore.addActionListener(changeMinMaxAction);
+
+    byAttributeValue.setText(MessageManager.getString("label.attribute"));
+    byAttributeValue.addActionListener(changeMinMaxAction);
+    byWhatPanel.add(byAttributeValue);
+
+    List<String[]> attNames = FeatureAttributes.getInstance()
+            .getAttributes(type);
+    valueAttributeCombo = populateAttributesDropdown(type, attNames, true);
+
+    /*
+     * if no numeric atttibutes found, disable colour by attribute value
+     */
+    if (valueAttributeCombo.getItemCount() == 0)
+    {
+      byAttributeValue.setEnabled(false);
+    }
+
+    byWhatPanel.add(valueAttributeCombo);
+
+    /*
+     * second row - min/max/no colours
+     */
+    JPanel colourRangePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    colourRangePanel.setBackground(Color.white);
+    byValuePanel.add(colourRangePanel);
+
+    minColour.setFont(JvSwingUtils.getLabelFont());
     minColour.setBorder(BorderFactory.createLineBorder(Color.black));
     minColour.setPreferredSize(new Dimension(40, 20));
-    minColour.setToolTipText("Minimum Colour");
+    minColour.setToolTipText(MessageManager.getString("label.min_colour"));
     minColour.addMouseListener(new MouseAdapter()
     {
+      @Override
       public void mousePressed(MouseEvent e)
       {
         if (minColour.isEnabled())
@@ -194,12 +469,14 @@ public class FeatureColourChooser extends JPanel
         }
       }
     });
-    maxColour.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
+
+    maxColour.setFont(JvSwingUtils.getLabelFont());
     maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
     maxColour.setPreferredSize(new Dimension(40, 20));
-    maxColour.setToolTipText("Maximum Colour");
+    maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
     maxColour.addMouseListener(new MouseAdapter()
     {
+      @Override
       public void mousePressed(MouseEvent e)
       {
         if (maxColour.isEnabled())
@@ -209,49 +486,65 @@ public class FeatureColourChooser extends JPanel
       }
     });
     maxColour.setBorder(new LineBorder(Color.black));
-    minText.setText("Min:");
-    minText.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
-    maxText.setText("Max:");
-    maxText.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
-    ok.setOpaque(false);
-    ok.setText("OK");
-    ok.addActionListener(new ActionListener()
-    {
-      public void actionPerformed(ActionEvent e)
-      {
-        ok_actionPerformed(e);
-      }
-    });
-    cancel.setOpaque(false);
-    cancel.setText("Cancel");
-    cancel.addActionListener(new ActionListener()
+
+    noValueCombo = new JComboBox<>();
+    noValueCombo.addItem(MessageManager.getString("label.no_colour"));
+    noValueCombo.addItem(MessageManager.getString("label.min_colour"));
+    noValueCombo.addItem(MessageManager.getString("label.max_colour"));
+    noValueCombo.addItemListener(new ItemListener()
     {
-      public void actionPerformed(ActionEvent e)
+      @Override
+      public void itemStateChanged(ItemEvent e)
       {
-        cancel_actionPerformed(e);
+        setNoValueColour();
       }
     });
-    this.setLayout(borderLayout1);
-    jPanel2.setLayout(flowLayout1);
-    jPanel1.setBackground(Color.white);
-    jPanel2.setBackground(Color.white);
-    threshold.addActionListener(new ActionListener()
+
+    JLabel minText = new JLabel(MessageManager.getString("label.min_value"));
+    minText.setFont(JvSwingUtils.getLabelFont());
+    JLabel maxText = new JLabel(MessageManager.getString("label.max_value"));
+    maxText.setFont(JvSwingUtils.getLabelFont());
+    JLabel noText = new JLabel(MessageManager.getString("label.no_value"));
+    noText.setFont(JvSwingUtils.getLabelFont());
+
+    colourRangePanel.add(minText);
+    colourRangePanel.add(minColour);
+    colourRangePanel.add(maxText);
+    colourRangePanel.add(maxColour);
+    colourRangePanel.add(noText);
+    colourRangePanel.add(noValueCombo);
+
+    /*
+     * third row - threshold options and value
+     */
+    JPanel thresholdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    thresholdPanel.setBackground(Color.white);
+    byValuePanel.add(thresholdPanel);
+
+    threshold.addActionListener(changeColourAction);
+    threshold.setToolTipText(MessageManager
+            .getString("label.threshold_feature_display_by_score"));
+    threshold.addItem(MessageManager
+            .getString("label.threshold_feature_no_threshold")); // index 0
+    threshold.addItem(MessageManager
+            .getString("label.threshold_feature_above_threshold")); // index 1
+    threshold.addItem(MessageManager
+            .getString("label.threshold_feature_below_threshold")); // index 2
+
+    thresholdValue.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        threshold_actionPerformed(e);
+        thresholdValue_actionPerformed();
       }
     });
-    threshold.setToolTipText("Threshold the feature display by score.");
-    threshold.addItem("No Threshold"); // index 0
-    threshold.addItem("Above Threshold"); // index 1
-    threshold.addItem("Below Threshold"); // index 2
-    jPanel3.setLayout(flowLayout2);
-    thresholdValue.addActionListener(new ActionListener()
+    thresholdValue.addFocusListener(new FocusAdapter()
     {
-      public void actionPerformed(ActionEvent e)
+      @Override
+      public void focusLost(FocusEvent e)
       {
-        thresholdValue_actionPerformed(e);
+        thresholdValue_actionPerformed();
       }
     });
     slider.setPaintLabels(false);
@@ -260,117 +553,150 @@ public class FeatureColourChooser extends JPanel
     slider.setEnabled(false);
     slider.setOpaque(false);
     slider.setPreferredSize(new Dimension(100, 32));
-    slider.setToolTipText("Adjust threshold");
+    slider.setToolTipText(MessageManager
+            .getString("label.adjust_threshold"));
     thresholdValue.setEnabled(false);
     thresholdValue.setColumns(7);
-    jPanel3.setBackground(Color.white);
+
+    thresholdPanel.add(threshold);
+    thresholdPanel.add(slider);
+    thresholdPanel.add(thresholdValue);
+
+    /*
+     * 4th row - threshold is min / max
+     */
+    JPanel isMinMaxPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    isMinMaxPanel.setBackground(Color.white);
+    byValuePanel.add(isMinMaxPanel);
     thresholdIsMin.setBackground(Color.white);
-    thresholdIsMin.setText("Threshold is Min/Max");
-    thresholdIsMin.setToolTipText("Toggle between absolute and relative display threshold.");
-    thresholdIsMin.addActionListener(new ActionListener()
+    thresholdIsMin.setText(MessageManager
+            .getString("label.threshold_minmax"));
+    thresholdIsMin.setToolTipText(MessageManager
+            .getString("label.toggle_absolute_relative_display_threshold"));
+    thresholdIsMin.addActionListener(changeColourAction);
+    isMinMaxPanel.add(thresholdIsMin);
+
+    return byValuePanel;
+  }
+
+  /**
+   * Action on user choice of no / min / max colour to use when there is no
+   * value to colour by
+   */
+  protected void setNoValueColour()
+  {
+    int i = noValueCombo.getSelectedIndex();
+    if (i == NO_COLOUR_OPTION)
     {
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        thresholdIsMin_actionPerformed(actionEvent);
-      }
-    });
-    colourByLabel.setBackground(Color.white);
-    colourByLabel.setText("Colour by Label");
-    colourByLabel.setToolTipText("Display features of the same type with a different label using a different colour. (e.g. domain features)");
-    colourByLabel.addActionListener(new ActionListener()
+      noColour = null;
+    }
+    else if (i == MIN_COLOUR_OPTION)
     {
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        colourByLabel_actionPerformed(actionEvent);
-      }
-    });
-    colourPanel.setBackground(Color.white);
-    jPanel1.add(ok);
-    jPanel1.add(cancel);
-    jPanel2.add(colourByLabel,java.awt.BorderLayout.WEST);
-    jPanel2.add(colourPanel,java.awt.BorderLayout.EAST);
-    colourPanel.add(minText);
-    colourPanel.add(minColour);
-    colourPanel.add(maxText);
-    colourPanel.add(maxColour);
-    this.add(jPanel3, java.awt.BorderLayout.CENTER);
-    jPanel3.add(threshold);
-    jPanel3.add(slider);
-    jPanel3.add(thresholdValue);
-    jPanel3.add(thresholdIsMin);
-    this.add(jPanel1, java.awt.BorderLayout.SOUTH);
-    this.add(jPanel2, java.awt.BorderLayout.NORTH);
+      noColour = minColour.getBackground();
+    }
+    else if (i == MAX_COLOUR_OPTION)
+    {
+      noColour = maxColour.getBackground();
+    }
+    changeColour(true);
   }
 
+  /**
+   * Lay out OK and Cancel buttons
+   * 
+   * @return
+   */
+  protected JPanel initOkCancelPanel()
+  {
+    JPanel okCancelPanel = new JPanel();
+    okCancelPanel.setBackground(Color.white);
+    okCancelPanel.add(ok);
+    okCancelPanel.add(cancel);
+    return okCancelPanel;
+  }
 
-  JLabel minText = new JLabel();
-  JLabel maxText = new JLabel();
-  JPanel minColour = new JPanel();
-
-  JPanel maxColour = new JPanel();
-
-  JButton ok = new JButton();
-
-  JButton cancel = new JButton();
-  JPanel colourPanel = new JPanel();
-  JPanel jPanel1 = new JPanel();
-
-  JPanel jPanel2 = new JPanel();
-
-  BorderLayout borderLayout1 = new BorderLayout();
-
-  JComboBox threshold = new JComboBox();
-
-  FlowLayout flowLayout1 = new FlowLayout();
-
-  JPanel jPanel3 = new JPanel();
-
-  FlowLayout flowLayout2 = new FlowLayout();
-
-  JSlider slider = new JSlider();
-
-  JTextField thresholdValue = new JTextField(20);
-  // TODO implement GUI for tolower flag
-  // JCheckBox toLower = new JCheckBox();
-
-  JCheckBox thresholdIsMin = new JCheckBox();
-  JCheckBox colourByLabel = new JCheckBox();
-
-  private GraphLine threshline;
-
-
-  private Color oldmaxColour;
-
+  /**
+   * Lay out Colour by Label and attribute choice elements
+   * 
+   * @return
+   */
+  protected JPanel initColourByTextPanel()
+  {
+    JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    byTextPanel.setBackground(Color.white);
+    JvSwingUtils.createItalicTitledBorder(byTextPanel,
+            MessageManager.getString("label.colour_by_text"), true);
+
+    byDescription.setText(MessageManager.getString("label.label"));
+    byDescription.setToolTipText(MessageManager
+            .getString("label.colour_by_label_tip"));
+    byDescription.addActionListener(changeColourAction);
+    byTextPanel.add(byDescription);
+
+    byAttributeText.setText(MessageManager.getString("label.attribute"));
+    byAttributeText.addActionListener(changeColourAction);
+    byTextPanel.add(byAttributeText);
+
+    List<String[]> attNames = FeatureAttributes.getInstance()
+            .getAttributes(type);
+    textAttributeCombo = populateAttributesDropdown(type, attNames, false);
+    byTextPanel.add(textAttributeCombo);
+
+    /*
+     * disable colour by attribute if no attributes
+     */
+    if (attNames.isEmpty())
+    {
+      byAttributeText.setEnabled(false);
+    }
 
-  private Color oldminColour;
+    return byTextPanel;
+  }
 
-  public void minColour_actionPerformed()
+  /**
+   * Action on clicking the 'minimum colour' - open a colour chooser dialog, and
+   * set the selected colour (if the user does not cancel out of the dialog)
+   */
+  protected void minColour_actionPerformed()
   {
     Color col = JColorChooser.showDialog(this,
-            "Select Colour for Minimum Value", minColour.getBackground());
+            MessageManager.getString("label.select_colour_minimum_value"),
+            minColour.getBackground());
     if (col != null)
     {
       minColour.setBackground(col);
       minColour.setForeground(col);
     }
     minColour.repaint();
-    changeColour();
+    changeColour(true);
   }
 
-  public void maxColour_actionPerformed()
+  /**
+   * Action on clicking the 'maximum colour' - open a colour chooser dialog, and
+   * set the selected colour (if the user does not cancel out of the dialog)
+   */
+  protected void maxColour_actionPerformed()
   {
     Color col = JColorChooser.showDialog(this,
-            "Select Colour for Maximum Value", maxColour.getBackground());
+            MessageManager.getString("label.select_colour_maximum_value"),
+            maxColour.getBackground());
     if (col != null)
     {
       maxColour.setBackground(col);
       maxColour.setForeground(col);
     }
     maxColour.repaint();
-    changeColour();
+    changeColour(true);
   }
 
-  void changeColour()
+  /**
+   * Constructs and sets the selected colour options as the colour for the
+   * feature type, and repaints the alignment, and optionally the Overview
+   * and/or structure viewer if open
+   * 
+   * @param updateStructsAndOverview
+   */
+  void changeColour(boolean updateStructsAndOverview)
   {
     // Check if combobox is still adjusting
     if (adjusting)
@@ -378,188 +704,313 @@ public class FeatureColourChooser extends JPanel
       return;
     }
 
-
-    int aboveThreshold = AnnotationColourGradient.NO_THRESHOLD;
-    if (threshold.getSelectedItem().equals("Above Threshold"))
+    boolean aboveThreshold = false;
+    boolean belowThreshold = false;
+    if (threshold.getSelectedIndex() == 1)
     {
-      aboveThreshold = AnnotationColourGradient.ABOVE_THRESHOLD;
+      aboveThreshold = true;
     }
-    else if (threshold.getSelectedItem().equals("Below Threshold"))
+    else if (threshold.getSelectedIndex() == 2)
     {
-      aboveThreshold = AnnotationColourGradient.BELOW_THRESHOLD;
-    } 
+      belowThreshold = true;
+    }
+    boolean hasThreshold = aboveThreshold || belowThreshold;
 
     slider.setEnabled(true);
     thresholdValue.setEnabled(true);
-    
-    GraduatedColor acg;
+
+    /*
+     * make the feature colour
+     */
+    FeatureColourI acg;
     if (cs.isColourByLabel())
-      {
-        acg = new GraduatedColor(oldminColour, oldmaxColour, min, max);
-      } else {
-        acg = new GraduatedColor(oldminColour=minColour.getBackground(), oldmaxColour=maxColour.getBackground(), min, max);
-        
-      }
+    {
+      acg = new FeatureColour(oldminColour, oldmaxColour, min, max);
+    }
+    else
+    {
+      acg = new FeatureColour(oldminColour = minColour.getBackground(),
+              oldmaxColour = maxColour.getBackground(),
+              oldNoColour = noColour, min, max);
+    }
+    String attribute = null;
+    textAttributeCombo.setEnabled(false);
+    valueAttributeCombo.setEnabled(false);
+    if (byAttributeText.isSelected())
+    {
+      attribute = (String) textAttributeCombo.getSelectedItem();
+      textAttributeCombo.setEnabled(true);
+      acg.setAttributeName(fromAttributeDisplayName(attribute));
+    }
+    else if (byAttributeValue.isSelected())
+    {
+      attribute = (String) valueAttributeCombo.getSelectedItem();
+      valueAttributeCombo.setEnabled(true);
+      acg.setAttributeName(fromAttributeDisplayName(attribute));
+    }
+    else
+    {
+      acg.setAttributeName((String[]) null);
+    }
 
-    if (aboveThreshold == AnnotationColourGradient.NO_THRESHOLD)
+    if (!hasThreshold)
     {
       slider.setEnabled(false);
       thresholdValue.setEnabled(false);
       thresholdValue.setText("");
       thresholdIsMin.setEnabled(false);
     }
-    else if (aboveThreshold != AnnotationColourGradient.NO_THRESHOLD
-            && threshline == null)
+    else if (threshline == null)
     {
-      // todo visual indication of feature threshold
-      threshline = new jalview.datamodel.GraphLine(
-                      (max - min) / 2f,
-                      "Threshold", Color.black);
+      /*
+       * todo not yet implemented: visual indication of feature threshold
+       */
+      threshline = new GraphLine((max - min) / 2f, "Threshold",
+              Color.black);
     }
 
-    if (aboveThreshold != AnnotationColourGradient.NO_THRESHOLD)
+    if (hasThreshold)
     {
       adjusting = true;
-      acg.setThresh(threshline.value);
+      acg.setThreshold(threshline.value);
 
-      float range = max * 1000f
-              - min * 1000f;
+      float range = (max - min) * scaleFactor;
 
-      slider.setMinimum((int) (min * 1000));
-      slider.setMaximum((int) (max * 1000));
-      slider.setValue((int) (threshline.value * 1000));
+      slider.setMinimum((int) (min * scaleFactor));
+      slider.setMaximum((int) (max * scaleFactor));
+      // slider.setValue((int) (threshline.value * scaleFactor));
+      slider.setValue(Math.round(threshline.value * scaleFactor));
       thresholdValue.setText(threshline.value + "");
       slider.setMajorTickSpacing((int) (range / 10f));
       slider.setEnabled(true);
       thresholdValue.setEnabled(true);
-      thresholdIsMin.setEnabled(!colourByLabel.isSelected());
+      thresholdIsMin.setEnabled(!byDescription.isSelected());
       adjusting = false;
     }
 
-    acg.setThreshType(aboveThreshold);
-    if (thresholdIsMin.isSelected() && aboveThreshold != AnnotationColourGradient.NO_THRESHOLD)
+    acg.setAboveThreshold(aboveThreshold);
+    acg.setBelowThreshold(belowThreshold);
+    if (thresholdIsMin.isSelected() && hasThreshold)
     {
       acg.setAutoScaled(false);
-      if (aboveThreshold==AnnotationColourGradient.ABOVE_THRESHOLD)
-      { 
-        acg = new GraduatedColor(acg, threshline.value, max);
-      } else { 
-        acg = new GraduatedColor(acg, min,threshline.value);
+      if (aboveThreshold)
+      {
+        acg = new FeatureColour((FeatureColour) acg, threshline.value, max);
+      }
+      else
+      {
+        acg = new FeatureColour((FeatureColour) acg, min, threshline.value);
       }
-    } else {
+    }
+    else
+    {
       acg.setAutoScaled(true);
     }
-    acg.setColourByLabel(colourByLabel.isSelected());
+    acg.setColourByLabel(byDescription.isSelected()
+            || byAttributeText.isSelected());
+
     if (acg.isColourByLabel())
     {
       maxColour.setEnabled(false);
       minColour.setEnabled(false);
+      noValueCombo.setEnabled(false);
       maxColour.setBackground(this.getBackground());
       maxColour.setForeground(this.getBackground());
       minColour.setBackground(this.getBackground());
       minColour.setForeground(this.getBackground());
-      
-    } else {
+    }
+    else
+    {
       maxColour.setEnabled(true);
       minColour.setEnabled(true);
+      noValueCombo.setEnabled(true);
       maxColour.setBackground(oldmaxColour);
-      minColour.setBackground(oldminColour);
       maxColour.setForeground(oldmaxColour);
+      minColour.setBackground(oldminColour);
       minColour.setForeground(oldminColour);
+      noColour = oldNoColour;
     }
-    fr.featureColours.put(type,acg);
+
+    /*
+     * save the colour, and repaint stuff
+     */
+    fr.setColour(type, acg);
     cs = acg;
-    ap.paintAlignment(false);
+    ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
   }
-  private void raiseClosed() {
-    if (this.colourEditor!=null)
-    {
-      colourEditor.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
-    }
-  }
-  public void ok_actionPerformed(ActionEvent e)
+
+  private String[] fromAttributeDisplayName(String attribute)
   {
-    changeColour();
-    try
-    {
-      frame.dispose();
-      raiseClosed();
-    } catch (Exception ex)
-    {
-    }
+    return attribute == null ? null : attribute.split(COLON);
   }
 
-  public void cancel_actionPerformed(ActionEvent e)
+  @Override
+  protected void raiseClosed()
   {
-    reset();
-    try
-    {
-      frame.dispose();
-//      frame.setClosed(true);
-      raiseClosed();
-    } catch (Exception ex)
+    if (this.colourEditor != null)
     {
+      colourEditor.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
     }
   }
 
-  void reset()
+  @Override
+  public void okPressed()
   {
-    fr.featureColours.put(type, oldcs);
-    ap.paintAlignment(false);
-    cs = null;
+    changeColour(false);
   }
 
-  public void thresholdCheck_actionPerformed(ActionEvent e)
+  @Override
+  public void cancelPressed()
   {
-    changeColour();
-  }
-
-  public void annotations_actionPerformed(ActionEvent e)
-  {
-    changeColour();
+    reset();
   }
 
-  public void threshold_actionPerformed(ActionEvent e)
+  /**
+   * Action when the user cancels the dialog. All previous settings should be
+   * restored and rendered on the alignment, and any linked Overview window or
+   * structure.
+   */
+  void reset()
   {
-    changeColour();
+    fr.setColour(type, oldcs);
+    ap.paintAlignment(true, true);
+    cs = null;
   }
 
-  public void thresholdValue_actionPerformed(ActionEvent e)
+  /**
+   * Action on text entry of a threshold value
+   */
+  protected void thresholdValue_actionPerformed()
   {
     try
     {
       float f = Float.parseFloat(thresholdValue.getText());
-      slider.setValue((int) (f * 1000));
+      slider.setValue((int) (f * scaleFactor));
       threshline.value = f;
+
+      /*
+       * force repaint of any Overview window or structure
+       */
+      ap.paintAlignment(true, true);
     } catch (NumberFormatException ex)
     {
     }
   }
 
-  public void valueChanged()
+  /**
+   * Action on change of threshold slider value. This may be done interactively
+   * (by moving the slider), or programmatically (to update the slider after
+   * manual input of a threshold value).
+   */
+  protected void sliderValueChanged()
   {
-    threshline.value = (float) slider.getValue() / 1000f;
-    cs.setThresh(threshline.value);
-    changeColour();
-    ap.paintAlignment(false);
+    /*
+     * squash rounding errors by forcing min/max of slider to 
+     * actual min/max of feature score range
+     */
+    int value = slider.getValue();
+    threshline.value = value == slider.getMaximum() ? max
+            : (value == slider.getMinimum() ? min : value / scaleFactor);
+    cs.setThreshold(threshline.value);
+
+    /*
+     * repaint alignment, but not Overview or structure,
+     * to avoid overload while dragging the slider
+     */
+    changeColour(false);
   }
 
-  public void thresholdIsMin_actionPerformed(ActionEvent actionEvent)
+  void addActionListener(ActionListener graduatedColorEditor)
   {
-    changeColour();
+    if (colourEditor != null)
+    {
+      System.err.println(
+              "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
+    }
+    colourEditor = graduatedColorEditor;
   }
-  public void colourByLabel_actionPerformed(ActionEvent actionEvent)
+
+  /**
+   * Answers the last colour setting selected by user - either oldcs (which may
+   * be a java.awt.Color) or the new GraduatedColor
+   * 
+   * @return
+   */
+  FeatureColourI getLastColour()
   {
-    changeColour();
+    if (cs == null)
+    {
+      return oldcs;
+    }
+    return cs;
   }
-  ActionListener colourEditor=null;
-  public void addActionListener(ActionListener graduatedColorEditor)
+
+  /**
+   * A helper method to build the drop-down choice of attributes for a feature.
+   * Where metadata is available with a description for an attribute, that is
+   * added as a tooltip. The list may optionally be restricted to attributes for
+   * which we hold a range of numerical values (so suitable candidates for a
+   * graduated colour scheme).
+   * <p>
+   * Attribute names may be 'simple' e.g. "AC" or 'compound' e.g. {"CSQ",
+   * "Allele"}. Compound names are rendered for display as (e.g.) CSQ:Allele.
+   * 
+   * @param featureType
+   * @param attNames
+   * @param withNumericRange
+   */
+  protected JComboBox<String> populateAttributesDropdown(
+          String featureType, List<String[]> attNames,
+          boolean withNumericRange)
   {
-    if (colourEditor!=null)
+    List<String> validAtts = new ArrayList<>();
+    List<String> tooltips = new ArrayList<>();
+
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    for (String[] attName : attNames)
     {
-      System.err.println("IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
+      if (withNumericRange)
+      {
+        float[] minMax = fa.getMinMax(featureType, attName);
+        if (minMax == null)
+        {
+          continue;
+        }
+      }
+      validAtts.add(toAttributeDisplayName(attName));
+      String desc = fa.getDescription(featureType, attName);
+      if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
+      {
+        desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
+      }
+      tooltips.add(desc == null ? "" : desc);
     }
-    colourEditor = graduatedColorEditor;
+
+    JComboBox<String> attCombo = JvSwingUtils.buildComboWithTooltips(
+            validAtts, tooltips);
+
+    attCombo.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        changeMinMaxAction.actionPerformed(null);
+      }
+    });
+
+    if (validAtts.isEmpty())
+    {
+      attCombo.setToolTipText(MessageManager
+              .getString(withNumericRange ? "label.no_numeric_attributes"
+                      : "label.no_attributes"));
+    }
+
+    return attCombo;
+  }
+
+  private String toAttributeDisplayName(String[] attName)
+  {
+    return attName == null ? "" : String.join(COLON, attName);
   }
 
 }