JAL-2069 add 'by attribute' options to graduated feature colour
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 7 Nov 2017 11:48:16 +0000 (11:48 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 7 Nov 2017 11:48:16 +0000 (11:48 +0000)
resources/lang/Messages.properties
src/jalview/datamodel/features/FeatureAttributes.java
src/jalview/gui/FeatureColourChooser.java
src/jalview/gui/FeatureSettings.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java

index 851585a..35ead5e 100644 (file)
@@ -283,7 +283,6 @@ label.sequence = Sequence
 label.view_pdb_structure = View PDB Structure
 label.min = Min:
 label.max = Max:
-label.colour_by_label = Colour by label
 label.new_feature = New Feature
 label.match_case = Match Case
 label.view_alignment_editor = View in alignment editor
@@ -532,7 +531,7 @@ label.threshold_feature_above_threshold = Above Threshold
 label.threshold_feature_below_threshold = Below Threshold
 label.adjust_threshold = Adjust threshold
 label.toggle_absolute_relative_display_threshold = Toggle between absolute and relative display threshold.
-label.display_features_same_type_different_label_using_different_colour = Display features of the same type with a different label using a different colour. (e.g. domain features)
+label.colour_by_label_tip = Display features of the same type with a different label using a different colour. (e.g. domain features)
 label.select_colour_minimum_value = Select Colour for Minimum Value
 label.select_colour_maximum_value = Select Colour for Maximum Value
 label.open_url_param = Open URL {0}
@@ -1335,8 +1334,18 @@ label.matchCondition_le = <=
 label.matchCondition_gt = >
 label.matchCondition_ge = >=
 label.numeric_required = The value should be numeric
-label.no_attributes_known = No attributes known
+label.no_attributes = No attributes known
+label.no_numeric_attributes = No numeric attributes known
 label.filters = Filters
 label.match_condition = Match condition
 label.join_conditions = Join conditions with
 label.feature_to_filter = Feature to filter
+label.colour_by_value = Colour by value
+label.colour_by_text = Colour by text
+label.score = Score
+label.attribute = Attribute
+label.colour_by_label = Colour by label
+label.variable_colour = Variable colour
+label.no_colour = No colour:
+label.select_no_value_colour = Select colour when no value
+label.select_new_colour = Select new colour
index ed65750..3dc4f19 100644 (file)
@@ -34,6 +34,11 @@ public class FeatureAttributes
      */
     float max = 0f;
 
+    /*
+     * flag is set true if any numeric value is detected for this attribute
+     */
+    boolean hasValue = false;
+
     /**
      * Note one instance of this attribute, recording unique, non-null names,
      * and the min/max of any numerical values
@@ -43,27 +48,19 @@ public class FeatureAttributes
      */
     void addInstance(String desc, String value)
     {
-      if (desc != null)
+      addDescription(desc);
+
+      if (value != null)
       {
-        if (description == null)
-        {
-          description = new ArrayList<>();
-        }
-        if (!description.contains(desc))
+        try
         {
-          description.add(desc);
-        }
-        if (value != null)
+          float f = Float.valueOf(value);
+          min = Float.min(min, f);
+          max = Float.max(max, f);
+          hasValue = true;
+        } catch (NumberFormatException e)
         {
-          try
-          {
-            float f = Float.valueOf(value);
-            min = Float.min(min, f);
-            max = Float.max(max, f);
-          } catch (NumberFormatException e)
-          {
-            // ok, wasn't a number
-          }
+          // ok, wasn't a number, ignore for min-max purposes
         }
       }
     }
@@ -80,6 +77,27 @@ public class FeatureAttributes
       }
       return null;
     }
