/*
* $Id$
*
* Copyright (c) 2000-2003 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.Frame;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import VASSAL.build.Configurable;
import VASSAL.build.GameModule;
import VASSAL.build.GpIdSupport;
import VASSAL.build.module.BasicCommandEncoder;
import VASSAL.build.module.Chatter;
import VASSAL.build.module.Map;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.build.widget.CardSlot;
import VASSAL.build.widget.PieceSlot;
import VASSAL.command.AddPiece;
import VASSAL.command.ChangeTracker;
import VASSAL.command.Command;
import VASSAL.configure.BooleanConfigurer;
import VASSAL.configure.ChooseComponentPathDialog;
import VASSAL.configure.ConfigurerWindow;
import VASSAL.configure.IntConfigurer;
import VASSAL.configure.NamedHotKeyConfigurer;
import VASSAL.configure.StringConfigurer;
import VASSAL.i18n.PieceI18nData;
import VASSAL.i18n.Resources;
import VASSAL.i18n.TranslatablePiece;
import VASSAL.tools.ComponentPathBuilder;
import VASSAL.tools.NamedKeyStroke;
import VASSAL.tools.SequenceEncoder;
/**
* This Decorator defines a key command to places another counter on top of this one.
*/
public class PlaceMarker extends Decorator implements TranslatablePiece {
public static final String ID = "placemark;";
protected KeyCommand command;
protected NamedKeyStroke key;
protected String markerSpec;
protected String markerText = "";
protected int xOffset = 0;
protected int yOffset = 0;
protected boolean matchRotation = false;
protected KeyCommand[] commands;
protected NamedKeyStroke afterBurnerKey;
protected String description = "";
protected String gpId = "";
protected String newGpId;
protected GpIdSupport gpidSupport; // The component that generates unique Slot Id's for us
protected static final int STACK_TOP = 0;
protected static final int STACK_BOTTOM = 1;
protected static final int ABOVE = 2;
protected static final int BELOW = 3;
protected int placement = STACK_TOP;
protected boolean above;
public PlaceMarker() {
this(ID + "Place Marker;M;null;null;null", null);
}
public PlaceMarker(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() {
command.setEnabled(getMap() != null && markerSpec != null);
return commands;
}
public String myGetState() {
return "";
}
public String myGetType() {
final SequenceEncoder se = new SequenceEncoder(';');
se.append(command.getName())
.append(key)
.append(markerSpec == null ? "null" : markerSpec)
.append(markerText == null ? "null" : markerText)
.append(xOffset).append(yOffset)
.append(matchRotation)
.append(afterBurnerKey)
.append(description)
.append(gpId)
.append(placement)
.append(above);
return ID + se.getValue();
}
public Command myKeyEvent(KeyStroke stroke) {
myGetKeyCommands();
if (command.matches(stroke)) {
return placeMarker();
}
else {
return null;
}
}
protected Command placeMarker() {
final Map m = getMap();
if (m == null) return null;
final GamePiece marker = createMarker();
if (marker == null) return null;
Command c = null;
final GamePiece outer = getOutermost(this);
Point p = getPosition();
p.translate(xOffset, -yOffset);
if (matchRotation) {
final FreeRotator myRotation =
(FreeRotator) Decorator.getDecorator(outer, FreeRotator.class);
final FreeRotator markerRotation =
(FreeRotator) Decorator.getDecorator(marker, FreeRotator.class);
if (myRotation != null && markerRotation != null) {
markerRotation.setAngle(myRotation.getAngle());
final Point2D myPosition = getPosition().getLocation();
Point2D markerPosition = p.getLocation();
markerPosition = AffineTransform.getRotateInstance(
myRotation.getAngleInRadians(),
myPosition.getX(), myPosition.getY()
).transform(markerPosition, null);
p = new Point((int) markerPosition.getX(), (int) markerPosition.getY());
}
}
if (!Boolean.TRUE.equals(marker.getProperty(Properties.IGNORE_GRID))) {
p = getMap().snapTo(p);
}
if (m.getStackMetrics().isStackingEnabled() &&
!Boolean.TRUE.equals(marker.getProperty(Properties.NO_STACK)) &&
!Boolean.TRUE.equals(outer.getProperty(Properties.NO_STACK)) &&
m.getPieceCollection().canMerge(outer, marker)) {
final Stack parent = getParent();
GamePiece target = outer;
int index = -1;
switch (placement) {
case ABOVE:
target = outer;
break;
case BELOW:
index = parent == null ? 0 : parent.indexOf(outer);
break;
case STACK_BOTTOM:
index = 0;
break;
case STACK_TOP:
target = parent == null ? outer : parent;
}
c = m.getStackMetrics().merge(target, marker);
if (index >= 0) {
final ChangeTracker ct = new ChangeTracker(parent);
parent.insert(marker,index);
c = c.append(ct.getChangeCommand());
}
}
else {
c = m.placeAt(marker, p);
}
if (afterBurnerKey != null && !afterBurnerKey.isNull()) {
marker.setProperty(Properties.SNAPSHOT,
PieceCloner.getInstance().clonePiece(marker));
c.append(marker.keyEvent(afterBurnerKey.getKeyStroke()));
}
if (getProperty(Properties.SELECTED) == Boolean.TRUE)
selectMarker(marker);
if (markerText != null) {
if (!Boolean.TRUE.equals(
outer.getProperty(Properties.OBSCURED_TO_OTHERS)) &&
!Boolean.TRUE.equals(
outer.getProperty(Properties.OBSCURED_TO_ME)) &&
!Boolean.TRUE.equals(
outer.getProperty(Properties.INVISIBLE_TO_OTHERS))) {
final String location = m.locationName(getPosition());
if (location != null) {
Command display = new Chatter.DisplayText(
GameModule.getGameModule().getChatter(),
" * " + location + ": " + outer.getName() +
" " + markerText + " * ");
display.execute();
c = c == null ? display : c.append(display);
}
}
}
return c;
}
protected void selectMarker(GamePiece marker) {
if (marker.getProperty(Properties.SELECT_EVENT_FILTER) == null) {
if (marker.getParent() != null && marker.getParent().equals(getParent())) {
KeyBuffer.getBuffer().add(marker);
}
}
}
/**
* The marker, with prototypes fully expanded
*
* @return
*/
public GamePiece createMarker() {
GamePiece piece = createBaseMarker();
if (piece == null) {
piece = new BasicPiece();
newGpId = getGpId();
}
else {
piece = PieceCloner.getInstance().clonePiece(piece);
}
piece.setProperty(Properties.PIECE_ID, newGpId);
return piece;
}
/**
* The marker, with prototypes unexpanded
*
* @return
*/
public GamePiece createBaseMarker() {
if (markerSpec == null) {
return null;
}
GamePiece piece = null;
if (isMarkerStandalone()) {
final AddPiece comm =
(AddPiece) GameModule.getGameModule().decode(markerSpec);
piece = comm.getTarget();
piece.setState(comm.getState());
newGpId = getGpId();
}
else {
try {
final Configurable[] c =
ComponentPathBuilder.getInstance().getPath(markerSpec);
final Configurable conf = c[c.length-1];
if (conf instanceof PieceSlot) {
piece = ((PieceSlot) conf).getPiece();
newGpId = ((PieceSlot) conf).getGpId();
}
}
catch (ComponentPathBuilder.PathFormatException e) {
reportDataError(this, Resources.getString("Resources.place_error"), e.getMessage()+" markerSpec="+markerSpec, e);
}
}
return piece;
}
/**
* @return true if the marker is defined from scratch. Return false if the marker is defined as a component in the
* Game Piece Palette
*/
public boolean isMarkerStandalone() {
return markerSpec != null && markerSpec.startsWith(BasicCommandEncoder.ADD);
}
public void mySetState(String newState) {
}
public Shape getShape() {
return piece.getShape();
}
public String getDescription() {
String d = "Place Marker";
if (description.length() > 0) {
d += " - " + description;
}
return d;
}
public HelpFile getHelpFile() {
return HelpFile.getReferenceManualPage("Marker.htm");
}
public void mySetType(String type) {
SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(type, ';');
st.nextToken();
String name = st.nextToken();
key = st.nextNamedKeyStroke(null);
command = new KeyCommand(name, key, this, this);
if (name.length() > 0 && key != null) {
commands = new KeyCommand[]{command};
}
else {
commands = new KeyCommand[0];
}
markerSpec = st.nextToken();
if ("null".equals(markerSpec)) {
markerSpec = null;
}
markerText = st.nextToken("null");
if ("null".equals(markerText)) {
markerText = null;
}
xOffset = st.nextInt(0);
yOffset = st.nextInt(0);
matchRotation = st.nextBoolean(false);
afterBurnerKey = st.nextNamedKeyStroke(null);
description = st.nextToken("");
setGpId(st.nextToken(""));
placement = st.nextInt(STACK_TOP);
above = st.nextBoolean(false);
gpidSupport = GameModule.getGameModule().getGpIdSupport();
}
public PieceEditor getEditor() {
return new Ed(this);
}
public PieceI18nData getI18nData() {
return getI18nData(command.getName(), getCommandDescription(description, "Place Marker command"));
}
public String getGpId() {
return gpId;
}
public void setGpId(String s) {
gpId = s;
}
public void updateGpId(GpIdSupport s) {
gpidSupport = s;
updateGpId();
}
public void updateGpId() {
setGpId(gpidSupport.generateGpId());
}
protected static class Ed implements PieceEditor {
private NamedHotKeyConfigurer keyInput;
private StringConfigurer commandInput;
private PieceSlot pieceInput;
private JPanel p = new JPanel();
private String markerSlotPath;
protected JButton defineButton = new JButton("Define Marker");
protected JButton selectButton = new JButton("Select");
protected IntConfigurer xOffsetConfig = new IntConfigurer(null, "Horizontal offset: ");
protected IntConfigurer yOffsetConfig = new IntConfigurer(null, "Vertical offset: ");
protected BooleanConfigurer matchRotationConfig;
protected BooleanConfigurer aboveConfig;
protected JComboBox placementConfig;
protected NamedHotKeyConfigurer afterBurner;
protected StringConfigurer descConfig;
private String slotId;
protected Ed(PlaceMarker piece) {
matchRotationConfig = createMatchRotationConfig();
aboveConfig = createAboveConfig();
descConfig = new StringConfigurer(null, "Description: ", piece.description);
keyInput = new NamedHotKeyConfigurer(null, "Keyboard Command: ", piece.key);
afterBurner = new NamedHotKeyConfigurer(null, "Keystroke to apply after placement: ", piece.afterBurnerKey);
commandInput = new StringConfigurer(null, "Command: ", piece.command.getName());
GamePiece marker = piece.createBaseMarker();
pieceInput = new PieceSlot(marker);
pieceInput.updateGpId(piece.gpidSupport);
pieceInput.setGpId(piece.getGpId());
markerSlotPath = piece.markerSpec;
p = new JPanel();
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
p.add(descConfig.getControls());
p.add(commandInput.getControls());
p.add(keyInput.getControls());
Box b = Box.createHorizontalBox();
b.add(pieceInput.getComponent());
defineButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
markerSlotPath = null;
new ConfigurerWindow(pieceInput.getConfigurer()).setVisible(true);
}
});
b.add(defineButton);
selectButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
ChoosePieceDialog d = new ChoosePieceDialog((Frame) SwingUtilities.getAncestorOfClass(Frame.class, p), PieceSlot.class);
d.setVisible(true);
if (d.getTarget() instanceof PieceSlot) {
pieceInput.setPiece(((PieceSlot) d.getTarget()).getPiece());
}
if (d.getPath() != null) {
markerSlotPath = ComponentPathBuilder.getInstance().getId(d.getPath());
slotId = "";
}
else {
markerSlotPath = null;
}
}
});
b.add(selectButton);
p.add(b);
xOffsetConfig.setValue(piece.xOffset);
p.add(xOffsetConfig.getControls());
yOffsetConfig.setValue(piece.yOffset);
p.add(yOffsetConfig.getControls());
matchRotationConfig.setValue(Boolean.valueOf(piece.matchRotation));
p.add(matchRotationConfig.getControls());
if (aboveConfig != null) {
aboveConfig.setValue(Boolean.valueOf(piece.above));
p.add(aboveConfig.getControls());
((JCheckBox) matchRotationConfig.getControls()).addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
aboveConfig.getControls().setVisible(((JCheckBox) matchRotationConfig.getControls()).isSelected());
}
});
aboveConfig.getControls().setVisible(Boolean.valueOf(piece.matchRotation));
}
placementConfig = new JComboBox(new String[]{"On top of stack","On bottom of stack","Above this piece","Below this piece"});
placementConfig.setSelectedIndex(piece.placement);
Box placementBox = Box.createHorizontalBox();
placementBox.add(new JLabel("Place marker: "));
placementBox.add(placementConfig);
p.add(placementBox);
p.add(afterBurner.getControls());
slotId = piece.getGpId();
}
protected BooleanConfigurer createMatchRotationConfig() {
return new BooleanConfigurer(null, "Match Rotation?");
}
protected BooleanConfigurer createAboveConfig() {
return null;
}
public Component getControls() {
return p;
}
public String getState() {
return "";
}
public String getType() {
SequenceEncoder se = new SequenceEncoder(';');
se.append(commandInput.getValueString());
se.append(keyInput.getValueString());
if (pieceInput.getPiece() == null) {
se.append("null");
}
else if (markerSlotPath != null) {
se.append(markerSlotPath);
}
else {
String spec = GameModule.getGameModule().encode(new AddPiece(pieceInput.getPiece()));
se.append(spec);
}
se.append("null"); // Older versions specified a text message to echo. Now performed by the ReportState trait,
// but we remain backward-compatible.
se.append(xOffsetConfig.getValueString());
se.append(yOffsetConfig.getValueString());
se.append(matchRotationConfig.getValueString());
se.append(afterBurner.getValueString());
se.append(descConfig.getValueString());
se.append(slotId);
se.append(placementConfig.getSelectedIndex());
se.append(aboveConfig == null ? "false" : aboveConfig.getValueString());
return ID + se.getValue();
}
public static class ChoosePieceDialog extends ChooseComponentPathDialog {
private static final long serialVersionUID = 1L;
public ChoosePieceDialog(Frame owner, Class<PieceSlot> targetClass) {
super(owner, targetClass);
}
protected boolean isValidTarget(Object selected) {
return super.isValidTarget(selected) || CardSlot.class.isInstance(selected);
}
}
}
}