/* * 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.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; public OptionBox(OptionI opt) { option = opt; setLayout(new FlowLayout(FlowLayout.LEFT)); enabled = new JCheckBox(); enabled.setSelected(opt.isRequired()); // TODO: lock required options enabled.setFont(new Font("Verdana", Font.PLAIN, 11)); enabled.setText(""); enabled.setText(opt.getName()); enabled.addActionListener(this); finfo = option.getFurtherDetails(); String desc = opt.getDescription(); if (finfo != null) { hasLink = true; enabled.setToolTipText(JvSwingUtils.wrapTooltip(true, ((desc == null || desc.trim().length() == 0) ? MessageManager.getString( "label.opt_and_params_further_details") : desc) + "
")); enabled.addMouseListener(this); } else { if (desc != null && desc.trim().length() > 0) { enabled.setToolTipText( JvSwingUtils.wrapTooltip(true, opt.getDescription())); } } add(enabled); // todo combo or radio buttons? val = new JComboBox<>(); for (String str : opt.getPossibleValues()) { val.addItem(str); } val.setSelectedItem(opt.getValue()); if (opt.getPossibleValues().size() > 1 || opt.isRequired()) { val.addActionListener(this); add(val); } // TODO: add actionListeners for popup (to open further info), // and to update list of parameters if an option is enabled // that takes a value. JBPNote: is this TODO still valid ? setInitialValue(); } @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); } public OptionI getOptionIfEnabled() { if (!enabled.isSelected()) { return null; } OptionI opt = option.copy(); if (opt.getPossibleValues() != null && opt.getPossibleValues().size() == 1) { // Hack to make sure the default value for an enabled option with only // one value is actually returned opt.setValue(opt.getPossibleValues().get(0)); } if (val.getSelectedItem() != null) { opt.setValue((String) val.getSelectedItem()); } else { if (option.getValue() != null) { opt.setValue(option.getValue()); } } 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.size() > 1; 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; } public ParameterI getParameter() { ParameterI prm = parameter.copy(); if (isChoiceParameter) { prm.setValue((String) choicebox.getSelectedItem()); } 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) { 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(); } } /** * 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 = new JComboBox<>(); 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); } } if (parm != null) { if (isChoiceParameter) { if (init) { for (String val : parm.getPossibleValues()) { choicebox.addItem(val); } } if (parm.getValue() != null) { choicebox.setSelectedItem(parm.getValue()); } } else { valueField.setText(parm.getValue()); } } 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 choicebox.getSelectedItem(); } 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); } } /** * recover options and parameters from GUI * * @return */ public List getCurrentSettings() { List argSet = new ArrayList<>(); for (OptionBox opts : getOptSet().values()) { OptionI opt = opts.getOptionIfEnabled(); if (opt != null) { argSet.add(opt); } } for (ParamBox parambox : getParamSet().values()) { ParameterI parm = parambox.getParameter(); if (parm != null) { argSet.add(parm); } } return argSet; } }