+
+    /**
+     * Adds the given description to the list of known descriptions (without
+     * duplication)
+     * 
+     * @param desc
+     */
+    public void addDescription(String desc)
+    {
+      if (desc != null)
+      {
+        if (description == null)
+        {
+          description = new ArrayList<>();
+        }
+        if (!description.contains(desc))
+        {
+          description.add(desc);
+        }
+      }
+    }
   }
 
   /**
@@ -188,4 +206,59 @@ public class FeatureAttributes
     }
     return desc;
   }
+
+  /**
+   * Answers the [min, max] value range of the given attribute for the given
+   * feature type, if known, else null. Attributes which only have text values
+   * would normally return null, however text values which happen to be numeric
+   * could result in a 'min-max' range.
+   * 
+   * @param featureType
+   * @param attName
+   * @return
+   */
+  public float[] getMinMax(String featureType, String attName)
+  {
+    Map<String, AttributeData> atts = attributes.get(featureType);
+    if (atts != null)
+    {
+      AttributeData attData = atts.get(attName);
+      if (attData != null && attData.hasValue)
+      {
+        return new float[] { attData.min, attData.max };
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Records the given attribute description for the given feature type
+   * 
+   * @param featureType
+   * @param attName
+   * @param description
+   */
+  public void addDescription(String featureType, String attName,
+          String description)
+  {
+    if (featureType == null || attName == null)
+    {
+      return;
+    }
+  
+    Map<String, AttributeData> atts = attributes.get(featureType);
+    if (atts == null)
+    {
+      atts = new TreeMap<String, AttributeData>(
+              String.CASE_INSENSITIVE_ORDER);
+      attributes.put(featureType, atts);
+    }
+    AttributeData attData = atts.get(attName);
+    if (attData == null)
+    {
+      attData = new AttributeData();
+      atts.put(attName, attData);
+    }
+    attData.addDescription(description);
+  }
 }
index 89b64a7..3fc3116 100644 (file)
@@ -22,27 +22,35 @@ package jalview.gui;
 
 import jalview.api.FeatureColourI;
 import jalview.datamodel.GraphLine;
+import jalview.datamodel.features.FeatureAttributes;
 import jalview.schemes.FeatureColour;
 import jalview.util.MessageManager;
 
-import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.FlowLayout;
-import java.awt.GridLayout;
 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.JMenuItem;
 import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JRadioButton;
 import javax.swing.JSlider;
 import javax.swing.JTextField;
 import javax.swing.border.LineBorder;
@@ -51,7 +59,8 @@ import javax.swing.event.ChangeListener;
 
 public class FeatureColourChooser extends JalviewDialog
 {
-  // FeatureSettings fs;
+  private static final int MAX_TOOLTIP_LENGTH = 50;
+
   private FeatureRenderer fr;
 
   private FeatureColourI cs;
@@ -62,11 +71,11 @@ public class FeatureColourChooser extends JalviewDialog
 
   private boolean adjusting = false;
 
-  final private float min;
+  private float min;
 
-  final private float max;
+  private float max;
 
-  final private float scaleFactor;
+  private float scaleFactor;
 
   private String type = null;
 
@@ -74,27 +83,50 @@ public class FeatureColourChooser extends JalviewDialog
 
   private JPanel maxColour = new JPanel();
 
+  private JPanel noColour = new JPanel();
+
   private JComboBox<String> threshold = new JComboBox<>();
 
   private JSlider slider = new JSlider();
 
   private JTextField thresholdValue = new JTextField(20);
 
-  // TODO implement GUI for tolower flag
-  // JCheckBox toLower = new JCheckBox();
-
   private JCheckBox thresholdIsMin = new JCheckBox();
 
-  private JCheckBox colourByLabel = 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;
+
+  /*
+   * 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
    * 
@@ -123,7 +155,7 @@ public class FeatureColourChooser extends JalviewDialog
     String title = MessageManager
             .formatMessage("label.graduated_color_for_params", new String[]
             { theType });
-    initDialogFrame(this, true, blocking, title, 480, 185);
+    initDialogFrame(this, true, blocking, title, 450, 300);
 
     slider.addChangeListener(new ChangeListener()
     {
@@ -179,7 +211,10 @@ public class FeatureColourChooser extends JalviewDialog
     }
     else
     {
-      // promote original color to a graduated color
+      /*
+       * 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)
       {
@@ -187,10 +222,11 @@ public class FeatureColourChooser extends JalviewDialog
       }
       // original colour becomes the maximum colour
       cs = new FeatureColour(Color.white, bl, mm[0], mm[1]);
-      cs.setColourByLabel(false);
+      cs.setColourByLabel(mm[0] == mm[1]);
     }
     minColour.setBackground(oldminColour = cs.getMinColour());
     maxColour.setBackground(oldmaxColour = cs.getMaxColour());
+    noColour.setBackground(oldNoColour = cs.getNoColour());
     adjusting = true;
 
     try
@@ -198,10 +234,46 @@ public class FeatureColourChooser extends JalviewDialog
       jbInit();
     } catch (Exception ex)
     {
+      ex.printStackTrace();
+      return;
     }
-    // update the gui from threshold state
+
+    /*
+     * set the initial state of options on screen
+     */
     thresholdIsMin.setSelected(!cs.isAutoScaled());
-    colourByLabel.setSelected(cs.isColourByLabel());
+
+    if (cs.isColourByLabel())
+    {
+      if (cs.isColourByAttribute())
+      {
+        byAttributeText.setSelected(true);
+        textAttributeCombo.setEnabled(true);
+        textAttributeCombo.setSelectedItem(cs.getAttributeName());
+      }
+      else
+      {
+        byDescription.setSelected(true);
+        textAttributeCombo.setEnabled(false);
+      }
+    }
+    else
+    {
+      if (cs.isColourByAttribute())
+      {
+        byAttributeValue.setSelected(true);
+        String attributeName = cs.getAttributeName();
+        valueAttributeCombo.setSelectedItem(attributeName);
+        valueAttributeCombo.setEnabled(true);
+        setAttributeMinMax(attributeName);
+      }
+      else
+      {
+        byScore.setSelected(true);
+        valueAttributeCombo.setEnabled(false);
+      }
+    }
+
     if (cs.hasThreshold())
     {
       // initialise threshold slider and selector
@@ -220,39 +292,193 @@ public class FeatureColourChooser extends JalviewDialog
     waitForInput();
   }
 
-  private void jbInit() throws Exception
+  /**
+   * Configures the initial layout
+   */
+  private void jbInit()
   {
-    this.setLayout(new GridLayout(4, 1));
+    this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+    this.setBackground(Color.white);
+
+    changeColourAction = new ActionListener() {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        changeColour(true);
+      }
+    };
+
+    /*
+     * this panel
+     *     detailsPanel
+     *         colourByTextPanel
+     *         colourByScorePanel
+     *     okCancelPanel
+     */
+    JPanel detailsPanel = new JPanel();
+    detailsPanel.setLayout(new BoxLayout(detailsPanel, BoxLayout.Y_AXIS));
 
-    JPanel colourByPanel = initColoursPanel();
+    JPanel colourByTextPanel = initColourByTextPanel();
+    detailsPanel.add(colourByTextPanel);
 
-    JPanel thresholdPanel = initThresholdPanel();
+    JPanel colourByValuePanel = initColourByValuePanel();
+    detailsPanel.add(colourByValuePanel);
 
-    JPanel okCancelPanel = initOkCancelPanel();
+    /*
+     * 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);
 
-    this.add(colourByPanel);
-    this.add(thresholdPanel);
+    JPanel okCancelPanel = initOkCancelPanel();
 
+    this.add(detailsPanel);
     this.add(okCancelPanel);
   }
 
   /**
-   * Lay out fields for threshold options
+   * Lay out fields for graduated colour by value
    * 
    * @return
    */
-  protected JPanel initThresholdPanel()
+  protected JPanel initColourByValuePanel()
   {
-    JPanel thresholdPanel = new JPanel();
-    thresholdPanel.setLayout(new FlowLayout());
-    threshold.addActionListener(new ActionListener()
+    JPanel byValuePanel = new JPanel();
+    byValuePanel.setLayout(new BoxLayout(byValuePanel, BoxLayout.Y_AXIS));
+    byValuePanel.setBorder(BorderFactory.createTitledBorder(MessageManager
+            .getString("label.colour_by_value")));
+    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(changeColourAction);
+
+    byAttributeValue.setText(MessageManager
+.getString("label.attribute"));
+    byAttributeValue.addActionListener(changeColourAction);
+    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(MessageManager.getString("label.min_colour"));
+    minColour.addMouseListener(new MouseAdapter()
     {
       @Override
-      public void actionPerformed(ActionEvent e)
+      public void mousePressed(MouseEvent e)
       {
-        changeColour(true);
+        if (minColour.isEnabled())
+        {
+          minColour_actionPerformed();
+        }
+      }
+    });
+
+    maxColour.setFont(JvSwingUtils.getLabelFont());
+    maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
+    maxColour.setPreferredSize(new Dimension(40, 20));
+    maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
+    maxColour.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mousePressed(MouseEvent e)
+      {
+        if (maxColour.isEnabled())
+        {
+          maxColour_actionPerformed();
+        }
+      }
+    });
+    maxColour.setBorder(new LineBorder(Color.black));
+
+    noColour.setFont(JvSwingUtils.getLabelFont());
+    noColour.setBorder(BorderFactory.createLineBorder(Color.black));
+    noColour.setPreferredSize(new Dimension(40, 20));
+    noColour.setToolTipText("Colour if feature has no attribute value");
+    noColour.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mousePressed(MouseEvent e)
+      {
+        if (e.isPopupTrigger()) // Mac: mouseReleased
+        {
+          showNoColourPopup(e);
+          return;
+        }
+        if (noColour.isEnabled())
+        {
+          noColour_actionPerformed();
+        }
+      }
+
+      @Override
+      public void mouseReleased(MouseEvent e)
+      {
+        if (e.isPopupTrigger()) // Windows: mouseReleased
+        {
+          showNoColourPopup(e);
+          e.consume();
+          return;
+        }
       }
     });
+    noColour.setBorder(new LineBorder(Color.black));
+
+    JLabel minText = new JLabel(MessageManager.getString("label.min"));
+    minText.setFont(JvSwingUtils.getLabelFont());
+    JLabel maxText = new JLabel(MessageManager.getString("label.max"));
+    maxText.setFont(JvSwingUtils.getLabelFont());
+    JLabel noText = new JLabel(MessageManager.getString("label.no_colour"));
+    noText.setFont(JvSwingUtils.getLabelFont());
+
+    colourRangePanel.add(minText);
+    colourRangePanel.add(minColour);
+    colourRangePanel.add(maxText);
+    colourRangePanel.add(maxColour);
+    colourRangePanel.add(noText);
+    colourRangePanel.add(noColour);
+
+    /*
+     * 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
@@ -288,25 +514,65 @@ public class FeatureColourChooser extends JalviewDialog
             MessageManager.getString("label.adjust_threshold"));
     thresholdValue.setEnabled(false);
     thresholdValue.setColumns(7);
-    thresholdPanel.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(MessageManager.getString("label.threshold_minmax"));
     thresholdIsMin.setToolTipText(MessageManager
             .getString("label.toggle_absolute_relative_display_threshold"));
-    thresholdIsMin.addActionListener(new ActionListener()
+    thresholdIsMin.addActionListener(changeColourAction);
+    isMinMaxPanel.add(thresholdIsMin);
+
+    return byValuePanel;
+  }
+
+  /**
+   * Show a popup menu with options to make 'no value colour' the same as Min
+   * Colour or Max Colour
+   * 
+   * @param evt
+   */
+  protected void showNoColourPopup(MouseEvent evt)
+  {
+    JPopupMenu pop = new JPopupMenu();
+
+    JMenuItem copyMin = new JMenuItem(
+            MessageManager.getString("label.min_colour"));
+    copyMin.addActionListener((new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        noColour.setBackground(minColour.getBackground());
+        changeColour(true);
+      }
+    }));
+    pop.add(copyMin);
+
+    JMenuItem copyMax = new JMenuItem(
+            MessageManager.getString("label.max_colour"));
+    copyMax.addActionListener((new ActionListener()
     {
       @Override
-      public void actionPerformed(ActionEvent actionEvent)
+      public void actionPerformed(ActionEvent e)
       {
+        noColour.setBackground(maxColour.getBackground());
         changeColour(true);
       }
-    });
-    thresholdPanel.add(threshold);
-    thresholdPanel.add(slider);
-    thresholdPanel.add(thresholdValue);
-    thresholdPanel.add(thresholdIsMin);
-    return thresholdPanel;
+    }));
+    pop.add(copyMax);
+
+    pop.show(noColour, evt.getX(), evt.getY());
   }
 
   /**
@@ -324,76 +590,41 @@ public class FeatureColourChooser extends JalviewDialog
   }
 
   /**
-   * Lay out Colour by Label and min/max colour widgets
+   * Lay out Colour by Label and attribute choice elements
    * 
    * @return
    */
