Java tutorial
/* * This software copyright by various authors including the RPTools.net * development team, and licensed under the LGPL Version 3 or, at your option, * any later version. * * Portions of this software were originally covered under the Apache Software * License, Version 1.1 or Version 2.0. * * See the file LICENSE elsewhere in this distribution for license details. */ package net.rptools.maptool.client.functions; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Image; import java.awt.Insets; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.Transparency; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.awt.image.ImageObserver; import java.math.BigDecimal; 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.ImageIcon; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JTabbedPane; import javax.swing.JTextField; import javax.swing.ListCellRenderer; import javax.swing.Scrollable; import javax.swing.border.EmptyBorder; import javax.swing.border.EtchedBorder; import javax.swing.border.TitledBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import net.rptools.lib.MD5Key; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.MapToolVariableResolver; import net.rptools.maptool.client.functions.InputFunction.InputType.OptionException; import net.rptools.maptool.client.ui.htmlframe.HTMLPane; import net.rptools.maptool.language.I18N; import net.rptools.maptool.model.Token; import net.rptools.maptool.util.ImageManager; import net.rptools.parser.Parser; import net.rptools.parser.ParserException; import net.rptools.parser.VariableResolver; import net.rptools.parser.function.AbstractFunction; import net.rptools.parser.function.EvaluationException; import net.rptools.parser.function.ParameterException; import org.apache.commons.lang.StringUtils; import de.muntjak.tinylookandfeel.TinyComboBoxButton; /** * <pre> * <span style="font-family:sans-serif;">The input() function prompts the user to input several variable values at once. * * Each of the string parameters has the following format: * "varname|value|prompt|inputType|options" * * Only the first section is required. * varname - the variable name to be assigned * value - sets the initial contents of the input field * prompt - UI text shown for the variable * inputType - specifies the type of input field * options - a string of the form "opt1=val1; opt2=val2; ..." * * The inputType field can be any of the following (defaults to TEXT): * TEXT - A text field. * "value" sets the initial contents. * The return value is the string in the text field. * Option: WIDTH=nnn sets the width of the text field (default 16). * LIST - An uneditable combo box. * "value" populates the list, and has the form "item1,item2,item3..." (trailing empty strings are dropped) * The return value is the numeric index of the selected item. * Option: SELECT=nnn sets the initial selection (default 0). * Option: VALUE=STRING returns the string contents of the selected item (default NUMBER). * Option: TEXT=FALSE suppresses the text of the list item (default TRUE). * Option: ICON=TRUE causes icon asset URLs to be extracted from the "value" and displayed (default FALSE). * Option: ICONSIZE=nnn sets the size of the icons (default 50). * CHECK - A checkbox. * "value" sets the initial state of the box (anything but "" or "0" checks the box) * The return value is 0 or 1. * No options. * RADIO - A group of radio buttons. * "value" is a list "name1, name2, name3, ..." which sets the labels of the buttons. * The return value is the index of the selected item. * Option: SELECT=nnn sets the initial selection (default 0). * Option: ORIENT=H causes the radio buttons to be laid out on one line (default V). * Option: VALUE=STRING causes the return value to be the string of the selected item (default NUMBER). * LABEL - A label. * The "varname" is ignored and no value is assigned to it. * Option: TEXT=FALSE, ICON=TRUE, ICONSIZE=nnn, as in the LIST type. * PROPS - A sub-panel with multiple text boxes. * "value" contains a StrProp of the form "key1=val1; key2=val2; ..." * One text box is created for each key, populated with the matching value. * Option: SETVARS=SUFFIXED causes variable assignment to each key name, with appended "_" (default NONE). * Option: SETVARS=UNSUFFIXED causes variable assignment to each key name. * TAB - A tabbed dialog tab is created. Subsequent variables are contained in the tab. * Option: SELECT=TRUE causes this tab to be shown at start (default SELECT=FALSE). * * All inputTypes except TAB accept the option SPAN=TRUE, which causes the prompt to be hidden and the input * control to span both columns of the dialog layout (default FALSE). * </span> * </pre> * * @author knizia.fan */ public class InputFunction extends AbstractFunction { private static final Pattern ASSET_PATTERN = Pattern.compile("^(.*)asset://(\\w+)"); /** The singleton instance. */ private final static InputFunction instance = new InputFunction(); private InputFunction() { super(1, -1, "input"); } /** Gets the singleton instance. */ public static InputFunction getInstance() { return instance; } /******************************************************************** * Enum of input types; also stores their default option values. ********************************************************************/ public enum InputType { // The regexp for the option strings is strict: no spaces, and trailing semicolon required. // @formatter: off TEXT(false, false, "WIDTH=16;SPAN=FALSE;"), LIST(true, false, "VALUE=NUMBER;TEXT=TRUE;ICON=FALSE;ICONSIZE=50;SELECT=0;SPAN=FALSE;"), CHECK(false, false, "SPAN=FALSE;"), RADIO(true, false, "ORIENT=V;VALUE=NUMBER;SELECT=0;SPAN=FALSE;"), LABEL( false, false, "TEXT=TRUE;ICON=FALSE;ICONSIZE=50;SPAN=FALSE;"), PROPS(false, true, "SETVARS=NONE;SPAN=FALSE;"), TAB(false, true, "SELECT=FALSE;"); // @formatter: on public final OptionMap defaultOptions; // maps option name to default value public final boolean isValueComposite; // can "value" section be a list of values? public final boolean isControlComposite; // does this control contain sub-controls? InputType(boolean isValueComposite, boolean isControlComposite, String nameval) { this.isValueComposite = isValueComposite; this.isControlComposite = isControlComposite; defaultOptions = new OptionMap(); Pattern pattern = Pattern.compile("(\\w+)=([\\w-]+)\\;"); // no spaces allowed, semicolon required Matcher matcher = pattern.matcher(nameval); while (matcher.find()) { defaultOptions.put(matcher.group(1).toUpperCase(), matcher.group(2).toUpperCase()); } } /** * Obtain one of the enum values, or null if <code>strName</code> * doesn't match any of them. */ public static InputType inputTypeFromName(String strName) { for (InputType it : InputType.values()) { if (strName.equalsIgnoreCase(it.name())) return it; } return null; } /** Gets the default value for an option. */ public String getDefault(String option) { return defaultOptions.get(option.toUpperCase()); } /** * Parses a string and returns a Map of options for the given type. * Options not found are set to the default value for the type. */ public OptionMap parseOptionString(String s) throws OptionException { OptionMap ret = new OptionMap(); ret.putAll(defaultOptions); // copy the default values first Pattern pattern = Pattern.compile("\\s*(\\w+)\\s*\\=\\s*([\\w-]+)\\s*"); Matcher matcher = pattern.matcher(s); while (matcher.find()) { String key = matcher.group(1); String value = matcher.group(2); if (ret.get(key) == null) throw new OptionException(this, key, value); if (ret.getNumeric(key, -9998) != -9998) { // minor hack to detect if the option is numeric boolean valueIsNumeric; try { Integer.decode(value); valueIsNumeric = true; } catch (Exception e) { valueIsNumeric = false; } if (!valueIsNumeric) throw new OptionException(this, key, value); } ret.put(key, value); } return ret; } /********************************************************* * Stores option settings as case-insensitive strings. *********************************************************/ @SuppressWarnings("serial") public final class OptionMap extends HashMap<String, String> { /** Case-insensitive put. */ @Override public String put(String key, String value) { return super.put(key.toUpperCase(), value.toUpperCase()); } /** Case-insensitive string get. */ @Override public String get(Object key) { return super.get(key.toString().toUpperCase()); } /** * Case-insensitive numeric get. <br> * Returns <code>defaultValue</code> if the option's value is * non-numeric. <br> * Use when caller wants to override erroneous option settings. */ public int getNumeric(String key, int defaultValue) { int ret; try { ret = Integer.decode(get(key)); } catch (Exception e) { ret = defaultValue; } return ret; } /** * Case-insensitive numeric get. <br> * Returns the default value for the input type if option's value is * non-numeric. <br> * Use when caller wants to ignore erroneous option settings. */ public int getNumeric(String key) { String defstr = getDefault(key); int def; try { def = Integer.decode(defstr); } catch (Exception e) { def = -1; // Should never happen, since the defaults are set in the source code. } return getNumeric(key, def); } /** Tests for a given option value. */ public boolean optionEquals(String key, String value) { if (get(key) == null) return false; return get(key).equalsIgnoreCase(value); } } ////////////////////////// end of OptionMap class /** Thrown when an option value is invalid. */ @SuppressWarnings("serial") public class OptionException extends Exception { public String key, value, type; public OptionException(InputType it, String key, String value) { super(); this.key = key; this.value = value; this.type = it.name(); } } } ///////////////////// end of InputType enum /********************************************************************************** * Variable Specifier structure - holds extracted bits of info for a * variable. **********************************************************************************/ final class VarSpec { public String name, value, prompt; public InputType inputType; public InputType.OptionMap optionValues; public List<String> valueList; // used for types with composite "value" properties public VarSpec(String name, String value, String prompt, InputType inputType, String options) throws InputType.OptionException { initialize(name, value, prompt, inputType, options); } /** Create a VarSpec from a non-empty specifier string. */ public VarSpec(String specifier) throws SpecifierException, InputType.OptionException { String[] parts = (specifier).split("\\|"); int numparts = parts.length; String name, value, prompt; InputType inputType; name = (numparts > 0) ? parts[0].trim() : ""; if (StringUtils.isEmpty(name)) throw new SpecifierException(I18N.getText("macro.function.input.invalidSpecifier", specifier)); value = (numparts > 1) ? parts[1].trim() : ""; if (StringUtils.isEmpty(value)) value = "0"; // Avoids having a default value of "" prompt = (numparts > 2) ? parts[2].trim() : ""; if (StringUtils.isEmpty(prompt)) prompt = name; String inputTypeStr = (numparts > 3) ? parts[3].trim() : ""; inputType = InputType.inputTypeFromName(inputTypeStr); if (inputType == null) { if (StringUtils.isEmpty(inputTypeStr)) { inputType = InputType.TEXT; // default } else { throw new SpecifierException( I18N.getText("macro.function.input.invalidType", inputTypeStr, specifier)); } } String options = (numparts > 4) ? parts[4].trim() : ""; initialize(name, value, prompt, inputType, options); } public void initialize(String name, String value, String prompt, InputType inputType, String options) throws InputType.OptionException { this.name = name; this.value = value; this.prompt = prompt; this.inputType = inputType; this.optionValues = inputType.parseOptionString(options); if (inputType != null && inputType.isValueComposite) this.valueList = parseStringList(this.value); } /** * Parses a string into a list of values, for composite types. <br> * Before calling, the <code>inputType</code> and <code>value</code> * must be set. <br> * After calling, the <code>listIndex</code> member is adjusted if * necessary. */ public List<String> parseStringList(String valueString) { List<String> ret = new ArrayList<String>(); if (valueString != null) { String[] values = valueString.split(","); int i = 0; for (String s : values) { ret.add(s.trim()); i++; } } return ret; } /** Thrown when a variable specifier string is invalid. */ @SuppressWarnings("serial") public class SpecifierException extends Exception { public String msg; public SpecifierException(String msg) { super(); this.msg = msg; } } } ///////////////////// end of VarSpec class /** * Contains input controls, which are arranged in a two-column label + * control layout. */ @SuppressWarnings("serial") final class ColumnPanel extends JPanel { 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() { 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); } 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 JComboBox) { // HACK: to fix a Swing issue. // The stupid JComboBox has two subcomponents, BOTH of which accept the focus. // Thus it takes two Tab presses to move to the next control, and if you // tab once and then hit the down arrow, you can then tab away while the dropdown // list remains displayed. (Other comboboxes in MapTool have similar problems.) // Since the user is likely to tab between values when inputting, this is a // confusing nuisance. // // The hack used here is to make one of the two components (TinyComboBoxButton) // not focusable. We have to do it in a callback like this because the subcomponents // don't exist until the dialog is created (I think?). The code has a hardcoded index of 0, // which is where the TinyComboBoxButton lives on my Windows box (discovered using the debugger). // The code may fail on other OSs, or if a future version of Swing is used. // You're not supposed to mess with the internals like this. // But the resulting behavior is so much nicer with this fix in place, that I'm keeping it in. Component list[] = c.getComponents(); for (int i = 0; i < list.length; i++) if (list[i] instanceof TinyComboBoxButton) list[i].setFocusable(false); // HACK! // } else if (c instanceof JTextField) { // // Select all text when the text field gains focus // final JTextField textFieldFinal = (JTextField) c; // textFieldFinal.addFocusListener(new FocusListener() { // public void focusGained(FocusEvent fe) { // textFieldFinal.selectAll(); // } // // public void focusLost(FocusEvent fe) { // } // }); } else 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 = 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 = 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>(); List<String> oldKeysNormalized = new ArrayList<String>(); StrPropFunctions.parse(vs.value, map, oldKeys, oldKeysNormalized, ";"); // 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; } } ///////////// end of ColumnPanel class /** * Wrapper panel provides alignment and scrolling to a ColumnPanel. * ColumnPanel uses GridBagLayout, which ignores size requests. Thus, the * ColumnPanel is always the size of its container, and is always centered. * We wrap the ColumnPanel in a ScrollingPanel to set the alignment and * desired maximum size. The ScrollingPanel is wrapped in a JScrollPane to * provide scrolling support. */ @SuppressWarnings("serial") final class ColumnPanelHost extends JScrollPane { ScrollingPanel panel; ColumnPanel columnPanel; ColumnPanelHost(ColumnPanel cp) { // get the width of the vertical scrollbar JScrollBar vscroll = getVerticalScrollBar(); int scrollBarWidth = 20; // default value if we can't find the scrollbar if (vscroll != null) { Dimension d = vscroll.getMaximumSize(); scrollBarWidth = (d == null) ? 20 : d.width; } // Cap the height of the contents at 3/4 of the screen height int screenHeight = Toolkit.getDefaultToolkit().getScreenSize().height; panel = new ScrollingPanel(cp, scrollBarWidth, screenHeight * 5 / 8); setViewportView(panel); columnPanel = cp; panel.setLayout(new FlowLayout(FlowLayout.LEFT)); panel.add(columnPanel); // Restores focus to the appropriate input field in the nested ColumnPanel. ComponentAdapter onPanelShow = new ComponentAdapter() { @Override public void componentShown(ComponentEvent ce) { columnPanel.restoreFocus(); } }; setBorder(null); addComponentListener(onPanelShow); } final class ScrollingPanel extends JPanel implements Scrollable { ColumnPanel cp; int scrollBarWidth; int maxHeight; ScrollingPanel(ColumnPanel cp, int scrollBarWidth, int maxHeight) { this.cp = cp; this.scrollBarWidth = scrollBarWidth; this.maxHeight = maxHeight; } // The Scrollable interface methods public Dimension getPreferredScrollableViewportSize() { Dimension d = cp.getPreferredSize(); if (d.height > maxHeight || (d.height > cp.getParent().getHeight() && cp.getParent().getHeight() > 0)) { d.height = maxHeight + cp.maxHeightModifier; d.width += scrollBarWidth; // make room for the vertical scrollbar } return d; } public int getScrollableBlockIncrement(Rectangle visRect, int orientation, int direction) { int retval = visRect.height - 10; if (retval < 0) retval = 10; return retval; } public int getScrollableUnitIncrement(Rectangle visRect, int orientation, int direction) { return scrollBarWidth; } public boolean getScrollableTracksViewportHeight() { return false; } public boolean getScrollableTracksViewportWidth() { return false; } } } @SuppressWarnings("serial") final class InputPanel extends JPanel { public List<ColumnPanel> columnPanels; public JTabbedPane tabPane = null; public int initialTab = 0; // Which one is first visible InputPanel(List<VarSpec> varSpecs) throws ParameterException { ColumnPanel curcp; columnPanels = new ArrayList<ColumnPanel>(); // Only allow tabs if the first item is a TAB specifier boolean useTabs = (varSpecs.get(0).inputType == InputType.TAB); int nextTabIndex = 0; if (useTabs) { // The top-level control in the InputPanel is a JTabbedPane tabPane = new JTabbedPane(); add(tabPane); curcp = null; // Will get initialized on first step of loop below } else { // The top-level control is just a single ColumnPanelHost curcp = new ColumnPanel(); columnPanels.add(curcp); ColumnPanelHost cph = new ColumnPanelHost(curcp); add(cph); } for (VarSpec vs : varSpecs) { if (vs.inputType == InputType.TAB) { if (useTabs) { curcp = new ColumnPanel(); curcp.tabVarSpec = vs; curcp.setBorder(new EmptyBorder(5, 5, 5, 5)); columnPanels.add(curcp); ColumnPanelHost cph = new ColumnPanelHost(curcp); tabPane.addTab(vs.value, null, cph, vs.prompt); if (vs.optionValues.optionEquals("SELECT", "TRUE")) { initialTab = nextTabIndex; } nextTabIndex++; } else { throw new ParameterException(I18N.getText("macro.function.input.invalidTAB")); } } else { // Not a TAB variable, so just add to the current ColumnPanel curcp.addVariable(vs); } } } /** * Returns the first focusable control on the tab which is shown * initially. */ public JComponent findFirstFocusable() { ColumnPanel cp = columnPanels.get(initialTab); JComponent first = cp.findFirstFocusable(); return first; } /** * Adjusts the runtime behavior of components, and sets the initial * focus. */ public void runtimeFixup() { for (ColumnPanel cp : columnPanels) { cp.runtimeFixup(); } // Select the initial tab, if any if (tabPane != null) { tabPane.setSelectedIndex(initialTab); } // Start the focus in the first input field, so the user can type immediately JComponent compFirst = findFirstFocusable(); if (compFirst != null) compFirst.requestFocusInWindow(); // When tab changes, save the last field that had the focus. // (The first field in the panel will gain focus before the panel is shown, // so we have to save cp.lastFocus before it's overwritten.) if (tabPane != null) { tabPane.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { int newTabIndex = tabPane.getSelectedIndex(); ColumnPanel cp = columnPanels.get(newTabIndex); cp.onShowFocus = cp.lastFocus; // // debugging // JComponent foc = cp.onShowFocus; // String s = (foc instanceof JTextField) ? // " (" + ((JTextField)foc).getText() + ")" : ""; // String c = (foc!=null) ? foc.getClass().getName() : ""; // System.out.println("tabpane foc = " + c + s); } }); } } public void modifyMaxHeightBy(int mod) { for (ColumnPanel cpanel : columnPanels) { cpanel.maxHeightModifier = mod; } } } // The function that does all the work @Override public Object childEvaluate(Parser parser, String functionName, List<Object> parameters) throws EvaluationException, ParserException { // Extract the list of specifier strings from the parameters // "name | value | prompt | inputType | options" List<String> varStrings = new ArrayList<String>(); for (Object param : parameters) { String paramStr = (String) param; if (StringUtils.isEmpty(paramStr)) { continue; } // Multiple vars can be packed into a string, separated by "##" List<String> substrings = new ArrayList<String>(); StrListFunctions.parse(paramStr, substrings, "##"); for (String varString : substrings) { if (StringUtils.isEmpty(paramStr)) { continue; } varStrings.add(varString); } } // Create VarSpec objects from each variable's specifier string List<VarSpec> varSpecs = new ArrayList<VarSpec>(); for (String specifier : varStrings) { VarSpec vs; try { vs = new VarSpec(specifier); } catch (VarSpec.SpecifierException se) { throw new ParameterException(se.msg); } catch (InputType.OptionException oe) { throw new ParameterException(I18N.getText("macro.function.input.invalidOptionType", oe.key, oe.value, oe.type, specifier)); } varSpecs.add(vs); } // Check if any variables were defined if (varSpecs.isEmpty()) return BigDecimal.ONE; // No work to do, so treat it as a successful invocation. // UI step 1 - First, see if a token is in context. VariableResolver varRes = parser.getVariableResolver(); Token tokenInContext = null; if (varRes instanceof MapToolVariableResolver) { tokenInContext = ((MapToolVariableResolver) varRes).getTokenInContext(); } String dialogTitle = "Input Values"; if (tokenInContext != null) { String name = tokenInContext.getName(), gm_name = tokenInContext.getGMName(); boolean isGM = MapTool.getPlayer().isGM(); String extra = ""; if (isGM && gm_name != null && gm_name.compareTo("") != 0) extra = " for " + gm_name; else if (name != null && name.compareTo("") != 0) extra = " for " + name; dialogTitle = dialogTitle + extra; } // UI step 2 - build the panel with the input fields InputPanel ip = new InputPanel(varSpecs); // Calculate the height // TODO: remove this workaround int screenHeight = Toolkit.getDefaultToolkit().getScreenSize().height; int maxHeight = screenHeight * 3 / 4; Dimension ipPreferredDim = ip.getPreferredSize(); if (maxHeight < ipPreferredDim.height) { ip.modifyMaxHeightBy(maxHeight - ipPreferredDim.height); } // UI step 3 - show the dialog JOptionPane jop = new JOptionPane(ip, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION); JDialog dlg = jop.createDialog(MapTool.getFrame(), dialogTitle); // Set up callbacks needed for desired runtime behavior dlg.addComponentListener(new FixupComponentAdapter(ip)); dlg.setVisible(true); int dlgResult = JOptionPane.CLOSED_OPTION; try { dlgResult = (Integer) jop.getValue(); } catch (NullPointerException npe) { } dlg.dispose(); if (dlgResult == JOptionPane.CANCEL_OPTION || dlgResult == JOptionPane.CLOSED_OPTION) return BigDecimal.ZERO; // Finally, assign values from the dialog box to the variables for (ColumnPanel cp : ip.columnPanels) { List<VarSpec> panelVars = cp.varSpecs; List<JComponent> panelControls = cp.inputFields; int numPanelVars = panelVars.size(); StringBuilder allAssignments = new StringBuilder(); // holds all values assigned in this tab for (int varCount = 0; varCount < numPanelVars; varCount++) { VarSpec vs = panelVars.get(varCount); JComponent comp = panelControls.get(varCount); String newValue = null; switch (vs.inputType) { case TEXT: { newValue = ((JTextField) comp).getText(); break; } case LIST: { Integer index = ((JComboBox) comp).getSelectedIndex(); if (vs.optionValues.optionEquals("VALUE", "STRING")) { newValue = vs.valueList.get(index); } else { // default is "NUMBER" newValue = index.toString(); } break; } case CHECK: { Integer value = ((JCheckBox) comp).isSelected() ? 1 : 0; newValue = value.toString(); break; } case RADIO: { // This code assumes that the Box container returns components // in the same order that they were added. Component[] comps = ((Box) comp).getComponents(); int componentCount = 0; Integer index = 0; for (Component c : comps) { if (c instanceof JRadioButton) { JRadioButton radio = (JRadioButton) c; if (radio.isSelected()) index = componentCount; } componentCount++; } if (vs.optionValues.optionEquals("VALUE", "STRING")) { newValue = vs.valueList.get(index); } else { // default is "NUMBER" newValue = index.toString(); } break; } case LABEL: { newValue = null; // The variable name is ignored and not set. break; } case PROPS: { // Read out and assign all the subvariables. // The overall return value is a property string (as in StrPropFunctions.java) with all the new settings. Component[] comps = ((JPanel) comp).getComponents(); StringBuilder sb = new StringBuilder(); int setVars = 0; // "NONE", no assignments made if (vs.optionValues.optionEquals("SETVARS", "SUFFIXED")) setVars = 1; if (vs.optionValues.optionEquals("SETVARS", "UNSUFFIXED")) setVars = 2; if (vs.optionValues.optionEquals("SETVARS", "TRUE")) setVars = 2; // for backward compatibility for (int compCount = 0; compCount < comps.length; compCount += 2) { String key = ((JLabel) comps[compCount]).getText().split("\\:")[0]; // strip trailing colon String value = ((JTextField) comps[compCount + 1]).getText(); sb.append(key); sb.append("="); sb.append(value); sb.append(" ; "); switch (setVars) { case 0: // Do nothing break; case 1: parser.setVariable(key + "_", value); break; case 2: parser.setVariable(key, value); break; } } newValue = sb.toString(); break; } default: // should never happen newValue = null; break; } // Set the variable to the value we got from the dialog box. if (newValue != null) { parser.setVariable(vs.name, newValue.trim()); allAssignments.append(vs.name + "=" + newValue.trim() + " ## "); } } if (cp.tabVarSpec != null) { parser.setVariable(cp.tabVarSpec.name, allAssignments.toString()); } } return BigDecimal.ONE; // success // for debugging: //return debugOutput(varSpecs); } @Override public void checkParameters(List<Object> parameters) throws ParameterException { super.checkParameters(parameters); for (Object param : parameters) { if (!(param instanceof String)) throw new ParameterException(I18N.getText("macro.function.input.illegalArgumentType", param.getClass().getName(), String.class.getName())); } } /** * Gets icon from the asset manager. Code copied and modified from * EditTokenDialog.java */ private ImageIcon getIcon(String id, int size, ImageObserver io) { // Extract the MD5Key from the URL if (id == null) return null; MD5Key assetID = new MD5Key(id); // Get the base image && find the new size for the icon BufferedImage assetImage = ImageManager.getImage(assetID, io); // Resize if (assetImage.getWidth() > size || assetImage.getHeight() > size) { Dimension dim = new Dimension(assetImage.getWidth(), assetImage.getWidth()); if (dim.height < dim.width) { dim.height = (int) ((dim.height / (double) dim.width) * size); dim.width = size; } else { dim.width = (int) ((dim.width / (double) dim.height) * size); dim.height = size; } BufferedImage image = new BufferedImage(dim.width, dim.height, Transparency.BITMASK); Graphics2D g = image.createGraphics(); g.drawImage(assetImage, 0, 0, dim.width, dim.height, null); assetImage = image; } return new ImageIcon(assetImage); } /** JLabel variant that listens for new image data, and redraws its icon. */ public class UpdatingLabel extends JLabel { private String macroLink; @Override public boolean imageUpdate(Image img, int infoflags, int x, int y, int w, int h) { Icon curIcon = getIcon(); int curWidth = curIcon.getIconWidth(); int curHeight = curIcon.getIconHeight(); // Are we receiving the final image data? int flags = ImageObserver.ALLBITS | ImageObserver.FRAMEBITS; if ((infoflags & flags) == 0) return true; // Resize Dimension dim = new Dimension(curWidth, curHeight); BufferedImage sizedImage = new BufferedImage(dim.width, dim.height, Transparency.BITMASK); Graphics2D g = sizedImage.createGraphics(); g.drawImage(img, 0, 0, dim.width, dim.height, null); // Update our Icon setIcon(new ImageIcon(sizedImage)); return false; } public void setMacroLink(String link) { macroLink = link; this.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { // System.out.println(macroLink); } }); } } /** Custom renderer to display icons and text inside a combo box */ private class ComboBoxRenderer implements ListCellRenderer { public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { JLabel label = null; if (value instanceof JLabel) { label = (JLabel) value; if (isSelected) { label.setBackground(list.getSelectionBackground()); label.setForeground(list.getSelectionForeground()); } else { label.setBackground(list.getBackground()); label.setForeground(list.getForeground()); } } return label; } } /** Adjusts the runtime behavior of components */ public class FixupComponentAdapter extends ComponentAdapter { final InputPanel ip; public FixupComponentAdapter(InputPanel ip) { super(); this.ip = ip; } @Override public void componentShown(ComponentEvent ce) { ip.runtimeFixup(); } } /** Class found on web to work around a STUPID SWING BUG with JComboBox */ public class NoEqualString { private final String text; public NoEqualString(String txt) { text = txt; } @Override public String toString() { return text; } } // Dumps out the parsed input specifications for debugging purposes // private String debugOutput(ArrayList<VarSpec> varSpecs) { // StringBuilder builder = new StringBuilder(); // builder.append("<br><table border='1' padding='2px 2px'>"); // builder.append( // "<tr style='font-weight:bold'><td>Name</td><td>Value</td><td>Prompt</td><td>Input Type</td><td>Options</td></tr>" // ); // for (VarSpec vs : varSpecs) { // builder.append("<tr>"); // builder.append("<td>"); // builder.append(vs.name); // builder.append("</td><td>"); // if (vs.inputType == InputType.LIST) { // builder.append("(( "); // for (String s : vs.valueList) { // builder.append(s); // builder.append(","); // } // builder.append(" ))"); // } else { // builder.append(vs.value); // } // builder.append("</td><td>"); // builder.append(vs.prompt); // builder.append("</td><td>"); // builder.append(vs.inputType); // builder.append("</td><td>"); // for (Map.Entry<String, String> entry : vs.optionValues.entrySet()) // builder.append(entry.getKey() + "=" + entry.getValue() + "<br>"); // builder.append("</td></tr>"); // } // builder.append("</table>"); // return builder.toString(); // } } // A sample for the TAB control /* * * [h: status = input( "tab0 | Abilities | Enter abilities here | TAB", * "blah|Max value is 18|Note|LABEL ## abils|Str=8;Con=8;Dex=8;Int=8;Wis=8;Cha=8|Abilities|PROPS" * , "txt0|text on tab 0|", * "tab1 | Options | Various options | TAB |select=true", * "txt|default text ## ck|1|Toggle me|CHECK ## list|a,b,This is a very long item,d|Pick one|LIST ## foo|foo" * )] [h: abort(status)] tab0 is set to [tab0] <br>tab1 is set to [tab1] <br> * <br>list is set to [list] <br>get list from the tab1 variable: * [getStrProp(tab1,"list","","##")] */ // A tall tab control, to demonstrate scrolling /* * [h: props = * "a=3;b=bob;c=cow;d=40;e=55;f=33;g=big time;h=hello;i=interesting;j=jack;k=kow;l=leap;m=moon;" * ] [h: status = input( "tab0 | Settings | Settings tooltip | TAB", * "foo|||CHECK ## bar|bar", * "tab1 | Options | Options tooltip | TAB | select=true", * "num|a,b,c,d|Pick one|list ## zot|zot ## zam|zam", * "tab2 | Options2 | Options2 tooltip | TAB | ", "p | " + props + * " | Sample props | PROPS ## p2 | " + props + " | More props | PROPS ## p3 | " * + props + " | Even more props | PROPS ## p4 | " + props + * " | Still more props | PROPS", "num2|a,b,c,d|Pick one|list ", * "num3|after all it's only a listbox here now isn't it dear?,b,c,d|Pick one|list|span=true ## ee|ee ## ff|ff ## gg|gg ## hh|hh ## ii|ii ## jj|jj ## kk|kk" * , "tab3 | Empty | nothin' | TAB" )] [h: abort(status)] tab0 is [tab0]<br> foo * is [foo]<br> tab1 is [tab1]<br> num is [num]<br> tab2 is [tab2]<br> tab3 is * [tab3]<br> */ // Here's a sample input to exercise the options /* * * Original props = [props = * "Name=Longsword +1; Damage=1d8+1; Crit=1d6; Keyword=fire;"] [H: input( "foo", * "YourName|George Washington|Your name|TEXT", * "Weapon|Axe,Sword,Mace|Choose weapon|LIST", * "WarCry|Attack!,No surrender!,I give up!|Pick a war cry|LIST|VALUE=STRING select=1" * , "CA || Combat advantage| CHECK|", * "props |"+props+"|Weapon properties|PROPS|setvars=true", * "UsePower |1|Use the power|CHECK", * "Weight|light,medium,heavy||RADIO|ORIENT=H select=1", * "Ambition|Survive today, Defeat my enemies, Rule the world, Become immortal||RADIO|VALUE=STRING" * , * "bar | a, b, c, d, e, f, g , h ,i j, k |Radio button test | RADIO | select=5 value = string ; oRiEnT =h;;;;" * )]<br> <i>New values of variables:</i> <br>foo is [foo] <br>YourName is * [YourName] <br>Weapon is [Weapon] <br>WarCry is [WarCry] <br>CA is [CA] * <br>props is [props] <br>UsePower is [UsePower] <br>Weight is [Weight] * <br>Ambition is [Ambition] <br> <br>Name is [Name], Damage is [Damage], Crit * is [Crit], Keyword is [Keyword] */ // Here's a longer version of that sample, but the 9/14/08 checked in version of MapTool gets // a stack overflow when this is pasted into chat (due to its length?) /* * * Original props = [props = * "Name=Longsword +1; Damage=1d8+1; Crit=1d6; Keyword=fire;"] [h: setPropVars = * 1] [H: input( "foo", "YourName|George Washington|Your name|TEXT", * "Weapon|Axe,Sword,Mace|Choose weapon|LIST", * "WarCry|Attack!,No surrender!,I give up!|Pick a war cry|LIST|VALUE=STRING select=1" * , "CA || Combat advantage| CHECK|", * "props |"+props+"|Weapon properties|PROPS|setvars=true", * "UsePower |1|Use the power|CHECK", * "Weight|light,medium,heavy||RADIO|ORIENT=H select=1", * "Ambition|Survive today, Defeat my enemies, Rule the world, Become immortal||RADIO|VALUE=STRING" * , * "bar | a, b, c, d, e, f, g , h ,i j, k |Radio button test | RADIO | select=5 value = string ; oRiEnT =h;;;;" * )]<br> <i>New values of variables:</i> <table border=0><tr * style='font-weight:bold;'><td>Name </td><td>Value</td></tr> * <tr><td>foo </td><td>{foo}</td></tr> * <tr><td>YourName </td><td>{YourName}</td></tr> * <tr><td>Weapon </td><td>{Weapon}</td></tr> * <tr><td>WarCry </td><td>{WarCry}</td></tr> * <tr><td>CA </td><td>{CA} </td></tr> * <tr><td>props </td><td>{props} </td></tr> * <tr * ><td>UsePower </td><td>{UsePower} </td></ * tr> * <tr><td>Weight </td><td>{Weight} </td></tr> * < * tr><td>Ambition </td><td>{Ambition} </td></ * tr> <tr><td> </td><td> </td></tr> * <tr><td> </td><td> </td></tr> {if * (setPropVars, * "<tr><td>Name </td><td>"+Name+" </td></tr> * < * tr><td>Damage </td><td>"+Damage+" </td></tr * > <tr><td>Crit </td><td>"+Crit+" </td></tr> * < * tr><td>Keyword </td><td>"+Keyword+" </td></ * tr>", "")} </td></tr> </table> New props = [props] */