/* * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * Jalview is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ package jalview.gui; import jalview.util.MessageManager; import jalview.ws.jws2.dm.JabaOption; import jalview.ws.params.ArgumentI; import jalview.ws.params.OptionI; import jalview.ws.params.ParameterI; import jalview.ws.params.ValueConstrainI; import jalview.ws.params.ValueConstrainI.ValueType; import jalview.ws.params.simple.LogarithmicParameter; import jalview.ws.params.simple.StringParameter; 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.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.net.URL; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.border.TitledBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import net.miginfocom.swing.MigLayout; /** * GUI generator/manager for options and parameters. Originally abstracted from * the WsJobParameters dialog box. * * @author jprocter * */ public class OptsAndParamsPage { public static final int PARAM_WIDTH = 340; public static final int PARAM_HEIGHT = 150; public static final int PARAM_CLOSEDHEIGHT = 80; URL linkImageURL = getClass().getResource("/images/link.gif"); Map optSet = new LinkedHashMap<>(); Map paramSet = new LinkedHashMap<>(); /* * compact or verbose style parameters */ boolean compact = false; OptsParametersContainerI poparent; /** * A class that models a panel rendering a single option (checkbox or choice * list) */ public class OptionBox extends JPanel implements MouseListener, ActionListener { JCheckBox enabled; final URL finfo; boolean hasLink = false; boolean initEnabled = false; String initVal = null; OptionI option; JComboBox val; /** * Constructs and adds labels and controls to the panel for one Option * * @param opt */ public OptionBox(OptionI opt) { option = opt; setLayout(new FlowLayout(FlowLayout.LEFT)); enabled = new JCheckBox(opt.getName()); enabled.setSelected(opt.isRequired()); /* * If option is required, show a label, if optional a checkbox * (but not for Jabaws pending JWS-126 resolution) */ if (opt.isRequired() && !(opt instanceof JabaOption)) { finfo = null; add(new JLabel(opt.getName())); } else { finfo = option.getFurtherDetails(); configureCheckbox(opt); add(enabled); } /* * construct the choice box with possible values, * or their display names if provided */ val = buildComboBox(opt); val.setSelectedItem(opt.getValue()); /* * only show the choicebox if there is more than one option, * or the option is mandatory */ if (opt.getPossibleValues().size() > 1 || opt.isRequired()) { val.addActionListener(this); add(val); } setInitialValue(); } /** * Configures the checkbox that controls whether or not the option is * selected * * @param opt */ protected void configureCheckbox(OptionI opt) { enabled.setFont(new Font("Verdana", Font.PLAIN, 11)); enabled.addActionListener(this); final String desc = opt.getDescription(); if (finfo != null) { hasLink = true; String description = desc; if (desc == null || desc.trim().isEmpty()) { description = MessageManager .getString("label.opt_and_params_further_details"); } description = description + "
"; String text = JvSwingUtils.wrapTooltip(true, description); enabled.setToolTipText(text); enabled.addMouseListener(this); // for popup menu to show link } else { if (desc != null && desc.trim().length() > 0) { enabled.setToolTipText(JvSwingUtils.wrapTooltip(true, desc)); } } } @Override public void actionPerformed(ActionEvent e) { if (e.getSource() != enabled) { enabled.setSelected(true); } checkIfModified(); } private void checkIfModified() { boolean notmod = (initEnabled == enabled.isSelected()); if (enabled.isSelected()) { if (initVal != null) { notmod &= initVal.equals(val.getSelectedItem()); } else { // compare against default service setting notmod &= option.getValue() == null || option.getValue().equals(val.getSelectedItem()); } } else { notmod &= (initVal != null) ? initVal.equals(val.getSelectedItem()) : val.getSelectedItem() != initVal; } poparent.argSetModified(this, !notmod); } /** * Answers null if the option is not selected, else a new Option holding the * selected value * * @return */ public ArgumentI getSelectedOption() { if (!enabled.isSelected()) { return null; } String value = getSelectedValue(option, val.getSelectedIndex()); OptionI opt = option.copy(); opt.setValue(value); return opt; } @Override public void mouseClicked(MouseEvent e) { if (e.isPopupTrigger()) // for Windows { showUrlPopUp(this, finfo.toString(), e.getX(), e.getY()); } } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) // Mac { showUrlPopUp(this, finfo.toString(), e.getX(), e.getY()); } } @Override public void mouseReleased(MouseEvent e) { } public void resetToDefault(boolean setDefaultParams) { enabled.setSelected(false); if (option.isRequired() || (setDefaultParams && option.getValue() != null)) { // Apply default value selectOption(option, option.getValue()); } } public void setInitialValue() { initEnabled = enabled.isSelected(); if (option.getPossibleValues() != null && option.getPossibleValues().size() > 1) { initVal = (String) val.getSelectedItem(); } else { initVal = (initEnabled) ? (String) val.getSelectedItem() : null; } } /** * toString representation for identification in the debugger only */ @Override public String toString() { return option == null ? super.toString() : option.toString(); } } /** * A class that models a panel rendering a single parameter */ public class ParamBox extends JPanel implements ChangeListener, ActionListener, MouseListener { /* * parameter values (or their logs) are multiplied by this * scaling factor to ensure an integer range for the slider */ private int sliderScaleFactor = 1; boolean isLogarithmicParameter; boolean isChoiceParameter; boolean isIntegerParameter; boolean isStringParameter; boolean adjusting; JComboBox choicebox; JPanel controlsPanel = new JPanel(); boolean descriptionIsVisible = false; JScrollPane descPanel = new JScrollPane(); final URL finfo; Object lastVal; ParameterI parameter; final OptsParametersContainerI pmdialogbox; JPanel settingPanel = new JPanel(); JSlider slider; JTextArea descriptionText = new JTextArea(); ValueConstrainI validator; JTextField valueField; private String descTooltip; public ParamBox(final OptsParametersContainerI paramContainer, ParameterI parm) { pmdialogbox = paramContainer; finfo = parm.getFurtherDetails(); validator = parm.getValidValue(); parameter = parm; isLogarithmicParameter = parm instanceof LogarithmicParameter; if (validator != null) { ValueType type = validator.getType(); isIntegerParameter = type == ValueType.Integer; isStringParameter = type == ValueType.String; /* * ensure slider has an integer range corresponding to * the min-max range of the parameter */ if (validator.getMin() != null && validator.getMax() != null // && !isIntegerParameter && !isStringParameter) { double min = validator.getMin().doubleValue(); double max = validator.getMax().doubleValue(); if (isLogarithmicParameter) { min = Math.log(min); max = Math.log(max); } sliderScaleFactor = (int) (1000000 / (max - min)); // todo scaleMin, scaleMax could also be final fields } } List possibleValues = parameter.getPossibleValues(); isChoiceParameter = possibleValues != null && !possibleValues.isEmpty(); if (compact) { addCompactParameter(parm); } else { addExpandableParam(parm); } } /** * Adds a 'compact' format parameter, with any help text shown as a tooltip * * @param parm */ private void addCompactParameter(ParameterI parm) { setLayout(new MigLayout("", "[][grow]")); String ttipText = null; controlsPanel.setLayout(new BorderLayout()); if (parm.getDescription() != null && parm.getDescription().trim().length() > 0) { ttipText = (JvSwingUtils.wrapTooltip(true, parm.getDescription() + (finfo != null ? "
" + MessageManager.getString( "label.opt_and_params_further_details") : ""))); } JvSwingUtils.addtoLayout(this, ttipText, new JLabel(parm.getName()), controlsPanel, ""); updateControls(parm); validate(); } /** * Adds an 'expanded' format parameter, with any help shown in a panel that * may be shown or hidden * * @param parm */ private void addExpandableParam(ParameterI parm) { setPreferredSize(new Dimension(PARAM_WIDTH, PARAM_CLOSEDHEIGHT)); setBorder(new TitledBorder(parm.getName())); setLayout(null); descriptionText.setFont(new Font("Verdana", Font.PLAIN, 11)); descriptionText.setBackground(getBackground()); descriptionText.setEditable(false); descPanel.getViewport().setView(descriptionText); descPanel.setVisible(false); JPanel firstrow = new JPanel(); firstrow.setLayout(null); controlsPanel.setLayout(new BorderLayout()); controlsPanel.setBounds(new Rectangle(39, 10, PARAM_WIDTH - 70, PARAM_CLOSEDHEIGHT - 50)); firstrow.add(controlsPanel); firstrow.setBounds(new Rectangle(10, 20, PARAM_WIDTH - 30, PARAM_CLOSEDHEIGHT - 30)); if (parm.getDescription() != null && parm.getDescription().trim().length() > 0) { addExpandableHelp(firstrow, parm); } add(firstrow); validator = parm.getValidValue(); parameter = parm; if (validator != null) { isIntegerParameter = validator.getType() == ValueType.Integer; } else { if (parameter.getPossibleValues() != null) { isChoiceParameter = true; } } updateControls(parm); descPanel.setBounds(new Rectangle(10, PARAM_CLOSEDHEIGHT, PARAM_WIDTH - 20, PARAM_HEIGHT - PARAM_CLOSEDHEIGHT - 5)); add(descPanel); validate(); } /** * Adds a button which can be clicked to show or hide help text * * @param container * @param param */ protected void addExpandableHelp(JPanel container, ParameterI param) { JButton showDescBtn = new JButton("+"); showDescBtn.setFont(new Font("Verdana", Font.PLAIN, 8)); if (finfo != null) { descTooltip = JvSwingUtils.wrapTooltip(true, MessageManager.formatMessage( "label.opt_and_params_show_brief_desc_image_link", new String[] { linkImageURL.toExternalForm() })); showDescBtn.addMouseListener(this); } else { descTooltip = JvSwingUtils.wrapTooltip(true, MessageManager .getString("label.opt_and_params_show_brief_desc")); } showDescBtn.setToolTipText(descTooltip); showDescBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { descriptionIsVisible = !descriptionIsVisible; showDescBtn.setText(descriptionIsVisible ? "-" : "+"); showDescBtn.setToolTipText( descriptionIsVisible ? null : descTooltip); descPanel.setVisible(descriptionIsVisible); descPanel.getVerticalScrollBar().setValue(0); ParamBox.this.setPreferredSize(new Dimension(PARAM_WIDTH, (descriptionIsVisible) ? PARAM_HEIGHT : PARAM_CLOSEDHEIGHT)); ParamBox.this.validate(); pmdialogbox.refreshParamLayout(); } }); descriptionText.setWrapStyleWord(true); descriptionText.setLineWrap(true); descriptionText.setColumns(32); descriptionText.setText(param.getDescription()); showDescBtn.setBounds(new Rectangle(10, 10, 16, 16)); container.add(showDescBtn); } @Override public void actionPerformed(ActionEvent e) { if (adjusting) { return; } if (!isChoiceParameter) { updateSliderFromValueField(); } checkIfModified(); } /** * Checks whether the value of this parameter has been changed and notifies * the parent page accordingly */ private void checkIfModified() { Object newValue = updateSliderFromValueField(); boolean modified = true; if (newValue.getClass() == lastVal.getClass()) { modified = !newValue.equals(lastVal); } pmdialogbox.argSetModified(this, modified); } @Override public int getBaseline(int width, int height) { return 0; } // from // http://stackoverflow.com/questions/2743177/top-alignment-for-flowlayout // helpful hint of using the Java 1.6 alignBaseLine property of FlowLayout @Override public Component.BaselineResizeBehavior getBaselineResizeBehavior() { return Component.BaselineResizeBehavior.CONSTANT_ASCENT; } /** * Answers an argument holding the value entered or selected in the dialog * * @return */ public ArgumentI getParameter() { ParameterI prm = parameter.copy(); if (isChoiceParameter) { String value = getSelectedValue(this.parameter, choicebox.getSelectedIndex()); prm.setValue(value); } else { prm.setValue(valueField.getText()); } return prm; } public void init() { // reset the widget's initial value. lastVal = null; } @Override public void mouseClicked(MouseEvent e) { if (e.isPopupTrigger()) // for Windows { showUrlPopUp(this, finfo.toString(), e.getX(), e.getY()); } } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) // for Mac { showUrlPopUp(this, finfo.toString(), e.getX(), e.getY()); } } @Override public void mouseReleased(MouseEvent e) { } @Override public void stateChanged(ChangeEvent e) { if (adjusting) { return; } try { adjusting = true; if (!isLogarithmicParameter) { /* * set (int or float formatted) text field value */ valueField.setText(isIntegerParameter ? String.valueOf(slider.getValue()) : formatDouble( slider.getValue() / (float) sliderScaleFactor)); } else { double value = Math.pow(Math.E, slider.getValue() / (double) sliderScaleFactor); valueField.setText(formatDouble(value)); } checkIfModified(); } finally { adjusting = false; } } /** * Answers the value formatted as a string to 3 decimal places - in * scientific notation if the value is less than 0.001 * * @param value * @return */ String formatDouble(double value) { String format = value < 0.001 ? "%3.1E" : "%3.3f"; return String.format(format, value); } /** * Formats a number as integer or float (3dp) or scientific notation (1dp) * * @param n * @return */ String formatNumber(Number n) { return n instanceof Integer ? String.valueOf(n.intValue()) : formatDouble(n.doubleValue()); } void updateControls(ParameterI parm) { adjusting = true; boolean init = (choicebox == null && valueField == null); if (init) { if (isChoiceParameter) { choicebox = buildComboBox(parm); choicebox.addActionListener(this); controlsPanel.add(choicebox, BorderLayout.CENTER); } else { slider = new JSlider(); slider.addChangeListener(this); int cols = parm instanceof StringParameter ? 20 : 0; valueField = new JTextField(cols); valueField.addActionListener(this); valueField.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { int keyCode = e.getKeyCode(); if (e.isActionKey() && keyCode != KeyEvent.VK_LEFT && keyCode != KeyEvent.VK_RIGHT) { if (valueField.getText().trim().length() > 0) { actionPerformed(null); } } } }); valueField.setPreferredSize(new Dimension(65, 25)); controlsPanel.add(slider, BorderLayout.WEST); controlsPanel.add(valueField, BorderLayout.EAST); } } String value = parm.getValue(); if (value != null) { if (isChoiceParameter) { choicebox.setSelectedItem(value); } else { valueField.setText(value); } } lastVal = updateSliderFromValueField(); adjusting = false; } /** * Action depends on the type of the input parameter: *
    *
  • if a text input, returns the trimmed value
  • *
  • if a choice list, returns the selected value
  • *
  • if a value slider and input field, sets the value of the slider from * the value in the text field, limiting it to any defined min-max * range.
  • *
* Answers the (possibly modified) input value, as a String, Integer, Float * or Double. * * @return */ Object updateSliderFromValueField() { if (validator == null || isStringParameter) { if (isChoiceParameter) { return getSelectedValue(this.parameter, choicebox.getSelectedIndex()); } slider.setVisible(false); return valueField.getText().trim(); } valueField.setText(valueField.getText().trim()); /* * ensure not outside min-max range * TODO: provide some visual indicator if limit reached */ try { valueField.setBackground(Color.WHITE); double d = Double.parseDouble(valueField.getText()); if (validator.getMin() != null && validator.getMin().doubleValue() > d) { valueField.setText(formatNumber(validator.getMin())); } if (validator.getMax() != null && validator.getMax().doubleValue() < d) { valueField.setText(formatNumber(validator.getMax())); } } catch (NumberFormatException e) { valueField.setBackground(Color.yellow); return Float.NaN; } if (isIntegerParameter) { int iVal = 0; try { iVal = Integer.valueOf(valueField.getText()); } catch (Exception e) { valueField.setBackground(Color.yellow); return Integer.valueOf(0); } if (validator.getMin() != null && validator.getMax() != null) { slider.getModel().setRangeProperties(iVal, 1, validator.getMin().intValue(), validator.getMax().intValue() + 1, true); } else { slider.setVisible(false); } return new Integer(iVal); } if (isLogarithmicParameter) { double dVal = 0d; try { double eValue = Double.valueOf(valueField.getText()); dVal = Math.log(eValue) * sliderScaleFactor; } catch (Exception e) { // shouldn't be possible here valueField.setBackground(Color.yellow); return Double.NaN; } if (validator.getMin() != null && validator.getMax() != null) { double scaleMin = Math.log(validator.getMin().doubleValue()) * sliderScaleFactor; double scaleMax = Math.log(validator.getMax().doubleValue()) * sliderScaleFactor; slider.getModel().setRangeProperties((int) (dVal), 1, (int) scaleMin, 1 + (int) scaleMax, true); } else { slider.setVisible(false); } return new Double(dVal); } float fVal = 0f; try { fVal = Float.valueOf(valueField.getText()); } catch (Exception e) { return Float.valueOf(0f); // shouldn't happen } if (validator.getMin() != null && validator.getMax() != null) { float scaleMin = validator.getMin().floatValue() * sliderScaleFactor; float scaleMax = validator.getMax().floatValue() * sliderScaleFactor; slider.getModel().setRangeProperties( (int) (fVal * sliderScaleFactor), 1, (int) scaleMin, 1 + (int) scaleMax, true); } else { slider.setVisible(false); } return new Float(fVal); } } /** * Constructor with the option to show 'compact' format (parameter description * as tooltip) or 'expanded' format (parameter description in a textbox which * may be opened or closed). Use compact for simple description text, expanded * for more wordy or formatted text. * * @param paramContainer */ public OptsAndParamsPage(OptsParametersContainerI paramContainer, boolean compact) { poparent = paramContainer; this.compact = compact; } public static void showUrlPopUp(JComponent invoker, final String finfo, int x, int y) { JPopupMenu mnu = new JPopupMenu(); JMenuItem mitem = new JMenuItem( MessageManager.formatMessage("label.view_params", new String[] { finfo })); mitem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Desktop.showUrl(finfo); } }); mnu.add(mitem); mnu.show(invoker, x, y); } public Map getOptSet() { return optSet; } public void setOptSet(Map optSet) { this.optSet = optSet; } public Map getParamSet() { return paramSet; } public void setParamSet(Map paramSet) { this.paramSet = paramSet; } OptionBox addOption(OptionI opt) { OptionBox cb = optSet.get(opt.getName()); if (cb == null) { cb = new OptionBox(opt); optSet.put(opt.getName(), cb); // jobOptions.add(cb, FlowLayout.LEFT); } return cb; } ParamBox addParameter(ParameterI arg) { ParamBox pb = paramSet.get(arg.getName()); if (pb == null) { pb = new ParamBox(poparent, arg); paramSet.put(arg.getName(), pb); // paramList.add(pb); } pb.init(); // take the defaults from the parameter pb.updateControls(arg); return pb; } void selectOption(OptionI option, String string) { OptionBox cb = optSet.get(option.getName()); if (cb == null) { cb = addOption(option); } cb.enabled.setSelected(string != null); // initial state for an option. if (string != null) { if (option.getPossibleValues().contains(string)) { cb.val.setSelectedItem(string); } else { throw new Error(String.format("Invalid value '%s' for option '%s'", string, option.getName())); } } if (option.isRequired() && !cb.enabled.isSelected()) { // TODO: indicate paramset is not valid.. option needs to be selected! } cb.setInitialValue(); } void setParameter(ParameterI arg) { ParamBox pb = paramSet.get(arg.getName()); if (pb == null) { addParameter(arg); } else { pb.updateControls(arg); } } /** * Answers a list of arguments representing all the options and arguments * selected on the dialog, holding their chosen or input values. Optional * parameters which were not selected are not included. * * @return */ public List getCurrentSettings() { List argSet = new ArrayList<>(); for (OptionBox opts : getOptSet().values()) { ArgumentI opt = opts.getSelectedOption(); if (opt != null) { argSet.add(opt); } } for (ParamBox parambox : getParamSet().values()) { ArgumentI parm = parambox.getParameter(); if (parm != null) { argSet.add(parm); } } return argSet; } /** * A helper method that constructs and returns a CombBox for choice of the * possible option values. If display names are provided, then these are added * as options, otherwise the actual values are added. * * @param opt * @return */ protected JComboBox buildComboBox(OptionI opt) { JComboBox cb = null; List displayNames = opt.getDisplayNames(); if (displayNames != null) { cb = JvSwingUtils.buildComboWithTooltips(displayNames, opt.getPossibleValues()); } else { cb = new JComboBox<>(); for (String v : opt.getPossibleValues()) { cb.addItem(v); } } return cb; } /* * Answers the value corresponding to the selected item in the choice combo * box. If display names were not provided, this is simply the selected * value. If display names were provided, it is the value corresponding to * the selected item index. * * @return */ protected static String getSelectedValue(OptionI opt, int sel) { List possibleValues = opt.getPossibleValues(); String value = null; if (possibleValues != null && possibleValues.size() == 1) { // Hack to make sure the default value for an enabled option with only // one value is actually returned even if this.val is not displayed value = possibleValues.get(0); } else { if (sel >= 0 && sel < possibleValues.size()) { value = possibleValues.get(sel); } else { value = opt.getValue(); } } return value; } }