-  protected JPanel initColoursPanel()
+  protected JPanel initColourByTextPanel()
   {
-    JPanel colourByPanel = new JPanel();
-    colourByPanel.setLayout(new FlowLayout());
-    colourByPanel.setBackground(Color.white);
-    minColour.setFont(JvSwingUtils.getLabelFont());
-    minColour.setBorder(BorderFactory.createLineBorder(Color.black));
-    minColour.setPreferredSize(new Dimension(40, 20));
-    minColour.setToolTipText(MessageManager.getString("label.min_colour"));
-    minColour.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mousePressed(MouseEvent e)
-      {
-        if (minColour.isEnabled())
-        {
-          minColour_actionPerformed();
-        }
-      }
-    });
-    maxColour.setFont(JvSwingUtils.getLabelFont());
-    maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
-    maxColour.setPreferredSize(new Dimension(40, 20));
-    maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
-    maxColour.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mousePressed(MouseEvent e)
-      {
-        if (maxColour.isEnabled())
-        {
-          maxColour_actionPerformed();
-        }
-      }
-    });
-    maxColour.setBorder(new LineBorder(Color.black));
-    JLabel minText = new JLabel(MessageManager.getString("label.min"));
-    minText.setFont(JvSwingUtils.getLabelFont());
-    JLabel maxText = new JLabel(MessageManager.getString("label.max"));
-    maxText.setFont(JvSwingUtils.getLabelFont());
+    JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    byTextPanel.setBackground(Color.white);
+    byTextPanel.setBorder(BorderFactory.createTitledBorder(MessageManager
+            .getString("label.colour_by_text")));
+
+    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);
 
