QWindowBuilder.java
package com.github.celldynamics.quimp.plugin.utils;
import java.awt.BorderLayout;
import java.awt.Choice;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.Label;
import java.awt.Panel;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTextPane;
import javax.swing.SpinnerNumberModel;
import org.apache.commons.lang3.text.WordUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.celldynamics.quimp.plugin.ParamList;
/**
* Simple window builder for QuimP plugins
*
* <p>Allow user to construct simple window for his plugins just by passing textual description of
* what
* that window should contain.
*
* <p>Main function (BuildWindow) accepts HashMap with pairs [name,params] where name is unique name
* of
* the parameter and params defines how this parameter will be displayed in UI (see
* BuildWindow(final ParamList)). Using this mapping there is next list ui created that contains
* the same names but now joined with UI components. This list is used for addressing these
* component basing on theirs names. The UI controls are stored at ui which is protected and
* may be used for influencing these controls by user. To identify certain UI control its name is
* required which is the string passed as first dimension of def definition passed to to
* BuildWindow method. Below code shows how to change property of control
*
* <pre>
* <code>
* String key = "paramname"; // case insensitive JSpinner comp = (JSpinner)
* ui.get(key); // get control using its name
* comp.getEditor()).getTextField().setColumns(5);
* </code>
* </pre>
*
* <p><b>Warning</b>
*
* <p>UI type as JSpinner keeps data in double format even in values passed through by
* setValues(ParamList) are integer (ParamList keeps data as String). Therefore getValues can return
* this list with the same data but in double syntax (5 -> 5.0). Any try of convention of "5.0" to
* integer value will cause NumberFormatException. To avoid this problem use
* QuimP.plugin.ParamList.getIntValue(String) from ParamList of treat all strings in ParamList as
* Double.
*
* <p>Methods getValues() and setValues() should be used by class extending QWindowBuilder for
* setting
* and achieving parameters from GUI. Note that parameters in UIs are validated only when they
* become out of focus. Until cursor is in UI its value is not updated internally, thus getValues()
* returns its old snapshot.
*
* <p>reservedKeys is list of reserved keys that are not UI elements. They are processed in
* different
* way. Other behaviour: By default on close or when user clicked Cancel window is hided only, not
* destroyed. This is due to preservation of all settings. Lifetime of window depends on QuimP
*
* <p>All parameters passed to and from QWindowBuilder as ParamList are encoded as {@link String}
*
* @author p.baniukiewicz
*/
public abstract class QWindowBuilder {
/**
* The Constant LOGGER.
*/
static final Logger LOGGER = LoggerFactory.getLogger(QWindowBuilder.class.getName());
/**
* Delimiter used in UI definition strings.
*/
public static final char DELIMITER = ':';
/**
* The plugin wnd.
*/
protected JFrame pluginWnd; // main window object
/**
* The window state.
*/
protected boolean windowState; // current window state \c true if visible
/**
* The plugin panel.
*/
protected JPanel pluginPanel; // Main panel extended on whole \c pluginWnd
/**
* The ui.
*/
protected ComponentList ui; // list of all UI elements
/** The def. */
private ParamList def; // definition of window and parameters
/** The reserved keys. */
private final HashSet<String> reservedKeys =
new HashSet<String>(Arrays.asList(new String[] { "help", "name" })); // reserved keys
/** The ui type. */
// definition string - positions of configuration data in value string (see BuildWindow)
private final int uiType = 0; // type of UI control to create
/** The sr min. */
private final int srMin = 1; // spinner min value
/** The sr max. */
private final int srMax = 2; // spinner max value
/** The sr step. */
private final int srStep = 3; // spinner step value
/** The sr default. */
private final int srDefault = 4; // spinner default value
/** The sr fract. */
private final int srFract = 5; // spinner number of fractional places
/**
* The apply B.
*/
protected JButton applyB; // Apply button (do nothing but may be overwritten)
/**
* The cancel B.
*/
protected JButton cancelB; // Cancel button (hides it)
/**
* Default constructor.
*/
public QWindowBuilder() {
LOGGER.trace("Entering constructor");
ui = new ComponentList();
def = null;
}
/**
* Main window builder.
*
* <p>The window is constructed using configuration provided by def parameter which is Map of
* [key,value]. The key is the name of the parameter that should be related to value held in it
* (e.g window, smooth, step, etc.). The name is not case sensitive but should not contain spaces
* (use underscore). Keys are strictly related
* to UI elements that are created by this method (basing on configuration passed in value). Keys
* are used to produce UI control label, any underscore is replaced by space and key is
* capitalised.
* There are two special names that are not related to UI directly:
* <ol>
* <li>help - defines textual help provided as parameter. It supports HTML
* <li>name - defines plugin name provided as parameter
* </ol>
*
* <p>The parameter list is defined as String and its content is depended on key. For help and
* name it contains single string with help text and plugin name respectively.
*
* <p>The UI elements are defined for all other cases in value filed of Map as comma separated
* string. UI element name is case insensitive. Known UI are as follows:
* <ul>
* <li>spinner - creates Spinner control. It requires 4 parameters (in order). Fifth parameter is
* help text and it is <b>optional</b>
* <ol>
* <li>minimal range
* <li>maximal range
* <li>step
* <li>default value
* </ol>
* <li>spinnerd - creates Spinner control. It requires 5 parameters (in order). Sixth parameter is
* help text and it is <b>optional</b>
* <ol>
* <li>minimal range
* <li>maximal range
* <li>step
* <li>default value
* <li>Precision as number of fractional numbers
* </ol>
* <li>choiceh - creates Choice control. It requires 1 or more parameters - entries in list. Last
* parameter is help text and it is <b>compulsory</b>
* <ol>
* <li>first entry
* <li>second entry
* <li>...
* <li>help
* </ol>
* <li>choice - creates Choice control without help. It requires 0 or more parameters
* <ol>
* <li>first entry
* <li>second entry
* <li>...
* </ol>
* <li>button - creates JButton control. It requires 1 parameter. Last parameter is
* help text and it is <b>optional</b>
* <ol>
* <li>Name on the button
* </ol>
* <li>checkbox - creates JCheckBox control. It requires 2 parameters (in order). Last parameter
* is help text and it is <b>optional</b>
* <ol>
* <li>checkbox name
* <li>initial value (true or false)
* </ol>
* </ul>
* For choice calling {@link QWindowBuilder#setValues(ParamList)} is justified only if passed
* parameters will be present in list already (so it has
* been used during creation of window, passed in constructor) In this case it causes selection
* of this entry in list. Otherwise passed value will be ignored. setVales for Choice does
* not add new entry to list.
*
* <p>The type of required UI element associated with given parameter name (Key) is coded in
* value of given Key in accordance with list above. The correct order of sub-parameters must be
* preserved. By default window is not visible yet. User must call ShowWindow or ToggleWindow. The
* Apply
* button does nothing. It is only to refocus after change of values in spinners. They are not
* updated until unfocused. User can overwrite this behaviour in his own class derived from
* QWindowBuilder
*
* <p>This method can be overridden in implementing class that allows for e.g. setting size of the
* window or add listeners:
*
* <pre>
* <code>
* public void buildWindow(ParamList def) {
* super.buildWindow(def); // add preferred size to this window
* pluginWnd.setPreferredSize(new Dimension(300, 450));
* pluginWnd.pack();
* pluginWnd.setVisible(true);
* pluginWnd.addWindowListener(new myWindowAdapter()); // close not hide ((JButton)
* ui.get("Load Mask")).addActionListener(this);
* applyB.addActionListener(this);
* }
* </code>
* </pre>
*
* <p>Throw IllegalArgumentException or other unchecked exceptions on wrong syntax of def
*
* @param def Configuration as described
*/
public void buildWindow(final ParamList def) {
if (def.size() < 2) {
throw new IllegalArgumentException("Window must contain title and" + " at least one control");
}
this.def = def; // remember parameters
ui.clear(); // clear all ui stored on second call of third method
pluginWnd = new JFrame(); // create frame with title given as first position in table
pluginPanel = new JPanel(); // main panel on whole window
// divide window on two zones
// - upper for controls,
// - middle for help
pluginPanel.setLayout(new BorderLayout());
Panel north = new Panel(); // panel for controls
// get layout size
int siz = def.size(); // total size of Map
// but grid layout does not contain help and name or other reserved non
// UI keys
Set<String> s = def.keySet(); // get Set of keys
for (String k : reservedKeys) { // and check if any of them is in s
if (s.contains(k)) {
siz--;
}
}
GridLayout gridL = new GridLayout(siz, 3); // Nx3, by default in row we have control and its des
north.setLayout(gridL);
gridL.setVgap(5); // set bigger gaps
gridL.setHgap(5);
String helpText;
// iterate over def entries except first one which is always title
// every decoded control is put into ordered hashmap together with its
// descriptor (label)
for (Map.Entry<String, String> e : def.entrySet()) {
String key = e.getKey();
if (reservedKeys.contains(key)) {
continue;
}
String[] uiparams = StringParser.getParams(e.getValue(), DELIMITER);
if (uiparams.length == 0) {
throw new IllegalArgumentException("Probably wrong syntax in UI definition");
}
switch (uiparams[uiType].toLowerCase()) {
case "spinner": // by default all spinners are double
{
helpText = spinnerVerify(uiparams);
SpinnerNumberModel model = new SpinnerNumberModel(Double.parseDouble(uiparams[srDefault]),
Double.parseDouble(uiparams[srMin]), // min
Double.parseDouble(uiparams[srMax]), // max
Double.parseDouble(uiparams[srStep])); // step
JSpinner sp = new JSpinner(model);
ui.put(key, sp);
String lab = WordUtils.capitalize(key.replaceAll("_", " "));
ui.put(key + "label", new Label(lab)); // des
ui.put(key + "help", new Label(helpText));
}
break;
case "spinnerd": // by default all spinners are double
{
helpText = spinnerdVerify(uiparams);
SpinnerNumberModel model = new SpinnerNumberModel(Double.parseDouble(uiparams[srDefault]),
Double.parseDouble(uiparams[srMin]), // min
Double.parseDouble(uiparams[srMax]), // max
Double.parseDouble(uiparams[srStep])); // step
JSpinner sp = new JSpinner(model);
String c = "";
Double val = Double.parseDouble(uiparams[srFract]);
if (val == 0) {
c = "0";
} else {
c = "0." + String.join("", Collections.nCopies(val.intValue(), "0"));
}
sp.setEditor(new JSpinner.NumberEditor(sp, c));
ui.put(key, sp);
String lab = WordUtils.capitalize(key.replaceAll("_", " "));
ui.put(key + "label", new Label(lab)); // des
ui.put(key + "help", new Label(helpText));
}
break;
case "choiceh": {
helpText = choiceVerify(uiparams);
Choice c = new Choice();
for (int i = uiType + 1; i < uiparams.length - 1; i++) {
c.add(uiparams[i]);
}
c.select(0);
ui.put(key, c);
String lab = WordUtils.capitalize(key.replaceAll("_", " "));
ui.put(key + "label", new Label(lab)); // des
ui.put(key + "help", new Label(helpText));
}
break;
case "choice": {
if (uiparams.length < 1) { // default
throw new IllegalArgumentException(
"Probably wrong syntax in UI definition for " + uiparams[uiType]);
}
Choice c1 = new Choice();
for (int i = uiType + 1; i < uiparams.length; i++) {
c1.add(uiparams[i]);
}
c1.select(0);
ui.put(key, c1);
String lab = WordUtils.capitalize(key.replaceAll("_", " "));
ui.put(key + "label", new Label(lab)); // add description
ui.put(key + "help", new Label(""));
}
break;
case "button": {
helpText = buttonVerify(uiparams);
JButton b = new JButton(uiparams[1]);
ui.put(key, b);
String lab = WordUtils.capitalize(key.replaceAll("_", " "));
ui.put(key + "label", new Label(lab)); // des
ui.put(key + "help", new Label(helpText));
}
break;
case "checkbox": {
helpText = checkboxVerify(uiparams);
JCheckBox cb = new JCheckBox(WordUtils.capitalize(uiparams[1]),
Boolean.parseBoolean(uiparams[2]));
ui.put(key, cb);
String lab = WordUtils.capitalize(key.replaceAll("_", " "));
ui.put(key + "label", new Label(lab)); // des
ui.put(key + "help", new Label(helpText));
}
break;
default:
// wrong param syntax
throw new IllegalArgumentException("Unknown ui type" + " provided: " + key);
}
}
// iterate over all components and add them to grid layout
for (Map.Entry<String, Component> me : ui.entrySet()) {
north.add(me.getValue());
}
// add non ui elements
if (def.containsKey("name")) {
// border on whole window
pluginPanel.setBorder(BorderFactory.createTitledBorder("Plugin " + def.get("name")));
}
if (def.containsKey("help")) {
JTextPane helpArea = new JTextPane(); // default size of text area
// helpArea.setLineWrap(true);
// helpArea.setWrapStyleWord(true);
helpArea.setPreferredSize(new Dimension(80, 200));
helpArea.setContentType("text/html");
JScrollPane helpPanel = new JScrollPane(helpArea);
helpArea.setEditable(false);
pluginPanel.add(helpPanel, BorderLayout.CENTER); // locate at center position
helpArea.setText(def.get("help")); // set help text
}
// add Apply button on south
Panel south = new Panel();
south.setLayout(new FlowLayout());
applyB = new JButton("Apply");
south.add(applyB);
cancelB = new JButton("Cancel");
south.add(cancelB);
// set action on Cancel - window is hided
cancelB.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
pluginWnd.setVisible(false);
windowState = false;
}
});
// build window
pluginPanel.add(north, BorderLayout.NORTH);
pluginPanel.add(south, BorderLayout.SOUTH);
pluginWnd.add(pluginPanel);
pluginWnd.pack();
// add listener on close - window is hidden to preserve settings
pluginWnd.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent we) {
LOGGER.trace("Window closing");
windowState = false;
pluginWnd.setVisible(false);
}
});
windowState = false; // by default window is not visible. User must call ShowWindow or ToggleWi
}
/**
* Verify syntax for spinner and return help text if any.
*
* @param uiparams uiparams
* @return Help text (last from list)
*/
private String spinnerVerify(String[] uiparams) {
String helpText = "";
if (uiparams.length != 5) { // default
if (uiparams.length != 6) { // with help text
throw new IllegalArgumentException(
"Probably wrong syntax in UI definition for " + uiparams[uiType]);
} else {
helpText = uiparams[5];
}
}
return helpText;
}
/**
* Verify syntax for spinner and return help text if any.
*
* @param uiparams uiparams
* @return Help text (last from list)
*/
private String spinnerdVerify(String[] uiparams) {
String helpText = "";
if (uiparams.length != 6) { // default
if (uiparams.length != 7) { // with help text
throw new IllegalArgumentException(
"Probably wrong syntax in UI definition for " + uiparams[uiType]);
} else {
helpText = uiparams[5];
}
}
return helpText;
}
/**
* Verify syntax for choice and return help text if any.
*
* @param uiparams uiparams
* @return Help text (last from list)
*/
private String choiceVerify(String[] uiparams) {
String helpText = "";
if (uiparams.length < 2) { // default
throw new IllegalArgumentException(
"Probably wrong syntax in UI definition for " + uiparams[uiType]);
} else {
helpText = uiparams[uiparams.length - 1];
}
return helpText;
}
/**
* Verify syntax for button and return help text if any.
*
* @param uiparams uiparams
* @return Help text (last from list)
*/
private String buttonVerify(String[] uiparams) {
String helpText = "";
if (uiparams.length != 2) { // default
if (uiparams.length != 3) {
throw new IllegalArgumentException(
"Probably wrong syntax in UI definition for " + uiparams[uiType]);
} else {
helpText = uiparams[2];
}
}
return helpText;
}
/**
* Verify syntax for checkbox and return help text if any.
*
* @param uiparams uiparams
* @return Help text (last from list)
*/
private String checkboxVerify(String[] uiparams) {
String helpText = "";
if (uiparams.length != 3) { // default
if (uiparams.length != 4) {
throw new IllegalArgumentException(
"Probably wrong syntax in UI definition for " + uiparams[uiType]);
} else {
helpText = uiparams[3];
}
}
return helpText;
}
/**
* Show or hide window.
*
* @param state State of the window true to show, false to hide
*/
public void showWindow(boolean state) {
pluginWnd.setVisible(state);
windowState = state;
}
/**
* Toggle window visibility if input is \c true. Close it immediately if input is \c false
*
* @param val Demanded state of window. If \c true visibility of window is toggled, if \c false
* window is closing.
* @return Current status of window \c true if visible, \c false if not
*/
public boolean toggleWindow(boolean val) {
if (!val) {
windowState = false;
showWindow(windowState);
} else {
windowState = !windowState;
showWindow(windowState);
}
return windowState;
}
/**
* Check if window is visible.
*
* @return true if it is visible, false otherwise
*/
public boolean isWindowVisible() {
return windowState;
}
/**
* Set plugin parameters.
*
* <p>Use the same parameters names as in BuildWindow(Map[String, String[]]). The name of the
* parameter is key in Map. Every parameter passed to this method should have its
* representation in GUI and thus it must be present in def parameter of
* BuildWindow(Map[String, String[]]) All values are passed as:
* <ol>
* <li>Double in case of spinners
* </ol>
*
* <p>User has to care for correct format passed to UI control. If input values are above range
* defined in def, new range is set for UI control. Unknown keys are skipped.
*
* @param vals [key,value] pairs to fill UI.
*/
public void setValues(final ParamList vals) {
// iterate over parameters and match names to UIs
for (Map.Entry<String, String> e : vals.entrySet()) {
String key = e.getKey();
String val = e.getValue();
// find key in def and get type of control and its instance
String[] ret = def.getParsed(key, DELIMITER);
if (ret.length == 0) { // skip unknown codes
return;
}
switch (ret[uiType].toLowerCase()) { // first string in vals is type
// control, see BuildWindow
case "spinnerd":
case "spinner": {
// get UI component of name key (keys in vals must match to keys in BuildWindow(def))
JSpinner comp = (JSpinner) ui.get(key);
comp.setValue(Double.parseDouble(val)); // set value from vals
SpinnerNumberModel sm = (SpinnerNumberModel) comp.getModel();
if (sm.getNextValue() == null) {
sm.setMaximum(Double.parseDouble(val));
} else if (sm.getPreviousValue() == null) {
sm.setMinimum(Double.parseDouble(val));
}
break;
}
case "choiceh":
case "choice": {
Choice comp = (Choice) ui.get(key);
comp.select(val);
break;
}
case "button": {
break;
}
case "checkbox": {
JCheckBox c = (JCheckBox) ui.get(key);
c.setSelected(Boolean.parseBoolean(val));
break;
}
default:
throw new IllegalArgumentException(
"Unknown UI type in setValues: " + def.getParsed(key, DELIMITER)[uiType]);
}
}
}
/**
* Receives parameters related to UI elements as Map.
*
* <p>To get one particular parameter use getIntegerFromUI(String) or getDoubleFromUI(String)
*
* <p>JSpinners are set to support double values and that values are returned here. It means that
* originally pushed to UI integers are changed to Double what can affect set/getpluginConfig
* from filter interface as well
*
* @return List of [key,param], where key is the name of parameter passed to QWindowBuilder
* class through BuildWindow method. The method remaps those keys to related UI controls
* and reads values associated to them.
* @see #getDoubleFromUI(String)
* @see #getIntegerFromUI(String)
*
*/
public ParamList getValues() {
ParamList ret = new ParamList();
// iterate over all UI elements
Iterator<Map.Entry<String, Component>> entryIterator = ui.entrySet().iterator();
while (entryIterator.hasNext()) {
Map.Entry<String, Component> m = entryIterator.next();
String key = m.getKey();
// check type of component
switch (def.getParsed(key, DELIMITER)[uiType].toLowerCase()) {
case "spinnerd":
case "spinner": {
JSpinner val = (JSpinner) m.getValue(); // get value
ret.put(key, String.valueOf(val.getValue())); // store it in returned Map at
// the same key
break;
}
case "choiceh":
case "choice": {
Choice val = (Choice) m.getValue();
ret.put(key, val.getSelectedItem());
break;
}
case "button": {
break;
}
case "checkbox": {
JCheckBox c = (JCheckBox) m.getValue();
ret.put(key, String.valueOf(c.isSelected()));
break;
}
default:
throw new IllegalArgumentException(
"Unknown UI type in getValues: " + def.getParsed(key, DELIMITER)[uiType]);
}
entryIterator.next(); // skip label. ui Map has repeating entries UI,label,UI1,label1,..
entryIterator.next(); // skip help
}
return ret;
}
/**
* Return value related to given key.
*
* <p>Value is retrieved from ui element related to given \b key. Relation between keys and ui
* elements is defined by user in configuration list provided to buildWindow(final ParamList).
*
* <p>The key must be defined and exists in that list. In case of wrong conversion it may be
* exception thrown. User is responsible to call this method for proper key.
*
* @param key Key to be read from configuration list, case insensitive
* @return integer representation of value under \c key
* @see #buildWindow(ParamList)
*/
public int getIntegerFromUI(final String key) {
return (int) getDoubleFromUI(key);
}
/**
* Return value related to given key.
*
* @param key key
* @return {@link #getIntegerFromUI(String)}
*
* @see #getIntegerFromUI(String)
*/
public double getDoubleFromUI(final String key) {
// get list of all params from ui as <key,val> list
ParamList uiParam = getValues();
return uiParam.getDoubleValue(key);
}
/**
* Return value related to given key.
*
* @param key key
* @return {@link #getIntegerFromUI(String)}
*
* @see #getIntegerFromUI(String)
*/
public boolean getBooleanFromUI(final String key) {
// get list of all params from ui as <key,val> list
ParamList uiParam = getValues();
return uiParam.getBooleanValue(key);
}
/**
* Return value related to given key. Added for convenience
*
* @param key key
* @return {@link #getIntegerFromUI(String)}
*
* @see #getIntegerFromUI(String)
*/
public String getStringFromUI(final String key) {
// get list of all params from ui as <key,val> list
ParamList uiParam = getValues();
return uiParam.getStringValue(key);
}
/**
* Stores components under Keys that are not case insensitive.
*
* @author p.baniukiewicz
*/
public class ComponentList extends LinkedStringMap<Component> {
/** The Constant serialVersionUID. */
private static final long serialVersionUID = -5157229346595354602L;
}
}