/*
* Copyright (c) 2014 tabletoptool.com team.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*
* Contributors:
* rptools.com team - initial implementation
* tabletoptool.com team - further development
*/
package com.t3.model;
import groovy.lang.Script;
import java.awt.Color;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import com.t3.client.AppPreferences;
import com.t3.client.T3MacroContext;
import com.t3.client.T3Util;
import com.t3.client.TabletopTool;
import com.t3.client.ui.MacroButtonHotKeyManager;
import com.t3.client.ui.macrobuttons.buttons.MacroButton;
import com.t3.client.ui.macrobuttons.buttons.MacroButtonPrefs;
import com.t3.macro.MacroEngine;
import com.t3.macro.MacroException;
import com.t3.model.chat.TextMessage;
import com.t3.util.StringUtil;
import com.t3.xstreamversioned.version.SerializationVersion;
/**
* This (data)class is used by all Macro Buttons, including campaign, global and token macro buttons.
*
* @see com.t3.client.ui.macrobuttons.buttons.MacroButton
*/
@SerializationVersion(0)
public class MacroButtonProperties implements Comparable<MacroButtonProperties> {
private static final Logger log = Logger.getLogger(MacroButtonProperties.class);
// private transient static final List<String> HTMLColors = Arrays.asList("aqua", "black", "blue", "fuchsia", "gray", "green", "lime", "maroon", "navy", "olive", "purple", "red", "silver", "teal",
// "white", "yellow");
private transient MacroButton button;
private transient Token token;
private String saveLocation;
private int index;
private String colorKey;
private String hotKey;
private String command;
private String label;
private String group;
private String sortby;
private String fontColorKey;
private String fontSize;
private String minWidth;
private String maxWidth;
private boolean allowPlayerEdits = true;
private String toolTip;
/**this will be automatically generated from the command string whenever it is changed */
private transient Script compiledCommand;
// constructor that creates a new instance, doesn't auto-save
public MacroButtonProperties(int index, String colorKey, String hotKey, String command, String label, String group, String sortby, String fontColorKey, String fontSize, String minWidth, String maxWidth, String toolTip) {
setIndex(index);
setColorKey(colorKey);
setHotKey(hotKey);
setCommand(command);
setLabel(label);
setGroup(group);
setSortby(sortby);
setFontColorKey(fontColorKey);
setFontSize(fontSize);
setMinWidth(minWidth);
setMaxWidth(maxWidth);
setButton(null);
setToken(null);
setSaveLocation("");
setAllowPlayerEdits(AppPreferences.getAllowPlayerMacroEditsDefault());
setCompareGroup(true);
setCompareSortPrefix(true);
setCompareCommand(true);
setToolTip(toolTip);
}
// constructor that creates a new instance, doesn't auto save
public MacroButtonProperties(int index) {
setIndex(index);
setColorKey("");
setHotKey(MacroButtonHotKeyManager.HOTKEYS[0]);
setCommand("");
setLabel("(new)");
setGroup("");
setSortby("");
setFontColorKey("");
setFontSize("");
setMinWidth("");
setMaxWidth("");
setButton(null);
setToken(null);
setSaveLocation("");
setAllowPlayerEdits(AppPreferences.getAllowPlayerMacroEditsDefault());
setCompareGroup(true);
setCompareSortPrefix(true);
setCompareCommand(true);
setToolTip(null);
}
// constructor for creating a new button in a specific button group, auto-saves
public MacroButtonProperties(String panelClass, int index, String group) {
this(index);
setSaveLocation(panelClass);
setGroup(group);
setAllowPlayerEdits(AppPreferences.getAllowPlayerMacroEditsDefault());
setCompareGroup(true);
setCompareSortPrefix(true);
setCompareCommand(true);
setToolTip(null);
save();
}
// constructor for creating a new token button in a specific button group, auto-saves
public MacroButtonProperties(Token token, int index, String group) {
this(index);
setSaveLocation("Token");
setToken(token);
setGroup(group);
setAllowPlayerEdits(AppPreferences.getAllowPlayerMacroEditsDefault());
setCompareGroup(true);
setCompareSortPrefix(true);
setCompareCommand(true);
setToolTip(null);
save();
}
// constructor for creating a new copy of an existing button, auto-saves
public MacroButtonProperties(String panelClass, int index, MacroButtonProperties properties) {
this(index);
setSaveLocation(panelClass);
setColorKey(properties.getColorKey());
// use the default hot key
setCommand(properties.getCommand());
setLabel(properties.getLabel());
setGroup(properties.getGroup());
setSortby(properties.getSortby());
setFontColorKey(properties.getFontColorKey());
setFontSize(properties.getFontSize());
setMinWidth(properties.getMinWidth());
setMaxWidth(properties.getMaxWidth());
setAllowPlayerEdits(properties.getAllowPlayerEdits());
setCompareGroup(properties.getCompareGroup());
setCompareSortPrefix(properties.getCompareSortPrefix());
setCompareCommand(properties.getCompareCommand());
String tt = properties.getToolTip();
setToolTip(tt);
save();
}
// constructor for creating a new copy of an existing token button, auto-saves
public MacroButtonProperties(Token token, int index, MacroButtonProperties properties) {
this(index);
setSaveLocation("Token");
setToken(token);
setColorKey(properties.getColorKey());
// use the default hot key
setCommand(properties.getCommand());
setLabel(properties.getLabel());
setGroup(properties.getGroup());
setSortby(properties.getSortby());
setFontColorKey(properties.getFontColorKey());
setFontSize(properties.getFontSize());
setMinWidth(properties.getMinWidth());
setMaxWidth(properties.getMaxWidth());
setAllowPlayerEdits(properties.getAllowPlayerEdits());
setCompareGroup(properties.getCompareGroup());
setCompareSortPrefix(properties.getCompareSortPrefix());
setCompareCommand(properties.getCompareCommand());
String tt = properties.getToolTip();
setToolTip(tt);
save();
}
// constructor for creating common macro buttons on selection panel
public MacroButtonProperties(int index, MacroButtonProperties properties) {
this(index);
setToken((Token) null);
setColorKey(properties.getColorKey());
// use the default hot key
setCommand(properties.getCommand());
setLabel(properties.getLabel());
setGroup(properties.getGroup());
setSortby(properties.getSortby());
setFontColorKey(properties.getFontColorKey());
setFontSize(properties.getFontSize());
setMinWidth(properties.getMinWidth());
setMaxWidth(properties.getMaxWidth());
setAllowPlayerEdits(properties.getAllowPlayerEdits());
setCompareGroup(properties.getCompareGroup());
setCompareSortPrefix(properties.getCompareSortPrefix());
setCompareCommand(properties.getCompareCommand());
setToolTip(properties.getToolTip());
commonMacro = true;
}
public MacroButtonProperties(Token token, Map<String, String> props) {
this(props.containsKey("index") ? Integer.parseInt(props.get("index")) : token.getMacroNextIndex());
setToken(token);
if (props.containsKey("saveLocation"))
setSaveLocation(props.get("saveLocation"));
if (props.containsKey("colorKey"))
setColorKey(props.get("colorKey"));
if (props.containsKey("hotKey"))
setHotKey(props.get("hotKey"));
if (props.containsKey("command"))
setCommand(props.get("command"));
if (props.containsKey("label"))
setLabel(props.get("label"));
if (props.containsKey("group"))
setGroup(props.get("group"));
if (props.containsKey("sortby"))
setSortby(props.get("sortby"));
if (props.containsKey("fontColorKey"))
setFontColorKey(props.get("fontColorKey"));
if (props.containsKey("fontSize"))
setFontSize(props.get("fontSize"));
if (props.containsKey("minWidth"))
setMinWidth(props.get("minWidth"));
if (props.containsKey("maxWidth"))
setMaxWidth(props.get("maxWidth"));
if (props.containsKey("allowPlayerEdits"))
setAllowPlayerEdits(Boolean.valueOf(props.get("allowPlayerEdits")));
if (props.containsKey("toolTip"))
setToolTip(props.get("toolTip"));
if (props.containsKey("commonMacro"))
setCommonMacro(Boolean.valueOf(props.get("commonMacro")));
if (props.containsKey("compareGroup"))
setCompareGroup(Boolean.valueOf(props.get("compareGroup")));
if (props.containsKey("compareSortPrefix"))
setCompareSortPrefix(Boolean.valueOf(props.get("compareSortPrefix")));
if (props.containsKey("compareCommand"))
setCompareCommand(Boolean.valueOf(props.get("compareCommand")));
}
public void save() {
if (saveLocation.equals("Token") && token != null) {
getToken().saveMacroButtonProperty(this);
} else if (saveLocation.equals("GlobalPanel")) {
MacroButtonPrefs.savePreferences(this);
} else if (saveLocation.equals("CampaignPanel")) {
TabletopTool.getCampaign().saveMacroButtonProperty(this);
}
}
public void executeMacro() {
executeCommand(token);
}
/**
* The top level method for executing macros with the given list of tokens as context. In essence, this method calls
* the macro once for each token, and the token becomes the "impersonated" token for the duration of the macro
* barring any use of the <b>token()</b> or <b>switchToken()</b> roll options inside the macro itself.
*
* @param tokenList
*/
public void executeMacro(Collection<Token> tokenList) {
if (tokenList == null || tokenList.size() == 0) {
executeCommand();
} else if (commonMacro) {
executeCommonMacro(tokenList);
} else {
for (Token token : tokenList)
executeCommand(token);
}
}
private void executeCommonMacro(Collection<Token> tokenList) {
if (compareCommand) {
for (Token token : tokenList) {
executeCommand(token, null);
}
} else {
// We need to find the "matching" button for each token and ensure to run that one.
for (Token nextToken : tokenList) {
for (MacroButtonProperties nextMacro : nextToken.getMacroList(true)) {
if (nextMacro.hashCodeForComparison() == hashCodeForComparison()) {
nextMacro.executeCommand(nextToken);
}
}
}
}
}
private void executeCommand(Token token) {
if (getCommand() != null)
executeCommand(token,null);
}
private void executeCommand() {
executeCommand(null,null);
}
public Object executeMacro(Token token) {
return executeCommand(token,null);
}
private Object executeCommand(Token contextToken, Map<String, Object> arguments) {
String loc;
Object o = null;
boolean trusted = false;
if (saveLocation.equals("CampaignPanel") || !allowPlayerEdits) {
trusted = true;
}
if (saveLocation.equals("GlobalPanel")) {
loc = "global";
trusted = TabletopTool.getPlayer().isGM();
} else if (saveLocation.equals("CampaignPanel")) {
loc = "campaign";
} else if (contextToken != null) {
loc = "Token:" + contextToken.getName();
} else {
loc = "chat";
}
T3MacroContext newMacroContext = new T3MacroContext(label, loc, trusted, index);
try {
if(compiledCommand==null)
compileCommand();
if(compiledCommand!=null)
o=MacroEngine.getInstance().run(compiledCommand,arguments,contextToken,newMacroContext);
} catch (MacroException e) {
log.error("Error while trying to execute a macro from button",e);
TabletopTool.addMessage(TextMessage.me(e.getHTMLErrorMessage()));
}
TabletopTool.getFrame().getCommandPanel().getCommandTextArea().requestFocusInWindow();
return o;
}
private void compileCommand() throws MacroException {
if(command!=null && !StringUtils.isEmpty(command))
this.compiledCommand=MacroEngine.getInstance().compile(command);
}
public Token getToken() {
return token;
}
public void setToken(Token token) {
this.token = token;
}
public void setSaveLocation(String saveLocation) {
if (saveLocation.equals("ImpersonatePanel") || saveLocation.equals("SelectionPanel")) {
this.saveLocation = "Token";
} else {
this.saveLocation = saveLocation;
}
}
public void setButton(MacroButton button) {
this.button = button;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public String getColorKey() {
if (colorKey == null || colorKey.equals("")) {
return "default";
}
return colorKey;
}
public void setColorKey(String colorKey) {
if (T3Util.getColor(colorKey) != null)
this.colorKey = colorKey;
}
public String getHotKey() {
return hotKey;
}
public void setHotKey(String hotKey) {
this.hotKey = hotKey;
}
public String getCommand() {
return command;
}
public void setCommand(String command) {
try {
this.command = command;
compileCommand();
} catch (MacroException e) {
//TODO replace this with a better error dialog
TabletopTool.showError("This script you tried to save is not valid.",e);
}
}
public String getLabel() {
return (label == null ? "" : label);
}
public void setLabel(String label) {
this.label = label;
}
public String getGroup() {
return (group == null ? "" : group);
}
public String getGroupForDisplay() {
return this.group;
}
public void setGroup(String group) {
this.group = group;
}
public String getSortby() {
return (sortby == null ? "" : sortby);
}
public void setSortby(String sortby) {
this.sortby = sortby;
}
public static String[] getFontColors() {
// return (String[]) HTMLColors.toArray();
String[] array = T3Util.getColorNames().toArray(new String[0]);
return array;
}
/**
* Returns the font color of this button as an HTML string. It might be one of the 16 colors defined by the W3C as a
* standard HTML color (see <code>COLOR_MAP_HTML</code> for a list), but if it's not then the color is converted to
* CSS format <b>#FF00FF</b> format and that string is returned.
*
* @return
*/
public String getFontColorAsHtml() {
Color c = null;
String font = getFontColorKey();
if (T3Util.isHtmlColor(font)) {
return font;
}
c = T3Util.getColor(font);
if (c != null) {
return "#" + Integer.toHexString(c.getRGB()).substring(2);
}
return "black";
}
public String getFontColorKey() {
if (fontColorKey == null || StringUtil.isEmpty(fontColorKey)) {
fontColorKey = "black";
return fontColorKey;
}
Color c = T3Util.getColor(fontColorKey);
if (c != null) {
return fontColorKey;
}
return "black";
}
public void setFontColorKey(String fontColorKey) {
if (T3Util.getColor(fontColorKey) != null)
this.fontColorKey = fontColorKey;
}
public String getFontSize() {
return (fontSize == null || fontSize.equals("") ? "1.00em" : fontSize);
}
public void setFontSize(String fontSize) {
this.fontSize = (fontSize == null || fontSize.equals("") ? "1.00em" : fontSize);
}
public String getMinWidth() {
return (minWidth == null ? "" : minWidth);
}
public void setMinWidth(String minWidth) {
this.minWidth = minWidth;
}
public String getMaxWidth() {
return (maxWidth == null ? "" : maxWidth);
}
public void setMaxWidth(String maxWidth) {
this.maxWidth = maxWidth;
}
public Boolean getAllowPlayerEdits() {
return allowPlayerEdits;
}
public void setAllowPlayerEdits(Boolean value) {
allowPlayerEdits = value;
}
public String getSaveLocation() {
return saveLocation;
}
public void setToolTip(String tt) {
toolTip = (tt == null ? "" : tt);
}
public String getToolTip() {
if (toolTip == null)
toolTip = "";
return toolTip;
}
public String getEvaluatedToolTip() {
if (toolTip == null) {
return "";
}
if (!toolTip.trim().startsWith("{") && !toolTip.trim().startsWith("[")) {
return toolTip;
}
T3MacroContext context = new T3MacroContext("ToolTip", token != null ? token.getName() : "", false, index);
if (log.isDebugEnabled()) {
log.debug("Evaluating toolTip: " + (token != null ? "for token " + token.getName() + "(" + token.getId() + ")" : "")
+ "----------------------------------------------------------------------------------");
}
return TabletopTool.getParser().parseLine(token, toolTip, context);
}
public boolean isDuplicateMacro(String source, Token token) {
int macroHashCode = hashCodeForComparison();
List<MacroButtonProperties> existingMacroList = null;
if (source.equalsIgnoreCase("CampaignPanel")) {
existingMacroList = TabletopTool.getCampaign().getMacroButtonPropertiesArray();
} else if (source.equalsIgnoreCase("GlobalPanel")) {
existingMacroList = MacroButtonPrefs.getButtonProperties();
} else if (token != null) {
existingMacroList = token.getMacroList(false);
} else {
return false;
}
for (MacroButtonProperties existingMacro : existingMacroList) {
if (existingMacro.hashCodeForComparison() == macroHashCode) {
return true;
}
}
return false;
}
public void reset() {
colorKey = "default";
hotKey = MacroButtonHotKeyManager.HOTKEYS[0];
command = "";
compiledCommand=null;
label = String.valueOf(index);
group = "";
sortby = "";
fontColorKey = "black";
fontSize = "";
minWidth = "";
maxWidth = "";
toolTip = "";
}
//TODO: may have to rewrite hashcode and equals to only take index into account
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MacroButtonProperties that = (MacroButtonProperties) o;
if (index != that.index) {
return false;
}
if (colorKey != null ? !colorKey.equals(that.colorKey) : that.colorKey != null) {
return false;
}
if (command != null ? !command.equals(that.command) : that.command != null) {
return false;
}
if (hotKey != null ? !hotKey.equals(that.hotKey) : that.hotKey != null) {
return false;
}
if (label != null ? !label.equals(that.label) : that.label != null) {
return false;
}
if (group != null ? !group.equals(that.group) : that.group != null) {
return false;
}
if (sortby != null ? !sortby.equals(that.sortby) : that.sortby != null) {
return false;
}
if (fontColorKey != null ? !fontColorKey.equals(that.fontColorKey) : that.fontColorKey != null) {
return false;
}
if (fontSize != null ? !fontSize.equals(that.fontSize) : that.fontSize != null) {
return false;
}
if (minWidth != null ? !minWidth.equals(that.minWidth) : that.minWidth != null) {
return false;
}
if (maxWidth != null ? !maxWidth.equals(that.maxWidth) : that.maxWidth != null) {
return false;
}
return true;
}
@Override
public int hashCode() { // modified so longest strings are at the end
int result;
result = index;
result = 31 * result + (minWidth != null ? minWidth.hashCode() : 0);
result = 31 * result + (maxWidth != null ? maxWidth.hashCode() : 0);
result = 31 * result + (fontSize != null ? fontSize.hashCode() : 0);
result = 31 * result + (fontColorKey != null ? fontColorKey.hashCode() : 0);
result = 31 * result + (colorKey != null ? colorKey.hashCode() : 0);
result = 31 * result + (hotKey != null ? hotKey.hashCode() : 0);
result = 31 * result + (label != null ? label.hashCode() : 0);
result = 31 * result + (group != null ? group.hashCode() : 0);
result = 31 * result + (sortby != null ? sortby.hashCode() : 0);
result = 31 * result + (command != null ? command.hashCode() : 0);
return result;
}
// Don't include the index, so you can compare all the other properties between two macros
// Also don't include hot key since they can't be the same anyway, or cosmetic fields
public int hashCodeForComparison() {
int result;
result = 0;
result = 31 * result + (getLabel() != null ? label.hashCode() : 0);
result = 31 * result + (getCompareGroup() && group != null ? group.hashCode() : 0);
result = 31 * result + (getCompareSortPrefix() && sortby != null ? sortby.hashCode() : 0);
result = 31 * result + (getCompareCommand() && command != null ? command.hashCode() : 0);
return result;
}
// function to enable sorting of buttons; uses the group first, then sortby field
// concatenated with the label field. Case Insensitive
@Override
public int compareTo(MacroButtonProperties b2) throws ClassCastException {
if (b2 != this) {
String b1group = getGroup();
if (b1group == null)
b1group = "";
String b1sortby = getSortby();
if (b1sortby == null)
b1sortby = "";
String b1label = getLabel();
if (b1label == null)
b1label = "";
String b2group = b2.getGroup();
if (b2group == null)
b2group = "";
String b2sortby = b2.getSortby();
if (b2sortby == null)
b2sortby = "";
String b2label = b2.getLabel();
if (b2label == null)
b2label = "";
// now parse the sort strings to help dice codes sort properly, use space as a separator
String b1string = modifySortString(" " + b1group + " " + b1sortby + " " + b1label);
String b2string = modifySortString(" " + b2group + " " + b2sortby + " " + b2label);
return b1string.compareToIgnoreCase(b2string);
}
return 0;
}
// function to pad numbers with leading zeroes to help sort them appropriately.
// So this will turn a 2d6 into 0002d0006, and 10d6 into 0010d0006, so the 2d6
// will sort as lower.
private static final Pattern sortStringPattern = Pattern.compile("(\\d+)");
private static String modifySortString(String str) {
StringBuffer result = new StringBuffer();
Matcher matcher = sortStringPattern.matcher(str);
while (matcher.find()) {
matcher.appendReplacement(result, paddingString(matcher.group(1), 4, '0', true));
}
matcher.appendTail(result);
return result.toString();
}
// function found at http://www.rgagnon.com/javadetails/java-0448.html
// to pad a string by inserting additional characters
public static String paddingString(String s, int n, char c, boolean paddingLeft) {
StringBuffer str = new StringBuffer(s);
int strLength = str.length();
if (n > 0 && n > strLength) {
for (int i = 0; i <= n; i++) {
if (paddingLeft) {
if (i < n - strLength)
str.insert(0, c);
} else {
if (i > strLength)
str.append(c);
}
}
}
return str.toString();
}
// Begin comparison customization
private boolean commonMacro = false;
private boolean compareGroup = true;
private boolean compareSortPrefix = true;
private boolean compareCommand = true;
public boolean getCommonMacro() {
return commonMacro;
}
public void setCommonMacro(boolean value) {
commonMacro = value;
}
public boolean getCompareGroup() {
return compareGroup;
}
public void setCompareGroup(boolean value) {
compareGroup = value;
}
public boolean getCompareSortPrefix() {
return compareSortPrefix;
}
public void setCompareSortPrefix(boolean value) {
compareSortPrefix = value;
}
public boolean getCompareCommand() {
return compareCommand;
}
public void setCompareCommand(boolean value) {
compareCommand = value;
}
public Object executeMacro(Token token, Map<String, Object> arguments) {
return executeCommand(token,arguments);
}
}