/*
* Copyright (c) 2014 tabletoptool.com team.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*
* Contributors:
* rptools.com team - initial implementation
* tabletoptool.com team - further development
*/
package com.t3.macro.api.functions.input;
import java.awt.Color;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.Box;
import javax.swing.ButtonGroup;
import javax.swing.Icon;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JTextField;
import javax.swing.border.EtchedBorder;
import javax.swing.border.TitledBorder;
import org.apache.commons.lang3.StringUtils;
import com.t3.client.ui.htmlframe.HTMLPane;
import com.t3.macro.api.functions.input.InputType.OptionException;
/**
* Contains input controls, which are arranged in a two-column label +
* control layout.
*/
@SuppressWarnings("serial")
public final class ColumnPanel extends JPanel {
private static final Pattern ASSET_PATTERN = Pattern.compile("^(.*)asset://(\\w+)");
public VarSpec tabVarSpec; // VarSpec for this subpanel's tab, if any
public List<VarSpec> varSpecs;
public List<JComponent> labels; // the labels in the left column
public List<JComponent> inputFields; // the input controls (some may be panels for composite inputs)
public JComponent lastFocus; // last input field with the focus
public JComponent onShowFocus; // field to gain focus when shown
private Insets textInsets = new Insets(0, 2, 0, 2); // used by all text controls
private GridBagConstraints gbc = new GridBagConstraints();
private int componentCount;
public int maxHeightModifier = 0;
public ColumnPanel() {
tabVarSpec = null;
varSpecs = new ArrayList<VarSpec>();
labels = new ArrayList<JComponent>();
inputFields = new ArrayList<JComponent>();
lastFocus = null;
onShowFocus = null;
textInsets = new Insets(0, 2, 0, 2); // used by all TEXT controls
setLayout(new GridBagLayout());
gbc = new GridBagConstraints();
gbc.anchor = GridBagConstraints.NORTHWEST;
gbc.insets = new Insets(2, 2, 2, 2);
componentCount = 0;
}
public ColumnPanel(List<VarSpec> lvs) {
this(); // Initialize various member variables
varSpecs = lvs;
for (VarSpec vs : varSpecs) {
addVariable(vs, false);
}
}
/** Adds a row to the ColumnPanel with a label and input field. */
public void addVariable(VarSpec vs) {
addVariable(vs, true);
}
/**
* Adds a row to the ColumnPanel with a label and input field.
* <code>addToVarList</code> controls whether the VarSpec is added to
* the local listing.
*/
protected void addVariable(VarSpec vs, boolean addToVarList) {
if (addToVarList)
varSpecs.add(vs);
gbc.gridy = componentCount;
gbc.gridwidth = 1;
// add the label
gbc.gridx = 0;
JComponent l;
Matcher m = Pattern.compile("^\\s*<html>(.*)<\\/html>\\s*$").matcher(vs.prompt);
if (m.find()) {
// For HTML values we use a HTMLPane.
HTMLPane htmlp = new HTMLPane();
htmlp.setText("<html>" + m.group(1) + ":</html>");
htmlp.setBackground(Color.decode("0xECE9D8"));
l = htmlp;
} else {
l = new JLabel(vs.prompt + ":");
}
labels.add(l);
if (!vs.optionValues.optionEquals("SPAN", "TRUE")) {
// if the control is not set to span, we include the prompt label
add(l, gbc);
}
// add the input component
JComponent inputField = createInputControl(vs);
if (vs.optionValues.optionEquals("SPAN", "TRUE")) {
gbc.gridwidth = 2; // the control spans both columns
inputField.setToolTipText(vs.prompt);
} else {
gbc.gridx = 1; // the control lives in the second column
}
inputFields.add(inputField);
add(inputField, gbc);
componentCount++;
}
/** Finds the first focusable control. */
public JComponent findFirstFocusable() {
for (JComponent c : inputFields) {
if (c instanceof ColumnPanel) {
ColumnPanel cp = (ColumnPanel) c;
JComponent firstInPanel = cp.findFirstFocusable();
if (firstInPanel != null) {
return firstInPanel;
}
} else if (!(c instanceof JLabel)) {
return c;
}
}
return null;
}
/** Sets the focus to the control that last had it. */
public void restoreFocus() {
// // debugging
// String s = (onShowFocus instanceof JTextField) ?
// " (" + ((JTextField)onShowFocus).getText() + ")" : "";
// String c = (onShowFocus == null) ? "null" : onShowFocus.getClass().getName();
// System.out.println(" Shown: onShowFocus is " + c + s);
if (onShowFocus != null) {
onShowFocus.requestFocusInWindow();
} else {
JComponent first = findFirstFocusable();
if (first != null)
first.requestFocusInWindow();
}
}
/** Adjusts the runtime behavior of controls. Called when displayed. */
public void runtimeFixup() {
// When first displayed, the focus will go to the first field.
lastFocus = findFirstFocusable();
// When a field gains the focus, save it in lastFocus.
FocusListener listener = new FocusListener() {
@Override
public void focusGained(FocusEvent fe) {
JComponent src = (JComponent) fe.getSource();
lastFocus = src;
if (src instanceof JTextField)
((JTextField) src).selectAll();
// // debugging
// String s = (src instanceof JTextField) ?
// " (" + ((JTextField)src).getText() + ")" : "";
// System.out.println(" Got focus " + src.getClass().getName() + s);
}
@Override
public void focusLost(FocusEvent arg0) {
}
};
for (JComponent c : inputFields) {
// Each control saves itself to lastFocus when it gains focus.
c.addFocusListener(listener);
// Implement control-specific adjustments
if (c instanceof ColumnPanel) {
ColumnPanel cp = (ColumnPanel) c;
cp.runtimeFixup();
}
}
if (lastFocus != null)
scrollRectToVisible(lastFocus.getBounds());
}
/** Creates the appropriate type of input control. */
public JComponent createInputControl(VarSpec vs) {
switch (vs.inputType) {
case TEXT:
return createTextControl(vs);
case LIST:
return createListControl(vs);
case CHECK:
return createCheckControl(vs);
case RADIO:
return createRadioControl(vs);
case LABEL:
return createLabelControl(vs);
case PROPS:
return createPropsControl(vs);
case TAB:
return null; // should never happen
default:
return null;
}
}
/** Creates a text input control. */
public JComponent createTextControl(VarSpec vs) {
int width = vs.optionValues.getNumeric("Width");
JTextField txt = new JTextField(vs.value, width);
txt.setMargin(textInsets);
return txt;
}
/** Creates a dropdown list control. */
public JComponent createListControl(VarSpec vs) {
JComboBox combo;
boolean showText = vs.optionValues.optionEquals("TEXT", "TRUE");
boolean showIcons = vs.optionValues.optionEquals("ICON", "TRUE");
int iconSize = vs.optionValues.getNumeric("ICONSIZE", 0);
if (iconSize <= 0)
showIcons = false;
// Build the combo box
for (int j = 0; j < vs.valueList.size(); j++) {
if (StringUtils.isEmpty(vs.valueList.get(j))) {
// Using a non-empty string prevents the list entry from having zero height.
vs.valueList.set(j, " ");
}
}
if (!showIcons) {
// Swing has an UNBELIEVABLY STUPID BUG when multiple items in a JComboBox compare as equal.
// The combo box then stops supporting navigation with arrow keys, and
// no matter which of the identical items is chosen, it returns the index
// of the first one. Sun closed this bug as "by design" in 1998.
// A workaround found on the web is to use this alternate string class (defined below)
// which never reports two items as being equal.
NoEqualString[] nesValues = new NoEqualString[vs.valueList.size()];
for (int i = 0; i < nesValues.length; i++)
nesValues[i] = new NoEqualString(vs.valueList.get(i));
combo = new JComboBox(nesValues);
} else {
combo = new JComboBox();
combo.setRenderer(new ComboBoxRenderer());
Pattern pattern = ASSET_PATTERN;
for (String value : vs.valueList) {
Matcher matcher = pattern.matcher(value);
String valueText, assetID;
Icon icon = null;
// See if the value string for this item has an image URL inside it
if (matcher.find()) {
valueText = matcher.group(1);
assetID = matcher.group(2);
} else {
valueText = value;
assetID = null;
}
// Assemble a JLabel and put it in the list
UpdatingLabel label = new UpdatingLabel();
icon = InputFunctions.getIcon(assetID, iconSize, label);
label.setOpaque(true); // needed to make selection highlighting show up
if (showText)
label.setText(valueText);
if (icon != null)
label.setIcon(icon);
combo.addItem(label);
}
}
int listIndex = vs.optionValues.getNumeric("SELECT");
if (listIndex < 0 || listIndex >= vs.valueList.size())
listIndex = 0;
combo.setSelectedIndex(listIndex);
combo.setMaximumRowCount(20);
return combo;
}
/** Creates a single checkbox control. */
public JComponent createCheckControl(VarSpec vs) {
JCheckBox check = new JCheckBox();
check.setText(" "); // so a focus indicator will appear
if (vs.value.compareTo("0") != 0)
check.setSelected(true);
return check;
}
/** Creates a group of radio buttons. */
public JComponent createRadioControl(VarSpec vs) {
int listIndex = vs.optionValues.getNumeric("SELECT");
if (listIndex < 0 || listIndex >= vs.valueList.size())
listIndex = 0;
ButtonGroup bg = new ButtonGroup();
Box box = (vs.optionValues.optionEquals("ORIENT", "H")) ? Box.createHorizontalBox() : Box.createVerticalBox();
// If the prompt is suppressed by SPAN=TRUE, use it as the border title
String title = "";
if (vs.optionValues.optionEquals("SPAN", "TRUE"))
title = vs.prompt;
box.setBorder(new TitledBorder(new EtchedBorder(), title));
int radioCount = 0;
for (String value : vs.valueList) {
JRadioButton radio = new JRadioButton(value, false);
bg.add(radio);
box.add(radio);
if (listIndex == radioCount)
radio.setSelected(true);
radioCount++;
}
return box;
}
/** Creates a label control, with optional icon. */
public JComponent createLabelControl(VarSpec vs) {
boolean hasText = vs.optionValues.optionEquals("TEXT", "TRUE");
boolean hasIcon = vs.optionValues.optionEquals("ICON", "TRUE");
// If the string starts with "<html>" then Swing will consider it HTML...
if (hasText && vs.value.matches("^\\s*<html>")) {
// For HTML values we use a HTMLPane.
HTMLPane htmlp = new HTMLPane();
htmlp.setText(vs.value);
htmlp.setBackground(Color.decode("0xECE9D8"));
return htmlp;
}
UpdatingLabel label = new UpdatingLabel();
int iconSize = vs.optionValues.getNumeric("ICONSIZE", 0);
if (iconSize <= 0)
hasIcon = false;
String valueText = "", assetID = "";
Icon icon = null;
// See if the string has an image URL inside it
Matcher matcher = ASSET_PATTERN.matcher(vs.value);
if (matcher.find()) {
valueText = matcher.group(1);
assetID = matcher.group(2);
} else {
hasIcon = false;
valueText = vs.value;
}
// Try to get the icon
if (hasIcon) {
icon = InputFunctions.getIcon(assetID, iconSize, label);
if (icon == null)
hasIcon = false;
}
// Assemble the label
if (hasText)
label.setText(valueText);
if (hasIcon)
label.setIcon(icon);
return label;
}
/** Creates a subpanel with controls for each property. */
public JComponent createPropsControl(VarSpec vs) {
// Get the key/value pairs from the property string
Map<String, String> map = new HashMap<String, String>();
List<String> oldKeys = new ArrayList<String>();
for(String entry:org.apache.commons.lang3.StringUtils.split(vs.value,";")) {
String[] e=org.apache.commons.lang3.StringUtils.split(entry.trim(),"=");
map.put(e[0], e[1]);
oldKeys.add(e[0]);
}
// Create list of VarSpecs for the subpanel
List<VarSpec> varSpecs = new ArrayList<VarSpec>();
for (String key : oldKeys) {
String name = key;
String value = map.get(key.toUpperCase());
String prompt = key;
InputType it = InputType.TEXT;
String options = "WIDTH=14;";
VarSpec subvs;
try {
subvs = new VarSpec(name, value, prompt, it, options);
} catch (OptionException e) {
// Should never happen
e.printStackTrace();
subvs = null;
}
varSpecs.add(subvs);
}
// Create the subpanel
ColumnPanel cp = new ColumnPanel(varSpecs);
// If the prompt is suppressed by SPAN=TRUE, use it as the border title
String title = "";
if (vs.optionValues.optionEquals("SPAN", "TRUE"))
title = vs.prompt;
cp.setBorder(new TitledBorder(new EtchedBorder(), title));
return cp;
}
}