JAL-2808 set (read-only) condition text field as disabled
[jalview.git] / src / jalview / gui / FeatureSettings.java
index 0f14bed..974b387 100644 (file)
@@ -1,6 +1,6 @@
 /*
- * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.2)
- * Copyright (C) 2014 The Jalview Authors
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
  * 
  * This file is part of Jalview.
  * 
  */
 package jalview.gui;
 
-import jalview.analysis.AlignmentSorter;
+import jalview.api.FeatureColourI;
+import jalview.api.FeatureSettingsControllerI;
 import jalview.bin.Cache;
-import jalview.commands.OrderCommand;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.SequenceFeature;
-import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureAttributes;
 import jalview.gui.Help.HelpId;
 import jalview.io.JalviewFileChooser;
-import jalview.schemes.AnnotationColourGradient;
-import jalview.schemes.GraduatedColor;
+import jalview.io.JalviewFileView;
+import jalview.schemabinding.version2.JalviewUserColours;
+import jalview.schemes.FeatureColour;
+import jalview.util.Format;
 import jalview.util.MessageManager;
+import jalview.util.Platform;
+import jalview.util.QuickSort;
+import jalview.util.ReverseListIterator;
+import jalview.util.matcher.Condition;
+import jalview.util.matcher.KeyedMatcher;
+import jalview.util.matcher.KeyedMatcherI;
+import jalview.util.matcher.KeyedMatcherSet;
+import jalview.util.matcher.KeyedMatcherSetI;
+import jalview.viewmodel.AlignmentViewport;
+import jalview.ws.DasSequenceFeatureFetcher;
 import jalview.ws.dbsources.das.api.jalviewSourceI;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
 import java.awt.Font;
 import java.awt.Graphics;
 import java.awt.GridLayout;
+import java.awt.LayoutManager;
 import java.awt.Rectangle;
 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;
@@ -57,47 +73,64 @@ import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 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;
 import javax.swing.JCheckBox;
 import javax.swing.JCheckBoxMenuItem;
 import javax.swing.JColorChooser;
+import javax.swing.JComboBox;
 import javax.swing.JDialog;
 import javax.swing.JInternalFrame;
 import javax.swing.JLabel;
 import javax.swing.JLayeredPane;
 import javax.swing.JMenuItem;
-import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JPopupMenu;
+import javax.swing.JRadioButton;
 import javax.swing.JScrollPane;
 import javax.swing.JSlider;
 import javax.swing.JTabbedPane;
 import javax.swing.JTable;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
 import javax.swing.ListSelectionModel;
 import javax.swing.SwingConstants;
 import javax.swing.SwingUtilities;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
+import javax.swing.plaf.basic.BasicArrowButton;
 import javax.swing.table.AbstractTableModel;
 import javax.swing.table.TableCellEditor;
 import javax.swing.table.TableCellRenderer;
 
 public class FeatureSettings extends JPanel
