/*
* $Id$
*
* Copyright (c) 2004-2012 by Michael Blumohr, 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.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JDialog;
import javax.swing.JLabel;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.MutablePropertiesContainer;
import VASSAL.build.module.properties.MutableProperty;
import VASSAL.build.module.properties.MutableProperty.Impl;
import VASSAL.command.Command;
import VASSAL.command.CommandEncoder;
import VASSAL.command.NullCommand;
import VASSAL.configure.ColorConfigurer;
import VASSAL.configure.Configurer;
import VASSAL.configure.ConfigurerFactory;
import VASSAL.configure.IconConfigurer;
import VASSAL.configure.PlayerIdFormattedStringConfigurer;
import VASSAL.configure.VisibilityCondition;
import VASSAL.i18n.Resources;
import VASSAL.i18n.TranslatableConfigurerFactory;
import VASSAL.tools.ArrayUtils;
import VASSAL.tools.FormattedString;
import VASSAL.tools.KeyStrokeListener;
import VASSAL.tools.LaunchButton;
import VASSAL.tools.NamedKeyStroke;
import VASSAL.tools.SequenceEncoder;
import VASSAL.tools.UniqueIdManager;
import VASSAL.tools.imageop.Op;
/**
* ...
*/
// TODO Expose result as property
public class SpecialDiceButton extends AbstractConfigurable implements CommandEncoder, UniqueIdManager.Identifyable {
private static final Logger logger =
LoggerFactory.getLogger(SpecialDiceButton.class);
protected static UniqueIdManager idMgr = new UniqueIdManager("SpecialDiceButton"); //$NON-NLS-1$
public static final String SHOW_RESULTS_COMMAND = "SHOW_RESULTS\t"; //$NON-NLS-1$
protected List<SpecialDie> dice = new ArrayList<SpecialDie>();
protected java.util.Random ran;
protected boolean reportResultAsText = true;
protected boolean reportResultInWindow = false;
protected boolean reportResultInButton = false;
private LaunchButton launch;
protected String id;
protected String sMapName;
protected JDialog dialog; // Dialog to show results graphical
protected JLabel dialogLabel;
protected Color bgColor;
protected ResultsIcon resultsIcon = new ResultsIcon();
protected FormattedString format = new FormattedString();
protected String chatResultFormat = "** $" + NAME + "$ = [$result1$] *** <$" + GlobalOptions.PLAYER_NAME + "$>"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
protected String windowTitleResultFormat = "$" + NAME + "$"; //$NON-NLS-1$ //$NON-NLS-2$
protected String tooltip = ""; //$NON-NLS-1$
protected MutableProperty.Impl property = new Impl("",this); //$NON-NLS-1$
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 ICON = "icon"; //$NON-NLS-1$
public static final String RESULT_CHATTER = "resultChatter"; //$NON-NLS-1$
public static final String CHAT_RESULT_FORMAT = "format"; //$NON-NLS-1$
public static final String RESULT_N = "result#"; //$NON-NLS-1$
public static final String RESULT_TOTAL = "numericalTotal"; //$NON-NLS-1$
public static final String RESULT_WINDOW = "resultWindow"; //$NON-NLS-1$
public static final String WINDOW_TITLE_RESULT_FORMAT = "windowTitleResultFormat"; //$NON-NLS-1$
public static final String RESULT_BUTTON = "resultButton"; //$NON-NLS-1$
public static final String WINDOW_X = "windowX"; //$NON-NLS-1$
public static final String WINDOW_Y = "windowY"; //$NON-NLS-1$
public static final String BACKGROUND_COLOR = "backgroundColor"; //$NON-NLS-1$
public static final String DICE_SET = "diceSet"; //$NON-NLS-1$
public static final String HOTKEY = "hotkey"; //$NON-NLS-1$
public static final String NONE = "<none>"; //$NON-NLS-1$
private static final int[] EMPTY = new int[0];
public SpecialDiceButton() {
dialog = new JDialog(GameModule.getGameModule().getFrame());
dialog.setLayout(new MigLayout("ins 0"));
dialogLabel = new JLabel();
dialogLabel.setIcon(resultsIcon);
dialog.add(dialogLabel);
final ActionListener rollAction = new ActionListener() {
public void actionPerformed(ActionEvent e) {
DR();
}
};
launch = new LaunchButton(null, TOOLTIP, BUTTON_TEXT, HOTKEY, ICON, rollAction);
final String desc = Resources.getString("Editor.SpecialDiceButton.symbols"); //$NON-NLS-1$
setAttribute(NAME, desc);
setAttribute(BUTTON_TEXT, desc);
launch.setAttribute(TOOLTIP, desc);
}
public static String getConfigureTypeName() {
return Resources.getString("Editor.SpecialDiceButton.component_type"); //$NON-NLS-1$
}
/**
* The text reported before the results of the roll
*/
protected String getReportPrefix() {
return " *** " + getConfigureName() + " = "; //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* The text reported after the results of the roll;
*
* @deprecated
*/
@Deprecated
protected String getReportSuffix() {
return " *** <" //$NON-NLS-1$
+ GameModule.getGameModule().getChatter().getHandle() + ">"; //$NON-NLS-1$
}
/**
* Forwards the result of the roll to the {@link Chatter#send} method of the {@link Chatter} of the {@link GameModule}.
* Format is prefix+[comma-separated roll list]+suffix additionally a command for every die is generated
*/
protected void DR() {
final int[] results = new int[dice.size()];
int i = 0;
for (SpecialDie sd : dice) {
final int faceCount = sd.getFaceCount();
results[i++] = faceCount == 0 ? 0 : ran.nextInt(sd.getFaceCount());
}
setFormat(results);
Command c = reportResults(results);
if (reportResultAsText) {
c = c.append(reportTextResults(results));
}
GameModule.getGameModule().sendAndLog(c);
}
private Command reportResults(int[] results) {
resultsIcon.setResults(results);
if (reportResultInWindow) {
dialogLabel.setSize(new Dimension(resultsIcon.width, resultsIcon.height));
dialogLabel.setMinimumSize(new Dimension(resultsIcon.width, resultsIcon.height));
format.setFormat(windowTitleResultFormat);
dialog.setTitle(format.getLocalizedText());
dialog.pack();
dialog.setVisible(true);
dialogLabel.repaint();
}
if (reportResultInButton) {
launch.repaint();
}
return new ShowResults(this, results);
}
private Command reportTextResults(int[] results) {
int total = 0;
for (int i = 0; i < dice.size(); ++i) {
final SpecialDie die = dice.get(i);
total += die.getIntValue(results[i]);
}
format.setFormat(chatResultFormat);
String msg = format.getLocalizedText();
if (msg.length() > 0) {
if (msg.startsWith("*")) { //$NON-NLS-1$
msg = "*" + msg; //$NON-NLS-1$
}
else {
msg = "* " + msg; //$NON-NLS-1$
}
}
final Command c = msg.length() == 0 ? new NullCommand() : new Chatter.DisplayText(GameModule.getGameModule().getChatter(), msg);
c.execute();
c.append(property.setPropertyValue(String.valueOf(total)));
return c;
}
protected void setFormat(int[] results) {
format.setProperty(NAME, getLocalizedConfigureName());
int total = 0;
for (int i = 0; i < dice.size(); ++i) {
final SpecialDie die = dice.get(i);
format.setProperty("result" + (i + 1), die.getTextValue(results[i])); //$NON-NLS-1$
total += die.getIntValue(results[i]);
}
format.setProperty(RESULT_TOTAL, String.valueOf(total)); //$NON-NLS-1$
format.setFormat(chatResultFormat);
}
/**
* The Attributes of a DiceButton are:
*
* <code>BUTTON_TEXT</code> the label of the button in the toolbar <code>ICON</code> the icon of the button in the
* toolbar <code>HOTKEY</code> the hotkey equivalent of the button <code>DICE_SET</code> list of dice sets, an
* entry can be: [number]name of die[+|-modifier] "name of die" must be SpecialDie "modifier" is added/subtracted
* to/from total of dice [number]Dnumber of sides (e.g. 2D6) <code>NUMERIC</code> result of all dice is numeric
* <code>REPORT_TOTAL</code> If numeric and true, add the results of the dice together and report the total.
* Otherwise, report the individual results <code>SORT</code> if true sort results per die by numeric value
* <code>RESULT_CHATTER</code> if true report results in chatter <code>RESULT_WINDOW</code> if true show result
* graphical in extra window <code>WINDOW_X</code> width of window or button <code>WINDOW_Y</code> height of
* window or button <code>RESULT_MAP</code> :TODO: if true show result in special area in map <code>MAP_NAME</code>
* :TODO: name of map <code>RESULT_BUTTON</code> if true show result graphical in button
*/
public String[] getAttributeNames() {
return new String[]{
NAME,
BUTTON_TEXT,
TOOLTIP,
ICON,
HOTKEY,
RESULT_CHATTER,
CHAT_RESULT_FORMAT,
RESULT_WINDOW,
WINDOW_TITLE_RESULT_FORMAT,
RESULT_BUTTON,
WINDOW_X,
WINDOW_Y,
BACKGROUND_COLOR
};
}
public String[] getAttributeDescriptions() {
return new String[]{
Resources.getString(Resources.NAME_LABEL),
Resources.getString(Resources.BUTTON_TEXT),
Resources.getString(Resources.TOOLTIP_TEXT),
Resources.getString(Resources.BUTTON_ICON),
Resources.getString(Resources.HOTKEY_LABEL),
Resources.getString("Editor.SpecialDiceButton.report_results_text"), //$NON-NLS-1$
Resources.getString("Editor.report_format"), //$NON-NLS-1$
Resources.getString("Editor.SpecialDiceButton.result_window"), //$NON-NLS-1$
Resources.getString("Editor.SpecialDiceButton.window_title"), //$NON-NLS-1$
Resources.getString("Editor.SpecialDiceButton.result_button"), //$NON-NLS-1$
Resources.getString("Editor.SpecialDiceButton.width"), //$NON-NLS-1$
Resources.getString("Editor.SpecialDiceButton.height"), //$NON-NLS-1$
Resources.getString("Editor.SpecialDiceButton.background") //$NON-NLS-1$
};
}
public Class<?>[] getAttributeTypes() {
return new Class<?>[]{
String.class,
String.class,
String.class,
IconConfig.class,
NamedKeyStroke.class,
Boolean.class,
ReportFormatConfig.class,
Boolean.class,
ReportFormatConfig.class,
Boolean.class,
Integer.class,
Integer.class,
Color.class
};
}
public static class IconConfig implements ConfigurerFactory {
public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
return new IconConfigurer(key, name, "/images/die.gif"); //$NON-NLS-1$
}
}
public static class ReportFormatConfig implements TranslatableConfigurerFactory {
public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
return new PlayerIdFormattedStringConfigurer(key, name, new String[]{NAME, RESULT_N, RESULT_TOTAL});
}
}
public VisibilityCondition getAttributeVisibility(String name) {
// get size only when output in window or on button
if (WINDOW_X.equals(name) || WINDOW_Y.equals(name) || BACKGROUND_COLOR.equals(name)) {
return new VisibilityCondition() {
public boolean shouldBeVisible() {
return reportResultInWindow || reportResultInButton;
}
};
}
else if (CHAT_RESULT_FORMAT.equals(name)) {
return new VisibilityCondition() {
public boolean shouldBeVisible() {
return reportResultAsText;
}
};
}
else if (WINDOW_TITLE_RESULT_FORMAT.equals(name)) {
return new VisibilityCondition() {
public boolean shouldBeVisible() {
return reportResultInWindow;
}
};
}
else
return null;
}
public void addSpecialDie(SpecialDie d) {
dice.add(d);
}
public void removeSpecialDie(SpecialDie d) {
dice.remove(d);
}
/**
* Expects to be added to a SymbolDice. Adds the button to the control window's toolbar and registers itself as a
* {@link KeyStrokeListener}
*/
public void addTo(Buildable parent) {
resultsIcon.setResults(new int[dice.size()]);
launch.addHierarchyListener(new HierarchyListener() {
public void hierarchyChanged(HierarchyEvent e) {
if (launch.isShowing()) {
dialog.setLocationRelativeTo(launch);
launch.removeHierarchyListener(this);
}
}
});
final GameModule mod = GameModule.getGameModule();
ran = mod.getRNG();
mod.getToolBar().add(launch);
idMgr.add(this);
mod.addCommandEncoder(this);
property.addTo((MutablePropertiesContainer)parent);
}
public void removeFrom(Buildable b) {
final GameModule mod = GameModule.getGameModule();
mod.removeCommandEncoder(this);
mod.getToolBar().remove(launch);
mod.getToolBar().revalidate();
}
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
/**
* Make a best gues for a unique identifier for the target. Use
* {@link VASSAL.tools.UniqueIdManager.Identifyable#getConfigureName if non-null, otherwise use
* {@link VASSAL.tools.UniqueIdManager.Identifyable#getId
*
* @param target
* @return
*/
public String getIdentifier() {
return UniqueIdManager.getIdentifier(this);
}
/**
* Get boolean value of object.
*
* @param o object as input for setAttribute()
* @return boolean value of object
*/
private boolean getBoolVal(Object o) {
if (o instanceof Boolean) {
return ((Boolean) o).booleanValue();
}
else if (o instanceof String) {
return "true".equals(o); //$NON-NLS-1$
}
else
return false;
}
public void setAttribute(String key, Object o) {
if (NAME.equals(key)) {
setConfigureName((String) o);
property.setPropertyName(getConfigureName()+"_result"); //$NON-NLS-1$
launch.setToolTipText((String) o);
}
else if (RESULT_CHATTER.equals(key)) {
reportResultAsText = getBoolVal(o);
}
else if (CHAT_RESULT_FORMAT.equals(key)) {
chatResultFormat = (String) o;
}
else if (RESULT_BUTTON.equals(key)) {
reportResultInButton = getBoolVal(o);
if (reportResultInButton) {
launch.setIcon(resultsIcon);
}
}
else if (RESULT_WINDOW.equals(key)) {
reportResultInWindow = getBoolVal(o);
}
else if (WINDOW_TITLE_RESULT_FORMAT.equals(key)) {
windowTitleResultFormat = (String) o;
}
else if (WINDOW_X.equals(key)) {
if (o instanceof String) {
o = Integer.valueOf((String) o);
}
resultsIcon.width = ((Integer) o).intValue();
dialog.pack();
}
else if (WINDOW_Y.equals(key)) {
if (o instanceof String) {
o = Integer.valueOf((String) o);
}
resultsIcon.height = ((Integer) o).intValue();
dialog.pack();
}
else if (BACKGROUND_COLOR.equals(key)) {
if (o instanceof String) {
o = ColorConfigurer.stringToColor((String) o);
}
bgColor = (Color) o;
}
else if (TOOLTIP.equals(key)) {
tooltip = (String) o;
launch.setAttribute(key, o);
}
else {
launch.setAttribute(key, o);
}
}
public String getAttributeValueString(String key) {
if (NAME.equals(key)) {
return getConfigureName();
}
else if (RESULT_CHATTER.equals(key)) {
return String.valueOf(reportResultAsText);
}
else if (CHAT_RESULT_FORMAT.equals(key)) {
return chatResultFormat;
}
else if (RESULT_BUTTON.equals(key)) {
return String.valueOf(reportResultInButton);
}
else if (RESULT_WINDOW.equals(key)) {
return String.valueOf(reportResultInWindow);
}
else if (WINDOW_TITLE_RESULT_FORMAT.equals(key)) {
return windowTitleResultFormat;
}
else if (WINDOW_X.equals(key)) {
return String.valueOf(resultsIcon.width);
}
else if (WINDOW_Y.equals(key)) {
return String.valueOf(resultsIcon.height);
}
else if (BACKGROUND_COLOR.equals(key)) {
return ColorConfigurer.colorToString(bgColor);
}
else if (TOOLTIP.equals(name)) {
return tooltip.length() == 0 ? launch.getAttributeValueString(name) : tooltip;
}
else {
return launch.getAttributeValueString(key);
}
}
public Class<?>[] getAllowableConfigureComponents() {
return new Class<?>[] {SpecialDie.class};
}
public HelpFile getHelpFile() {
return HelpFile.getReferenceManualPage("SpecialDiceButton.htm"); //$NON-NLS-1$
}
/**
* create String from int array
*
* @param ia
* int-array
* @return encoded String
*/
public static String intArrayToString(int[] ia) {
if (ia == null || ia.length == 0) {
return ""; //$NON-NLS-1$
}
final SequenceEncoder se = new SequenceEncoder(',');
for (int i = 0; i < ia.length; ++i) {
se.append(String.valueOf(ia[i]));
}
return se.getValue();
}
/**
* get int array from string
*
* @param s
* string with encoded int array
* @return int array
*/
public static int[] stringToIntArray(String s) {
if (s == null || s.length() == 0) {
return EMPTY;
}
final SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(s, ',');
final ArrayList<String> l = new ArrayList<String>();
while (st.hasMoreTokens()) {
l.add(st.nextToken());
}
final int[] val = new int[l.size()];
for (int i = 0; i < val.length; ++i) {
val[i] = Integer.parseInt(l.get(i));
}
return val;
}
/**
* Implement PropertyNameSource - Expose roll result property
*/
public List<String> getPropertyNames() {
final ArrayList<String> l = new ArrayList<String>();
l.add(getConfigureName()+"_result");
return l;
}
public String encode(Command c) {
if (c instanceof ShowResults) {
final ShowResults c2 = (ShowResults) c;
final SequenceEncoder se = new SequenceEncoder(c2.target.getIdentifier(), '\t');
for (int i = 0; i < c2.rolls.length; ++i) {
se.append(c2.rolls[i] + ""); //$NON-NLS-1$
}
return SHOW_RESULTS_COMMAND + se.getValue();
}
else {
return null;
}
}
public Command decode(String s) {
SequenceEncoder.Decoder st = null;
if (s.startsWith(SHOW_RESULTS_COMMAND + getConfigureName()) || s.startsWith(SHOW_RESULTS_COMMAND + getId())) {
st = new SequenceEncoder.Decoder(s, '\t');
st.nextToken();
st.nextToken();
}
else if (s.startsWith(getId() + '\t')) { // Backward compatibility
st = new SequenceEncoder.Decoder(s, '\t');
st.nextToken();
}
if (st != null) {
final ArrayList<String> l = new ArrayList<String>();
while (st.hasMoreTokens()) {
l.add(st.nextToken());
}
final int[] results = new int[l.size()];
int i = 0;
for (String n : l) {
results[i++] = Integer.parseInt(n);
}
return new ShowResults(this, results);
}
else {
return null;
}
}
/**
* Command for displaying the results of a roll of the dice
*/
public static class ShowResults extends Command {
private SpecialDiceButton target;
private int[] rolls;
public ShowResults(SpecialDiceButton oTarget, int[] results) {
target = oTarget;
rolls = ArrayUtils.copyOf(results);
}
protected void executeCommand() {
target.setFormat(rolls);
target.reportResults(rolls);
}
protected Command myUndoCommand() {
return null;
}
}
/** Icon class for graphical display of a dice roll */
private class ResultsIcon implements Icon {
// FIXME: because Sun checks what class Icon implementations are,
// this won't display as disabled properly
// FIXME: how does this work? where are width and height set?
private int width, height;
private Icon[] icons;
public ResultsIcon() {
}
private void setResults(int[] results) {
icons = new Icon[results.length];
if (results.length > dice.size()) {
logger.warn(
"Special Die Button (" + getConfigureName() +
"): more results (" + results.length + ") requested than dice (" +
dice.size() +")"
);
}
for (int i = 0; i < results.length; ++i) {
if (i >= dice.size()) break;
final String imageName = dice.get(i).getImageName(results[i]);
if (imageName.length() > 0) {
final Image img = Op.load(imageName).getImage();
if (img != null) icons[i] = new ImageIcon(img);
}
}
}
public void paintIcon(Component c, Graphics g, int x, int y) {
if (bgColor != null) {
g.setColor(bgColor);
g.fillRect(x, y, width, height);
}
int offset = 0;
for (int i = 0; i < icons.length; ++i) {
if (icons[i] != null) {
icons[i].paintIcon(c, g, x + offset, y);
offset += icons[i].getIconWidth();
}
}
}
public int getIconWidth() {
return width;
}
public int getIconHeight() {
return height;
}
}
}