-    JPanel colourPanel = new JPanel();
-    colourPanel.setBackground(Color.white);
-    colourPanel.add(minText);
-    colourPanel.add(minColour);
-    colourPanel.add(maxText);
-    colourPanel.add(maxColour);
-    colourByPanel.add(colourByLabel, BorderLayout.WEST);
-    colourByPanel.add(colourPanel, BorderLayout.EAST);
-
-    colourByLabel.setBackground(Color.white);
-    colourByLabel
-            .setText(MessageManager.getString("label.colour_by_label"));
-    colourByLabel
-            .setToolTipText(MessageManager
-                    .getString("label.display_features_same_type_different_label_using_different_colour"));
-    colourByLabel.addActionListener(new ActionListener()
+    /*
+     * disable colour by attribute if no attributes
+     */
+    if (attNames.isEmpty())
     {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        changeColour(true);
-      }
-    });
+      byAttributeText.setEnabled(false);
+    }
 
-    return colourByPanel;
+    return byTextPanel;
   }
 
   /**
@@ -433,6 +664,24 @@ public class FeatureColourChooser extends JalviewDialog
   }
 
   /**
+   * Action on clicking the 'no colour' - open a colour chooser dialog, and set
+   * the selected colour (if the user does not cancel out of the dialog)
+   */
+  protected void noColour_actionPerformed()
+  {
+    Color col = JColorChooser.showDialog(this,
+            MessageManager.getString("label.select_no_value_colour"),
+            noColour.getBackground());
+    if (col != null)
+    {
+      noColour.setBackground(col);
+      noColour.setForeground(col);
+    }
+    noColour.repaint();
+    changeColour(true);
+  }
+
+  /**
    * 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
@@ -462,6 +711,9 @@ public class FeatureColourChooser extends JalviewDialog
     slider.setEnabled(true);
     thresholdValue.setEnabled(true);
 
+    /*
+     * make the feature colour
+     */
     FeatureColourI acg;
     if (cs.isColourByLabel())
     {
@@ -470,8 +722,23 @@ public class FeatureColourChooser extends JalviewDialog
     else
     {
       acg = new FeatureColour(oldminColour = minColour.getBackground(),
-              oldmaxColour = maxColour.getBackground(), min, max);
+              oldmaxColour = maxColour.getBackground(),
+              oldNoColour = noColour.getBackground(), min, max);
+    }
+    String attribute = null;
+    textAttributeCombo.setEnabled(false);
+    valueAttributeCombo.setEnabled(false);
+    if (byAttributeText.isSelected())
+    {
+      attribute = (String) textAttributeCombo.getSelectedItem();
+      textAttributeCombo.setEnabled(true);
+    }
+    else if (byAttributeValue.isSelected())
+    {
+      attribute = (String) valueAttributeCombo.getSelectedItem();
+      valueAttributeCombo.setEnabled(true);
     }