+        implements FeatureSettingsControllerI
 {
-  DasSourceBrowser dassourceBrowser;
+  private static final String COLON = ":";
+
+  private static final int MIN_WIDTH = 400;
 
-  jalview.ws.DasSequenceFeatureFetcher dasFeatureFetcher;
+  private static final int MIN_HEIGHT = 400;
 
-  JPanel settingsPane = new JPanel();
+  private static final int MAX_TOOLTIP_LENGTH = 50;
+
+  DasSourceBrowser dassourceBrowser;
+
+  DasSequenceFeatureFetcher dasFeatureFetcher;
 
   JPanel dasSettingsPane = new JPanel();
 
@@ -105,8 +138,15 @@ public class FeatureSettings extends JPanel
 
   public final AlignFrame af;
 
+  /*
+   * 'original' fields hold settings to restore on Cancel
+   */
   Object[][] originalData;
 
+  private float originalTransparency;
+
+  private Map<String, KeyedMatcherSetI> originalFilters;
+
   final JInternalFrame frame;
 
   JScrollPane scrollPane = new JScrollPane();
@@ -116,15 +156,71 @@ public class FeatureSettings extends JPanel
   JPanel groupPanel;
 
   JSlider transparency = new JSlider();
+  
+  /*
+   * when true, constructor is still executing - so ignore UI events
+   */
+  protected volatile boolean inConstruction = true;
+
+  int selectedRow = -1;
+
+  JButton fetchDAS = new JButton();
+
+  JButton saveDAS = new JButton();
+
+  JButton cancelDAS = new JButton();
+
+  boolean resettingTable = false;
+
+  /*
+   * true when Feature Settings are updating from feature renderer
+   */
+  private boolean handlingUpdate = false;
+
+  /*
+   * holds {featureCount, totalExtent} for each feature type
+   */
+  Map<String, float[]> typeWidth = null;
+
+  /*
+   * fields of the feature filters tab
+   */
+  private JPanel filtersPane;
+
+  private JPanel chooseFiltersPanel;
 
-  JPanel transPanel = new JPanel(new GridLayout(1, 2));
+  private JComboBox<String> filteredFeatureChoice;
 
-  public FeatureSettings(AlignFrame af)
+  private JRadioButton andFilters;
+
+  private JRadioButton orFilters;
+
+  /*
+   * filters for the currently selected feature type
+   */
+  private List<KeyedMatcherI> filters;
+
+  private JTextArea filtersAsText;
+
+  // set white normally, black to debug layout
+  private Color debugBorderColour = Color.white;
+
+  /**
+   * Constructor
+   * 
+   * @param af
+   */
+  public FeatureSettings(AlignFrame alignFrame)
   {
-    this.af = af;
+    this.af = alignFrame;
     fr = af.getFeatureRenderer();
 
-    transparency.setMaximum(100 - (int) (fr.transparency * 100));
+    // save transparency for restore on Cancel
+    originalTransparency = fr.getTransparency();
+    int originalTransparencyAsPercent = (int) (originalTransparency * 100);
+    transparency.setMaximum(100 - originalTransparencyAsPercent);
+
+    originalFilters = fr.getFeatureFilters();
 
     try
     {
@@ -134,15 +230,18 @@ public class FeatureSettings extends JPanel
       ex.printStackTrace();
     }
 
-    table = new JTable() {
+    table = new JTable()
+    {
       @Override
-      public String getToolTipText(MouseEvent e) {
-        if (table.columnAtPoint(e.getPoint()) == 0) {
+      public String getToolTipText(MouseEvent e)
+      {
+        if (table.columnAtPoint(e.getPoint()) == 0)
+        {
           /*
            * Tooltip for feature name only
            */
-          return JvSwingUtils.wrapTooltip(true,
-                MessageManager.getString("label.feature_settings_click_drag"));
+          return JvSwingUtils.wrapTooltip(true, MessageManager
+                  .getString("label.feature_settings_click_drag"));
         }
         return null;
       }
@@ -153,31 +252,34 @@ public class FeatureSettings extends JPanel
 
     table.setDefaultEditor(Color.class, new ColorEditor(this));
 
-    table.setDefaultEditor(GraduatedColor.class, new ColorEditor(this));
-    table.setDefaultRenderer(GraduatedColor.class, new ColorRenderer());
+    table.setDefaultEditor(FeatureColour.class, new ColorEditor(this));
+    table.setDefaultRenderer(FeatureColour.class, new ColorRenderer());
     table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
 
     table.addMouseListener(new MouseAdapter()
     {
+      @Override
       public void mousePressed(MouseEvent evt)
       {
         selectedRow = table.rowAtPoint(evt.getPoint());
-        if (SwingUtilities.isRightMouseButton(evt))
+        if (evt.isPopupTrigger())
         {
           popupSort(selectedRow, (String) table.getValueAt(selectedRow, 0),
-                  table.getValueAt(selectedRow, 1), fr.minmax, evt.getX(),
-                  evt.getY());
+                  table.getValueAt(selectedRow, 1), fr.getMinMax(),
+                  evt.getX(), evt.getY());
         }
         else if (evt.getClickCount() == 2)
         {
+          boolean invertSelection = evt.isAltDown();
+          boolean toggleSelection = Platform.isControlDown(evt);
+          boolean extendSelection = evt.isShiftDown();
           fr.ap.alignFrame.avc.markColumnsContainingFeatures(
-                  evt.isAltDown(), evt.isShiftDown() || evt.isMetaDown(),
-                  evt.isMetaDown(),
+                  invertSelection, extendSelection, toggleSelection,
                   (String) table.getValueAt(selectedRow, 0));
         }
       }
 
-      // isPopupTrigger fires on mouseReleased on Mac
+      // isPopupTrigger fires on mouseReleased on Windows
       @Override
       public void mouseReleased(MouseEvent evt)
       {
@@ -185,54 +287,58 @@ public class FeatureSettings extends JPanel
         if (evt.isPopupTrigger())
         {
           popupSort(selectedRow, (String) table.getValueAt(selectedRow, 0),
-                  table.getValueAt(selectedRow, 1), fr.minmax, evt.getX(),
-                  evt.getY());
+                  table.getValueAt(selectedRow, 1), fr.getMinMax(),
+                  evt.getX(), evt.getY());
         }
       }
     });
 
     table.addMouseMotionListener(new MouseMotionAdapter()
     {
+      @Override
       public void mouseDragged(MouseEvent evt)
       {
         int newRow = table.rowAtPoint(evt.getPoint());
         if (newRow != selectedRow && selectedRow != -1 && newRow != -1)
         {
-          Object[] temp = new Object[3];
-          temp[0] = table.getValueAt(selectedRow, 0);
-          temp[1] = table.getValueAt(selectedRow, 1);
-          temp[2] = table.getValueAt(selectedRow, 2);
-
-          table.setValueAt(table.getValueAt(newRow, 0), selectedRow, 0);
-          table.setValueAt(table.getValueAt(newRow, 1), selectedRow, 1);
-          table.setValueAt(table.getValueAt(newRow, 2), selectedRow, 2);
-
-          table.setValueAt(temp[0], newRow, 0);
-          table.setValueAt(temp[1], newRow, 1);
-          table.setValueAt(temp[2], newRow, 2);
-
+          /*
+           * reposition 'selectedRow' to 'newRow' (the dragged to location)
+           * this could be more than one row away for a very fast drag action
+           * so just swap it with adjacent rows until we get it there
+           */
+          Object[][] data = ((FeatureTableModel) table.getModel())
+                  .getData();
+          int direction = newRow < selectedRow ? -1 : 1;
+          for (int i = selectedRow; i != newRow; i += direction)
+          {
+            Object[] temp = data[i];
+            data[i] = data[i + direction];
+            data[i + direction] = temp;
+          }
+          updateFeatureRenderer(data);
+          table.repaint();
           selectedRow = newRow;
         }
       }
     });
-//    table.setToolTipText(JvSwingUtils.wrapTooltip(true,
-//            MessageManager.getString("label.feature_settings_click_drag")));
+    // table.setToolTipText(JvSwingUtils.wrapTooltip(true,
+    // MessageManager.getString("label.feature_settings_click_drag")));
     scrollPane.setViewportView(table);
 
     dassourceBrowser = new DasSourceBrowser(this);
     dasSettingsPane.add(dassourceBrowser, BorderLayout.CENTER);
 
-    if (af.getViewport().getFeaturesDisplayed() == null
-            || fr.renderOrder == null)
+    if (af.getViewport().isShowSequenceFeatures() || !fr.hasRenderOrder())
     {
       fr.findAllFeatures(true); // display everything!
     }
 
-    setTableData();
+    discoverAllFeatureData();
     final PropertyChangeListener change;
     final FeatureSettings fs = this;
     fr.addPropertyChangeListener(change = new PropertyChangeListener()
     {
+      @Override
       public void propertyChange(PropertyChangeEvent evt)
       {
         if (!fs.resettingTable && !fs.handlingUpdate)
@@ -248,36 +354,43 @@ public class FeatureSettings extends JPanel
 
     frame = new JInternalFrame();
     frame.setContentPane(this);
-    if (new jalview.util.Platform().isAMac())
+    if (Platform.isAMac())
     {
       Desktop.addInternalFrame(frame,
               MessageManager.getString("label.sequence_feature_settings"),
-              475, 480);
+              600, 480);
     }
     else
     {
       Desktop.addInternalFrame(frame,
               MessageManager.getString("label.sequence_feature_settings"),
-              400, 450);
+              600, 450);
     }
+    frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
 
-    frame.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter()
-    {
-      public void internalFrameClosed(
-              javax.swing.event.InternalFrameEvent evt)
-      {
-        fr.removePropertyChangeListener(change);
-        dassourceBrowser.fs = null;
-      };
-    });
+    frame.addInternalFrameListener(
+            new javax.swing.event.InternalFrameAdapter()
+            {
+              @Override
+              public void internalFrameClosed(
+                      javax.swing.event.InternalFrameEvent evt)
+              {
+                fr.removePropertyChangeListener(change);
+                dassourceBrowser.fs = null;
+              };
+            });
     frame.setLayer(JLayeredPane.PALETTE_LAYER);
+    inConstruction = false;
   }
 
   protected void popupSort(final int selectedRow, final String type,
-          final Object typeCol, final Hashtable minmax, int x, int y)
+          final Object typeCol, final Map<String, float[][]> minmax, int x,
+          int y)
   {
-    JPopupMenu men = new JPopupMenu(MessageManager.formatMessage(
-            "label.settings_for_param", new String[]
+    final FeatureColourI featureColour = (FeatureColourI) typeCol;
+
+    JPopupMenu men = new JPopupMenu(MessageManager
+            .formatMessage("label.settings_for_param", new String[]
             { type }));
     JMenuItem scr = new JMenuItem(
             MessageManager.getString("label.sort_by_score"));
@@ -286,10 +399,12 @@ public class FeatureSettings extends JPanel
     scr.addActionListener(new ActionListener()
     {
 
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        me.sortByScore(new String[]
-        { type });
+        me.af.avc
+                .sortAlignmentByFeatureScore(Arrays.asList(new String[]
+                { type }));
       }
 
     });
@@ -298,96 +413,77 @@ public class FeatureSettings extends JPanel
     dens.addActionListener(new ActionListener()
     {
 
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        me.sortByDens(new String[]
-        { type });
+        me.af.avc
+                .sortAlignmentByFeatureDensity(Arrays.asList(new String[]
+                { type }));
       }
 
     });
     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 Object 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 && ((float[][]) 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(!(typeCol instanceof Color));
-        men.add(mxcol);
-        mxcol.addActionListener(new ActionListener()
-        {
-          JColorChooser colorChooser;
+      JColorChooser colorChooser;
 
-          public void actionPerformed(ActionEvent e)
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        if (e.getSource() == mxcol)
+        {
+          if (featureColour.isSimpleColour())
           {
-            if (e.getSource() == mxcol)
-            {
-              if (typeCol instanceof Color)
-              {
-                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(((GraduatedColor) typeCol)
-                        .getMaxColor());
-                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(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()
     {
-
       @Override
       public void actionPerformed(ActionEvent arg0)
       {
@@ -395,11 +491,10 @@ public class FeatureSettings extends JPanel
                 false, type);
       }
     });
-    JMenuItem clearCols = new JMenuItem(
-            MessageManager.getString("label.select_columns_not_containing"));
+    JMenuItem clearCols = new JMenuItem(MessageManager
+            .getString("label.select_columns_not_containing"));
     clearCols.addActionListener(new ActionListener()
     {
-
       @Override
       public void actionPerformed(ActionEvent arg0)
       {
@@ -407,237 +502,177 @@ public class FeatureSettings extends JPanel
                 false, type);
       }
     });
+    JMenuItem hideCols = new JMenuItem(
+            MessageManager.getString("label.hide_columns_containing"));
+    hideCols.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent arg0)
+      {
+        fr.ap.alignFrame.hideFeatureColumns(type, true);
+      }
+    });
+    JMenuItem hideOtherCols = new JMenuItem(
+            MessageManager.getString("label.hide_columns_not_containing"));
+    hideOtherCols.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent arg0)
+      {
+        fr.ap.alignFrame.hideFeatureColumns(type, false);
+      }
+    });
     men.add(selCols);
     men.add(clearCols);
+    men.add(hideCols);
+    men.add(hideOtherCols);
     men.show(table, x, y);
   }
 
-  /**
-   * true when Feature Settings are updating from feature renderer
-   */
-  private boolean handlingUpdate = false;
-
-  /**
-   * contains a float[3] for each feature type string. created by setTableData
-   */
-  Hashtable typeWidth = null;
-
-  synchronized public void setTableData()
+  @Override
+  synchronized public void discoverAllFeatureData()
   {
-    if (fr.featureGroups == null)
-    {
-      fr.featureGroups = new Hashtable();
-    }
-    Vector allFeatures = new Vector();
-    Vector allGroups = new Vector();
-    SequenceFeature[] tmpfeatures;
-    String group;
-    for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++)
-    {
-      if (af.getViewport().getAlignment().getSequenceAt(i)
-              .getDatasetSequence().getSequenceFeatures() == null)
-      {
-        continue;
-      }
-
-      tmpfeatures = af.getViewport().getAlignment().getSequenceAt(i)
-              .getDatasetSequence().getSequenceFeatures();
+    Set<String> allGroups = new HashSet<>();
+    AlignmentI alignment = af.getViewport().getAlignment();
 
-      int index = 0;
-      while (index < tmpfeatures.length)
+    for (int i = 0; i < alignment.getHeight(); i++)
+    {
+      SequenceI seq = alignment.getSequenceAt(i);
+      for (String group : seq.getFeatures().getFeatureGroups(true))
       {
-        if (tmpfeatures[index].begin == 0 && tmpfeatures[index].end == 0)
-        {
-          index++;
-          continue;
-        }
-
-        if (tmpfeatures[index].getFeatureGroup() != null)
-        {
-          group = tmpfeatures[index].featureGroup;
-          if (!allGroups.contains(group))
-          {
-            allGroups.addElement(group);
-            if (group != null)
-            {
-              checkGroupState(group);
-            }
-          }
-        }
-
-        if (!allFeatures.contains(tmpfeatures[index].getType()))
+        if (group != null && !allGroups.contains(group))
         {
-          allFeatures.addElement(tmpfeatures[index].getType());
+          allGroups.add(group);
+          checkGroupState(group);
         }
-        index++;
       }
     }
 
+    populateFilterableFeatures();
+
     resetTable(null);
 
     validate();
   }
 
   /**
+   * Synchronise gui group list and check visibility of group
    * 
    * @param group
-   * @return true if group has been seen before and is already added to set.
+   * @return true if group is visible
    */
   private boolean checkGroupState(String group)
   {
-    boolean visible;
-    if (fr.featureGroups.containsKey(group))
-    {
-      visible = ((Boolean) fr.featureGroups.get(group)).booleanValue();
-    }
-    else
-    {
-      visible = true; // new group is always made visible
-    }
+    boolean visible = fr.checkGroupVisibility(group, true);
 
-    if (groupPanel == null)
-    {
-      groupPanel = new JPanel();
-    }
-
-    boolean alreadyAdded = false;
     for (int g = 0; g < groupPanel.getComponentCount(); g++)
     {
       if (((JCheckBox) groupPanel.getComponent(g)).getText().equals(group))
       {
-        alreadyAdded = true;
         ((JCheckBox) groupPanel.getComponent(g)).setSelected(visible);
-        break;
+        return visible;
       }
     }
 
-    if (alreadyAdded)
-    {
-
-      return true;
-    }
-
-    fr.featureGroups.put(group, new Boolean(visible));
     final String grp = group;
     final JCheckBox check = new JCheckBox(group, visible);
     check.setFont(new Font("Serif", Font.BOLD, 12));
+    check.setToolTipText(group);
     check.addItemListener(new ItemListener()
     {
+      @Override
       public void itemStateChanged(ItemEvent evt)
       {
-        fr.featureGroups.put(check.getText(),
-                new Boolean(check.isSelected()));
-        af.alignPanel.getSeqPanel().seqCanvas.repaint();
-        if (af.alignPanel.overviewPanel != null)
-        {
-          af.alignPanel.overviewPanel.updateOverviewImage();
-        }
-
-        resetTable(new String[]
-        { grp });
+        fr.setGroupVisibility(check.getText(), check.isSelected());
+        resetTable(new String[] { grp });
+        af.alignPanel.paintAlignment(true, true);
       }
     });
     groupPanel.add(check);
-    return false;
+    return visible;
   }
 
-  boolean resettingTable = false;
-
   synchronized void resetTable(String[] groupChanged)
   {
-    if (resettingTable == true)
+    if (resettingTable)
     {
       return;
     }
     resettingTable = true;
-    typeWidth = new Hashtable();
+    typeWidth = new Hashtable<>();
     // TODO: change avWidth calculation to 'per-sequence' average and use long
     // rather than float
-    float[] avWidth = null;
-    SequenceFeature[] tmpfeatures;
-    String group = null, type;
-    Vector visibleChecks = new Vector();
-
-    // Find out which features should be visible depending on which groups
-    // are selected / deselected
-    // and recompute average width ordering
+
+    Set<String> displayableTypes = new HashSet<>();
+    Set<String> foundGroups = new HashSet<>();
+
+    /*
+     * determine which feature types may be visible depending on 
+     * which groups are selected, and recompute average width data
+     */
     for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++)
     {
 
-      tmpfeatures = af.getViewport().getAlignment().getSequenceAt(i)
-              .getDatasetSequence().getSequenceFeatures();
-      if (tmpfeatures == null)
-      {
-        continue;
-      }
+      SequenceI seq = af.getViewport().getAlignment().getSequenceAt(i);
 
-      int index = 0;
-      while (index < tmpfeatures.length)
+      /*
+       * get the sequence's groups for positional features
+       * and keep track of which groups are visible
+       */
+      Set<String> groups = seq.getFeatures().getFeatureGroups(true);
+      Set<String> visibleGroups = new HashSet<>();
+      for (String group : groups)
       {
-        group = tmpfeatures[index].featureGroup;
-
-        if (tmpfeatures[index].begin == 0 && tmpfeatures[index].end == 0)
+        if (group == null || checkGroupState(group))
         {
-          index++;
-          continue;
+          visibleGroups.add(group);
         }
+      }
+      foundGroups.addAll(groups);
 
-        if (group == null || fr.featureGroups.get(group) == null
-                || ((Boolean) fr.featureGroups.get(group)).booleanValue())
-        {
-          if (group != null)
-          {
-            checkGroupState(group);
-          }
-          type = tmpfeatures[index].getType();
-          if (!visibleChecks.contains(type))
-          {
-            visibleChecks.addElement(type);
-          }
-        }
-        if (!typeWidth.containsKey(tmpfeatures[index].getType()))
-        {
-          typeWidth.put(tmpfeatures[index].getType(),
-                  avWidth = new float[3]);
-        }
-        else
-        {
-          avWidth = (float[]) typeWidth.get(tmpfeatures[index].getType());
-        }
-        avWidth[0]++;
-        if (tmpfeatures[index].getBegin() > tmpfeatures[index].getEnd())
-        {
-          avWidth[1] += 1 + tmpfeatures[index].getBegin()
-                  - tmpfeatures[index].getEnd();
-        }
-        else
+      /*
+       * get distinct feature types for visible groups
+       * record distinct visible types, and their count and total length
+       */
+      Set<String> types = seq.getFeatures().getFeatureTypesForGroups(true,
+              visibleGroups.toArray(new String[visibleGroups.size()]));
+      for (String type : types)
+      {
+        displayableTypes.add(type);
+        float[] avWidth = typeWidth.get(type);
+        if (avWidth == null)
         {
-          avWidth[1] += 1 + tmpfeatures[index].getEnd()
-                  - tmpfeatures[index].getBegin();
+          avWidth = new float[2];
+          typeWidth.put(type, avWidth);
         }
-        index++;
+        // todo this could include features with a non-visible group
+        // - do we greatly care?
+        // todo should we include non-displayable features here, and only
+        // update when features are added?
+        avWidth[0] += seq.getFeatures().getFeatureCount(true, type);
+        avWidth[1] += seq.getFeatures().getTotalFeatureLength(type);
       }
     }
 
-    int fSize = visibleChecks.size();
-    Object[][] data = new Object[fSize][3];
+    Object[][] data = new Object[displayableTypes.size()][3];
     int dataIndex = 0;
 
-    if (fr.renderOrder != null)
+    if (fr.hasRenderOrder())
     {
       if (!handlingUpdate)
-       {
+      {
         fr.findAllFeatures(groupChanged != null); // prod to update
+        // colourschemes. but don't
+        // affect display
+        // First add the checks in the previous render order,
+        // in case the window has been closed and reopened
       }
-      // colourschemes. but don't
-      // affect display
-      // First add the checks in the previous render order,
-      // in case the window has been closed and reopened
-      for (int ro = fr.renderOrder.length - 1; ro > -1; ro--)
+      List<String> frl = fr.getRenderOrder();
+      for (int ro = frl.size() - 1; ro > -1; ro--)
       {
-        type = fr.renderOrder[ro];
+        String type = frl.get(ro);
 
-        if (!visibleChecks.contains(type))
+        if (!displayableTypes.contains(type))
         {
           continue;
         }
@@ -645,30 +680,32 @@ public class FeatureSettings extends JPanel
         data[dataIndex][0] = type;
         data[dataIndex][1] = fr.getFeatureStyle(type);
         data[dataIndex][2] = new Boolean(
-                af.getViewport().getFeaturesDisplayed().containsKey(type));
+                af.getViewport().getFeaturesDisplayed().isVisible(type));
         dataIndex++;
-        visibleChecks.removeElement(type);
+        displayableTypes.remove(type);
       }
     }
 
-    fSize = visibleChecks.size();
-    for (int i = 0; i < fSize; i++)
+    /*
+     * process any extra features belonging only to 
+     * a group which was just selected
+     */
+    while (!displayableTypes.isEmpty())
     {
-      // These must be extra features belonging to the group
-      // which was just selected
-      type = visibleChecks.elementAt(i).toString();
+      String type = displayableTypes.iterator().next();
       data[dataIndex][0] = type;
 
       data[dataIndex][1] = fr.getFeatureStyle(type);
       if (data[dataIndex][1] == null)
       {
         // "Colour has been updated in another view!!"
-        fr.renderOrder = null;
+        fr.clearRenderOrder();
         return;
       }
 
       data[dataIndex][2] = new Boolean(true);
       dataIndex++;
+      displayableTypes.remove(type);
     }
 
     if (originalData == null)
@@ -679,24 +716,105 @@ public class FeatureSettings extends JPanel
         System.arraycopy(data[i], 0, originalData[i], 0, 3);
       }
     }
