/*
* $Id$
*
* Copyright (c) 2000-2012 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.build.module;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import VASSAL.build.AbstractConfigurable;
import VASSAL.build.AutoConfigurable;
import VASSAL.build.Buildable;
import VASSAL.build.GameModule;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.build.module.properties.MutableProperty;
import VASSAL.command.Command;
import VASSAL.command.NullCommand;
import VASSAL.command.PlayAudioClipCommand;
import VASSAL.configure.AudioClipConfigurer;
import VASSAL.configure.Configurer;
import VASSAL.configure.ConfigurerFactory;
import VASSAL.configure.FormattedExpressionConfigurer;
import VASSAL.configure.IconConfigurer;
import VASSAL.configure.ListConfigurer;
import VASSAL.configure.NamedHotKeyConfigurer;
import VASSAL.configure.PlayerIdFormattedStringConfigurer;
import VASSAL.configure.PropertyExpression;
import VASSAL.configure.StringEnumConfigurer;
import VASSAL.configure.VisibilityCondition;
import VASSAL.i18n.Resources;
import VASSAL.i18n.TranslatableConfigurerFactory;
import VASSAL.tools.FormattedString;
import VASSAL.tools.LaunchButton;
import VASSAL.tools.LoopControl;
import VASSAL.tools.NamedKeyStroke;
import VASSAL.tools.RecursionLimitException;
import VASSAL.tools.RecursionLimiter;
import VASSAL.tools.SequenceEncoder;
/**
* This component places a button into the controls window toolbar.
* Pressing the button displays a message, plays a sound and/or sends hotkeys */
public class DoActionButton extends AbstractConfigurable
implements RecursionLimiter.Loopable {
public static final String BUTTON_TEXT = "text"; //$NON-NLS-1$
public static final String TOOLTIP = "tooltip"; //$NON-NLS-1$
public static final String NAME = "name"; //$NON-NLS-1$
public static final String HOTKEY = "hotkey"; //$NON-NLS-1$
public static final String ICON = "icon"; //$NON-NLS-1$
public static final String DO_REPORT = "doReport"; //$NON-NLS-1$
public static final String REPORT_FORMAT = "reportFormat"; //$NON-NLS-1$
public static final String DO_SOUND = "doSound"; //$NON-NLS-1$
public static final String SOUND_CLIP = "soundClip"; //$NON-NLS-1$
public static final String DO_HOTKEY = "doHotkey"; //$NON-NLS-1$
public static final String HOTKEYS = "hotkeys"; //$NON-NLS-1$
public static final String DO_LOOP = "doLoop"; //$NON-NLS-1$
public static final String LOOP_TYPE = "loopType"; //$NON-NLS-1$
public static final String LOOP_COUNT = "loopCount"; //$NON-NLS-1$
public static final String WHILE_EXPRESSION = "whileExpression"; //$NON-NLS-1$
public static final String UNTIL_EXPRESSION = "untilExpression"; //$NON-NLS-1$
public static final String PRE_LOOP_HOTKEY = "preLoopKey"; //$NON-NLS-1$
public static final String POST_LOOP_HOTKEY = "postLoopKey"; //$NON-NLS-1$
public static final String INDEX = "index"; //$NON-NLS-1$
public static final String INDEX_PROPERTY = "indexProperty"; //$NON-NLS-1$
public static final String INDEX_START = "indexStart"; //$NON-NLS-1$
public static final String INDEX_STEP = "indexStep"; //$NON-NLS-1$
protected LaunchButton launch;
protected boolean doReport = false;
protected FormattedString reportFormat =
new FormattedString(GameModule.getGameModule());
protected boolean doSound = false;
protected String soundClip = ""; //$NON-NLS-1$
protected boolean doHotkey = false;
protected List<NamedKeyStroke> hotkeys = new ArrayList<NamedKeyStroke>();
protected boolean doLoop = false;
protected String loopType = LoopControl.LOOP_COUNTED;
protected FormattedString loopCount = new FormattedString("1"); //$NON-NLS-1$
protected PropertyExpression whileExpression = new PropertyExpression();
protected PropertyExpression untilExpression = new PropertyExpression();
protected NamedKeyStroke preLoopKey = NamedKeyStroke.NULL_KEYSTROKE;
protected NamedKeyStroke postLoopKey = NamedKeyStroke.NULL_KEYSTROKE;
protected boolean hasIndex = false;
protected String indexProperty = ""; //$NON-NLS-1$
protected int indexStart = 1;
protected int indexStep = 1;
protected int indexValue;
protected MutableProperty.Impl loopIndexProperty = new MutableProperty.Impl("",this);
protected boolean loopPropertyRegistered = false;
public DoActionButton() {
ActionListener rollAction = new ActionListener() {
public void actionPerformed(ActionEvent e) {
try {
doActions();
}
catch (RecursionLimitException ex) {
RecursionLimiter.infiniteLoop(ex);
}
}
};
final String description = Resources.getString("Editor.DoAction.component_type"); //$NON-NLS-1$
launch = new LaunchButton(
description, TOOLTIP, BUTTON_TEXT, HOTKEY, ICON, rollAction);
setAttribute(NAME, description);
setAttribute(TOOLTIP, description);
launch.setAttribute(BUTTON_TEXT, description);
}
public static String getConfigureTypeName() {
return Resources.getString("Editor.DoAction.component_type"); //$NON-NLS-1$
}
public String[] getAttributeNames() {
return new String[]{
NAME,
BUTTON_TEXT,
TOOLTIP,
ICON,
HOTKEY,
DO_REPORT,
REPORT_FORMAT,
DO_SOUND,
SOUND_CLIP,
DO_HOTKEY,
HOTKEYS,
DO_LOOP,
LOOP_TYPE,
LOOP_COUNT,
WHILE_EXPRESSION,
UNTIL_EXPRESSION,
PRE_LOOP_HOTKEY,
POST_LOOP_HOTKEY,
INDEX,
INDEX_PROPERTY,
INDEX_START,
INDEX_STEP
};
}
public String[] getAttributeDescriptions() {
return new String[]{
Resources.getString(Resources.DESCRIPTION),
Resources.getString(Resources.BUTTON_TEXT),
Resources.getString(Resources.TOOLTIP_TEXT),
Resources.getString(Resources.BUTTON_ICON),
Resources.getString(Resources.HOTKEY_LABEL),
Resources.getString("Editor.DoAction.display_message"), //$NON-NLS-1$
Resources.getString("Editor.report_format"), //$NON-NLS-1$
Resources.getString("Editor.DoAction.play_sound"), //$NON-NLS-1$
Resources.getString("Editor.DoAction.sound_clip"), //$NON-NLS-1$
Resources.getString("Editor.DoAction.send_hotkeys"), //$NON-NLS-1$
Resources.getString("Editor.DoAction.hotkeys"), //$NON-NLS-1$
Resources.getString("Editor.DoAction.repeat_actions"), //$NON-NLS-1$
Resources.getString("Editor.LoopControl.type_of_loop"), //$NON-NLS-1$
Resources.getString("Editor.LoopControl.loop_how_many"), //$NON-NLS-1$
Resources.getString("Editor.LoopControl.looping_continues"), //$NON-NLS-1$
Resources.getString("Editor.LoopControl.looping_ends"), //$NON-NLS-1$
Resources.getString("Editor.DoAction.perform_before"), //$NON-NLS-1$
Resources.getString("Editor.DoAction.perform_after"), //$NON-NLS-1$
Resources.getString("Editor.LoopControl.loop_index"), //$NON-NLS-1$
Resources.getString("Editor.LoopControl.index_name"), //$NON-NLS-1$
Resources.getString("Editor.LoopControl.index_start"), //$NON-NLS-1$
Resources.getString("Editor.LoopControl.index_step") //$NON-NLS-1$
};
}
public static class IconConfig implements ConfigurerFactory {
public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
return new IconConfigurer(key, name, null);
}
}
public static class SoundConfig implements ConfigurerFactory {
public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
return new AudioClipConfigurer(key, name, GameModule.getGameModule().getArchiveWriter());
}
}
public static class ReportFormatConfig implements TranslatableConfigurerFactory {
public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
return new PlayerIdFormattedStringConfigurer(key, name, new String[]{});
}
}
public static class HotkeyConfig implements ConfigurerFactory {
public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
return new NamedHotkeyListConfigurer(key, name, ((DoActionButton) c).hotkeys);
}
}
public static class NamedHotkeyListConfigurer extends ListConfigurer {
public NamedHotkeyListConfigurer(String key, String name, List<NamedKeyStroke> list) {
super(key, name, list);
}
protected Configurer buildChildConfigurer() {
return new NamedHotKeyConfigurer(null, Resources.getString(Resources.HOTKEY_LABEL));
}
}
public static class LoopConfig implements ConfigurerFactory {
public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
return new LoopTypeConfig(key, name, ((DoActionButton) c).loopType);
}
}
public static class LoopTypeConfig extends StringEnumConfigurer {
public LoopTypeConfig(String key, String name, String loopType) {
super(key, name, LoopControl.LOOP_TYPE_DESCS);
setValue(LoopControl.loopTypeToDesc(loopType));
}
public String[] getValidValues(AutoConfigurable target) {
return LoopControl.LOOP_TYPE_DESCS;
}
public String getValueString() {
return LoopControl.loopDescToType(super.getValueString());
}
}
public static class LoopCountConfig implements ConfigurerFactory {
public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
return new FormattedExpressionConfigurer(key, name, ((DoActionButton) c).loopCount);
}
}
public Class<?>[] getAttributeTypes() {
return new Class<?>[]{
String.class,
String.class,
String.class,
IconConfig.class,
NamedKeyStroke.class,
Boolean.class,
ReportFormatConfig.class,
Boolean.class,
SoundConfig.class,
Boolean.class,
HotkeyConfig.class,
Boolean.class,
LoopConfig.class,
LoopCountConfig.class,
PropertyExpression.class,
PropertyExpression.class,
NamedKeyStroke.class,
NamedKeyStroke.class,
Boolean.class,
String.class,
Integer.class,
Integer.class
};
}
public void addTo(Buildable parent) {
GameModule.getGameModule().getToolBar().add(getComponent());
}
/**
* The component to be added to the control window toolbar
*/
protected Component getComponent() {
return launch;
}
@SuppressWarnings("unchecked")
public void setAttribute(String key, Object o) {
if (NAME.equals(key)) {
setConfigureName((String) o);
}
else if (DO_REPORT.equals(key)) {
if (o instanceof String) {
o = Boolean.valueOf((String) o);
}
doReport = ((Boolean) o).booleanValue();
}
else if (REPORT_FORMAT.equals(key)) {
reportFormat.setFormat((String) o);
}
else if (DO_SOUND.equals(key)) {
if (o instanceof String) {
o = Boolean.valueOf((String) o);
}
doSound = ((Boolean) o).booleanValue();
}
if (SOUND_CLIP.equals(key)) {
if (o instanceof File) {
o = ((File) o).getName();
}
soundClip = (String) o;
}
else if (DO_HOTKEY.equals(key)) {
if (o instanceof String) {
o = Boolean.valueOf((String) o);
}
doHotkey = ((Boolean) o).booleanValue();
}
else if (HOTKEYS.equals(key)) {
if (o instanceof String) {
o = decodeHotkeys((String) o);
}
hotkeys = (List<NamedKeyStroke>) o;
}
else if (DO_LOOP.equals(key)) {
if (o instanceof String) {
o = Boolean.valueOf((String) o);
}
doLoop = ((Boolean) o).booleanValue();
updateLoopPropertyRegistration();
}
else if (LOOP_TYPE.equals(key)) {
loopType = LoopControl.loopDescToType((String) o);
}
else if (LOOP_COUNT.equals(key)) {
loopCount.setFormat((String) o);
}
else if (WHILE_EXPRESSION.equals(key)) {
whileExpression.setExpression((String) o);
}
else if (UNTIL_EXPRESSION.equals(key)) {
untilExpression.setExpression((String) o);
}
else if (PRE_LOOP_HOTKEY.equals(key)) {
if (o instanceof String) {
o = NamedHotKeyConfigurer.decode((String) o);
}
preLoopKey = (NamedKeyStroke) o;
}
else if (POST_LOOP_HOTKEY.equals(key)) {
if (o instanceof String) {
o = NamedHotKeyConfigurer.decode((String) o);
}
postLoopKey = (NamedKeyStroke) o;
}
else if (INDEX.equals(key)) {
if (o instanceof String) {
o = Boolean.valueOf((String) o);
}
hasIndex = ((Boolean) o).booleanValue();
updateLoopPropertyRegistration();
}
else if (INDEX_PROPERTY.equals(key)) {
indexProperty = (String) o;
loopIndexProperty.setPropertyName(indexProperty);
updateLoopPropertyRegistration();
}
else if (INDEX_START.equals(key)) {
if (o instanceof String) {
o = Integer.valueOf((String) o);
}
indexStart = ((Integer) o).intValue();
}
else if (INDEX_STEP.equals(key)) {
if (o instanceof String) {
o = Integer.valueOf((String) o);
}
indexStep = ((Integer) o).intValue();
}
else {
launch.setAttribute(key, o);
}
}
public String getAttributeValueString(String key) {
if (NAME.equals(key)) {
return getConfigureName();
}
else if (DO_REPORT.equals(key)) {
return String.valueOf(doReport);
}
else if (REPORT_FORMAT.equals(key)) {
return reportFormat.getFormat();
}
else if (DO_SOUND.equals(key)) {
return String.valueOf(doSound);
}
else if (SOUND_CLIP.equals(key)) {
return soundClip;
}
else if (DO_HOTKEY.equals(key)) {
return String.valueOf(doHotkey);
}
else if (HOTKEYS.equals(key)) {
return encodeHotkeys();
}
else if (DO_LOOP.equals(key)) {
return String.valueOf(doLoop);
}
else if (LOOP_TYPE.equals(key)) {
return loopType;
}
else if (LOOP_COUNT.equals(key)) {
return loopCount.getFormat();
}
else if (WHILE_EXPRESSION.equals(key)) {
return whileExpression.getExpression();
}
else if (UNTIL_EXPRESSION.equals(key)) {
return untilExpression.getExpression();
}
else if (PRE_LOOP_HOTKEY.equals(key)) {
return NamedHotKeyConfigurer.encode(preLoopKey);
}
else if (POST_LOOP_HOTKEY.equals(key)) {
return NamedHotKeyConfigurer.encode(postLoopKey);
}
else if (INDEX.equals(key)) {
return String.valueOf(hasIndex);
}
else if (INDEX_PROPERTY.equals(key)) {
return indexProperty;
}
else if (INDEX_START.equals(key)) {
return String.valueOf(indexStart);
}
else if (INDEX_STEP.equals(key)) {
return String.valueOf(indexStep);
}
else {
return launch.getAttributeValueString(key);
}
}
public VisibilityCondition getAttributeVisibility(String name) {
if (REPORT_FORMAT.equals(name)) {
return new VisibilityCondition() {
public boolean shouldBeVisible() {
return doReport;
}};
}
else if (SOUND_CLIP.equals(name)) {
return new VisibilityCondition() {
public boolean shouldBeVisible() {
return doSound;
}};
}
else if (HOTKEYS.equals(name)) {
return new VisibilityCondition() {
public boolean shouldBeVisible() {
return doHotkey;
}};
}
else if (LOOP_COUNT.equals(name)) {
return new VisibilityCondition() {
public boolean shouldBeVisible() {
return doLoop && LoopControl.LOOP_COUNTED.equals(loopType);
}};
}
else if (WHILE_EXPRESSION.equals(name)) {
return new VisibilityCondition() {
public boolean shouldBeVisible() {
return doLoop && LoopControl.LOOP_WHILE.equals(loopType);
}};
}
else if (UNTIL_EXPRESSION.equals(name)) {
return new VisibilityCondition() {
public boolean shouldBeVisible() {
return doLoop && LoopControl.LOOP_UNTIL.equals(loopType);
}};
}
else if (LOOP_TYPE.equals(name) || PRE_LOOP_HOTKEY.equals(name) || POST_LOOP_HOTKEY.equals(name) || INDEX.equals(name)) {
return new VisibilityCondition() {
public boolean shouldBeVisible() {
return doLoop;
}};
}
else if (INDEX_PROPERTY.equals(name) || INDEX_START.equals(name) || INDEX_STEP.equals(name)) {
return new VisibilityCondition() {
public boolean shouldBeVisible() {
return doLoop && hasIndex;
}};
}
else {
return null;
}
}
protected String encodeHotkeys() {
final SequenceEncoder se = new SequenceEncoder(',');
for (NamedKeyStroke key : hotkeys) {
se.append(NamedHotKeyConfigurer.encode(key));
}
final String val = se.getValue();
return val == null ? "" : val; //$NON-NLS-1$
}
protected List<NamedKeyStroke> decodeHotkeys(String s) {
List<NamedKeyStroke> list = new ArrayList<NamedKeyStroke>();
SequenceEncoder.Decoder sd = new SequenceEncoder.Decoder(s, ',');
while (sd.hasMoreTokens()) {
NamedKeyStroke key = NamedHotKeyConfigurer.decode(sd.nextToken());
list.add(key);
}
return list;
}
public Class<?>[] getAllowableConfigureComponents() {
return new Class<?>[0];
}
public void removeFrom(Buildable b) {
GameModule.getGameModule().getToolBar().remove(getComponent());
GameModule.getGameModule().getToolBar().revalidate();
}
public HelpFile getHelpFile() {
return HelpFile.getReferenceManualPage("DoActionButton.htm"); //$NON-NLS-1$
}
/**
* Register/Deregister the Global Property exposing the index property. It
* is only visible if looping is turned on and an Index Property is specified
*/
protected void updateLoopPropertyRegistration() {
final boolean shouldBeRegistered = doLoop && hasIndex && indexProperty.length() > 0;
if (shouldBeRegistered && !loopPropertyRegistered) {
loopIndexProperty.addTo(GameModule.getGameModule());
loopPropertyRegistered = true;
}
else if (!shouldBeRegistered && loopPropertyRegistered) {
loopIndexProperty.removeFromContainer();
loopPropertyRegistered = false;
}
}
protected void setIndexPropertyValue() {
loopIndexProperty.setPropertyValue(String.valueOf(indexValue));
}
protected void doActions() throws RecursionLimitException {
final Command c = new NullCommand();
final GameModule mod = GameModule.getGameModule();
RecursionLimitException loopException = null;
// Non looping case
if (! doLoop) {
executeActions(c);
mod.sendAndLog(c);
return;
}
// Set up Index Property
indexValue = indexStart;
setIndexPropertyValue();
// Issue the Pre-loop key
doHotKey(c, preLoopKey);
// Set up counters for a counted loop
int loopCounter = 0;
int loopCountLimit = 0;
if (LoopControl.LOOP_COUNTED.equals(loopType)) {
loopCountLimit = loopCount.getTextAsInt(mod, Resources.getString("Editor.LoopControl.loop_count"), this); //$NON-NLS-1$
}
for (;;) {
// While loop - test condition is still true before actions
if (LoopControl.LOOP_WHILE.equals(loopType)) {
if (!whileExpression.isTrue(mod)) {
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 {
executeActions(c);
}
catch (RecursionLimitException ex) {
loopException = ex;
break;
}
// Until loop - test condition is not false after loop
if (LoopControl.LOOP_UNTIL.equals(loopType)) {
if (untilExpression.isTrue(mod)) {
break;
}
}
// Counted loop - Check if looped enough times
loopCounter++;
if (LoopControl.LOOP_COUNTED.equals(loopType)) {
if (loopCounter >= loopCountLimit) {
break;
}
}
// Otherwise check for too much looping.
else {
if (loopCounter >= LoopControl.LOOP_LIMIT) {
loopException = new RecursionLimitException(this);
break;
}
}
// Increment the Index Variable
indexValue += indexStep;
setIndexPropertyValue();
}
// Issue the Post-loop key
doHotKey(c, postLoopKey);
// Send the accumulated commands to the log
mod.sendAndLog(c);
// If the loop ended due to excessive looping, throw the
// Exception out to the caller.
if (loopException != null) {
throw loopException;
}
return;
}
/**
* Execute the set of actions that make up this button and
* return the set of Commands generated.
*
* @param command
* @throws RecursionLimitException
*/
protected void executeActions(Command command) throws RecursionLimitException {
final GameModule mod = GameModule.getGameModule();
// GameModule.pauseLogging() returns false if logging is already paused by
// a higher level component.
final boolean loggingPaused = mod.pauseLogging();
try {
RecursionLimiter.startExecution(this);
if (doReport) {
final String report = "* " + reportFormat.getLocalizedText(); //$NON-NLS-1$
final Command c = new Chatter.DisplayText(mod.getChatter(), report);
c.execute();
mod.sendAndLog(c);
}
if (doSound) {
final String clipName = new FormattedString(soundClip).getText(mod);
final Command c = new PlayAudioClipCommand(clipName);
c.execute();
mod.sendAndLog(c);
}
// Send the hotkeys. Individual hotkeys have already executed
// the commands they generated.
if (doHotkey) {
for (NamedKeyStroke key : hotkeys) {
mod.fireKeyStroke(key);
}
}
}
finally {
RecursionLimiter.endExecution();
// If we paused the log, then retrieve the accumulated commands
// generated by all actions and restart logging.
if (loggingPaused) {
command.append(mod.resumeLogging());
}
}
}
// Perform an individual Hotkey and return any generated commands
// if logging has not already been paused.
protected void doHotKey (Command c, NamedKeyStroke key) {
if (!key.isNull()) {
final GameModule mod = GameModule.getGameModule();
boolean loggingPaused = mod.pauseLogging();
try {
mod.fireKeyStroke(key);
}
finally {
if (loggingPaused) {
c.append(mod.resumeLogging());
}
}
}
return;
}
// Implement Loopable
public String getComponentTypeName () {
return getConfigureTypeName();
}
public String getComponentName() {
return getConfigureName();
}
/**
* Implement PropertyNameSource - Expose loop index property if looping turned on
*/
public List<String> getPropertyNames() {
if (doLoop && hasIndex) {
final ArrayList<String> l = new ArrayList<String>();
l.add(indexProperty);
return l;
}
else {
return super.getPropertyNames();
}
}
}