+    acg.setAttributeName(attribute);
 
     if (!hasThreshold)
     {
@@ -504,7 +771,7 @@ public class FeatureColourChooser extends JalviewDialog
       slider.setMajorTickSpacing((int) (range / 10f));
       slider.setEnabled(true);
       thresholdValue.setEnabled(true);
-      thresholdIsMin.setEnabled(!colourByLabel.isSelected());
+      thresholdIsMin.setEnabled(!byDescription.isSelected());
       adjusting = false;
     }
 
@@ -526,27 +793,37 @@ public class FeatureColourChooser extends JalviewDialog
     {
       acg.setAutoScaled(true);
     }
-    acg.setColourByLabel(colourByLabel.isSelected());
+    acg.setColourByLabel(byDescription.isSelected()
+            || byAttributeText.isSelected());
+
     if (acg.isColourByLabel())
     {
       maxColour.setEnabled(false);
       minColour.setEnabled(false);
+      noColour.setEnabled(false);
       maxColour.setBackground(this.getBackground());
       maxColour.setForeground(this.getBackground());
       minColour.setBackground(this.getBackground());
       minColour.setForeground(this.getBackground());
-
+      noColour.setBackground(this.getBackground());
+      noColour.setForeground(this.getBackground());
     }
     else
     {
       maxColour.setEnabled(true);
       minColour.setEnabled(true);
+      noColour.setEnabled(true);
       maxColour.setBackground(oldmaxColour);
-      minColour.setBackground(oldminColour);
       maxColour.setForeground(oldmaxColour);
+      minColour.setBackground(oldminColour);
       minColour.setForeground(oldminColour);
+      noColour.setBackground(oldNoColour);
+      noColour.setForeground(oldNoColour);
     }
 
+    /*
+     * save the colour, and repaint stuff
+     */
     fr.setColour(type, acg);
     cs = acg;
     ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
@@ -653,4 +930,82 @@ public class FeatureColourChooser extends JalviewDialog
     return cs;
   }
 
+  /**
+   * 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 be restricted to attributes for which we
+   * hold a range of numerical values (so suitable candidates for a graduated
+   * colour scheme).
+   * 
+   * @param featureType
+   * @param attNames
+   * @param withNumericRange
+   */
+  protected JComboBox<String> populateAttributesDropdown(
+          String featureType, List<String> attNames,
+          boolean withNumericRange)
+  {
+    List<String> validAtts = new ArrayList<>();
+    List<String> tooltips = new ArrayList<>();
+
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    for (String attName : attNames)
+    {
+      if (withNumericRange)
+      {
+        float[] minMax = fa.getMinMax(featureType, attName);
+        if (minMax == null)
+        {
+          continue;
+        }
+      }
+      validAtts.add(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);
+    }
+
+    JComboBox<String> attCombo = JvSwingUtils.buildComboWithTooltips(
+            validAtts, tooltips);
+
+    attCombo.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        setAttributeMinMax(attCombo.getSelectedItem().toString());
+        changeColour(true);
+      }
+    });
+
+    if (validAtts.isEmpty())
+    {
+      attCombo.setToolTipText(MessageManager
+              .getString(withNumericRange ? "label.no_numeric_attributes"
+                      : "label.no_attributes"));
+    }
+
+    return attCombo;
+  }
+
+  /**
+   * Updates the min-max range and scale to be that for the given attribute name
+   * 
+   * @param attributeName
+   */
+  protected void setAttributeMinMax(String attributeName)
+  {
+    float[] minMax = FeatureAttributes.getInstance().getMinMax(type,
+            attributeName);
+    if (minMax != null)
+    {
+      min = minMax[0];
+      max = minMax[1];
+      scaleFactor = (max == min) ? 1f : 100f / (max - min);
+    }
+  }
+
 }
index 0928ec1..3be597c 100644 (file)
@@ -84,6 +84,7 @@ import java.util.Vector;
 import javax.help.HelpSetException;
 import javax.swing.AbstractCellEditor;
 import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
 import javax.swing.ButtonGroup;
 import javax.swing.Icon;
 import javax.swing.JButton;
@@ -122,6 +123,8 @@ public class FeatureSettings extends JPanel
 
   private static final int MIN_HEIGHT = 400;
 