+    else
+    {
+      updateOriginalData(data);
+    }
 
     table.setModel(new FeatureTableModel(data));
     table.getColumnModel().getColumn(0).setPreferredWidth(200);
 
-    if (groupPanel != null)
-    {
-      groupPanel.setLayout(new GridLayout(fr.featureGroups.size() / 4 + 1,
-              4));
-
-      groupPanel.validate();
-      bigPanel.add(groupPanel, BorderLayout.NORTH);
-    }
+    groupPanel.setLayout(
+            new GridLayout(fr.getFeatureGroupsSize() / 4 + 1, 4));
+    pruneGroups(foundGroups);
+    groupPanel.validate();
 
     updateFeatureRenderer(data, groupChanged != null);
     resettingTable = false;
   }
 
   /**
+   * Updates 'originalData' (used for restore on Cancel) if we detect that
+   * changes have been made outwith this dialog
+   * <ul>
+   * <li>a new feature type added (and made visible)</li>
+   * <li>a feature colour changed (in the Amend Features dialog)</li>
+   * </ul>
+   * 
+   * @param foundData
+   */
+  protected void updateOriginalData(Object[][] foundData)
+  {
+    // todo LinkedHashMap instead of Object[][] would be nice
+
+    Object[][] currentData = ((FeatureTableModel) table.getModel())
+            .getData();
+    for (Object[] row : foundData)
+    {
+      String type = (String) row[0];
+      boolean found = false;
+      for (Object[] current : currentData)
+      {
+        if (type.equals(current[0]))
+        {
+          found = true;
+          /*
+           * currently dependent on object equality here;
+           * really need an equals method on FeatureColour
+           */
+          if (!row[1].equals(current[1]))
+          {
+            /*
+             * feature colour has changed externally - update originalData
+             */
+            for (Object[] original : originalData)
+            {
+              if (type.equals(original[0]))
+              {
+                original[1] = row[1];
+                break;
+              }
+            }
+          }
+          break;
+        }
+      }
+      if (!found)
+      {
+        /*
+         * new feature detected - add to original data (on top)
+         */
+        Object[][] newData = new Object[originalData.length + 1][3];
+        for (int i = 0; i < originalData.length; i++)
+        {
+          System.arraycopy(originalData[i], 0, newData[i + 1], 0, 3);
+        }
+        newData[0] = row;
+        originalData = newData;
+      }
+    }
+  }
+
+  /**
+   * Remove from the groups panel any checkboxes for groups that are not in the
+   * foundGroups set. This enables removing a group from the display when the
+   * last feature in that group is deleted.
+   * 
+   * @param foundGroups
+   */
+  protected void pruneGroups(Set<String> foundGroups)
+  {
+    for (int g = 0; g < groupPanel.getComponentCount(); g++)
+    {
+      JCheckBox checkbox = (JCheckBox) groupPanel.getComponent(g);
+      if (!foundGroups.contains(checkbox.getText()))
+      {
+        groupPanel.remove(checkbox);
+      }
+    }
+  }
+
+  /**
    * reorder data based on the featureRenderers global priority list.
    * 
    * @param data
@@ -725,12 +843,11 @@ public class FeatureSettings extends JPanel
 
   void load()
   {
-    JalviewFileChooser chooser = new JalviewFileChooser(
-            jalview.bin.Cache.getProperty("LAST_DIRECTORY"), new String[]
-            { "fc" }, new String[]
-            { "Sequence Feature Colours" }, "Sequence Feature Colours");
-    chooser.setFileView(new jalview.io.JalviewFileView());
-    chooser.setDialogTitle(MessageManager.getString("label.load_feature_colours"));
+    JalviewFileChooser chooser = new JalviewFileChooser("fc",
+            "Sequence Feature Colours");
+    chooser.setFileView(new JalviewFileView());
+    chooser.setDialogTitle(
+            MessageManager.getString("label.load_feature_colours"));
     chooser.setToolTipText(MessageManager.getString("action.load"));
 
     int value = chooser.showOpenDialog(this);
@@ -741,12 +858,10 @@ public class FeatureSettings extends JPanel
 
       try
       {
-        InputStreamReader in = new InputStreamReader(new FileInputStream(
-                file), "UTF-8");
+        InputStreamReader in = new InputStreamReader(
+                new FileInputStream(file), "UTF-8");
 
-        jalview.schemabinding.version2.JalviewUserColours jucs = new jalview.schemabinding.version2.JalviewUserColours();
-        jucs = jucs
-                .unmarshal(in);
+        JalviewUserColours jucs = JalviewUserColours.unmarshal(in);
 
         for (int i = jucs.getColourCount() - 1; i >= 0; i--)
         {
@@ -765,7 +880,7 @@ public class FeatureSettings extends JPanel
               Cache.log.warn("Couldn't parse out graduated feature color.",
                       e);
             }
-            GraduatedColor gcol = new GraduatedColor(mincol, maxcol,
+            FeatureColourI gcol = new FeatureColour(mincol, maxcol,
                     newcol.getMin(), newcol.getMax());
             if (newcol.hasAutoScale())
             {
@@ -777,31 +892,28 @@ public class FeatureSettings extends JPanel
             }
             if (newcol.hasThreshold())
             {
-              gcol.setThresh(newcol.getThreshold());
-              gcol.setThreshType(AnnotationColourGradient.NO_THRESHOLD); // default
+              gcol.setThreshold(newcol.getThreshold());
             }
             if (newcol.getThreshType().length() > 0)
             {
               String ttyp = newcol.getThreshType();
-              if (ttyp.equalsIgnoreCase("NONE"))
-              {
-                gcol.setThreshType(AnnotationColourGradient.NO_THRESHOLD);
-              }
               if (ttyp.equalsIgnoreCase("ABOVE"))
               {
-                gcol.setThreshType(AnnotationColourGradient.ABOVE_THRESHOLD);
+                gcol.setAboveThreshold(true);
               }
               if (ttyp.equalsIgnoreCase("BELOW"))
               {
-                gcol.setThreshType(AnnotationColourGradient.BELOW_THRESHOLD);
+                gcol.setBelowThreshold(true);
               }
             }
             fr.setColour(name = newcol.getName(), gcol);
           }
           else
           {
-            fr.setColour(name = jucs.getColour(i).getName(), new Color(
-                    Integer.parseInt(jucs.getColour(i).getRGB(), 16)));
+            Color color = new Color(
+                    Integer.parseInt(jucs.getColour(i).getRGB(), 16));
+            fr.setColour(name = jucs.getColour(i).getName(),
+                    new FeatureColour(color));
           }
           fr.setOrder(name, (i == 0) ? 0 : i / jucs.getColourCount());
         }
@@ -823,12 +935,11 @@ public class FeatureSettings extends JPanel
 
   void save()
   {
-    JalviewFileChooser chooser = new JalviewFileChooser(
-            jalview.bin.Cache.getProperty("LAST_DIRECTORY"), new String[]
-            { "fc" }, new String[]
-            { "Sequence Feature Colours" }, "Sequence Feature Colours");
-    chooser.setFileView(new jalview.io.JalviewFileView());
-    chooser.setDialogTitle(MessageManager.getString("label.save_feature_colours"));
+    JalviewFileChooser chooser = new JalviewFileChooser("fc",
+            "Sequence Feature Colours");
+    chooser.setFileView(new JalviewFileView());
+    chooser.setDialogTitle(
+            MessageManager.getString("label.save_feature_colours"));
     chooser.setToolTipText(MessageManager.getString("action.save"));
 
     int value = chooser.showSaveDialog(this);
@@ -843,49 +954,40 @@ public class FeatureSettings extends JPanel
         PrintWriter out = new PrintWriter(new OutputStreamWriter(
                 new FileOutputStream(choice), "UTF-8"));
 
-        Iterator e = fr.featureColours.keySet().iterator();
-        float[] sortOrder = new float[fr.featureColours.size()];
-        String[] sortTypes = new String[fr.featureColours.size()];
+        Set<String> fr_colours = fr.getAllFeatureColours();
+        Iterator<String> e = fr_colours.iterator();
+        float[] sortOrder = new float[fr_colours.size()];
+        String[] sortTypes = new String[fr_colours.size()];
         int i = 0;
         while (e.hasNext())
         {
-          sortTypes[i] = e.next().toString();
+          sortTypes[i] = e.next();
           sortOrder[i] = fr.getOrder(sortTypes[i]);
           i++;
         }
-        jalview.util.QuickSort.sort(sortOrder, sortTypes);
+        QuickSort.sort(sortOrder, sortTypes);
         sortOrder = null;
-        Object fcol;
-        GraduatedColor gcol;
         for (i = 0; i < sortTypes.length; i++)
         {
           jalview.schemabinding.version2.Colour col = new jalview.schemabinding.version2.Colour();
           col.setName(sortTypes[i]);
-          col.setRGB(jalview.util.Format.getHexString(fr.getColour(col
-                  .getName())));
-          fcol = fr.getFeatureStyle(sortTypes[i]);
-          if (fcol instanceof GraduatedColor)
+          FeatureColourI fcol = fr.getFeatureStyle(sortTypes[i]);
+          if (fcol.isSimpleColour())
           {
-            gcol = (GraduatedColor) fcol;
-            col.setMin(gcol.getMin());
-            col.setMax(gcol.getMax());
-            col.setMinRGB(jalview.util.Format.getHexString(gcol
-                    .getMinColor()));
-            col.setAutoScale(gcol.isAutoScale());
-            col.setThreshold(gcol.getThresh());
-            col.setColourByLabel(gcol.isColourByLabel());
-            switch (gcol.getThreshType())
-            {
-            case AnnotationColourGradient.NO_THRESHOLD:
-              col.setThreshType("NONE");
-              break;
-            case AnnotationColourGradient.ABOVE_THRESHOLD:
-              col.setThreshType("ABOVE");
-              break;
-            case AnnotationColourGradient.BELOW_THRESHOLD:
-              col.setThreshType("BELOW");
-              break;
-            }
+            col.setRGB(Format.getHexString(fcol.getColour()));
+          }
+          else
+          {
+            col.setRGB(Format.getHexString(fcol.getMaxColour()));
+            col.setMin(fcol.getMin());
+            col.setMax(fcol.getMax());
+            col.setMinRGB(
+                    jalview.util.Format.getHexString(fcol.getMinColour()));
+            col.setAutoScale(fcol.isAutoScaled());
+            col.setThreshold(fcol.getThreshold());
+            col.setColourByLabel(fcol.isColourByLabel());
+            col.setThreshType(fcol.isAboveThreshold() ? "ABOVE"
+                    : (fcol.isBelowThreshold() ? "BELOW" : "NONE"));
           }
           ucs.addColour(col);
         }
@@ -921,7 +1023,7 @@ public class FeatureSettings extends JPanel
     int num = 0;
     for (int i = 0; i < data.length; i++)
     {
-      awidth = (float[]) typeWidth.get(data[i][0]);
+      awidth = typeWidth.get(data[i][0]);
       if (awidth[0] > 0)
       {
         width[i] = awidth[1] / awidth[0];// *awidth[0]*awidth[2]; - better
@@ -962,9 +1064,9 @@ public class FeatureSettings extends JPanel
       }
     }
     if (sort)
-     {
+    {
       jalview.util.QuickSort.sort(width, data);
-    // update global priority order
+      // update global priority order
     }
 
     updateFeatureRenderer(data, false);
@@ -987,104 +1089,106 @@ public class FeatureSettings extends JPanel
     updateFeatureRenderer(data, true);
   }
 
+  /**
+   * Update the priority order of features; only repaint if this changed the
+   * order of visible features
+   * 
+   * @param data
+   * @param visibleNew
+   */
   private void updateFeatureRenderer(Object[][] data, boolean visibleNew)
   {
-    fr.setFeaturePriority(data, visibleNew);
-    af.alignPanel.paintAlignment(true);
+    if (fr.setFeaturePriority(data, visibleNew))
+    {
+      af.alignPanel.paintAlignment(true, true);
+    }
   }
 
