*/
package jalview.gui;
+import jalview.api.AlignViewportI;
import jalview.api.AlignmentViewPanel;
import jalview.api.FeatureColourI;
+import jalview.bin.Console;
import jalview.datamodel.GraphLine;
import jalview.datamodel.features.FeatureAttributes;
import jalview.datamodel.features.FeatureAttributes.Datatype;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.gui.JalviewColourChooser.ColourChooserListener;
import jalview.schemes.FeatureColour;
import jalview.util.ColorUtils;
import jalview.util.MessageManager;
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 java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
-import java.awt.LayoutManager;
+import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
+import java.math.BigDecimal;
+import java.math.MathContext;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
-import javax.swing.JColorChooser;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
-import javax.swing.JSlider;
-import javax.swing.JTabbedPane;
import javax.swing.JTextField;
-import javax.swing.SwingConstants;
+import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
-import javax.swing.plaf.basic.BasicArrowButton;
/**
* A dialog where the user can configure colour scheme, and any filters, for one
*/
public class FeatureTypeSettings extends JalviewDialog
{
+ private final static MathContext FOUR_SIG_FIG = new MathContext(4);
+
+ private final static String LABEL_18N = MessageManager
+ .getString("label.label");
+
+ private final static String SCORE_18N = MessageManager
+ .getString("label.score");
+
private static final int RADIO_WIDTH = 130;
private static final String COLON = ":";
/*
* FeatureRenderer holds colour scheme and filters for feature types
*/
- private final FeatureRenderer fr; // todo refactor to allow interface type here
+ private final FeatureRenderer fr; // todo refactor to allow interface type
+ // here
/*
* the view panel to update when settings change
*/
- private final AlignmentViewPanel ap;
+ final AlignmentViewPanel ap;
- private final String featureType;
+ final String featureType;
/*
* the colour and filters to reset to on Cancel
*/
private final FeatureColourI originalColour;
- private final KeyedMatcherSetI originalFilter;
+ private final FeatureMatcherSetI originalFilter;
/*
* set flag to true when setting values programmatically,
* to avoid invocation of action handlers
*/
- private boolean adjusting = false;
+ boolean adjusting = false;
+ /*
+ * minimum of the value range for graduated colour
+ * (may be for feature score or for a numeric attribute)
+ */
private float min;
+ /*
+ * maximum of the value range for graduated colour
+ */
private float max;
- private float scaleFactor;
-
/*
* radio button group, to select what to colour by:
* simple colour, by category (text), or graduated
*/
- private JRadioButton simpleColour = new JRadioButton();
+ JRadioButton simpleColour = new JRadioButton();
+
+ JRadioButton byCategory = new JRadioButton();
- private JRadioButton byCategory = new JRadioButton();
+ JRadioButton graduatedColour = new JRadioButton();
- private JRadioButton graduatedColour = new JRadioButton();
+ JPanel coloursPanel;
- private JPanel singleColour = new JPanel();
+ JPanel filtersPanel;
- private JPanel minColour = new JPanel();
+ JPanel singleColour = new JPanel();
- private JPanel maxColour = new JPanel();
+ JPanel minColour = new JPanel();
- private JComboBox<String> threshold = new JComboBox<>();
+ JPanel maxColour = new JPanel();
- private JSlider slider = new JSlider();
+ private JComboBox<Object> threshold = new JComboBox<>();
- private JTextField thresholdValue = new JTextField(20);
+ private Slider slider;
+
+ JTextField thresholdValue = new JTextField(20);
private JCheckBox thresholdIsMin = new JCheckBox();
/*
* choice of option for 'colour for no value'
*/
- private JComboBox<String> noValueCombo;
+ private JComboBox<Object> noValueCombo;
/*
* choice of what to colour by text (Label or attribute)
*/
- private JComboBox<String> colourByTextCombo;
+ private JComboBox<Object> colourByTextCombo;
/*
* choice of what to colour by range (Score or attribute)
*/
- private JComboBox<String> colourByRangeCombo;
+ private JComboBox<Object> colourByRangeCombo;
private JRadioButton andFilters;
/*
* filters for the currently selected feature type
*/
- private List<KeyedMatcherI> filters;
-
- // set white normally, black to debug layout
- private Color debugBorderColour = Color.white;
+ List<FeatureMatcherI> filters;
private JPanel chooseFiltersPanel;
- private JTabbedPane tabbedPane;
-
/**
* Constructor
*
*/
public FeatureTypeSettings(FeatureRenderer frender, String theType)
{
- this(frender, false, theType);
- }
-
- /**
- * Constructor, with option to make a blocking dialog (has to complete in the
- * AWT event queue thread). Currently this option is always set to false.
- *
- * @param frender
- * @param blocking
- * @param theType
- */
- FeatureTypeSettings(FeatureRenderer frender, boolean blocking,
- String theType)
- {
this.fr = frender;
this.featureType = theType;
ap = fr.ap;
return;
}
- updateColoursTab();
+ updateColoursPanel();
- updateFiltersTab();
+ updateFiltersPanel();
adjusting = false;
String title = MessageManager
.formatMessage("label.display_settings_for", new String[]
{ theType });
- initDialogFrame(this, true, blocking, title, 600, 360);
-
+ initDialogFrame(this, true, false, title, 580, 500);
waitForInput();
}
/**
- * Configures the widgets on the Colours tab according to the current feature
- * colour scheme
+ * Configures the widgets on the Colours panel according to the current
+ * feature colour scheme
*/
- private void updateColoursTab()
+ private void updateColoursPanel()
{
FeatureColourI fc = fr.getFeatureColours().get(featureType);
*/
if (fc.isSimpleColour())
{
- simpleColour.setSelected(true);
singleColour.setBackground(fc.getColour());
singleColour.setForeground(fc.getColour());
+ simpleColour.setSelected(true);
}
/*
if (fc.isColourByAttribute())
{
String[] attributeName = fc.getAttributeName();
- colourByTextCombo
- .setSelectedItem(toAttributeDisplayName(attributeName));
+ colourByTextCombo.setSelectedItem(
+ FeatureMatcher.toAttributeDisplayName(attributeName));
}
else
{
- colourByTextCombo
- .setSelectedItem(MessageManager.getString("label.label"));
+ colourByTextCombo.setSelectedItem(LABEL_18N);
}
}
else
* Graduated colour, by score or attribute value range
*/
graduatedColour.setSelected(true);
+ updateColourMinMax(); // ensure min, max are set
colourByRangeCombo.setEnabled(colourByRangeCombo.getItemCount() > 1);
minColour.setEnabled(true);
maxColour.setEnabled(true);
if (fc.isColourByAttribute())
{
String[] attributeName = fc.getAttributeName();
- colourByRangeCombo
- .setSelectedItem(toAttributeDisplayName(attributeName));
+ colourByRangeCombo.setSelectedItem(
+ FeatureMatcher.toAttributeDisplayName(attributeName));
}
else
{
- colourByRangeCombo
- .setSelectedItem(MessageManager.getString("label.score"));
+ colourByRangeCombo.setSelectedItem(SCORE_18N);
}
Color noColour = fc.getNoColour();
if (noColour == null)
* update min-max scaling if there is a range to work with,
* else disable the widgets (this shouldn't happen if only
* valid options are offered in the combo box)
+ * offset slider to have only non-negative values if necessary (JAL-2983)
*/
- scaleFactor = (max == min) ? 1f : 100f / (max - min);
- float range = (max - min) * scaleFactor;
- slider.setMinimum((int) (min * scaleFactor));
- slider.setMaximum((int) (max * scaleFactor));
- slider.setMajorTickSpacing((int) (range / 10f));
+ slider.setSliderModel(min, max, min);
+ slider.setMajorTickSpacing(
+ (int) ((slider.getMaximum() - slider.getMinimum()) / 10f));
threshline = new GraphLine((max - min) / 2f, "Threshold",
Color.black);
fc.isAboveThreshold() ? ABOVE_THRESHOLD_OPTION
: BELOW_THRESHOLD_OPTION);
slider.setEnabled(true);
- slider.setValue((int) (fc.getThreshold() * scaleFactor));
- thresholdValue.setText(String.valueOf(getRoundedSliderValue()));
+ slider.setSliderValue(fc.getThreshold());
+ setThresholdValueText(fc.getThreshold());
thresholdValue.setEnabled(true);
thresholdIsMin.setEnabled(true);
}
private void initialise()
{
this.setLayout(new BorderLayout());
- tabbedPane = new JTabbedPane();
- this.add(tabbedPane, BorderLayout.CENTER);
/*
* an ActionListener that applies colour changes
};
/*
- * first tab: colour options
+ * first panel: colour options
*/
JPanel coloursPanel = initialiseColoursPanel();
- tabbedPane.addTab(MessageManager.getString("action.colour"),
- coloursPanel);
+ this.add(coloursPanel, BorderLayout.NORTH);
/*
- * second tab: filter options
+ * second panel: filter options
*/
JPanel filtersPanel = initialiseFiltersPanel();
- tabbedPane.addTab(MessageManager.getString("label.filters"),
- filtersPanel);
+ this.add(filtersPanel, BorderLayout.CENTER);
JPanel okCancelPanel = initialiseOkCancelPanel();
* Updates the min-max range if Colour By selected item is Score, or an
* attribute, with a min-max range
*/
- protected void updateMinMax()
+ protected void updateColourMinMax()
{
if (!graduatedColour.isSelected())
{
return;
}
- float[] minMax = null;
String colourBy = (String) colourByRangeCombo.getSelectedItem();
- if (MessageManager.getString("label.score").equals(colourBy))
+ float[] minMax = getMinMax(colourBy);
+
+ if (minMax != null)
+ {
+ min = minMax[0];
+ max = minMax[1];
+ }
+ }
+
+ /**
+ * Retrieves the min-max range:
+ * <ul>
+ * <li>of feature score, if colour or filter is by Score</li>
+ * <li>else of the selected attribute</li>
+ * </ul>
+ *
+ * @param attName
+ * @return
+ */
+ private float[] getMinMax(String attName)
+ {
+ float[] minMax = null;
+ if (SCORE_18N.equals(attName))
{
minMax = fr.getMinMax().get(featureType)[0];
}
else
{
// colour by attribute range
- String[] attNames = fromAttributeDisplayName(colourBy);
minMax = FeatureAttributes.getInstance().getMinMax(featureType,
- attNames);
- }
-
- if (minMax != null)
- {
- min = minMax[0];
- max = minMax[1];
+ FeatureMatcher.fromAttributeDisplayName(attName));
}
+ return minMax;
}
/**
graduatedColour = new JRadioButton(
MessageManager.getString("label.by_range_of") + COLON);
graduatedColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
+ graduatedColour.setOpaque(false);
graduatedColour.addItemListener(new ItemListener()
{
@Override
{
if (minColour.isEnabled())
{
- showColourChooser(minColour, "label.select_colour_minimum_value");
+ String ttl = MessageManager
+ .getString("label.select_colour_minimum_value");
+ showColourChooser(minColour, ttl);
}
}
});
{
if (maxColour.isEnabled())
{
- showColourChooser(maxColour, "label.select_colour_maximum_value");
+ String ttl = MessageManager
+ .getString("label.select_colour_maximum_value");
+ showColourChooser(maxColour, ttl);
}
}
});
maxColour.setBorder(new LineBorder(Color.black));
/*
- * default max colour to current colour (if a plain colour),
- * or to Black if colour by label; make min colour a pale
- * version of max colour
+ * if not set, default max colour to last plain colour,
+ * and make min colour a pale version of max colour
*/
- FeatureColourI fc = fr.getFeatureColours().get(featureType);
- Color bg = fc.isSimpleColour() ? fc.getColour() : Color.BLACK;
- maxColour.setBackground(bg);
- minColour.setBackground(ColorUtils.bleachColour(bg, 0.9f));
+ Color max = originalColour.getMaxColour();
+ if (max == null)
+ {
+ max = originalColour.getColour();
+ minColour.setBackground(ColorUtils.bleachColour(max, 0.9f));
+ }
+ else
+ {
+ maxColour.setBackground(max);
+ minColour.setBackground(originalColour.getMinColour());
+ }
noValueCombo = new JComboBox<>();
noValueCombo.addItem(MessageManager.getString("label.no_colour"));
thresholdValue_actionPerformed();
}
});
+ slider = new Slider(0f, 100f, 50f);
slider.setPaintLabels(false);
slider.setPaintTicks(true);
slider.setBackground(Color.white);
{
if (!adjusting)
{
- thresholdValue
- .setText(String.valueOf(slider.getValue() / scaleFactor));
+ setThresholdValueText(slider.getSliderValue());
+ thresholdValue.setBackground(Color.white); // to reset red for invalid
sliderValueChanged();
}
}
*/
if (ap != null)
{
- ap.paintAlignment(true, true);
+ refreshDisplay(true);
}
}
});
private JPanel initialiseColoursPanel()
{
JPanel colourByPanel = new JPanel();
+ colourByPanel.setBackground(Color.white);
colourByPanel.setLayout(new BoxLayout(colourByPanel, BoxLayout.Y_AXIS));
+ JvSwingUtils.createTitledBorder(colourByPanel,
+ MessageManager.getString("action.colour"), true);
/*
* simple colour radio button and colour picker
*/
JPanel simpleColourPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
simpleColourPanel.setBackground(Color.white);
- JvSwingUtils.createTitledBorder(simpleColourPanel,
- MessageManager.getString("label.simple"), true);
colourByPanel.add(simpleColourPanel);
simpleColour = new JRadioButton(
MessageManager.getString("label.simple_colour"));
simpleColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
+ simpleColour.setOpaque(false);
simpleColour.addItemListener(new ItemListener()
{
@Override
{
if (simpleColour.isSelected() && !adjusting)
{
- showColourChooser(singleColour, "label.select_colour");
+ colourChanged(true);
}
}
-
});
-
+
singleColour.setFont(JvSwingUtils.getLabelFont());
singleColour.setBorder(BorderFactory.createLineBorder(Color.black));
singleColour.setPreferredSize(new Dimension(40, 20));
+ // if (originalColour.isGraduatedColour())
+ // {
+ // singleColour.setBackground(originalColour.getMaxColour());
+ // singleColour.setForeground(originalColour.getMaxColour());
+ // }
+ // else
+ // {
+ singleColour.setBackground(originalColour.getColour());
+ singleColour.setForeground(originalColour.getColour());
+ // }
singleColour.addMouseListener(new MouseAdapter()
{
@Override
{
if (simpleColour.isSelected())
{
- showColourChooser(singleColour, "label.select_colour");
+ String ttl = MessageManager
+ .formatMessage("label.select_colour_for", featureType);
+ showColourChooser(singleColour, ttl);
}
}
});
byCategory = new JRadioButton(
MessageManager.getString("label.by_text_of") + COLON);
byCategory.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
+ byCategory.setOpaque(false);
byCategory.addItemListener(new ItemListener()
{
@Override
return colourByPanel;
}
- private void showColourChooser(JPanel colourPanel, String key)
+ /**
+ * Shows a colour chooser dialog, and if a selection is made, updates the
+ * colour of the given panel
+ *
+ * @param colourPanel
+ * the panel whose background colour is being picked
+ * @param title
+ */
+ void showColourChooser(JPanel colourPanel, String title)
{
- Color col = JColorChooser.showDialog(this,
- MessageManager.getString(key), colourPanel.getBackground());
- if (col != null)
+ ColourChooserListener listener = new ColourChooserListener()
{
- colourPanel.setBackground(col);
- colourPanel.setForeground(col);
- }
- colourPanel.repaint();
- colourChanged(true);
+ @Override
+ public void colourSelected(Color col)
+ {
+ colourPanel.setBackground(col);
+ colourPanel.setForeground(col);
+ colourPanel.repaint();
+ colourChanged(true);
+ }
+ };
+ JalviewColourChooser.showColourChooser(this, title,
+ colourPanel.getBackground(), listener);
}
/**
- * 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
+ * Constructs and sets the selected colour options as the colour for the
+ * feature type, and repaints the alignment, and optionally the Overview
+ * and/or structure viewer if open
*
* @param updateStructsAndOverview
*/
* ensure min-max range is for the latest choice of
* 'graduated colour by'
*/
- updateMinMax();
+ updateColourMinMax();
FeatureColourI acg = makeColourFromInputs();
* save the colour, and repaint stuff
*/
fr.setColour(featureType, acg);
- ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
+ refreshDisplay(updateStructsAndOverview);
- updateColoursTab();
+ updateColoursPanel();
}
/**
private FeatureColourI makeColourFromInputs()
{
/*
- * easiest case - a single colour
- */
- if (simpleColour.isSelected())
- {
- return new FeatureColour(singleColour.getBackground());
- }
-
- /*
- * next easiest case - colour by Label, or attribute text
- */
- if (byCategory.isSelected())
- {
- Color c = this.getBackground();
- FeatureColourI fc = new FeatureColour(c, c, null, 0f, 0f);
- fc.setColourByLabel(true);
- String byWhat = (String) colourByTextCombo.getSelectedItem();
- if (!MessageManager.getString("label.label").equals(byWhat))
- {
- fc.setAttributeName(fromAttributeDisplayName(byWhat));
- }
- return fc;
- }
-
- /*
- * remaining case - graduated colour by score, or attribute value
+ * min-max range is to (or from) threshold value if
+ * 'threshold is min/max' is selected
*/
- Color noColour = null;
- if (noValueCombo.getSelectedIndex() == MIN_COLOUR_OPTION)
- {
- noColour = minColour.getBackground();
- }
- else if (noValueCombo.getSelectedIndex() == MAX_COLOUR_OPTION)
- {
- noColour = maxColour.getBackground();
- }
float thresh = 0f;
try
{
// invalid inputs are already handled on entry
}
-
- /*
- * min-max range is to (or from) threshold value if
- * 'threshold is min/max' is selected
- */
float minValue = min;
float maxValue = max;
- final int thresholdOption = threshold.getSelectedIndex();
+ int thresholdOption = threshold.getSelectedIndex();
if (thresholdIsMin.isSelected()
&& thresholdOption == ABOVE_THRESHOLD_OPTION)
{
{
maxValue = thresh;
}
+ Color noColour = null;
+ if (noValueCombo.getSelectedIndex() == MIN_COLOUR_OPTION)
+ {
+ noColour = minColour.getBackground();
+ }
+ else if (noValueCombo.getSelectedIndex() == MAX_COLOUR_OPTION)
+ {
+ noColour = maxColour.getBackground();
+ }
/*
- * make the graduated colour
+ * construct a colour that 'remembers' all the options, including
+ * those not currently selected
+ */
+ FeatureColourI fc = new FeatureColour(singleColour.getBackground(),
+ minColour.getBackground(), maxColour.getBackground(), noColour,
+ minValue, maxValue);
+
+ /*
+ * easiest case - a single colour
*/
- FeatureColourI fc = new FeatureColour(minColour.getBackground(),
- maxColour.getBackground(), noColour, minValue, maxValue);
+ if (simpleColour.isSelected())
+ {
+ ((FeatureColour) fc).setGraduatedColour(false);
+ return fc;
+ }
/*
+ * next easiest case - colour by Label, or attribute text
+ */
+ if (byCategory.isSelected())
+ {
+ fc.setColourByLabel(true);
+ String byWhat = (String) colourByTextCombo.getSelectedItem();
+ if (!LABEL_18N.equals(byWhat))
+ {
+ fc.setAttributeName(
+ FeatureMatcher.fromAttributeDisplayName(byWhat));
+ }
+ return fc;
+ }
+
+ /*
+ * remaining case - graduated colour by score, or attribute value;
* set attribute to colour by if selected
*/
String byWhat = (String) colourByRangeCombo.getSelectedItem();
- if (!MessageManager.getString("label.score").equals(byWhat))
+ if (!SCORE_18N.equals(byWhat))
{
- fc.setAttributeName(fromAttributeDisplayName(byWhat));
+ fc.setAttributeName(FeatureMatcher.fromAttributeDisplayName(byWhat));
}
/*
return fc;
}
- /**
- * A helper method that converts a 'compound' attribute name from its display
- * form, e.g. CSQ:PolyPhen to array form, e.g. { "CSQ", "PolyPhen" }
- *
- * @param attribute
- * @return
- */
- private String[] fromAttributeDisplayName(String attribute)
- {
- return attribute == null ? null : attribute.split(COLON);
- }
-
- /**
- * A helper method that converts a 'compound' attribute name to its display
- * form, e.g. CSQ:PolyPhen from its array form, e.g. { "CSQ", "PolyPhen" }
- *
- * @param attName
- * @return
- */
- private String toAttributeDisplayName(String[] attName)
- {
- return attName == null ? "" : String.join(COLON, attName);
- }
-
@Override
protected void raiseClosed()
{
{
fr.setColour(featureType, originalColour);
fr.setFeatureFilter(featureType, originalFilter);
- ap.paintAlignment(true, true);
+ refreshDisplay(true);
}
/**
{
try
{
+ /*
+ * set 'adjusting' flag while moving the slider, so it
+ * doesn't then in turn change the value (with rounding)
+ */
adjusting = true;
float f = Float.parseFloat(thresholdValue.getText());
- slider.setValue((int) (f * scaleFactor));
+ f = Float.max(f, this.min);
+ f = Float.min(f, this.max);
+ setThresholdValueText(f);
+ slider.setSliderValue(f);
threshline.value = f;
thresholdValue.setBackground(Color.white); // ok
-
- /*
- * force repaint of any Overview window or structure
- */
- ap.paintAlignment(true, true);
+ adjusting = false;
+ colourChanged(true);
} catch (NumberFormatException ex)
{
thresholdValue.setBackground(Color.red); // not ok
- } finally
- {
adjusting = false;
}
}
/**
+ * Sets the text field for threshold value, rounded to four significant
+ * figures
+ *
+ * @param f
+ */
+ void setThresholdValueText(float f)
+ {
+ BigDecimal formatted = new BigDecimal(f).round(FOUR_SIG_FIG)
+ .stripTrailingZeros();
+ thresholdValue.setText(formatted.toPlainString());
+ }
+
+ /**
* Action on change of threshold slider value. This may be done interactively
* (by moving the slider), or programmatically (to update the slider after
* manual input of a threshold value).
*/
protected void sliderValueChanged()
{
- threshline.value = getRoundedSliderValue();
+ threshline.value = slider.getSliderValue();
/*
* repaint alignment, but not Overview or structure,
colourChanged(false);
}
- /**
- * Converts the slider value to its absolute value by dividing by the
- * scaleFactor. Rounding errors are squashed by forcing min/max of slider range
- * to the actual min/max of feature score range
- *
- * @return
- */
- private float getRoundedSliderValue()
- {
- int value = slider.getValue();
- float f = value == slider.getMaximum() ? max
- : (value == slider.getMinimum() ? min : value / scaleFactor);
- return f;
- }
-
void addActionListener(ActionListener listener)
{
if (featureSettings != null)
}
/**
- * A helper method to build the drop-down choice of attributes for a feature. If
- * 'withRange' is true, then Score, and any attributes with a min-max range, are
- * added. If 'withText' is true, Label and any known attributes are added. This
- * allows 'categorical numerical' attributes e.g. codon position to be coloured
- * by text.
+ * A helper method to build the drop-down choice of attributes for a feature.
+ * If 'withRange' is true, then Score, and any attributes with a min-max
+ * range, are added. If 'withText' is true, Label and any known attributes are
+ * added. This allows 'categorical numerical' attributes e.g. codon position
+ * to be coloured by text.
* <p>
* Where metadata is available with a description for an attribute, that is
* added as a tooltip.
* @param withRange
* @param withText
*/
- protected JComboBox<String> populateAttributesDropdown(
+ protected JComboBox<Object> populateAttributesDropdown(
List<String[]> attNames, boolean withRange, boolean withText)
{
List<String> displayAtts = new ArrayList<>();
if (withText)
{
- displayAtts.add(MessageManager.getString("label.label"));
+ displayAtts.add(LABEL_18N);
tooltips.add(MessageManager.getString("label.description"));
}
if (withRange)
float[][] minMax = fr.getMinMax().get(featureType);
if (minMax != null && minMax[0][0] != minMax[0][1])
{
- displayAtts.add(MessageManager.getString("label.score"));
- tooltips.add(MessageManager.getString("label.score"));
+ displayAtts.add(SCORE_18N);
+ tooltips.add(SCORE_18N);
}
}
{
continue;
}
- displayAtts.add(toAttributeDisplayName(attName));
+ displayAtts.add(FeatureMatcher.toAttributeDisplayName(attName));
String desc = fa.getDescription(featureType, attName);
if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
{
tooltips.add(desc == null ? "" : desc);
}
- JComboBox<String> attCombo = JvSwingUtils
- .buildComboWithTooltips(displayAtts, tooltips);
+ // now convert String List to Object List for buildComboWithTooltips
+ List<Object> displayAttsObjects = new ArrayList<>(displayAtts);
+ JComboBox<Object> attCombo = JvSwingUtils
+ .buildComboWithTooltips(displayAttsObjects, tooltips);
return attCombo;
}
filtersPanel.add(andOrPanel);
/*
- * panel with filters - populated by refreshFiltersDisplay
+ * panel with filters - populated by refreshFiltersDisplay,
+ * which also sets the layout manager
*/
chooseFiltersPanel = new JPanel();
- LayoutManager box = new BoxLayout(chooseFiltersPanel, BoxLayout.Y_AXIS);
- chooseFiltersPanel.setLayout(box);
+ chooseFiltersPanel.setBackground(Color.white);
filtersPanel.add(chooseFiltersPanel);
return filtersPanel;
{
JPanel andOrPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
andOrPanel.setBackground(Color.white);
- andOrPanel.setBorder(BorderFactory.createLineBorder(debugBorderColour));
andFilters = new JRadioButton(MessageManager.getString("label.and"));
orFilters = new JRadioButton(MessageManager.getString("label.or"));
+ andFilters.setOpaque(false);
+ orFilters.setOpaque(false);
ActionListener actionListener = new ActionListener()
{
@Override
* for adding a condition. This should be called after a filter has been
* removed, added or amended.
*/
- private void updateFiltersTab()
+ private void updateFiltersPanel()
{
/*
* clear the panel and list of filter conditions
/*
* if this feature type has filters set, load them first
*/
- KeyedMatcherSetI featureFilters = fr.getFeatureFilter(featureType);
+ FeatureMatcherSetI featureFilters = fr.getFeatureFilter(featureType);
if (featureFilters != null)
{
if (!featureFilters.isAnded())
{
orFilters.setSelected(true);
}
- featureFilters.getMatchers().forEach(matcher -> filters.add(matcher));
+ // avoid use of lambda expression to keep SwingJS happy
+ // featureFilters.getMatchers().forEach(item -> filters.add(item));
+ for (FeatureMatcherI matcher : featureFilters.getMatchers())
+ {
+ 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);
+ filters.add(FeatureMatcher.NULL_MATCHER);
+
+ /*
+ * use GridLayout to 'justify' rows to the top of the panel, until
+ * there are too many to fit in, then fall back on BoxLayout
+ */
+ if (filters.size() <= 5)
+ {
+ chooseFiltersPanel.setLayout(new GridLayout(5, 1));
+ }
+ else
+ {
+ chooseFiltersPanel.setLayout(
+ new BoxLayout(chooseFiltersPanel, BoxLayout.Y_AXIS));
+ }
/*
* render the conditions in rows, each in its own JPanel
*/
int filterIndex = 0;
- for (KeyedMatcherI filter : filters)
+ for (FeatureMatcherI 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));
+ JPanel row = addFilter(filter, attNames, filterIndex);
chooseFiltersPanel.add(row);
filterIndex++;
}
/**
* A helper method that constructs a row (panel) with one filter condition:
* <ul>
- * <li>a drop-down list of attribute names to choose from</li>
+ * <li>a drop-down list of Label, Score and 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).
+ * The filter values are set as defaults for the input fields. The 'remove'
+ * button is added unless the pattern is empty (incomplete filter condition).
+ * <p>
+ * Action handlers on these fields provide for
+ * <ul>
+ * <li>validate pattern field - should be numeric if condition is numeric</li>
+ * <li>save filters and refresh display on any (valid) change</li>
+ * <li>remove filter and refresh on 'Remove'</li>
+ * <li>update conditions list on change of Label/Score/Attribute</li>
+ * <li>refresh value field tooltip with min-max range on change of
+ * attribute</li>
+ * </ul>
*
- * @param attName
+ * @param filter
* @param attNames
- * @param cond
- * @param pattern
* @param filterIndex
* @return
*/
- protected JPanel addFilter(String[] attName, List<String[]> attNames,
- Condition cond, String pattern, int filterIndex)
+ protected JPanel addFilter(FeatureMatcherI filter,
+ List<String[]> attNames, int filterIndex)
{
+ String[] attName = filter.getAttribute();
+ Condition cond = filter.getMatcher().getCondition();
+ String pattern = filter.getMatcher().getPattern();
+
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
*/
- final JComboBox<String> attCombo = populateAttributesDropdown(attNames,
+ final JComboBox<Object> attCombo = populateAttributesDropdown(attNames,
true, true);
+ String filterBy = setSelectedAttribute(attCombo, filter);
+
JComboBox<Condition> condCombo = new JComboBox<>();
+
JTextField patternField = new JTextField(8);
+ patternField.setText(pattern);
/*
* action handlers that validate and (if valid) apply changes
@Override
public void actionPerformed(ActionEvent e)
{
- if (attCombo.getSelectedItem() != null)
+ if (validateFilter(patternField, condCombo))
{
- if (validateFilter(patternField, condCombo))
+ if (updateFilter(attCombo, condCombo, patternField, filterIndex))
{
- updateFilter(attCombo, condCombo, patternField, filterIndex);
filtersChanged();
}
}
}
};
- if (attName == null) // the 'add a condition' row
+ if (filter == FeatureMatcher.NULL_MATCHER) // the 'add a condition' row
{
attCombo.setSelectedIndex(0);
}
else
{
- attCombo.setSelectedItem(toAttributeDisplayName(attName));
+ attCombo.setSelectedItem(
+ FeatureMatcher.toAttributeDisplayName(attName));
}
attCombo.addItemListener(new ItemListener()
{
* ensure it is appropriate for the attribute datatype
*/
populateConditions((String) attCombo.getSelectedItem(),
- (Condition) condCombo.getSelectedItem(), condCombo);
+ (Condition) condCombo.getSelectedItem(), condCombo,
+ patternField);
actionListener.actionPerformed(null);
}
});
/*
* drop-down choice of test condition
*/
- populateConditions((String) attCombo.getSelectedItem(), cond,
- condCombo);
+ populateConditions(filterBy, cond, condCombo, patternField);
condCombo.setPreferredSize(new Dimension(150, 20));
condCombo.addItemListener(itemListener);
filterRow.add(condCombo);
/*
* pattern to match against
*/
- patternField.setText(pattern);
patternField.addActionListener(actionListener);
patternField.addFocusListener(new FocusAdapter()
{
* disable pattern field for condition 'Present / NotPresent'
*/
Condition selectedCondition = (Condition) condCombo.getSelectedItem();
- if (selectedCondition == Condition.Present
- || selectedCondition == Condition.NotPresent)
- {
- patternField.setEnabled(false);
- }
+ patternField.setEnabled(selectedCondition.needsAPattern());
/*
* if a numeric condition is selected, show the value range
* as a tooltip on the value input field
*/
- if (selectedCondition.isNumeric())
- {
- float[] minMax = FeatureAttributes.getInstance()
- .getMinMax(featureType, attName);
- if (minMax != null)
- {
- String tip = String.format("(%s - %s)",
- DECFMT_2_2.format(minMax[0]), DECFMT_2_2.format(minMax[1]));
- patternField.setToolTipText(tip);
- }
- }
+ setNumericHints(filterBy, selectedCondition, 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"));
+ if (!patternField.isEnabled()
+ || (pattern != null && pattern.trim().length() > 0))
+ {
+ JButton removeCondition = new JButton("\u2717");
+ // Dingbats cursive x
+ removeCondition.setBorder(new EmptyBorder(0, 0, 0, 0));
+ removeCondition.setBackground(Color.WHITE);
+ removeCondition.setPreferredSize(new Dimension(23, 17));
+ removeCondition.setToolTipText(
+ MessageManager.getString("label.delete_condition"));
removeCondition.addActionListener(new ActionListener()
{
@Override
}
/**
- * Populates the drop-down list of comparison conditions for the given attribute
- * name. The conditions added depend on the datatype of the attribute values.
- * The supplied condition is set as the selected item in the list, provided it
- * is in the list.
+ * Sets the selected item in the Label/Score/Attribute drop-down to match the
+ * filter
+ *
+ * @param attCombo
+ * @param filter
+ */
+ private String setSelectedAttribute(JComboBox<Object> attCombo,
+ FeatureMatcherI filter)
+ {
+ String item = null;
+ if (filter.isByScore())
+ {
+ item = SCORE_18N;
+ }
+ else if (filter.isByLabel())
+ {
+ item = LABEL_18N;
+ }
+ else
+ {
+ item = FeatureMatcher.toAttributeDisplayName(filter.getAttribute());
+ }
+ attCombo.setSelectedItem(item);
+ return item;
+ }
+
+ /**
+ * If a numeric comparison condition is selected, retrieves the min-max range
+ * for the value (score or attribute), and sets it as a tooltip on the value
+ * field. If the field is currently empty, then pre-populates it with
+ * <ul>
+ * <li>the minimum value, if condition is > or >=</li>
+ * <li>the maximum value, if condition is < or <=</li>
+ * </ul>
+ *
+ * @param attName
+ * @param selectedCondition
+ * @param patternField
+ */
+ private void setNumericHints(String attName, Condition selectedCondition,
+ JTextField patternField)
+ {
+ patternField.setToolTipText("");
+
+ if (selectedCondition.isNumeric())
+ {
+ float[] minMax = getMinMax(attName);
+ if (minMax != null)
+ {
+ String minFormatted = DECFMT_2_2.format(minMax[0]);
+ String maxFormatted = DECFMT_2_2.format(minMax[1]);
+ String tip = String.format("(%s - %s)", minFormatted, maxFormatted);
+ patternField.setToolTipText(tip);
+ if (patternField.getText().isEmpty())
+ {
+ if (selectedCondition == Condition.GE
+ || selectedCondition == Condition.GT)
+ {
+ patternField.setText(minFormatted);
+ }
+ else
+ {
+ if (selectedCondition == Condition.LE
+ || selectedCondition == Condition.LT)
+ {
+ patternField.setText(maxFormatted);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Populates the drop-down list of comparison conditions for the given
+ * attribute name. The conditions added depend on the datatype of the
+ * attribute values. The supplied condition is set as the selected item in the
+ * list, provided it is in the list. If the pattern is now invalid
+ * (non-numeric pattern for a numeric condition), it is cleared.
*
* @param attName
* @param cond
* @param condCombo
+ * @param patternField
*/
- private void populateConditions(String attName, Condition cond,
- JComboBox<Condition> condCombo)
+ void populateConditions(String attName, Condition cond,
+ JComboBox<Condition> condCombo, JTextField patternField)
{
Datatype type = FeatureAttributes.getInstance().getDatatype(featureType,
- fromAttributeDisplayName(attName));
- if (MessageManager.getString("label.label").equals(attName))
+ FeatureMatcher.fromAttributeDisplayName(attName));
+ if (LABEL_18N.equals(attName))
{
type = Datatype.Character;
}
- else if (MessageManager.getString("label.score").equals(attName))
+ else if (SCORE_18N.equals(attName))
{
type = Datatype.Number;
}
ItemListener listener = condCombo.getItemListeners()[0];
condCombo.removeItemListener(listener);
boolean condIsValid = false;
+
condCombo.removeAllItems();
for (Condition c : Condition.values())
{
- if ((c.isNumeric() && type != Datatype.Character)
+ if ((c.isNumeric() && type == Datatype.Number)
|| (!c.isNumeric() && type != Datatype.Number))
{
condCombo.addItem(c);
condCombo.setSelectedIndex(0);
}
+ /*
+ * clear pattern if it is now invalid for condition
+ */
+ if (((Condition) condCombo.getSelectedItem()).isNumeric())
+ {
+ try
+ {
+ String pattern = patternField.getText().trim();
+ if (pattern.length() > 0)
+ {
+ Float.valueOf(pattern);
+ }
+ } catch (NumberFormatException e)
+ {
+ patternField.setText("");
+ }
+ }
+
+ /*
+ * restore the listener
+ */
condCombo.addItemListener(listener);
}
/**
- * 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.
+ * 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 is expected but 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.
+ * If the pattern is expected but 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
}
Condition cond = (Condition) condCombo.getSelectedItem();
- if (cond == Condition.Present || cond == Condition.NotPresent)
+ if (!cond.needsAPattern())
{
return true;
}
String v1 = value.getText().trim();
if (v1.length() == 0)
{
- return false;
+ // return false;
}
- if (cond.isNumeric())
+ if (cond.isNumeric() && v1.length() > 0)
{
try
{
/**
* Constructs a filter condition from the given input fields, and replaces the
- * condition at filterIndex with the new one
+ * condition at filterIndex with the new one. Does nothing if the pattern
+ * field is blank (unless the match condition is one that doesn't require a
+ * pattern, e.g. 'Is present'). Answers true if the filter was updated, else
+ * false.
+ * <p>
+ * This method may update the tooltip on the filter value field to show the
+ * value range, if a numeric condition is selected. This ensures the tooltip
+ * is updated when a numeric valued attribute is chosen on the last 'add a
+ * filter' row.
*
* @param attCombo
* @param condCombo
* @param valueField
* @param filterIndex
*/
- protected void updateFilter(JComboBox<String> attCombo,
+ protected boolean updateFilter(JComboBox<Object> attCombo,
JComboBox<Condition> condCombo, JTextField valueField,
int filterIndex)
{
- String attName = (String) attCombo.getSelectedItem();
+ String attName;
+ try
+ {
+ attName = (String) attCombo.getSelectedItem();
+ } catch (Exception e)
+ {
+ Console.error("Problem casting Combo box entry to String");
+ attName = attCombo.getSelectedItem().toString();
+ }
Condition cond = (Condition) condCombo.getSelectedItem();
- String pattern = valueField.getText();
- KeyedMatcherI km = new KeyedMatcher(cond, pattern,
- fromAttributeDisplayName(attName));
+ String pattern = valueField.getText().trim();
+
+ setNumericHints(attName, cond, valueField);
+
+ if (pattern.length() == 0 && cond.needsAPattern())
+ {
+ valueField.setEnabled(true); // ensure pattern field is enabled!
+ return false;
+ }
+
+ /*
+ * Construct a matcher that operates on Label, Score,
+ * or named attribute
+ */
+ FeatureMatcherI km = null;
+ if (LABEL_18N.equals(attName))
+ {
+ km = FeatureMatcher.byLabel(cond, pattern);
+ }
+ else if (SCORE_18N.equals(attName))
+ {
+ km = FeatureMatcher.byScore(cond, pattern);
+ }
+ else
+ {
+ km = FeatureMatcher.byAttribute(cond, pattern,
+ FeatureMatcher.fromAttributeDisplayName(attName));
+ }
filters.set(filterIndex, km);
- }
- /**
- * Makes the dialog visible, at the Feature Colour tab or at the Filters tab
- *
- * @param coloursTab
- */
- public void showTab(boolean coloursTab)
- {
- setVisible(true);
- tabbedPane.setSelectedIndex(coloursTab ? 0 : 1);
+ return true;
}
/**
* <li>change of match pattern</li>
* <li>removal of a condition</li>
* </ul>
- * The inputs are parsed into a combined filter and this is set for the feature
- * type, and the alignment redrawn.
+ * The inputs are parsed into a combined filter and this is set for the
+ * feature type, and the alignment redrawn.
*/
protected void filtersChanged()
{
* update the filter conditions for the feature type
*/
boolean anded = andFilters.isSelected();
- KeyedMatcherSetI combined = new KeyedMatcherSet();
+ FeatureMatcherSetI combined = new FeatureMatcherSet();
- for (KeyedMatcherI filter : filters)
+ for (FeatureMatcherI filter : filters)
{
String pattern = filter.getMatcher().getPattern();
Condition condition = filter.getMatcher().getCondition();
- if (pattern.trim().length() > 0 || condition == Condition.Present
- || condition == Condition.NotPresent)
+ if (pattern.trim().length() > 0 || !condition.needsAPattern())
{
if (anded)
{
* (note this might now be an empty filter with no conditions)
*/
fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined);
- ap.paintAlignment(true, true);
+ refreshDisplay(true);
- updateFiltersTab();
+ updateFiltersPanel();
+ }
+
+ /**
+ * Repaints alignment, structure and overview (if shown). If there is a
+ * complementary view which is showing this view's features, then also
+ * repaints that.
+ *
+ * @param updateStructsAndOverview
+ */
+ void refreshDisplay(boolean updateStructsAndOverview)
+ {
+ ap.paintAlignment(true, updateStructsAndOverview);
+ AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
+ if (complement != null && complement.isShowComplementFeatures())
+ {
+ AlignFrame af2 = Desktop.getAlignFrameFor(complement);
+ af2.alignPanel.paintAlignment(true, updateStructsAndOverview);
+ }
}
}