+  private static final int MAX_TOOLTIP_LENGTH = 50;
+
   DasSourceBrowser dassourceBrowser;
 
   DasSequenceFeatureFetcher dasFeatureFetcher;
@@ -414,84 +417,63 @@ public class FeatureSettings extends JPanel
 
     });
     men.add(dens);
-    if (minmax != null)
+
+    /*
+     * variable colour options include colour by label, by score,
+     * by selected attribute text, or attribute value
+     */
+    final JCheckBoxMenuItem mxcol = new JCheckBoxMenuItem(
+            MessageManager.getString("label.variable_colour"));
+    mxcol.setSelected(!featureColour.isSimpleColour());
+    men.add(mxcol);
+    mxcol.addActionListener(new ActionListener()
     {
-      final float[][] typeMinMax = minmax.get(type);
-      /*
-       * final JCheckBoxMenuItem chb = new JCheckBoxMenuItem("Vary Height"); //
-       * this is broken at the moment and isn't that useful anyway!
-       * chb.setSelected(minmax.get(type) != null); chb.addActionListener(new
-       * ActionListener() {
-       * 
-       * public void actionPerformed(ActionEvent e) {
-       * chb.setState(chb.getState()); if (chb.getState()) { minmax.put(type,
-       * null); } else { minmax.put(type, typeMinMax); } }
-       * 
-       * });
-       * 
-       * men.add(chb);
-       */
-      if (typeMinMax != null && typeMinMax[0] != null)
-      {
-        // if (table.getValueAt(row, column));
-        // graduated colourschemes for those where minmax exists for the
-        // positional features
-        final JCheckBoxMenuItem mxcol = new JCheckBoxMenuItem(
-                "Graduated Colour");
-        mxcol.setSelected(!featureColour.isSimpleColour());
-        men.add(mxcol);
-        mxcol.addActionListener(new ActionListener()
-        {
-          JColorChooser colorChooser;
+      JColorChooser colorChooser;
 
-          @Override
-          public void actionPerformed(ActionEvent e)
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        if (e.getSource() == mxcol)
+        {
+          if (featureColour.isSimpleColour())
           {
-            if (e.getSource() == mxcol)
-            {
-              if (featureColour.isSimpleColour())
-              {
-                FeatureColourChooser fc = new FeatureColourChooser(me.fr,
-                        type);
-                fc.addActionListener(this);
-              }
-              else
-              {
-                // bring up simple color chooser
-                colorChooser = new JColorChooser();
-                JDialog dialog = JColorChooser.createDialog(me,
-                        "Select new Colour", true, // modal
-                        colorChooser, this, // OK button handler
-                        null); // no CANCEL button handler
-                colorChooser.setColor(featureColour.getMaxColour());
-                dialog.setVisible(true);
-              }
-            }
-            else
-            {
-              if (e.getSource() instanceof FeatureColourChooser)
-              {
-                FeatureColourChooser fc = (FeatureColourChooser) e
-                        .getSource();
-                table.setValueAt(fc.getLastColour(), selectedRow, 1);
-                table.validate();
-              }
-              else
-              {
-                // probably the color chooser!
-                table.setValueAt(new FeatureColour(colorChooser.getColor()),
-                        selectedRow, 1);
-                table.validate();
-                me.updateFeatureRenderer(
-                        ((FeatureTableModel) table.getModel()).getData(),
-                        false);
-              }
-            }
+            FeatureColourChooser fc = new FeatureColourChooser(me.fr, type);
+            fc.addActionListener(this);
           }
-
-        });
+          else
+          {
+            // bring up simple color chooser
+            colorChooser = new JColorChooser();
+            JDialog dialog = JColorChooser.createDialog(me,
+                    "Select new Colour", true, // modal
+                    colorChooser, this, // OK button handler
+                    null); // no CANCEL button handler
+            colorChooser.setColor(featureColour.getMaxColour());
+            dialog.setVisible(true);
+          }
+        }
+        else
+        {
+          if (e.getSource() instanceof FeatureColourChooser)
+          {
+            FeatureColourChooser fc = (FeatureColourChooser) e.getSource();
+            table.setValueAt(fc.getLastColour(), selectedRow, 1);
+            table.validate();
+          }
+          else
+          {
+            // probably the color chooser!
+            table.setValueAt(new FeatureColour(colorChooser.getColor()),
+                    selectedRow, 1);
+            table.validate();
+            me.updateFeatureRenderer(
+                    ((FeatureTableModel) table.getModel()).getData(), false);
+          }
+        }
       }
-    }
+
+    });
+
     JMenuItem selCols = new JMenuItem(
             MessageManager.getString("label.select_columns_containing"));
     selCols.addActionListener(new ActionListener()
@@ -1379,7 +1361,8 @@ public class FeatureSettings extends JPanel
     /*
      * the panel with the filters for the selected feature type
      */
-    JPanel filtersPanel = new JPanel(new GridLayout(0, 1));
+    JPanel filtersPanel = new JPanel();
+    filtersPanel.setLayout(new BoxLayout(filtersPanel, BoxLayout.Y_AXIS));
     filtersPanel.setBackground(Color.white);
     filtersPanel.setBorder(BorderFactory
             .createTitledBorder(MessageManager.getString("label.filters")));
@@ -1414,7 +1397,9 @@ public class FeatureSettings extends JPanel
     /*
      * panel with filters - populated by refreshFiltersDisplay
      */
-    chooseFiltersPanel = new JPanel(new GridLayout(0, 1));
+    chooseFiltersPanel = new JPanel();
+    chooseFiltersPanel.setLayout(new BoxLayout(chooseFiltersPanel,
+            BoxLayout.Y_AXIS));
     filtersPanel.add(chooseFiltersPanel);
 
     /*
@@ -1471,7 +1456,7 @@ public class FeatureSettings extends JPanel
     }
     if (!found)
     {
-      filteredFeatureChoice
+      filteredFeatureChoice // todo i18n
               .addItem("No filterable feature attributes known");
     }
 
@@ -1491,14 +1476,12 @@ public class FeatureSettings extends JPanel
      * clear the panel and list of filter conditions
      */
     chooseFiltersPanel.removeAll();
-
-    String selectedType = (String) filteredFeatureChoice.getSelectedItem();
-
     filters.clear();
 
     /*
      * look up attributes known for feature type
      */
+    String selectedType = (String) filteredFeatureChoice.getSelectedItem();
     List<String> attNames = FeatureAttributes.getInstance().getAttributes(
             selectedType);
 
@@ -1514,11 +1497,7 @@ public class FeatureSettings extends JPanel
       {
         orFilters.setSelected(true);
       }
-      Iterator<KeyedMatcherI> matchers = featureFilters.getMatchers();
-      while (matchers.hasNext())
-      {
-        filters.add(matchers.next());
-      }
+      featureFilters.getMatchers().forEach(matcher -> filters.add(matcher));
     }
 
     /*
@@ -1530,16 +1509,16 @@ public class FeatureSettings extends JPanel
     /*
      * render the conditions in rows, each in its own JPanel
      */