-  int selectedRow = -1;
-
-  JTabbedPane tabbedPane = new JTabbedPane();
-
-  BorderLayout borderLayout1 = new BorderLayout();
-
-  BorderLayout borderLayout2 = new BorderLayout();
-
-  BorderLayout borderLayout3 = new BorderLayout();
-
-  JPanel bigPanel = new JPanel();
-
-  BorderLayout borderLayout4 = new BorderLayout();
-
-  JButton invert = new JButton();
-
-  JPanel buttonPanel = new JPanel();
-
-  JButton cancel = new JButton();
-
-  JButton ok = new JButton();
-
-  JButton loadColours = new JButton();
-
-  JButton saveColours = new JButton();
-
-  JPanel dasButtonPanel = new JPanel();
-
-  JButton fetchDAS = new JButton();
-
-  JButton saveDAS = new JButton();
-
-  JButton cancelDAS = new JButton();
+  private void jbInit() throws Exception
+  {
+    this.setLayout(new BorderLayout());
 
-  JButton optimizeOrder = new JButton();
+    JPanel settingsPane = new JPanel();
+    settingsPane.setLayout(new BorderLayout());
 
-  JButton sortByScore = new JButton();
+    filtersPane = new JPanel();
 
-  JButton sortByDens = new JButton();
+    dasSettingsPane.setLayout(new BorderLayout());
 
-  JButton help = new JButton();
+    JPanel bigPanel = new JPanel();
+    bigPanel.setLayout(new BorderLayout());
 
-  JPanel transbuttons = new JPanel(new GridLayout(5, 1));
+    groupPanel = new JPanel();
+    bigPanel.add(groupPanel, BorderLayout.NORTH);
 
-  private void jbInit() throws Exception
-  {
-    this.setLayout(borderLayout1);
-    settingsPane.setLayout(borderLayout2);
-    dasSettingsPane.setLayout(borderLayout3);
-    bigPanel.setLayout(borderLayout4);
+    JButton invert = new JButton(
+            MessageManager.getString("label.invert_selection"));
     invert.setFont(JvSwingUtils.getLabelFont());
-    invert.setText(MessageManager.getString("label.invert_selection"));
     invert.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         invertSelection();
       }
     });
