/* * $Id$ * * Copyright (c) 2007-2012 by Brent Easton * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License (LGPL) as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, copies are available * at http://www.opensource.org. */ package VASSAL.configure; import java.awt.Dimension; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.image.BufferedImage; import java.util.List; import javax.swing.BoxLayout; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JTextField; import javax.swing.SwingUtilities; import net.miginfocom.swing.MigLayout; import VASSAL.build.AbstractBuildable; import VASSAL.build.AbstractConfigurable; import VASSAL.build.Buildable; import VASSAL.build.GameModule; import VASSAL.build.module.GlobalOptions; import VASSAL.build.module.map.BoardPicker; import VASSAL.build.module.map.boardPicker.Board; import VASSAL.build.module.map.boardPicker.board.ZonedGrid; import VASSAL.build.module.properties.GlobalProperties; import VASSAL.build.module.properties.PropertyNameSource; import VASSAL.counters.Decorator; import VASSAL.counters.EditablePiece; import VASSAL.counters.GamePiece; import VASSAL.counters.Properties; import VASSAL.script.expression.FunctionBuilder; import VASSAL.script.expression.IntBuilder; import VASSAL.script.expression.StrBuilder; import VASSAL.tools.icon.IconFactory; import VASSAL.tools.icon.IconFamily; import VASSAL.tools.menu.MenuScroller; import bsh.BeanShellExpressionValidator; /** * A Configurer for Java Expressions */ public class BeanShellExpressionConfigurer extends StringConfigurer { protected static int maxScrollItems = 0; protected static final int MAX_SCROLL_ITEMS = 40; protected JPanel expressionPanel; protected JPanel detailPanel; protected Validator validator; protected JButton extraDetails; protected Icon up; protected Icon down; protected StringConfigurer errorMessage; protected JLabel variables; protected JLabel methods; protected EditablePiece target; public BeanShellExpressionConfigurer(String key, String name) { this(key, name, ""); } public BeanShellExpressionConfigurer(String key, String name, String val) { this(key, name, val, null); } public BeanShellExpressionConfigurer(String key, String name, String val, GamePiece piece) { super(key, name, val); if (piece instanceof EditablePiece) { target = (EditablePiece) piece; } else { target = null; } strip(); up = IconFactory.getIcon("go-up", IconFamily.XSMALL); down = IconFactory.getIcon("go-down", IconFamily.XSMALL); extraDetails = new JButton("Insert"); extraDetails.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { doPopup(); }}); } protected void strip() { final String s = getValueString().trim(); if (s.startsWith("{") && s.endsWith("}")) { setValue(s.substring(1, s.length()-1)); } } public String getValueString() { return (String) value; } public void setValue(String s) { if (!noUpdate && nameField != null) { nameField.setText(s); } setValue((Object) s); } public java.awt.Component getControls() { if (p == null) { expressionPanel = new JPanel(new MigLayout("fillx,ins 0","[][grow][][]")); //expressionPanel.setLayout(new BoxLayout(expressionPanel, BoxLayout.X_AXIS)); expressionPanel.add(new JLabel(getName())); validator = new Validator(); nameField = new JTextField(30); //nameField.setMaximumSize // (new Dimension(nameField.getMaximumSize().width, // nameField.getPreferredSize().height)); nameField.setText(getValueString()); expressionPanel.add(nameField, "growx"); nameField.addKeyListener(new KeyAdapter() { public void keyReleased(KeyEvent evt) { noUpdate = true; setValue(nameField.getText()); validator.validate(); noUpdate = false; } }); expressionPanel.add(validator); expressionPanel.add(extraDetails,"wrap"); validator.validate(); detailPanel = new JPanel(); detailPanel.setLayout(new BoxLayout(detailPanel, BoxLayout.Y_AXIS)); errorMessage = new StringConfigurer(null, "Error Message: ", ""); errorMessage.getControls().setEnabled(false); variables = new JLabel("Vassal Properties: "); methods = new JLabel("Methods: "); detailPanel.add(errorMessage.getControls()); detailPanel.add(variables); detailPanel.add(methods); detailPanel.setVisible(false); p = new JPanel(); p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS)); p.add(expressionPanel); p.add(detailPanel); } return p; } protected void doPopup() { final JPopupMenu popup = createPopup(); popup.show(extraDetails, 0, 0); } /** * Toggle the display of additional details */ protected void toggleDetails() { extraDetails.setIcon(detailPanel.isVisible() ? down : up); detailPanel.setVisible(! detailPanel.isVisible()); repack(); } protected void repack() { final Window w = SwingUtilities.getWindowAncestor(p); if (w != null) { w.pack(); } } /** * Build a popup menu * @return */ protected JPopupMenu createPopup() { JPopupMenu popup = new JPopupMenu(); final JMenu constantMenu = new JMenu("Constant"); final JMenuItem integerItem = new JMenuItem("Number"); integerItem.setToolTipText("A number"); integerItem.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { buildInteger(); }}); constantMenu.add(integerItem); final JMenuItem stringItem = new JMenuItem("String"); stringItem.setToolTipText("A character string"); stringItem.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { buildString(); }}); constantMenu.add(stringItem); popup.add(constantMenu); final JMenu propertyMenu = new JMenu("Property"); if (target != null) { final JMenu pieceMenu = new JMenu("Piece Property"); addProp(pieceMenu, Properties.MOVED); addProp(pieceMenu, Properties.SELECTED); addProp(pieceMenu, Properties.PIECE_ID); addPieceProps(pieceMenu, target); propertyMenu.add(pieceMenu); } final JMenu globalsMenu = new JMenu("Global Property"); buildGlobalMenu(globalsMenu, GameModule.getGameModule(), true); propertyMenu.add(globalsMenu); final JMenu vassalMenu = new JMenu("Vassal Property"); addProp(vassalMenu, GlobalOptions.PLAYER_SIDE); addProp(vassalMenu, GlobalOptions.PLAYER_NAME); addProp(vassalMenu, GlobalOptions.PLAYER_ID); propertyMenu.add(vassalMenu); popup.add(propertyMenu); final JMenu operatorMenu = new JMenu("Operator"); addOperator(operatorMenu, "+", "Add"); addOperator(operatorMenu, "-", "Subtract"); addOperator(operatorMenu, "*", "Multiply"); addOperator(operatorMenu, "/", "Divide"); addOperator(operatorMenu, "%", "Modulus"); popup.add(operatorMenu); final JMenu comparisonMenu = new JMenu("Comparison"); addOperator(comparisonMenu, "==", "Equals"); addOperator(comparisonMenu, "!=", "Not equals"); addOperator(comparisonMenu, ">", "Greater than"); addOperator(comparisonMenu, ">=", "Greater than or equal to"); addOperator(comparisonMenu, "<", "Less than"); addOperator(comparisonMenu, "<=", "Less than or equal to"); addOperator(comparisonMenu, "=~", "Matches Regular Expression"); addOperator(comparisonMenu, "!~", "Does not match Regular Expression"); popup.add(comparisonMenu); final JMenu logicalMenu = new JMenu("Logical"); addOperator(logicalMenu, "&&", "And"); addOperator(logicalMenu, "||", "Or"); addOperator(logicalMenu, "(", "Left parenthesis"); addOperator(logicalMenu, ")", "Right parenthesis"); popup.add(logicalMenu); final JMenu functionMenu = new JMenu("Function"); addFunction(functionMenu, "Alert", "Display text in a Dialog box", new String[] {"Text to display"}); addFunction(functionMenu, "Compare", "Compare two Strings or other objects", new String[]{"Object 1", "Object 2"}); addFunction(functionMenu, "GetProperty", "Get a property by name", new String[]{"Property name"}); addFunction(functionMenu, "If", "Return a different result depending on a logical expression", new String[]{"Logical expression", "Result if true", "Result if false"}); addFunction(functionMenu, "SumStack", "Sum the values of the named property in all counters in the same stack", new String[]{"Property name"}); popup.add(functionMenu); return popup; } protected void addFunction(JMenu menu, final String op, final String desc, final String[] parms) { final JMenuItem item = new JMenuItem(op); item.setToolTipText(desc); item.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { buildFunction(op, desc, parms); }}); menu.add(item); } protected void buildFunction(String op, String desc, String[] parmDesc) { final StringConfigurer result = new StringConfigurer(null, "", ""); new FunctionBuilder(result, (JDialog) p.getTopLevelAncestor(), op, desc, parmDesc, target).setVisible(true); if (result.getValue() != null && result.getValueString().length() > 0) { insertName(result.getValueString()); } } protected void buildInteger() { final StringConfigurer result = new StringConfigurer(null, "", ""); new IntBuilder(result, (JDialog) p.getTopLevelAncestor()).setVisible(true); if (result.getValue() != null && result.getValueString().length() > 0) { insertName(result.getValueString()); } } protected void buildString() { final StringConfigurer result = new StringConfigurer(null, "", ""); new StrBuilder(result, (JDialog) p.getTopLevelAncestor()).setVisible(true); if (result.getValue() != null && result.getValueString().length() > 0) { insertName(result.getValueString()); } } protected void addOperator(JMenu menu, final String op, String desc) { final JMenuItem item = new JMenuItem(op); item.setToolTipText(desc); item.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { insertName(op); }}); menu.add(item); } /** * Add straight property name to a menu * @param menu parent menu * @param propName property name to add */ protected void addProp(JMenu menu, final String propName) { addProp(menu, propName, false); } protected void addProp(JMenu menu, final String propName, boolean sort) { // Ignore any null propNames if (propName == null) { return; } final JMenuItem item = new JMenuItem(propName); item.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { insertPropertyName(propName); }}); if (sort) { int pos = -1; for (int i = 0; i < menu.getItemCount() && pos < 0; i++) { if (propName.compareTo(menu.getItem(i).getText()) <= 0) { pos = i; } } menu.add(item, pos); } else { menu.add(item); } } /** * Added the property names from an Editable Piece into their * own menu * @param menu parent menu * @param piece Piece containing property names */ protected void addPieceProps(JMenu menu, EditablePiece piece) { if (piece == null) { return; } JMenu pieceMenu = null; if (piece instanceof PropertyNameSource) { List<String> propNames = ((PropertyNameSource) piece).getPropertyNames(); for (String propName : propNames) { if (pieceMenu == null) { pieceMenu = new JMenu(); pieceMenu.setText(piece.getDescription()); } final JMenuItem item = new JMenuItem(propName); item.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { insertPropertyName(((JMenuItem) e.getSource()).getText()); }}); pieceMenu.add(item); } if (pieceMenu != null) { menu.add(pieceMenu); } if (piece instanceof Decorator) { addPieceProps(menu, (EditablePiece) ((Decorator) piece).getInner()); } } } /** * Create a menu of Global Properties recorded in this module, based on * the module build structure */ protected void buildGlobalMenu(JMenu parentMenu, AbstractBuildable target, boolean useParentMenu) { final List<Buildable> buildables = target.getBuildables(); String menuName = ConfigureTree.getConfigureName(target.getClass()); if (target instanceof AbstractConfigurable) { final String n = ((AbstractConfigurable) target).getConfigureName(); if (n != null && n.length() > 0) { menuName += " " + n; } } final JMenu myMenu = new JMenu(menuName); final List<String> propNames = target.getPropertyNames(); for (String propName : propNames) { addProp(useParentMenu ? parentMenu : myMenu, propName, true); } for (Buildable b : buildables) { if (b instanceof AbstractConfigurable) { // Remove 'filler' menu levels due to intermediate holding components final boolean useParent = (b instanceof GlobalProperties || b instanceof Board ||b instanceof ZonedGrid); buildGlobalMenu(useParentMenu ? parentMenu : myMenu, (AbstractConfigurable) b, useParent); } else if (b instanceof BoardPicker) { buildGlobalMenu(myMenu,(AbstractBuildable) b, true); } } if (! useParentMenu & myMenu.getItemCount() > 0) { MenuScroller.setScrollerFor(myMenu, getMaxScrollItems(), 100); int pos = -1; for (int i = 0; i < parentMenu.getItemCount() && pos < 0; i++) { if (myMenu.getText().compareTo(parentMenu.getItem(i).getText()) <= 0) { pos = i; } } parentMenu.add(myMenu, pos); } } protected int getMaxScrollItems() { if (maxScrollItems == 0) { final Dimension itemSize = (new JMenuItem("Testing")).getPreferredSize(); maxScrollItems = (int) (0.8 * Toolkit.getDefaultToolkit().getScreenSize().height/itemSize.height); } return maxScrollItems; } /** * Insert a property name into the expression * @param name property name */ protected void insertPropertyName(String name) { insertName (cleanName(name)); } protected void insertName(String name) { String work = nameField.getText(); int pos = nameField.getCaretPosition(); // Cut out any selected text if (nameField.getSelectedText() != null) { int start = nameField.getSelectionStart(); int end = nameField.getSelectionEnd(); work = work.substring(0, start) + work.substring(end); if (pos >= start && pos <= end) { pos = start; } } String news = work.substring(0, pos) + name + work.substring(pos); nameField.setText(news); nameField.setCaretPosition(pos + name.length()); // Update the text field and repaint it noUpdate = true; setValue(nameField.getText()); validator.validate(); noUpdate = false; nameField.repaint(); // Send focus back to text field nameField.requestFocusInWindow(); } /* * If the property name is not a valid java variable name, it * needs to be returned using the GetProperty() function. */ protected String cleanName(String name) { boolean valid = true; for (int i = 0; i < name.length() && valid; i++) { final char c = name.charAt(i); if (i==0) { valid = Character.isJavaIdentifierStart(c); } else { valid = Character.isJavaIdentifierPart(c); } } return valid ? name : "GetProperty(\""+name+"\")"; } protected void setDetails(String error, List<String> v, List<String> m) { errorMessage.setValue(error); String s = "Vassal Properties: " + (v == null ? "" : v.toString()); variables.setText(s); s = "Methods: " + (m == null ? "" : m.toString()); methods.setText(s); } protected void setDetails() { setDetails ("", null, null); } /* * Class to check and reflect the validity of the current expression. */ class Validator extends JLabel { protected static final int INVALID = 0; protected static final int VALID = 1; protected static final int UNKNOWN = 2; protected Icon tick; protected Icon cross; protected ImageIcon none; protected int status = UNKNOWN; protected boolean validating = false; protected boolean dirty = false; protected ValidationThread validationThread = new ValidationThread(); private static final long serialVersionUID = 1L; public Validator() { cross = IconFactory.getIcon("no", IconFamily.XSMALL); tick = IconFactory.getIcon("yes", IconFamily.XSMALL); BufferedImage image = new BufferedImage(cross.getIconWidth(), cross.getIconHeight(), BufferedImage.TYPE_INT_ARGB); none = new ImageIcon(image); setStatus(UNKNOWN); } public void setStatus(int status) { if (status == VALID) { setIcon(tick); } else if (status == INVALID){ setIcon(cross); } else { setIcon(none); } this.status = status; } public int getStatus() { return status; } /* * Run the validation in a separate thread. If the expression is updated * while validating, then revalidate. */ public void validate() { if (validating) { dirty = true; } else { validating = true; validator.setStatus(UNKNOWN); SwingUtilities.invokeLater(validationThread); } } class ValidationThread implements Runnable { public void run() { if (getValueString().length() == 0) { validator.setStatus(UNKNOWN); setDetails(); } else { BeanShellExpressionValidator v = new BeanShellExpressionValidator(getValueString()); if (v.isValid()) { validator.setStatus(VALID); setDetails(v.getError(), v.getVariables(), v.getMethods()); } else { validator.setStatus(INVALID); setDetails(v.getError(), v.getVariables(), v.getMethods()); } } validating = false; if (dirty) { dirty = false; validate(); } } } } }