-    int i = 0;
+    int filterIndex = 0;
     for (KeyedMatcherI filter : filters)
     {
       String key = filter.getKey();
       Condition condition = filter.getMatcher()
               .getCondition();
       String pattern = filter.getMatcher().getPattern();
-      JPanel row = addFilter(key, attNames, condition, pattern, i);
+      JPanel row = addFilter(key, attNames, condition, pattern, filterIndex);
       chooseFiltersPanel.add(row);
-      i++;
+      filterIndex++;
     }
 
     filtersPane.validate();
@@ -1574,7 +1553,13 @@ public class FeatureSettings extends JPanel
     /*
      * inputs for attribute, condition, pattern
      */
-    final JComboBox<String> attCombo = new JComboBox<>();
+    /*
+     * drop-down choice of attribute, with description as a tooltip 
+     * if we can obtain it
+     */
+    String featureType = (String) filteredFeatureChoice.getSelectedItem();
+    final JComboBox<String> attCombo = populateAttributesDropdown(
+            featureType, attNames);
     JComboBox<Condition> condCombo = new JComboBox<>();
     JTextField patternField = new JTextField(8);
 
@@ -1605,12 +1590,6 @@ public class FeatureSettings extends JPanel
       }
     };
 
-    /*
-     * drop-down choice of attribute, with description as a tooltip 
-     * if we can obtain it
-     */
-    String featureType = (String) filteredFeatureChoice.getSelectedItem();
-    populateAttributesDropdown(attCombo, featureType, attNames);
     if ("".equals(attribute))
     {
       attCombo.setSelectedItem(null);
@@ -1681,57 +1660,32 @@ public class FeatureSettings extends JPanel
    * Where metadata is available with a description for an attribute, that is
    * added as a tooltip.
    * 
-   * @param attCombo
    * @param featureType
    * @param attNames
    */
-  protected void populateAttributesDropdown(
-          final JComboBox<String> attCombo, String featureType,
-          List<String> attNames)
+  protected JComboBox<String> populateAttributesDropdown(
+          String featureType, List<String> attNames)
   {
-    final ComboBoxTooltipRenderer renderer = new ComboBoxTooltipRenderer();
-    attCombo.setRenderer(renderer);
-    List<String> tips = new ArrayList<String>();
-    if (attNames.isEmpty())
-    {
-      attCombo.addItem("---");
-      attCombo.setToolTipText(MessageManager
-              .getString("label.no_attributes_known"));
-    }
-    else
+    List<String> tooltips = new ArrayList<>();
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    for (String attName : attNames)
     {
-      attCombo.setToolTipText("");
-      FeatureAttributes fs = FeatureAttributes.getInstance();
-      for (String attName : attNames)
+      String desc = fa.getDescription(featureType, attName);
+      if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
       {
-        attCombo.addItem(attName);
-        String desc = fs.getDescription(featureType, attName);
-        tips.add(desc == null ? "" : desc);
+        desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
       }
+      tooltips.add(desc == null ? "" : desc);
     }
-    renderer.setTooltips(tips);
-    final MouseAdapter mouseListener = new MouseAdapter()
-    {
-      @Override
-      public void mouseEntered(MouseEvent e)
-      {
-        int j = attCombo.getSelectedIndex();
-        if (j > -1)
-        {
-          attCombo.setToolTipText(tips.get(j));
-        }
-      }
 
-      @Override
-      public void mouseExited(MouseEvent e)
-      {
-        attCombo.setToolTipText(null);
-      }
-    };
-    for (Component c : attCombo.getComponents())
+    JComboBox<String> attCombo = JvSwingUtils.buildComboWithTooltips(
+            attNames, tooltips);
+    if (attNames.isEmpty())
     {
-      c.addMouseListener(mouseListener);
+      attCombo.setToolTipText(MessageManager
+              .getString("label.no_attributes"));
     }
+    return attCombo;
   }
 
   /**
@@ -2108,12 +2062,7 @@ public class FeatureSettings extends JPanel
             boolean isSelected, boolean hasFocus, int row, int column)
     {
       FeatureColourI cellColour = (FeatureColourI) color;
-      // JLabel comp = new JLabel();
-      // comp.
       setOpaque(true);
-      // comp.
-      // setBounds(getBounds());
-      Color newColor;
       setToolTipText(baseTT);
       setBackground(tbl.getBackground());
       if (!cellColour.isSimpleColour())
@@ -2121,14 +2070,12 @@ public class FeatureSettings extends JPanel
         Rectangle cr = tbl.getCellRect(row, column, false);
         FeatureSettings.renderGraduatedColor(this, cellColour,
                 (int) cr.getWidth(), (int) cr.getHeight());
-
       }
       else
       {
         this.setText("");
         this.setIcon(null);
-        newColor = cellColour.getColour();
-        setBackground(newColor);
+        setBackground(cellColour.getColour());
       }
       if (isSelected)
       {
@@ -2179,28 +2126,43 @@ public class FeatureSettings extends JPanel
           int w, int h)
   {
     boolean thr = false;
-    String tt = "";
-    String tx = "";
+    StringBuilder tt = new StringBuilder();
+    StringBuilder tx = new StringBuilder();
+
+    if (gcol.isColourByAttribute())
+    {
+      tx.append(gcol.getAttributeName());
+    }
+    else if (!gcol.isColourByLabel())
+    {
+      tx.append(MessageManager.getString("label.score"));
+    }
+    tx.append(" ");
     if (gcol.isAboveThreshold())
     {
       thr = true;
-      tx += ">";
-      tt += "Thresholded (Above " + gcol.getThreshold() + ") ";
+      tx.append(">");
+      tt.append("Thresholded (Above ").append(gcol.getThreshold())
+              .append(") ");
     }
     if (gcol.isBelowThreshold())
     {
       thr = true;
-      tx += "<";
-      tt += "Thresholded (Below " + gcol.getThreshold() + ") ";
+      tx.append("<");
+      tt.append("Thresholded (Below ").append(gcol.getThreshold())
+              .append(") ");
     }
     if (gcol.isColourByLabel())
     {
-      tt = "Coloured by label text. " + tt;
+      tt.append("Coloured by label text. ").append(tt);
       if (thr)
       {
-        tx += " ";
+        tx.append(" ");
+      }
+      if (!gcol.isColourByAttribute())
+      {
+        tx.append("Label");
       }
-      tx += "Label";
       comp.setIcon(null);
     }
     else
@@ -2216,16 +2178,17 @@ public class FeatureSettings extends JPanel
       // + ", " + minCol.getBlue() + ")");
     }
     comp.setHorizontalAlignment(SwingConstants.CENTER);
-    comp.setText(tx);
+    comp.setText(tx.toString());
     if (tt.length() > 0)
     {
       if (comp.getToolTipText() == null)
       {
-        comp.setToolTipText(tt);
+        comp.setToolTipText(tt.toString());
       }
       else
       {
-        comp.setToolTipText(tt + " " + comp.getToolTipText());
+        comp.setToolTipText(tt.append(" ").append(comp.getToolTipText())
+                .toString());
       }
     }
   }
@@ -2347,7 +2310,8 @@ class ColorEditor extends AbstractCellEditor
     button.setBorderPainted(false);
     // Set up the dialog that the button brings up.
     colorChooser = new JColorChooser();
-    dialog = JColorChooser.createDialog(button, "Select new Colour", true, // modal
+    dialog = JColorChooser.createDialog(button,
+            MessageManager.getString("label.select_new_colour"), true, // modal
             colorChooser, this, // OK button handler
             null); // no CANCEL button handler
   }
index 3a97067..c2f5bb7 100644 (file)
@@ -490,7 +490,8 @@ public abstract class FeatureRendererModel
               if (mmrange != null)
               {
                 FeatureColourI fc = featureColours.get(oldRender[j]);
-                if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled())
+                if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()
+                        && !fc.isColourByAttribute())
                 {
                   fc.updateBounds(mmrange[0][0], mmrange[0][1]);
                 }
@@ -520,7 +521,8 @@ public abstract class FeatureRendererModel
         if (mmrange != null)
         {
           FeatureColourI fc = featureColours.get(newf[i]);
-          if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled())
+          if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()
+                  && !fc.isColourByAttribute())
           {
             fc.updateBounds(mmrange[0][0], mmrange[0][1]);
           }