+
+    JButton optimizeOrder = new JButton(
+            MessageManager.getString("label.optimise_order"));
     optimizeOrder.setFont(JvSwingUtils.getLabelFont());
-    optimizeOrder.setText(MessageManager.getString("label.optimise_order"));
     optimizeOrder.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         orderByAvWidth();
       }
     });
+
+    JButton sortByScore = new JButton(
+            MessageManager.getString("label.seq_sort_by_score"));
     sortByScore.setFont(JvSwingUtils.getLabelFont());
-    sortByScore
-            .setText(MessageManager.getString("label.seq_sort_by_score"));
     sortByScore.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        sortByScore(null);
+        af.avc.sortAlignmentByFeatureScore(null);
       }
     });
+    JButton sortByDens = new JButton(
+            MessageManager.getString("label.sequence_sort_by_density"));
     sortByDens.setFont(JvSwingUtils.getLabelFont());
-    sortByDens.setText(MessageManager
-            .getString("label.sequence_sort_by_density"));
     sortByDens.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        sortByDens(null);
+        af.avc.sortAlignmentByFeatureDensity(null);
+      }
+    });
+
+    JButton help = new JButton(MessageManager.getString("action.help"));
+    help.setFont(JvSwingUtils.getLabelFont());
+    help.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        try
+        {
+          Help.showHelpWindow(HelpId.SequenceFeatureSettings);
+        } catch (HelpSetException e1)
+        {
+          e1.printStackTrace();
+        }
       }
     });
     help.setFont(JvSwingUtils.getLabelFont());
     help.setText(MessageManager.getString("action.help"));
     help.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         try
@@ -1096,38 +1200,50 @@ public class FeatureSettings extends JPanel
         }
       }
     });
+
+    JButton cancel = new JButton(MessageManager.getString("action.cancel"));
     cancel.setFont(JvSwingUtils.getLabelFont());
-    cancel.setText(MessageManager.getString("action.cancel"));
     cancel.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
+        fr.setTransparency(originalTransparency);
+        fr.setFeatureFilters(originalFilters);
         updateFeatureRenderer(originalData);
         close();
       }
     });
+
+    JButton ok = new JButton(MessageManager.getString("action.ok"));
     ok.setFont(JvSwingUtils.getLabelFont());
-    ok.setText(MessageManager.getString("action.ok"));
     ok.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         close();
       }
     });
+
+    JButton loadColours = new JButton(
+            MessageManager.getString("label.load_colours"));
     loadColours.setFont(JvSwingUtils.getLabelFont());
-    loadColours.setText(MessageManager.getString("label.load_colours"));
     loadColours.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         load();
       }
     });
+
+    JButton saveColours = new JButton(
+            MessageManager.getString("label.save_colours"));
     saveColours.setFont(JvSwingUtils.getLabelFont());
-    saveColours.setText(MessageManager.getString("label.save_colours"));
     saveColours.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         save();
@@ -1135,19 +1251,24 @@ public class FeatureSettings extends JPanel
     });
     transparency.addChangeListener(new ChangeListener()
     {
+      @Override
       public void stateChanged(ChangeEvent evt)
       {
-        fr.setTransparency((100 - transparency.getValue()) / 100f);
-        af.alignPanel.paintAlignment(true);
+        if (!inConstruction)
+        {
+          fr.setTransparency((100 - transparency.getValue()) / 100f);
+          af.alignPanel.paintAlignment(true,true);
+        }
       }
     });
 
     transparency.setMaximum(70);
-    transparency.setToolTipText(MessageManager
-            .getString("label.transparency_tip"));
+    transparency.setToolTipText(
+            MessageManager.getString("label.transparency_tip"));
     fetchDAS.setText(MessageManager.getString("label.fetch_das_features"));
     fetchDAS.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         fetchDAS_actionPerformed(e);
@@ -1156,171 +1277,499 @@ public class FeatureSettings extends JPanel
     saveDAS.setText(MessageManager.getString("action.save_as_default"));
     saveDAS.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         saveDAS_actionPerformed(e);
       }
     });
+
+    JPanel dasButtonPanel = new JPanel();
     dasButtonPanel.setBorder(BorderFactory.createEtchedBorder());
     dasSettingsPane.setBorder(null);
     cancelDAS.setEnabled(false);
     cancelDAS.setText(MessageManager.getString("action.cancel_fetch"));
     cancelDAS.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         cancelDAS_actionPerformed(e);
       }
     });
-    this.add(tabbedPane, java.awt.BorderLayout.CENTER);
-    tabbedPane.addTab(MessageManager.getString("label.feature_settings"), settingsPane);
-    tabbedPane.addTab(MessageManager.getString("label.das_settings"), dasSettingsPane);
-    bigPanel.add(transPanel, java.awt.BorderLayout.SOUTH);
+
+    JTabbedPane tabbedPane = new JTabbedPane();
+    this.add(tabbedPane, BorderLayout.CENTER);
+    tabbedPane.addTab(MessageManager.getString("label.feature_settings"),
+            settingsPane);
+    tabbedPane.addTab(MessageManager.getString("label.filters"),
+            filtersPane);
+    // tabbedPane.addTab(MessageManager.getString("label.das_settings"),
+    // dasSettingsPane);
+
+    JPanel transPanel = new JPanel(new GridLayout(1, 2));
+    bigPanel.add(transPanel, BorderLayout.SOUTH);
+
+    JPanel transbuttons = new JPanel(new GridLayout(5, 1));
     transbuttons.add(optimizeOrder);
     transbuttons.add(invert);
     transbuttons.add(sortByScore);
     transbuttons.add(sortByDens);
     transbuttons.add(help);
-    JPanel sliderPanel = new JPanel();
-    sliderPanel.add(transparency);
     transPanel.add(transparency);
     transPanel.add(transbuttons);
+
+    JPanel buttonPanel = new JPanel();
     buttonPanel.add(ok);
     buttonPanel.add(cancel);
     buttonPanel.add(loadColours);
     buttonPanel.add(saveColours);
-    bigPanel.add(scrollPane, java.awt.BorderLayout.CENTER);
-    dasSettingsPane.add(dasButtonPanel, java.awt.BorderLayout.SOUTH);
+    bigPanel.add(scrollPane, BorderLayout.CENTER);
+    dasSettingsPane.add(dasButtonPanel, BorderLayout.SOUTH);
     dasButtonPanel.add(fetchDAS);
     dasButtonPanel.add(cancelDAS);
     dasButtonPanel.add(saveDAS);
-    settingsPane.add(bigPanel, java.awt.BorderLayout.CENTER);
-    settingsPane.add(buttonPanel, java.awt.BorderLayout.SOUTH);
-  }
+    settingsPane.add(bigPanel, BorderLayout.CENTER);
+    settingsPane.add(buttonPanel, BorderLayout.SOUTH);
 
-  protected void sortByDens(String[] typ)
-  {
-    sortBy(typ, "Sort by Density", AlignmentSorter.FEATURE_DENSITY);
+    initFiltersTab();
   }
 
