/* * 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; } }