/* * $Id$ * * Copyright (c) 2000-2009 by Rodney Kinney, 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.counters; import java.awt.Component; import java.awt.Graphics; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Window; import java.awt.event.InputEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.List; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JPanel; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import VASSAL.build.module.documentation.HelpFile; import VASSAL.command.Command; import VASSAL.command.NullCommand; import VASSAL.configure.BooleanConfigurer; import VASSAL.configure.FormattedExpressionConfigurer; import VASSAL.configure.FormattedStringConfigurer; import VASSAL.configure.IntConfigurer; import VASSAL.configure.NamedHotKeyConfigurer; import VASSAL.configure.NamedKeyStrokeArrayConfigurer; import VASSAL.configure.PropertyExpression; import VASSAL.configure.PropertyExpressionConfigurer; import VASSAL.configure.StringConfigurer; import VASSAL.configure.StringEnumConfigurer; import VASSAL.i18n.PieceI18nData; import VASSAL.i18n.Resources; import VASSAL.i18n.TranslatablePiece; import VASSAL.tools.FormattedString; import VASSAL.tools.LoopControl; import VASSAL.tools.NamedKeyStroke; import VASSAL.tools.RecursionLimitException; import VASSAL.tools.RecursionLimiter; import VASSAL.tools.RecursionLimiter.Loopable; import VASSAL.tools.SequenceEncoder; /** * Macro Execute a series of Keystrokes against this same piece - Triggered by * own KeyCommand or list of keystrokes - Match against an optional Property * Filter * */ public class TriggerAction extends Decorator implements TranslatablePiece, Loopable { public static final String ID = "macro;"; //$NON-NLS-1$ protected String name = ""; //$NON-NLS-1$ protected String command = ""; //$NON-NLS-1$ protected NamedKeyStroke key = NamedKeyStroke.NULL_KEYSTROKE; protected PropertyExpression propertyMatch = new PropertyExpression(); protected NamedKeyStroke[] watchKeys = new NamedKeyStroke[0]; protected NamedKeyStroke[] actionKeys = new NamedKeyStroke[0]; protected boolean loop = false; protected NamedKeyStroke preLoopKey = NamedKeyStroke.NULL_KEYSTROKE; protected NamedKeyStroke postLoopKey = NamedKeyStroke.NULL_KEYSTROKE; protected String loopType = LoopControl.LOOP_COUNTED; protected PropertyExpression whileExpression = new PropertyExpression(); protected PropertyExpression untilExpression = new PropertyExpression(); protected FormattedString loopCount = new FormattedString("1"); //$NON-NLS-1$ protected boolean index = false; protected String indexProperty = ""; //$NON-NLS-1$ protected FormattedString indexStart = new FormattedString("1"); protected FormattedString indexStep = new FormattedString("1"); protected int indexValue = 0; protected GamePiece outer; public TriggerAction() { this(ID, null); } public TriggerAction(String type, GamePiece inner) { mySetType(type); setInner(inner); } public Rectangle boundingBox() { return piece.boundingBox(); } public void draw(Graphics g, int x, int y, Component obs, double zoom) { piece.draw(g, x, y, obs, zoom); } public String getName() { return piece.getName(); } protected KeyCommand[] myGetKeyCommands() { if (command.length() > 0 && key != null) { final KeyCommand c = new KeyCommand(command, key, Decorator .getOutermost(this), matchesFilter()); if (getMap() == null) { c.setEnabled(false); } return new KeyCommand[] { c }; } else { return new KeyCommand[0]; } } public String myGetState() { return ""; //$NON-NLS-1$ } public String myGetType() { SequenceEncoder se = new SequenceEncoder(';'); se.append(name) .append(command) .append(key) .append(propertyMatch.getExpression()) .append(NamedKeyStrokeArrayConfigurer.encode(watchKeys)) .append(NamedKeyStrokeArrayConfigurer.encode(actionKeys)) .append(loop) .append(preLoopKey) .append(postLoopKey) .append(loopType) .append(whileExpression.getExpression()) .append(untilExpression.getExpression()) .append(loopCount.getFormat()) .append(index) .append(indexProperty) .append(indexStart.getFormat()) .append(indexStep.getFormat()); return ID + se.getValue(); } /** * Apply key commands to inner pieces first * * @param stroke * @return */ public Command keyEvent(KeyStroke stroke) { Command c = piece.keyEvent(stroke); return c == null ? myKeyEvent(stroke) : c.append(myKeyEvent(stroke)); } public Command myKeyEvent(KeyStroke stroke) { /* * 1. Are we interested in this key command? Is it our command key? Does it * match one of our watching keystrokes? */ boolean seen = false; if (key.equals(stroke)) { seen = true; } for (int i = 0; i < watchKeys.length && !seen; i++) { if (watchKeys[i].equals(stroke)) { seen = true; } } if (!seen) { return null; } // 2. Check the Property Filter if it exists. if (!matchesFilter()) { return null; } // 3. Initialise outer = Decorator.getOutermost(this); Command c = new NullCommand(); // 4. Handle non-looping case if (!loop) { try { doLoopOnce(c); } catch (RecursionLimitException e) { RecursionLimiter.infiniteLoop(e); } return c; } // 5. Looping // Set up Index Property indexValue = parse("Index Property Start Value", indexStart, outer); final int step = parse ("Index Property increment value", indexStep, outer); // Issue the Pre-loop key executeKey(c, preLoopKey); // Loop // Set up counters for a counted loop int loopCounter = 0; int loopCountLimit = 0; if (LoopControl.LOOP_COUNTED.equals(loopType)) { loopCountLimit = loopCount.getTextAsInt(outer, Resources.getString("Editor.LoopControl.loop_count"), this); //$NON-NLS-1$ } RecursionLimitException loopException = null; for (;;) { // While loop - test condition is still true before actions if (LoopControl.LOOP_WHILE.equals(loopType)) { if (!whileExpression.accept(outer)) { break; } } // Execute the actions and catch and looping. Save any // loop Exception to be thrown after the post-loop code // to ensure post-loop key is executed. try { doLoopOnce(c); } catch (RecursionLimitException ex) { loopException = ex; break; } // Until loop - test condition is not false after loop if (LoopControl.LOOP_UNTIL.equals(loopType)) { if (untilExpression.accept(outer)) { break; } } // Check for infinite looping. Save any // loop Exception to be thrown after the post-loop code // to ensure post-loop key is executed. if (loopCounter++ >= LoopControl.LOOP_LIMIT) { loopException = new RecursionLimitException(this); break; } // Counted loop - Check if looped enough times if (LoopControl.LOOP_COUNTED.equals(loopType)) { if (loopCounter >= loopCountLimit) { break; } } // Increment the Index Variable indexValue += step; } // Issue the Post-loop key executeKey(c, postLoopKey); // Report any loop exceptions if (loopException != null) { RecursionLimiter.infiniteLoop(loopException); } return c; } private int parse (String desc, FormattedString s, GamePiece outer) { int i = 0; String val = s.getText(outer, "0"); try { i = Integer.parseInt(val); } catch (NumberFormatException e) { reportDataError(this, Resources.getString("Error.non_number_error"), s.debugInfo(val, desc), e); } return i; } protected boolean isIndex() { return loop && index && indexProperty != null && indexProperty.length() > 0; } public Object getProperty(Object key) { if (isIndex() && indexProperty.equals(key)) { return String.valueOf(indexValue); } return super.getProperty(key); } public Object getLocalizedProperty(Object key) { if (isIndex() && indexProperty.equals(key)) { return String.valueOf(indexValue); } return super.getLocalizedProperty(key); } protected void doLoopOnce(Command c) throws RecursionLimitException { try { RecursionLimiter.startExecution(this); for (int i = 0; i < actionKeys.length && getMap() != null; i++) { c.append(outer.keyEvent(actionKeys[i].getKeyStroke())); } } finally { RecursionLimiter.endExecution(); } } protected void executeKey(Command c, NamedKeyStroke key) { if (key.isNull() || getMap() == null) { return; } try { RecursionLimiter.startExecution(this); c.append(outer.keyEvent(key.getKeyStroke())); } catch (RecursionLimitException e) { RecursionLimiter.infiniteLoop(e); } finally { RecursionLimiter.endExecution(); } } protected boolean matchesFilter() { final GamePiece outer = Decorator.getOutermost(this); if (!propertyMatch.isNull()) { if (!propertyMatch.accept(outer)) { return false; } } return true; } public void mySetState(String newState) { } public Shape getShape() { return piece.getShape(); } public String getDescription() { String s = Resources.getString("Editor.TriggerAction.component_type"); //$NON-NLS-1$ if (name.length() > 0) { s += " - " + name; //$NON-NLS-1$ } return s; } public HelpFile getHelpFile() { return HelpFile.getReferenceManualPage("TriggerAction.htm"); //$NON-NLS-1$ } public void mySetType(String type) { SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(type, ';'); st.nextToken(); name = st.nextToken(""); //$NON-NLS-1$ command = st.nextToken("Trigger"); //$NON-NLS-1$ key = st.nextNamedKeyStroke('T'); propertyMatch.setExpression(st.nextToken("")); //$NON-NLS-1$ String keys = st.nextToken(""); //$NON-NLS-1$ if (keys.indexOf(',') > 0) { watchKeys = NamedKeyStrokeArrayConfigurer.decode(keys); } else { watchKeys = new NamedKeyStroke[keys.length()]; for (int i = 0; i < watchKeys.length; i++) { watchKeys[i] = NamedKeyStroke.getNamedKeyStroke(keys.charAt(i), InputEvent.CTRL_MASK); } } keys = st.nextToken(""); //$NON-NLS-1$ if (keys.indexOf(',') > 0) { actionKeys = NamedKeyStrokeArrayConfigurer.decode(keys); } else { actionKeys = new NamedKeyStroke[keys.length()]; for (int i = 0; i < actionKeys.length; i++) { actionKeys[i] = NamedKeyStroke.getNamedKeyStroke(keys.charAt(i), InputEvent.CTRL_MASK); } } loop = st.nextBoolean(false); preLoopKey = st.nextNamedKeyStroke(); postLoopKey = st.nextNamedKeyStroke(); loopType = st.nextToken(LoopControl.LOOP_COUNTED); whileExpression.setExpression(st.nextToken("")); //$NON-NLS-1$ untilExpression.setExpression(st.nextToken("")); //$NON-NLS-1$ loopCount.setFormat(st.nextToken("")); //$NON-NLS-1$ index = st.nextBoolean(false); indexProperty = st.nextToken(""); //$NON-NLS-1$ indexStart.setFormat(st.nextToken("1")); indexStep.setFormat(st.nextToken("1")); } /** * Return Property names exposed by this trait */ public List<String> getPropertyNames() { if (isIndex()) { final ArrayList<String> l = new ArrayList<String>(); l.add(indexProperty); return l; } else { return super.getPropertyNames(); } } // Setters for JUnit testing public void setPropertyMatch(String s) { propertyMatch.setExpression(s); } public void setCommandName(String s) { command = s; } public void setKey(NamedKeyStroke k) { key = k; } public PieceEditor getEditor() { return new Ed(this); } public PieceI18nData getI18nData() { return getI18nData(command, getCommandDescription(name, "Trigger command")); //$NON-NLS-1$ } public static class Ed implements PieceEditor { private StringConfigurer name; private StringConfigurer command; private NamedHotKeyConfigurer key; private PropertyExpressionConfigurer propertyMatch; private NamedKeyStrokeArrayConfigurer watchKeys; private NamedKeyStrokeArrayConfigurer actionKeys; private JPanel box; private BooleanConfigurer loopConfig; private NamedHotKeyConfigurer preLoopKeyConfig; private NamedHotKeyConfigurer postLoopKeyConfig; private StringEnumConfigurer loopTypeConfig; private PropertyExpressionConfigurer whileExpressionConfig; private PropertyExpressionConfigurer untilExpressionConfig; private FormattedStringConfigurer loopCountConfig; private BooleanConfigurer indexConfig; private StringConfigurer indexPropertyConfig; private FormattedStringConfigurer indexStartConfig; private FormattedStringConfigurer indexStepConfig; public Ed(TriggerAction piece) { final PropertyChangeListener updateListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent arg0) { updateVisibility(); } }; box = new JPanel(); box.setLayout(new BoxLayout(box, BoxLayout.Y_AXIS)); name = new StringConfigurer(null, Resources.getString("Editor.description_label"), piece.name); //$NON-NLS-1$ box.add(name.getControls()); propertyMatch = new PropertyExpressionConfigurer(null, Resources.getString("Editor.TriggerAction.trigger_when_properties"), piece.propertyMatch, Decorator //$NON-NLS-1$ .getOutermost(piece)); box.add(propertyMatch.getControls()); Box commandBox = Box.createHorizontalBox(); command = new StringConfigurer(null, Resources.getString("Editor.menu_command"), piece.command); //$NON-NLS-1$ commandBox.add(command.getControls()); key = new NamedHotKeyConfigurer(null, Resources.getString("Editor.keyboard_command"), piece.key); //$NON-NLS-1$ commandBox.add(key.getControls()); box.add(commandBox); watchKeys = new NamedKeyStrokeArrayConfigurer(null, Resources.getString("Editor.TriggerAction.watch_for"), piece.watchKeys); //$NON-NLS-1$ box.add(watchKeys.getControls()); actionKeys = new NamedKeyStrokeArrayConfigurer(null, Resources.getString("Editor.TriggerAction.perform_keystrokes"), piece.actionKeys); //$NON-NLS-1$ box.add(actionKeys.getControls()); loopConfig = new BooleanConfigurer(null, Resources.getString("Editor.TriggerAction.repeat_this"), piece.loop); //$NON-NLS-1$ loopConfig.addPropertyChangeListener(updateListener); box.add(loopConfig.getControls()); loopTypeConfig = new StringEnumConfigurer(null, Resources.getString("Editor.LoopControl.type_of_loop"), //$NON-NLS-1$ LoopControl.LOOP_TYPE_DESCS); loopTypeConfig.setValue(LoopControl.loopTypeToDesc(piece.loopType)); loopTypeConfig.addPropertyChangeListener(updateListener); box.add(loopTypeConfig.getControls()); loopCountConfig = new FormattedExpressionConfigurer(null, Resources.getString("Editor.LoopControl.loop_how_many"), piece.loopCount.getFormat(), piece); //$NON-NLS-1$ box.add(loopCountConfig.getControls()); whileExpressionConfig = new PropertyExpressionConfigurer(null, Resources.getString("Editor.TriggerAction.looping_continues"), piece.whileExpression); //$NON-NLS-1$ box.add(whileExpressionConfig.getControls()); untilExpressionConfig = new PropertyExpressionConfigurer(null, Resources.getString("Editor.TriggerAction.looping_ends"), piece.untilExpression); //$NON-NLS-1$ box.add(untilExpressionConfig.getControls()); preLoopKeyConfig = new NamedHotKeyConfigurer(null, Resources.getString("Editor.TriggerAction.keystroke_before"), //$NON-NLS-1$ piece.preLoopKey); box.add(preLoopKeyConfig.getControls()); postLoopKeyConfig = new NamedHotKeyConfigurer(null, Resources.getString("Editor.TriggerAction.keystroke_after"), //$NON-NLS-1$ piece.postLoopKey); box.add(postLoopKeyConfig.getControls()); indexConfig = new BooleanConfigurer(null, Resources.getString("Editor.LoopControl.loop_index"), piece.index); //$NON-NLS-1$ indexConfig.addPropertyChangeListener(updateListener); box.add(indexConfig.getControls()); indexPropertyConfig = new StringConfigurer(null, Resources.getString("Editor.LoopControl.index_name"), piece.indexProperty); //$NON-NLS-1$ box.add(indexPropertyConfig.getControls()); indexStartConfig = new FormattedExpressionConfigurer(null, Resources.getString("Editor.LoopControl.index_start"), piece.indexStart.getFormat(), piece); //$NON-NLS-1$ box.add(indexStartConfig.getControls()); indexStepConfig = new FormattedExpressionConfigurer(null, Resources.getString("Editor.LoopControl.index_step"), piece.indexStep.getFormat(), piece); //$NON-NLS-1$ box.add(indexStepConfig.getControls()); updateVisibility(); } private void updateVisibility() { final boolean isLoop = loopConfig.booleanValue().booleanValue(); final boolean isIndex = indexConfig.booleanValue().booleanValue(); final String type = LoopControl.loopDescToType(loopTypeConfig.getValueString()); loopTypeConfig.getControls().setVisible(isLoop); loopCountConfig.getControls().setVisible( isLoop && type.equals(LoopControl.LOOP_COUNTED)); whileExpressionConfig.getControls().setVisible( isLoop && type.equals(LoopControl.LOOP_WHILE)); untilExpressionConfig.getControls().setVisible( isLoop && type.equals(LoopControl.LOOP_UNTIL)); preLoopKeyConfig.getControls().setVisible(isLoop); postLoopKeyConfig.getControls().setVisible(isLoop); indexConfig.getControls().setVisible(isLoop); indexPropertyConfig.getControls().setVisible(isLoop && isIndex); indexStartConfig.getControls().setVisible(isLoop && isIndex); indexStepConfig.getControls().setVisible(isLoop && isIndex); Window w = SwingUtilities.getWindowAncestor(box); if (w != null) { w.pack(); } } public Component getControls() { return box; } public String getState() { return ""; //$NON-NLS-1$ } public String getType() { SequenceEncoder se = new SequenceEncoder(';'); se.append(name.getValueString()) .append(command.getValueString()) .append(key.getValueString()) .append(propertyMatch.getValueString()) .append(watchKeys.getValueString()) .append(actionKeys.getValueString()) .append(loopConfig.getValueString()) .append(preLoopKeyConfig.getValueString()) .append(postLoopKeyConfig.getValueString()) .append(LoopControl.loopDescToType(loopTypeConfig.getValueString())) .append(whileExpressionConfig.getValueString()) .append(untilExpressionConfig.getValueString()) .append(loopCountConfig.getValueString()) .append(indexConfig.getValueString()) .append(indexPropertyConfig.getValueString()) .append(indexStartConfig.getValueString()) .append(indexStepConfig.getValueString()); return ID + se.getValue(); } } // Implement Loopable public String getComponentName() { // Use inner name to prevent recursive looping when reporting errors. return piece.getName(); } public String getComponentTypeName() { return getDescription(); } }