-  protected void sortBy(String[] typ, String methodText, final String method)
+  /**
+   * Populates initial layout of the feature attribute filters panel
+   */
+  protected void initFiltersTab()
   {
-    if (typ == null)
-    {
-      typ = getDisplayedFeatureTypes();
-    }
-    String gps[] = null;
-    gps = getDisplayedFeatureGroups();
-    if (typ != null)
+    filters = new ArrayList<>();
+
+    /*
+     * choose feature type
+     */
+    JPanel chooseTypePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    chooseTypePanel.setBackground(Color.white);
+    JvSwingUtils.createItalicTitledBorder(chooseTypePanel,
+            MessageManager.getString("label.feature_type"), true);
+    filteredFeatureChoice = new JComboBox<>();
+    filteredFeatureChoice.addItemListener(new ItemListener()
     {
-      ArrayList types = new ArrayList();
-      for (int i = 0; i < typ.length; i++)
+      @Override
+      public void itemStateChanged(ItemEvent e)
       {
-        if (typ[i] != null)
-        {
-          types.add(typ[i]);
-        }
-        typ = new String[types.size()];
-        types.toArray(typ);
+        refreshFiltersDisplay();
       }
-    }
-    if (gps != null)
+    });
+    chooseTypePanel.add(new JLabel(MessageManager
+            .getString("label.feature_to_filter")));
+    chooseTypePanel.add(filteredFeatureChoice);
+    populateFilterableFeatures();
+
+    /*
+     * the panel with the filters for the selected feature type
+     */
+    JPanel filtersPanel = new JPanel();
+    filtersPanel.setLayout(new BoxLayout(filtersPanel, BoxLayout.Y_AXIS));
+    filtersPanel.setBackground(Color.white);
+    JvSwingUtils.createItalicTitledBorder(filtersPanel,
+            MessageManager.getString("label.filters"), true);
+
+    /*
+     * add AND or OR radio buttons
+     */
+    JPanel andOrPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    andOrPanel.setBackground(Color.white);
+    andOrPanel.setBorder(BorderFactory.createLineBorder(debugBorderColour));
+    andFilters = new JRadioButton("And");
+    orFilters = new JRadioButton("Or");
+    ActionListener actionListener = new ActionListener()
     {
-      ArrayList grps = new ArrayList();
-
-      for (int i = 0; i < gps.length; i++)
+      @Override
+      public void actionPerformed(ActionEvent e)
       {
-        if (gps[i] != null)
-        {
-          grps.add(gps[i]);
-        }
+        filtersChanged();
       }
-      gps = new String[grps.size()];
-      grps.toArray(gps);
-    }
-    AlignmentPanel alignPanel = af.alignPanel;
-    AlignmentI al = alignPanel.av.getAlignment();
+    };
+    andFilters.addActionListener(actionListener);
+    orFilters.addActionListener(actionListener);
+    ButtonGroup andOr = new ButtonGroup();
+    andOr.add(andFilters);
+    andOr.add(orFilters);
+    andFilters.setSelected(true);
+    andOrPanel.add(new JLabel(MessageManager
+            .getString("label.join_conditions")));
+    andOrPanel.add(andFilters);
+    andOrPanel.add(orFilters);
+    filtersPanel.add(andOrPanel);
+
+    /*
+     * panel with filters - populated by refreshFiltersDisplay
+     */
+    chooseFiltersPanel = new JPanel();
+    LayoutManager box = new BoxLayout(chooseFiltersPanel,
+            BoxLayout.Y_AXIS);
+    chooseFiltersPanel.setLayout(box);
+    filtersPanel.add(chooseFiltersPanel);
+
+    /*
+     * a read-only text view of the current filters
+     */
+    JPanel showFiltersPanel = new JPanel(new BorderLayout(5, 5));
+    showFiltersPanel.setBackground(Color.white);
+    JvSwingUtils.createItalicTitledBorder(showFiltersPanel,
+            MessageManager.getString("label.match_condition"), true);
+    filtersAsText = new JTextArea();
+    filtersAsText.setLineWrap(true);
+    filtersAsText.setWrapStyleWord(true);
+    filtersAsText.setEnabled(false); // for display only
+    showFiltersPanel.add(filtersAsText);
+
+    filtersPane.setLayout(new BorderLayout());
+    filtersPane.add(chooseTypePanel, BorderLayout.NORTH);
+    filtersPane.add(filtersPanel, BorderLayout.CENTER);
+    filtersPane.add(showFiltersPanel, BorderLayout.SOUTH);
+
+    /*
+     * update display for initial feature type selection
+     */
+    refreshFiltersDisplay();
+  }
+
+  /**
+   * Adds entries to the 'choose feature to filter' drop-down choice. Only
+   * feature types which have known attributes (so can be filtered) are
+   * included, so recall this method to update the list (check for newly added
+   * attributes).
+   */
+  protected void populateFilterableFeatures()
+  {
+    /*
+     * suppress action handler while updating the list
+     */
+    ItemListener listener = filteredFeatureChoice.getItemListeners()[0];
+    filteredFeatureChoice.removeItemListener(listener);
+
+    filteredFeatureChoice.removeAllItems();
+    ReverseListIterator<String> types = new ReverseListIterator<>(
+            fr.getRenderOrder());
 
-    int start, stop;
-    SequenceGroup sg = alignPanel.av.getSelectionGroup();
-    if (sg != null)
+    boolean found = false;
+    while (types.hasNext())
     {
-      start = sg.getStartRes();
-      stop = sg.getEndRes();
+      String type = types.next();
+      if (FeatureAttributes.getInstance().hasAttributes(type))
+      {
+        filteredFeatureChoice.addItem(type);
+        found = true;
+      }
     }
-    else
+    if (!found)
     {
-      start = 0;
-      stop = al.getWidth();
+      filteredFeatureChoice.addItem(MessageManager
+              .getString("label.no_feature_attributes"));
+      filteredFeatureChoice.setEnabled(false);
     }
-    SequenceI[] oldOrder = al.getSequencesArray();
-    AlignmentSorter.sortByFeature(typ, gps, start, stop, al, method);
-    af.addHistoryItem(new OrderCommand(methodText, oldOrder, alignPanel.av
-            .getAlignment()));
-    alignPanel.paintAlignment(true);
 
+    filteredFeatureChoice.addItemListener(listener);
   }
 
-  protected void sortByScore(String[] typ)
+  /**
+   * Refreshes the display to show any filters currently configured for the
+   * selected feature type (editable, with 'remove' option), plus one extra row
+   * for adding a condition. This should be called on change of selected feature
+   * type, or after a filter has been removed, added or amended.
+   */
+  protected void refreshFiltersDisplay()
   {
-    sortBy(typ, "Sort by Feature Score", AlignmentSorter.FEATURE_SCORE);
+    /*
+     * clear the panel and list of filter conditions
+     */
+    chooseFiltersPanel.removeAll();
+    filters.clear();
+
+    /*
+     * look up attributes known for feature type
+     */
+    String selectedType = (String) filteredFeatureChoice.getSelectedItem();
+    List<String[]> attNames = FeatureAttributes.getInstance()
+            .getAttributes(selectedType);
+
+    /*
+     * if this feature type has filters set, load them first
+     */
+    KeyedMatcherSetI featureFilters = fr.getFeatureFilter(selectedType);
+    filtersAsText.setText("");
+    if (featureFilters != null)
+    {
+      filtersAsText.setText(featureFilters.toString());
+      if (!featureFilters.isAnded())
+      {
+        orFilters.setSelected(true);
+      }
+      featureFilters.getMatchers().forEach(matcher -> filters.add(matcher));
+    }
+
+    /*
+     * and an empty filter for the user to populate (add)
+     */
+    KeyedMatcherI noFilter = new KeyedMatcher(Condition.values()[0], "",
+            (String) null);
+    filters.add(noFilter);
+
+    /*
+     * render the conditions in rows, each in its own JPanel
+     */
+    int filterIndex = 0;
+    for (KeyedMatcherI filter : filters)
+    {
+      String[] attName = filter.getKey();
+      Condition condition = filter.getMatcher()
+              .getCondition();
+      String pattern = filter.getMatcher().getPattern();
+      JPanel row = addFilter(attName, attNames, condition, pattern, filterIndex);
+      row.setBorder(BorderFactory.createLineBorder(debugBorderColour));
+      chooseFiltersPanel.add(row);
+      filterIndex++;
+    }
+    // chooseFiltersPanel.add(Box.createVerticalGlue());
+
+    filtersPane.validate();
+    filtersPane.repaint();
   }
 
-  private String[] getDisplayedFeatureTypes()
+  /**
+   * A helper method that constructs a panel with one filter condition:
+   * <ul>
+   * <li>a drop-down list of attribute names to choose from</li>
+   * <li>a drop-down list of conditions to choose from</li>
+   * <li>a text field for input of a match pattern</li>
+   * <li>optionally, a 'remove' button</li>
+   * </ul>
+   * If attribute, condition or pattern are not null, they are set as defaults for
+   * the input fields. The 'remove' button is added unless the pattern is null or
+   * empty (incomplete filter condition).
+   * 
+   * @param attName
+   * @param attNames
+   * @param cond
+   * @param pattern
+   * @param filterIndex
+   * @return
+   */
+  protected JPanel addFilter(String[] attName, List<String[]> attNames,
+          Condition cond, String pattern, int filterIndex)
   {
-    String[] typ = null;
-    if (fr != null)
+    JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    filterRow.setBackground(Color.white);
+
+    /*
+     * 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);
+
+    /*
+     * action handlers that validate and (if valid) apply changes
+     */
+    ActionListener actionListener = new ActionListener()
     {
-      synchronized (fr.renderOrder)
+      @Override
+      public void actionPerformed(ActionEvent e)
       {
-        typ = new String[fr.renderOrder.length];
-        System.arraycopy(fr.renderOrder, 0, typ, 0, typ.length);
-        for (int i = 0; i < typ.length; i++)
+        if (attCombo.getSelectedItem() != null)
         {
-          if (af.viewport.getFeaturesDisplayed().get(typ[i]) == null)
+          if (validateFilter(patternField, condCombo))
           {
-            typ[i] = null;
+            updateFilter(attCombo, condCombo, patternField, filterIndex);
+            filtersChanged();
           }
         }
       }
+    };
+    ItemListener itemListener = new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        actionListener.actionPerformed(null);
+      }
+    };
+
+    if (attName == null) // the 'add a condition' row
+    {
+      attCombo.setSelectedItem(null);
     }
-    return typ;
-  }
+    else
+    {
+      attCombo.setSelectedItem(String.join(COLON, attName));
+    }
+    attCombo.addItemListener(itemListener);
 
