/*
* $Id$
*
* Copyright (c) 2000-2006 by Rodney Kinney
*
* 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.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import javax.swing.Box;
import javax.swing.KeyStroke;
import VASSAL.build.GameModule;
import VASSAL.build.module.Map;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.command.Command;
import VASSAL.configure.IntConfigurer;
import VASSAL.configure.NamedHotKeyConfigurer;
import VASSAL.configure.StringConfigurer;
import VASSAL.tools.NamedKeyStroke;
import VASSAL.tools.RecursionLimitException;
import VASSAL.tools.RecursionLimiter;
import VASSAL.tools.RecursionLimiter.Loopable;
import VASSAL.tools.SequenceEncoder;
/**
* A trait that acts like a button on a GamePiece, such that clicking on a
* particular area of the piece invokes a keyboard command
*
* @author rkinney
*
*/
public class ActionButton extends Decorator implements EditablePiece, Loopable {
public static final String ID = "button;";
protected NamedKeyStroke stroke;
protected Rectangle bounds = new Rectangle();
protected ButtonPusher pusher;
protected String description = "";
protected static ButtonPusher globalPusher = new ButtonPusher();
public ActionButton() {
this(ID, null);
}
public ActionButton(String type, GamePiece inner) {
mySetType(type);
setInner(inner);
pusher = globalPusher;
}
public void mySetState(String newState) {
}
public String myGetState() {
return "";
}
public String myGetType() {
SequenceEncoder se = new SequenceEncoder(';');
se.append(stroke).append(bounds.x).append(bounds.y).append(bounds.width).append(bounds.height).append(description);
return ID + se.getValue();
}
protected KeyCommand[] myGetKeyCommands() {
return new KeyCommand[0];
}
public Command myKeyEvent(KeyStroke stroke) {
return null;
}
public void draw(Graphics g, int x, int y, Component obs, double zoom) {
piece.draw(g, x, y, obs, zoom);
if (getMap() != null) {
pusher.register(getMap());
}
else {
// Do not allow button pushes if piece is not on a map
// pusher.register(obs, Decorator.getOutermost(this), x, y);
}
}
public Rectangle boundingBox() {
return piece.boundingBox();
}
public Shape getShape() {
return piece.getShape();
}
public String getName() {
return piece.getName();
}
public String getDescription() {
return description.length() == 0 ? "Action Button" : "Action Button - " + description;
}
public void mySetType(String type) {
SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(type, ';');
st.nextToken();
stroke = st.nextNamedKeyStroke('A');
bounds.x = st.nextInt(-20);
bounds.y = st.nextInt(-20);
bounds.width = st.nextInt(40);
bounds.height = st.nextInt(40);
description = st.nextToken("");
}
public HelpFile getHelpFile() {
return HelpFile.getReferenceManualPage("ActionButton.htm");
}
// Implement Loopable
public String getComponentName() {
// Use inner name to prevent recursive looping when reporting errors.
return piece.getName();
}
public String getComponentTypeName() {
return getDescription();
}
public PieceEditor getEditor() {
return new Ed(this);
}
public static class Ed implements PieceEditor {
private Box box;
private IntConfigurer xConfig;
private IntConfigurer yConfig;
private IntConfigurer widthConfig;
private IntConfigurer heightConfig;
private NamedHotKeyConfigurer strokeConfig;
protected StringConfigurer descConfig;
public Ed(ActionButton p) {
box = Box.createVerticalBox();
descConfig = new StringConfigurer(null, "Description: ", p.description);
box.add(descConfig.getControls());
strokeConfig = new NamedHotKeyConfigurer(null, "Invoke Key Command: ", p.stroke);
box.add(strokeConfig.getControls());
xConfig = new IntConfigurer(null, "Button X-offset: ", p.bounds.x);
box.add(xConfig.getControls());
yConfig = new IntConfigurer(null, "Button Y-offset: ", p.bounds.y);
box.add(yConfig.getControls());
widthConfig = new IntConfigurer(null, "Button Width: ", p.bounds.width);
box.add(widthConfig.getControls());
heightConfig = new IntConfigurer(null, "Button Height: ", p.bounds.height);
box.add(heightConfig.getControls());
}
public Component getControls() {
return box;
}
public String getType() {
SequenceEncoder se = new SequenceEncoder(';');
se.append(strokeConfig.getValueString()).append(xConfig.getValueString()).append(yConfig.getValueString()).append(widthConfig.getValueString()).append(
heightConfig.getValueString()).append(descConfig.getValueString());
return ID + se.getValue();
}
public String getState() {
return "";
}
}
/**
* Registers mouse listeners with Maps and other components. Clicking the
* mouse checks for pieces with an ActionButton trait and invokes them if the
* click falls within the button's boundaries
*/
protected static class ButtonPusher {
private Set<Map> maps = new HashSet<Map>();
private java.util.Map<Component,ComponentMouseListener>
componentMouseListeners = new HashMap<Component,ComponentMouseListener>();
public void register(Map map) {
if (map != null) {
if (!maps.contains(map)) {
map.addLocalMouseListener(new MapMouseListener(map));
maps.add(map);
}
}
}
public void register(Component obs, GamePiece piece, int x, int y) {
if (obs != null) {
ComponentMouseListener l = componentMouseListeners.get(obs);
if (l == null) {
l = new ComponentMouseListener(piece, x, y);
obs.addMouseListener(l);
componentMouseListeners.put(obs, l);
}
else {
l.xOffset = x;
l.yOffset = y;
l.target = piece;
}
}
}
/**
* Handle a mouse click on the given GamePiece at the given location (where
* 0,0 is the center of the piece). Activate all Action Buttons in sequence
* that are not Masked or Hidden
*
* @param p
* @param x
* @param y
* @param Offset
* A function to determine the offset of the target piece. This
* callback is done for efficiency reasons, since computing the
* offset may be expensive (as in the case of a piece in an
* expanded stack on a map) and is only needed if the piece has the
* ActionButton trait
*/
public void doClick(GamePiece p, Point point) {
for (GamePiece piece = p; piece instanceof Decorator;
piece = ((Decorator) piece).getInner()) {
if (piece instanceof Obscurable) {
if (((Obscurable) piece).obscuredToMe()) {
return;
}
}
else if (piece instanceof Hideable) {
if (((Hideable) piece).invisibleToMe()) {
return;
}
}
if (piece instanceof ActionButton) {
ActionButton action = (ActionButton) piece;
if (action.stroke != null && action.stroke.getKeyStroke() != null && action.bounds.contains(point)) {
// Save state prior to command
p.setProperty(Properties.SNAPSHOT,
PieceCloner.getInstance().clonePiece(p));
try {
RecursionLimiter.startExecution(action);
Command command = p.keyEvent(action.stroke.getKeyStroke());
GameModule.getGameModule().sendAndLog(command);
}
catch (RecursionLimitException e) {
RecursionLimiter.infiniteLoop(e);
}
finally {
RecursionLimiter.endExecution();
}
}
}
}
}
protected class MapMouseListener extends MouseAdapter {
private Map map;
public MapMouseListener(Map map) {
this.map = map;
}
public void mouseClicked(MouseEvent e) {
Point point = e.getPoint();
final GamePiece p = map.findPiece(point, PieceFinder.PIECE_IN_STACK);
if (p != null) {
Point rel = map.positionOf(p);
point.translate(-rel.x, -rel.y);
doClick(p, point);
}
}
}
protected class ComponentMouseListener extends MouseAdapter {
private GamePiece target;
private int xOffset;
private int yOffset;
public ComponentMouseListener(GamePiece piece, int x, int y) {
target = piece;
xOffset = x;
yOffset = y;
}
public void mouseClicked(MouseEvent e) {
Point point = e.getPoint();
point.translate(-xOffset,-yOffset);
doClick(target, point);
e.getComponent().repaint();
}
}
}
}