-  private String[] getDisplayedFeatureGroups()
-  {
-    String[] gps = null;
-    ArrayList<String> _gps = new ArrayList<String>();
-    if (fr != null)
+    filterRow.add(attCombo);
+
+    /*
+     * drop-down choice of test condition
+     */
+    for (Condition c : Condition.values())
     {
+      condCombo.addItem(c);
+    }
+    if (cond != null)
+    {
+      condCombo.setSelectedItem(cond);
+    }
+    condCombo.addItemListener(itemListener);
+    filterRow.add(condCombo);
 
-      if (fr.featureGroups != null)
+    /*
+     * pattern to match against
+     */
+    patternField.setText(pattern);
+    patternField.addActionListener(actionListener);
+    patternField.addFocusListener(new FocusAdapter()
+    {
+      @Override
+      public void focusLost(FocusEvent e)
       {
-        Iterator en = fr.featureGroups.keySet().iterator();
-        int g = 0;
-        boolean valid = false;
-        while (en.hasNext())
+        actionListener.actionPerformed(null);
+      }
+    });
+    filterRow.add(patternField);
+
+    /*
+     * add remove button if filter is populated (non-empty pattern)
+     */
+    if (pattern != null && pattern.trim().length() > 0)
+    {
+      // todo: gif for button drawing '-' or 'x'
+      JButton removeCondition = new BasicArrowButton(SwingConstants.WEST);
+      removeCondition.setToolTipText(MessageManager
+              .getString("label.delete_row"));
+      removeCondition.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
         {
-          String gp = (String) en.next();
-          Boolean on = (Boolean) fr.featureGroups.get(gp);
-          if (on != null && on.booleanValue())
-          {
-            valid = true;
-            _gps.add(gp);
-          }
+          filters.remove(filterIndex);
+          filtersChanged();
         }
-        if (!valid)
+      });
+      filterRow.add(removeCondition);
+    }
+
+    return filterRow;
+  }
+
+  /**
+   * 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.
+   * 
+   * @param featureType
+   * @param attNames
+   */
+  protected JComboBox<String> populateAttributesDropdown(
+          String featureType, List<String[]> attNames)
+  {
+    List<String> displayNames = new ArrayList<>();
+    List<String> tooltips = new ArrayList<>();
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    for (String[] attName : attNames)
+    {
+      String desc = fa.getDescription(featureType, attName);
+      if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
+      {
+        desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
+      }
+      displayNames.add(String.join(COLON, attName));
+      tooltips.add(desc == null ? "" : desc);
+    }
+
+    JComboBox<String> attCombo = JvSwingUtils.buildComboWithTooltips(
+            displayNames, tooltips);
+    if (attNames.isEmpty())
+    {
+      attCombo.setToolTipText(MessageManager
+              .getString("label.no_attributes"));
+    }
+    return attCombo;
+  }
+
+  /**
+   * Action on any change to feature filtering, namely
+   * <ul>
+   * <li>change of selected attribute</li>
+   * <li>change of selected condition</li>
+   * <li>change of match pattern</li>
+   * <li>removal of a condition</li>
+   * </ul>
+   * The action should be to
+   * <ul>
+   * <li>parse and validate the filters</li>
+   * <li>if valid, update the filter text box</li>
+   * <li>and apply the filters to the viewport</li>
+   * </ul>
+   */
+  protected void filtersChanged()
+  {
+    /*
+     * update the filter conditions for the feature type
+     */
+    String featureType = (String) filteredFeatureChoice.getSelectedItem();
+    boolean anded = andFilters.isSelected();
+    KeyedMatcherSetI combined = new KeyedMatcherSet();
+
+    for (KeyedMatcherI filter : filters)
+    {
+      String pattern = filter.getMatcher().getPattern();
+      if (pattern.trim().length() > 0)
+      {
+        if (anded)
         {
-          return null;
+          combined.and(filter);
         }
         else
         {
-          gps = new String[_gps.size()];
-          _gps.toArray(gps);
+          combined.or(filter);
         }
       }
     }
-    return gps;
+
+    /*
+     * save the filter conditions in the FeatureRenderer
+     * (note this might now be an empty filter with no conditions)
+     */
+    fr.setFeatureFilter(featureType, combined);
+
+    filtersAsText.setText(combined.toString());
+
+    refreshFiltersDisplay();
+
+    af.alignPanel.paintAlignment(true, true);
+  }
+
+  /**
+   * Constructs a filter condition from the given input fields, and replaces the
+   * condition at filterIndex with the new one
+   * 
+   * @param attCombo
+   * @param condCombo
+   * @param valueField
+   * @param filterIndex
+   */
+  protected void updateFilter(JComboBox<String> attCombo,
+          JComboBox<Condition> condCombo, JTextField valueField,
+          int filterIndex)
+  {
+    String attName = (String) attCombo.getSelectedItem();
+    Condition cond = (Condition) condCombo.getSelectedItem();
+    String pattern = valueField.getText();
+    KeyedMatcherI km = new KeyedMatcher(cond, pattern,
+            attName.split(COLON));
+
+    filters.set(filterIndex, km);
   }
 
   public void fetchDAS_actionPerformed(ActionEvent e)
@@ -1328,7 +1777,8 @@ public class FeatureSettings extends JPanel
     fetchDAS.setEnabled(false);
     cancelDAS.setEnabled(true);
     dassourceBrowser.setGuiEnabled(false);
-    Vector selectedSources = dassourceBrowser.getSelectedSources();
+    Vector<jalviewSourceI> selectedSources = dassourceBrowser
+            .getSelectedSources();
     doDasFeatureFetch(selectedSources, true, true);
   }
 
@@ -1344,7 +1794,7 @@ public class FeatureSettings extends JPanel
   {
     SequenceI[] dataset, seqs;
     int iSize;
-    AlignViewport vp = af.getViewport();
+    AlignmentViewport vp = af.getViewport();
     if (vp.getSelectionGroup() != null
             && vp.getSelectionGroup().getSize() > 0)
     {
@@ -1387,7 +1837,7 @@ public class FeatureSettings extends JPanel
    *          Vector of Strings to resolve to DAS source nicknames.
    * @return sources that are present in source list.
    */
-  public List<jalviewSourceI> resolveSourceNicknames(Vector sources)
+  public List<jalviewSourceI> resolveSourceNicknames(Vector<String> sources)
   {
     return dassourceBrowser.sourceRegistry.resolveSourceNicknames(sources);
   }
@@ -1398,7 +1848,7 @@ public class FeatureSettings extends JPanel
    * 
    * @return vector of selected das source nicknames
    */
-  public Vector getSelectedSources()
+  public Vector<jalviewSourceI> getSelectedSources()
   {
     return dassourceBrowser.getSelectedSources();
   }
@@ -1412,7 +1862,7 @@ public class FeatureSettings extends JPanel
    *          if true then runs in same thread, otherwise passes to the Swing
    *          executor
    */
-  public void fetchDasFeatures(Vector sources, boolean block)
+  public void fetchDasFeatures(Vector<String> sources, boolean block)
   {
     initDasSources();
     List<jalviewSourceI> resolved = dassourceBrowser.sourceRegistry
@@ -1429,6 +1879,7 @@ public class FeatureSettings extends JPanel
       Runnable fetcher = new Runnable()
       {
 
+        @Override
         public void run()
         {
           doDasFeatureFetch(dassources, true, false);
@@ -1472,15 +1923,55 @@ public class FeatureSettings extends JPanel
   public void noDasSourceActive()
   {
     complete();
-    JOptionPane
-            .showInternalConfirmDialog(
-                    Desktop.desktop,
-                    MessageManager
-                            .getString("label.no_das_sources_selected_warn"),
-                    MessageManager
-                            .getString("label.no_das_sources_selected_title"),
-                    JOptionPane.DEFAULT_OPTION,
-                    JOptionPane.INFORMATION_MESSAGE);
+    JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
+            MessageManager.getString("label.no_das_sources_selected_warn"),
+            MessageManager.getString("label.no_das_sources_selected_title"),
+            JvOptionPane.DEFAULT_OPTION, JvOptionPane.INFORMATION_MESSAGE);
+  }
+
+  /**
+   * Answers true unless a numeric condition has been selected with a
+   * non-numeric value. Sets the value field to RED with a tooltip if in error.
+   * <p>
+   * If the pattern entered is empty, this method returns false, but does not
+   * mark the field as invalid. This supports selecting an attribute for a new
+   * condition before a match pattern has been entered.
+   * 
+   * @param value
+   * @param condCombo
+   */
+  protected boolean validateFilter(JTextField value,
+          JComboBox<Condition> condCombo)
+  {
+    if (value == null || condCombo == null)
+    {
+      return true; // fields not populated
+    }
+  
+    Condition cond = (Condition) condCombo.getSelectedItem();
+    value.setBackground(Color.white);
+    value.setToolTipText("");
+    String v1 = value.getText().trim();
+    if (v1.length() == 0)
+    {
+      return false;
+    }
+
+    if (cond.isNumeric())
+    {
+      try
+      {
+        Float.valueOf(v1);
+      } catch (NumberFormatException e)
+      {
+        value.setBackground(Color.red);
+        value.setToolTipText(MessageManager
+                .getString("label.numeric_required"));
+        return false;
+      }
+    }
+  
+    return true;
   }
 
   // ///////////////////////////////////////////////////////////////////////
@@ -1493,8 +1984,10 @@ public class FeatureSettings extends JPanel
       this.data = data;
     }
 
-    private String[] columnNames =
-    { MessageManager.getString("label.feature_type"), MessageManager.getString("action.colour"), MessageManager.getString("label.display") };
+    private String[] columnNames = {
+        MessageManager.getString("label.feature_type"),
+        MessageManager.getString("action.colour"),
+        MessageManager.getString("label.display") };
 
     private Object[][] data;
 
@@ -1508,6 +2001,7 @@ public class FeatureSettings extends JPanel
       this.data = data;
     }
 
+    @Override
     public int getColumnCount()
     {
       return columnNames.length;
@@ -1518,31 +2012,37 @@ public class FeatureSettings extends JPanel
       return data[row];
     }
 
+    @Override
     public int getRowCount()
     {
       return data.length;
     }
 
+    @Override
     public String getColumnName(int col)
     {
       return columnNames[col];
     }
 
+    @Override
     public Object getValueAt(int row, int col)
     {
       return data[row][col];
     }
 
+    @Override
     public Class getColumnClass(int c)
     {
       return getValueAt(0, c).getClass();
     }
 
+    @Override
     public boolean isCellEditable(int row, int col)
     {
       return col == 0 ? false : true;
     }
 
+    @Override
     public void setValueAt(Object value, int row, int col)
     {
       data[row][col] = value;
@@ -1567,43 +2067,33 @@ public class FeatureSettings extends JPanel
       setVerticalTextPosition(SwingConstants.CENTER);
     }
 
-    public Component getTableCellRendererComponent(JTable table,
-            Object color, boolean isSelected, boolean hasFocus, int row,
-            int column)
+    @Override
+    public Component getTableCellRendererComponent(JTable tbl, Object color,
+            boolean isSelected, boolean hasFocus, int row, int column)
     {
-      // JLabel comp = new JLabel();
-      // comp.
+      FeatureColourI cellColour = (FeatureColourI) color;
       setOpaque(true);
-      // comp.
-      // setBounds(getBounds());
-      Color newColor;
       setToolTipText(baseTT);
-      setBackground(table.getBackground());
-      if (color instanceof GraduatedColor)
+      setBackground(tbl.getBackground());
+      if (!cellColour.isSimpleColour())
       {
-        Rectangle cr = table.getCellRect(row, column, false);
-        FeatureSettings.renderGraduatedColor(this, (GraduatedColor) color,
+        Rectangle cr = tbl.getCellRect(row, column, false);
+        FeatureSettings.renderGraduatedColor(this, cellColour,
                 (int) cr.getWidth(), (int) cr.getHeight());
-
       }
       else
       {
         this.setText("");
         this.setIcon(null);
-        newColor = (Color) color;
-        // comp.
-        setBackground(newColor);
-        // comp.setToolTipText("RGB value: " + newColor.getRed() + ", "
-        // + newColor.getGreen() + ", " + newColor.getBlue());
+        setBackground(cellColour.getColour());
       }
       if (isSelected)
       {
         if (selectedBorder == null)
         {
           selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
-                  table.getSelectionBackground());
+                  tbl.getSelectionBackground());
         }
-        // comp.
         setBorder(selectedBorder);
       }
       else
@@ -1611,9 +2101,8 @@ public class FeatureSettings extends JPanel
         if (unselectedBorder == null)
         {
           unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
-                  table.getBackground());
+                  tbl.getBackground());
         }
-        // comp.
         setBorder(unselectedBorder);
       }
 
@@ -1627,7 +2116,7 @@ public class FeatureSettings extends JPanel
    * @param comp
    * @param gcol
    */
-  public static void renderGraduatedColor(JLabel comp, GraduatedColor gcol)
+  public static void renderGraduatedColor(JLabel comp, FeatureColourI gcol)
   {
     int w = comp.getWidth(), h = comp.getHeight();
     if (w < 20)
@@ -1643,37 +2132,52 @@ public class FeatureSettings extends JPanel
     renderGraduatedColor(comp, gcol, w, h);
   }
 
-  public static void renderGraduatedColor(JLabel comp, GraduatedColor gcol,
+  public static void renderGraduatedColor(JLabel comp, FeatureColourI gcol,
           int w, int h)
   {
     boolean thr = false;
-    String tt = "";
-    String tx = "";
-    if (gcol.getThreshType() == AnnotationColourGradient.ABOVE_THRESHOLD)
+    StringBuilder tt = new StringBuilder();
+    StringBuilder tx = new StringBuilder();
+
+    if (gcol.isColourByAttribute())
+    {
+      tx.append(String.join(":", gcol.getAttributeName()));
+    }
+    else if (!gcol.isColourByLabel())
+    {
+      tx.append(MessageManager.getString("label.score"));
+    }
+    tx.append(" ");
+    if (gcol.isAboveThreshold())
     {
       thr = true;
-      tx += ">";
-      tt += "Thresholded (Above " + gcol.getThresh() + ") ";
+      tx.append(">");
+      tt.append("Thresholded (Above ").append(gcol.getThreshold())
+              .append(") ");
     }
-    if (gcol.getThreshType() == AnnotationColourGradient.BELOW_THRESHOLD)
+    if (gcol.isBelowThreshold())
     {
       thr = true;
-      tx += "<";
-      tt += "Thresholded (Below " + gcol.getThresh() + ") ";
+      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
     {
-      Color newColor = gcol.getMaxColor();
+      Color newColor = gcol.getMaxColour();
       comp.setBackground(newColor);
       // System.err.println("Width is " + w / 2);
       Icon ficon = new FeatureIcon(gcol, comp.getBackground(), w, h, thr);
@@ -1684,16 +2188,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());
       }
     }
   }
@@ -1701,7 +2206,7 @@ public class FeatureSettings extends JPanel
 
 class FeatureIcon implements Icon
 {
-  GraduatedColor gcol;
+  FeatureColourI gcol;
 
   Color backg;
 
@@ -1713,7 +2218,7 @@ class FeatureIcon implements Icon
 
   Color mpcolour = Color.white;
 
-  FeatureIcon(GraduatedColor gfc, Color bg, int w, int h, boolean mspace)
+  FeatureIcon(FeatureColourI gfc, Color bg, int w, int h, boolean mspace)
   {
     gcol = gfc;
     backg = bg;
@@ -1732,16 +2237,19 @@ class FeatureIcon implements Icon
     }
   }
 
+  @Override
   public int getIconWidth()
   {
     return width;
   }
 
+  @Override
   public int getIconHeight()
   {
     return height;
   }
 
+  @Override
   public void paintIcon(Component c, Graphics g, int x, int y)
   {
 
@@ -1750,7 +2258,7 @@ class FeatureIcon implements Icon
       g.setColor(backg);
       g.fillRect(0, 0, width, height);
       // need an icon here.
-      g.setColor(gcol.getMaxColor());
+      g.setColor(gcol.getMaxColour());
 
       g.setFont(new Font("Verdana", Font.PLAIN, 9));
 
@@ -1764,7 +2272,7 @@ class FeatureIcon implements Icon
     }
     else
     {
-      Color minCol = gcol.getMinColor();
+      Color minCol = gcol.getMinColour();
       g.setColor(minCol);
       g.fillRect(0, 0, s1, height);
       if (midspace)
@@ -1772,25 +2280,23 @@ class FeatureIcon implements Icon
         g.setColor(Color.white);
         g.fillRect(s1, 0, e1 - s1, height);
       }
-      g.setColor(gcol.getMaxColor());
+      g.setColor(gcol.getMaxColour());
       g.fillRect(0, e1, width - e1, height);
     }
   }
 }
 
-class ColorEditor extends AbstractCellEditor implements TableCellEditor,
-        ActionListener
+class ColorEditor extends AbstractCellEditor
+        implements TableCellEditor, ActionListener
 {
   FeatureSettings me;
 
-  GraduatedColor currentGColor;
+  FeatureColourI currentColor;
 
   FeatureColourChooser chooser;
 
   String type;
 
-  Color currentColor;
-
   JButton button;
 
   JColorChooser colorChooser;
@@ -1814,7 +2320,8 @@ class ColorEditor extends AbstractCellEditor implements TableCellEditor,
     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
   }
@@ -1822,6 +2329,7 @@ class ColorEditor extends AbstractCellEditor implements TableCellEditor,
   /**
    * Handles events from the editor button and from the dialog's OK button.
    */
+  @Override
   public void actionPerformed(ActionEvent e)
   {
 
@@ -1829,11 +2337,11 @@ class ColorEditor extends AbstractCellEditor implements TableCellEditor,
     {
       // The user has clicked the cell, so
       // bring up the dialog.
-      if (currentColor != null)
+      if (currentColor.isSimpleColour())
       {
         // bring up simple color chooser
-        button.setBackground(currentColor);
-        colorChooser.setColor(currentColor);
+        button.setBackground(currentColor.getColour());
+        colorChooser.setColor(currentColor.getColour());
         dialog.setVisible(true);
       }
       else
@@ -1850,15 +2358,13 @@ class ColorEditor extends AbstractCellEditor implements TableCellEditor,
     }
     else
     { // User pressed dialog's "OK" button.
-      if (currentColor != null)
+      if (currentColor.isSimpleColour())
       {
-        currentColor = colorChooser.getColor();
+        currentColor = new FeatureColour(colorChooser.getColor());
       }
       else
       {
-        // class cast exceptions may be raised if the chooser created on a
-        // non-graduated color
-        currentGColor = (GraduatedColor) chooser.getLastColour();
+        currentColor = chooser.getLastColour();
       }
       me.table.setValueAt(getCellEditorValue(), selectedRow, 1);
       fireEditingStopped();
@@ -1867,31 +2373,27 @@ class ColorEditor extends AbstractCellEditor implements TableCellEditor,
   }
 
   // Implement the one CellEditor method that AbstractCellEditor doesn't.
+  @Override
   public Object getCellEditorValue()
   {
-    if (currentColor == null)
-    {
-      return currentGColor;
-    }
     return currentColor;
   }
 
   // Implement the one method defined by TableCellEditor.
+  @Override
   public Component getTableCellEditorComponent(JTable table, Object value,
           boolean isSelected, int row, int column)
   {
-    currentGColor = null;
-    currentColor = null;
+    currentColor = (FeatureColourI) value;
     this.selectedRow = row;
     type = me.table.getValueAt(row, 0).toString();
     button.setOpaque(true);
     button.setBackground(me.getBackground());
-    if (value instanceof GraduatedColor)
+    if (!currentColor.isSimpleColour())
     {
-      currentGColor = (GraduatedColor) value;
       JLabel btn = new JLabel();
       btn.setSize(button.getSize());
-      FeatureSettings.renderGraduatedColor(btn, currentGColor);
+      FeatureSettings.renderGraduatedColor(btn, currentColor);
       button.setBackground(btn.getBackground());
       button.setIcon(btn.getIcon());
       button.setText(btn.getText());
@@ -1900,8 +2402,7 @@ class ColorEditor extends AbstractCellEditor implements TableCellEditor,
     {
       button.setText("");
       button.setIcon(null);
-      currentColor = (Color) value;
-      button.setBackground(currentColor);
+      button.setBackground(currentColor.getColour());
     }
     return button;
   }