/*
* 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.client.tool;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Composite;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import com.t3.client.AppActions;
import com.t3.client.AppStyle;
import com.t3.client.AppUtil;
import com.t3.client.ScreenPoint;
import com.t3.client.TabletopTool;
import com.t3.client.tool.LayerSelectionDialog.LayerSelectionListener;
import com.t3.client.ui.StampPopupMenu;
import com.t3.client.ui.TokenLocation;
import com.t3.client.ui.TokenPopupMenu;
import com.t3.client.ui.Tool;
import com.t3.client.ui.Toolbox;
import com.t3.client.ui.token.EditTokenDialog;
import com.t3.client.ui.zone.ZoneOverlay;
import com.t3.client.ui.zone.ZoneRenderer;
import com.t3.guid.GUID;
import com.t3.image.ImageUtil;
import com.t3.model.CellPoint;
import com.t3.model.Token;
import com.t3.model.Zone;
import com.t3.model.Zone.Layer;
import com.t3.model.ZonePoint;
import com.t3.swing.SwingUtil;
import com.t3.util.ImageManager;
/**
*/
public class StampTool extends DefaultTool implements ZoneOverlay {
private static BufferedImage resizeImage;
private boolean isShowingTokenStackPopup;
private boolean isDraggingToken;
private boolean isNewTokenSelected;
private boolean isDrawingSelectionBox;
private boolean isMovingWithKeys;
private boolean isResizingToken;
private boolean isResizingRotatedToken;
private Rectangle selectionBoundBox;
// The position with greater than integer accuracy of a rotated stamp that is being resized.
private Point2D.Double preciseStampZonePoint;
private ZonePoint lastResizeZonePoint;
private Token tokenBeingDragged;
private Token tokenUnderMouse;
private Token tokenBeingResized;
private final TokenStackPanel tokenStackPanel = new TokenStackPanel();
//private Map<Shape, Token> rotateBoundsMap = new HashMap<Shape, Token>();
private final Map<Shape, Token> resizeBoundsMap = new HashMap<Shape, Token>();
private final LayerSelectionDialog layerSelectionDialog;
// Offset from token's X,Y when dragging. Values are in cell coordinates.
private int dragOffsetX;
private int dragOffsetY;
private int dragStartX;
private int dragStartY;
static {
try {
resizeImage = ImageUtil.getCompatibleImage("com/t3/client/image/arrow_out.png");
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
public StampTool() {
layerSelectionDialog = new LayerSelectionDialog(new Zone.Layer[] { Zone.Layer.TOKEN, Zone.Layer.GM, Zone.Layer.OBJECT, Zone.Layer.BACKGROUND }, new LayerSelectionListener() {
@Override
public void layerSelected(Layer layer) {
if (renderer != null) {
renderer.setActiveLayer(layer);
if (layer == Zone.Layer.TOKEN) {
TabletopTool.getFrame().getToolbox().setSelectedTool(PointerTool.class);
}
}
}
});
try {
setIcon(new ImageIcon(ImageUtil.getImage("com/t3/client/image/tool/stamper.png")));
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
@Override
public boolean isAvailable() {
return TabletopTool.getPlayer().isGM();
}
@Override
protected void detachFrom(ZoneRenderer renderer) {
TabletopTool.getFrame().hideControlPanel();
super.detachFrom(renderer);
}
@Override
protected void attachTo(ZoneRenderer renderer) {
TabletopTool.getFrame().showControlPanel(layerSelectionDialog);
super.attachTo(renderer);
layerSelectionDialog.updateViewList();
}
@Override
public String getInstructions() {
return "tool.pointer.instructions";
}
@Override
public String getTooltip() {
return "tool.stamp.tooltip";
}
public void startTokenDrag(Token keyToken) {
tokenBeingDragged = keyToken;
if (!TabletopTool.getPlayer().isGM() && TabletopTool.getServerPolicy().isMovementLocked()) {
// Not allowed
return;
}
renderer.addMoveSelectionSet(TabletopTool.getPlayer().getName(), tokenBeingDragged.getId(), renderer.getSelectedTokenSet(), false);
TabletopTool.serverCommand().startTokenMove(TabletopTool.getPlayer().getName(), renderer.getZone().getId(), tokenBeingDragged.getId(), renderer.getSelectedTokenSet());
isDraggingToken = true;
}
public void stopTokenDrag() {
renderer.commitMoveSelectionSet(tokenBeingDragged.getId()); // TODO: figure out a better way
isDraggingToken = false;
isMovingWithKeys = false;
dragOffsetX = 0;
dragOffsetY = 0;
}
private void showTokenStackPopup(List<Token> tokenList, int x, int y) {
tokenStackPanel.show(tokenList, x, y);
isShowingTokenStackPopup = true;
repaint();
}
private class TokenStackPanel {
private static final int PADDING = 4;
private List<Token> tokenList;
private final List<TokenLocation> tokenLocationList = new ArrayList<TokenLocation>();
private int x;
private int y;
public void show(List<Token> tokenList, int x, int y) {
this.tokenList = tokenList;
this.x = x - TokenStackPanel.PADDING - getSize().width / 2;
this.y = y - TokenStackPanel.PADDING - getSize().height / 2;
}
public Dimension getSize() {
int gridSize = (int) renderer.getScaledGridSize();
return new Dimension(tokenList.size() * (gridSize + PADDING) + PADDING, gridSize + PADDING * 2);
}
public void handleMouseEvent(MouseEvent event) {
// Nothing to do right now
}
public void handleMouseMotionEvent(MouseEvent event) {
Point p = event.getPoint();
for (TokenLocation location : tokenLocationList) {
if (location.getBounds().contains(p.x, p.y)) {
if (!AppUtil.playerOwns(location.getToken())) {
return;
}
renderer.clearSelectedTokens();
boolean selected = renderer.selectToken(location.getToken().getId());
if (selected) {
Tool tool = TabletopTool.getFrame().getToolbox().getSelectedTool();
if (!(tool instanceof StampTool)) {
return;
}
tokenUnderMouse = location.getToken();
((StampTool) tool).startTokenDrag(location.getToken());
}
return;
}
}
}
public void paint(Graphics g) {
Dimension size = getSize();
int gridSize = (int) renderer.getScaledGridSize();
// Background
g.setColor(getBackground());
g.fillRect(x, y, size.width, size.height);
// Border
AppStyle.border.paintAround((Graphics2D) g, x, y, size.width - 1, size.height - 1);
// Images
tokenLocationList.clear();
for (int i = 0; i < tokenList.size(); i++) {
Token token = tokenList.get(i);
BufferedImage image = ImageManager.getImage(token.getImageAssetId(), renderer);
Dimension imgSize = new Dimension(image.getWidth(), image.getHeight());
SwingUtil.constrainTo(imgSize, gridSize);
Rectangle bounds = new Rectangle(x + PADDING + i * (gridSize + PADDING), y + PADDING, imgSize.width, imgSize.height);
g.drawImage(image, bounds.x, bounds.y, bounds.width, bounds.height, renderer);
tokenLocationList.add(new TokenLocation(bounds, token));
}
}
public boolean contains(int x, int y) {
return new Rectangle(this.x, this.y, getSize().width, getSize().height).contains(x, y);
}
}
// //
// Mouse
@Override
public void mousePressed(MouseEvent e) {
super.mousePressed(e);
if (isShowingTokenStackPopup) {
if (tokenStackPanel.contains(e.getX(), e.getY())) {
tokenStackPanel.handleMouseEvent(e);
return;
} else {
isShowingTokenStackPopup = false;
repaint();
}
}
// So that keystrokes end up in the right place
renderer.requestFocusInWindow();
if (isDraggingMap()) {
return;
}
if (isDraggingToken) {
return;
}
dragStartX = e.getX();
dragStartY = e.getY();
// Check token resizing
for (Entry<Shape, Token> entry : resizeBoundsMap.entrySet()) {
Shape bounds = entry.getKey();
if (bounds.contains(dragStartX, dragStartY)) {
dragOffsetX = bounds.getBounds().x + bounds.getBounds().width - e.getX();
dragOffsetY = bounds.getBounds().y + bounds.getBounds().height - e.getY();
isResizingToken = true;
// The token being resized does not necessarily = tokenUnderMouse. If there is more then one
// token under the mouse, the top token will be the tokenUnderMouse, but it is the selected
// that is intended to be resized.
tokenBeingResized = entry.getValue();
return;
}
}
// Check token rotation
// for (Entry<Shape, Token> entry : rotateBoundsMap.entrySet()) {
// if (entry.getKey().contains(dragStartX, dragStartY)) {
// isRotatingToken = true;
// return;
// }
// }
// Properties
if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e)) {
List<Token> tokenList = renderer.getTokenStackAt(mouseX, mouseY);
if (tokenList != null) {
// Stack
renderer.clearSelectedTokens();
showTokenStackPopup(tokenList, e.getX(), e.getY());
} else {
// Single
Token token = getTokenAt(e.getX(), e.getY());
if (token != null) {
EditTokenDialog tokenPropertiesDialog = TabletopTool.getFrame().getTokenPropertiesDialog();
tokenPropertiesDialog.showDialog(token);
if (tokenPropertiesDialog.isTokenSaved()) {
TabletopTool.serverCommand().putToken(renderer.getZone().getId(), token);
renderer.getZone().putToken(token);
renderer.repaint();
renderer.flush(token);
}
}
}
return;
}
// SELECTION
Token token = getTokenAt(e.getX(), e.getY());
if (token != null && !isDraggingToken && SwingUtilities.isLeftMouseButton(e)) {
// Permission
if (!AppUtil.playerOwns(token)) {
if (!SwingUtil.isShiftDown(e)) {
renderer.clearSelectedTokens();
}
return;
}
// Don't select if it's already being moved by someone
isNewTokenSelected = false;
if (!renderer.isTokenMoving(token)) {
if (!renderer.getSelectedTokenSet().contains(token.getId()) && !SwingUtil.isShiftDown(e)) {
isNewTokenSelected = true;
renderer.clearSelectedTokens();
}
if (SwingUtil.isShiftDown(e) && renderer.getSelectedTokenSet().contains(token.getId())) {
renderer.deselectToken(token.getId());
} else {
renderer.selectToken(token.getId());
}
// Dragging offset for currently selected token
ZonePoint pos = new ScreenPoint(e.getX(), e.getY()).convertToZone(renderer);
dragOffsetX = pos.x - token.getX();
dragOffsetY = pos.y - token.getY();
}
} else {
if (SwingUtilities.isLeftMouseButton(e)) {
// Starting a bound box selection
isDrawingSelectionBox = true;
selectionBoundBox = new Rectangle(e.getX(), e.getY(), 0, 0);
} else {
if (tokenUnderMouse != null) {
isNewTokenSelected = true;
}
}
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (isShowingTokenStackPopup) {
if (tokenStackPanel.contains(e.getX(), e.getY())) {
tokenStackPanel.handleMouseEvent(e);
return;
} else {
isShowingTokenStackPopup = false;
repaint();
}
}
if (isResizingToken) {
renderer.flush(tokenBeingResized);
TabletopTool.serverCommand().putToken(renderer.getZone().getId(), tokenBeingResized);
isResizingToken = false;
isResizingRotatedToken = false;
tokenBeingResized = null;
return;
}
if (SwingUtilities.isLeftMouseButton(e)) {
try {
SwingUtil.showPointer(renderer);
// SELECTION BOUND BOX
if (isDrawingSelectionBox) {
isDrawingSelectionBox = false;
if (!SwingUtil.isShiftDown(e)) {
renderer.clearSelectedTokens();
}
renderer.selectTokens(selectionBoundBox);
selectionBoundBox = null;
renderer.repaint();
return;
}
// DRAG TOKEN COMPLETE
if (isDraggingToken) {
stopTokenDrag();
}
// SELECT SINGLE TOKEN
Token token = getTokenAt(e.getX(), e.getY());
if (token != null && SwingUtilities.isLeftMouseButton(e) && !isDraggingToken && !SwingUtil.isShiftDown(e)) {
// Only if it isn't already being moved
if (!renderer.isTokenMoving(token)) {
renderer.clearSelectedTokens();
renderer.selectToken(token.getId());
}
}
} finally {
isDraggingToken = false;
isDrawingSelectionBox = false;
}
return;
}
// POPUP MENU
if (SwingUtilities.isRightMouseButton(e) && !isDraggingToken && !isDraggingMap()) {
if (tokenUnderMouse != null && !renderer.getSelectedTokenSet().contains(tokenUnderMouse.getId())) {
if (!SwingUtil.isShiftDown(e)) {
renderer.clearSelectedTokens();
}
renderer.selectToken(tokenUnderMouse.getId());
isNewTokenSelected = false;
}
if (tokenUnderMouse != null && renderer.getSelectedTokenSet().size() > 0) {
if (tokenUnderMouse.isStamp()) {
new StampPopupMenu(renderer.getSelectedTokenSet(), e.getX(), e.getY(), renderer, tokenUnderMouse).showPopup(renderer);
} else {
new TokenPopupMenu(renderer.getSelectedTokenSet(), e.getX(), e.getY(), renderer, tokenUnderMouse).showPopup(renderer);
}
return;
}
}
super.mouseReleased(e);
}
// //
// MouseMotion
/*
* (non-Javadoc)
*
* @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
*/
@Override
public void mouseMoved(MouseEvent e) {
if (renderer == null) {
return;
}
super.mouseMoved(e);
if (isShowingTokenStackPopup) {
if (tokenStackPanel.contains(e.getX(), e.getY())) {
return;
}
// Turn it off
isShowingTokenStackPopup = false;
repaint();
return;
}
mouseX = e.getX();
mouseY = e.getY();
if (isDraggingToken) {
if (isMovingWithKeys) {
return;
}
ZonePoint zonePoint = new ScreenPoint(mouseX, mouseY).convertToZone(renderer);
handleDragToken(zonePoint);
return;
}
tokenUnderMouse = getTokenAt(mouseX, mouseY);
renderer.setMouseOver(tokenUnderMouse);
}
private Token getTokenAt(int x, int y) {
Token token = renderer.getTokenAt(mouseX, mouseY);
if (token == null) {
for (Shape bounds : resizeBoundsMap.keySet()) {
if (bounds.contains(mouseX, mouseY)) {
token = resizeBoundsMap.get(bounds);
}
}
}
return token;
}
private ScreenPoint getNearestVertex(ScreenPoint point) {
ZonePoint zp = point.convertToZone(renderer);
zp = renderer.getZone().getNearestVertex(zp);
return ScreenPoint.fromZonePoint(renderer, zp);
}
ScreenPoint p = new ScreenPoint(0, 0);
@Override
public void mouseDragged(MouseEvent e) {
mouseX = e.getX();
mouseY = e.getY();
if (isShowingTokenStackPopup) {
isShowingTokenStackPopup = false;
if (tokenStackPanel.contains(e.getX(), e.getY())) {
tokenStackPanel.handleMouseMotionEvent(e);
return;
} else {
renderer.repaint();
}
}
if (isResizingToken) {
ScreenPoint sp = new ScreenPoint(mouseX + dragOffsetX, mouseY + dragOffsetY);
BufferedImage image = ImageManager.getImage(tokenBeingResized.getImageAssetId());
if (SwingUtil.isControlDown(e)) { // snap size to grid
sp = getNearestVertex(sp);
}
boolean isRotated = tokenBeingResized.hasFacing() && tokenBeingResized.getShape() == Token.TokenShape.TOP_DOWN && tokenBeingResized.getFacing() != -90;
if (!isRotated && SwingUtil.isShiftDown(e)) { // lock aspect ratio -- broken for rotated images
ScreenPoint tokenPoint = ScreenPoint.fromZonePoint(renderer, tokenBeingResized.getX(), tokenBeingResized.getY());
double ratio = image.getWidth() / (double) image.getHeight();
int dx = (int) (sp.x - tokenPoint.x);
sp.y = (int) (tokenPoint.y + (dx / ratio));
}
ZonePoint zp = sp.convertToZone(renderer);
p = ScreenPoint.fromZonePoint(renderer, zp);
int newWidth = Math.max(1, (zp.x - tokenBeingResized.getX()) * (tokenBeingResized.isSnapToGrid() && !tokenBeingResized.isBackgroundStamp() ? 2 : 1));
int newHeight = Math.max(1, (zp.y - tokenBeingResized.getY()) * (tokenBeingResized.isSnapToGrid() && !tokenBeingResized.isBackgroundStamp() ? 2 : 1));
if (SwingUtil.isControlDown(e) && tokenBeingResized.isSnapToGrid() && tokenBeingResized.isObjectStamp()) {
// Account for the 1/2 cell on each side of the stamp (since it's anchored in the center)
newWidth += renderer.getZone().getGrid().getSize();
newHeight += renderer.getZone().getGrid().getSize();
}
// take into account rotated stamps
if (isRotated) {
// if we are beginning a new resize, reset the resizing variables.
if (!isResizingRotatedToken) {
isResizingRotatedToken = true;
preciseStampZonePoint = new Point2D.Double(tokenBeingResized.getX(), tokenBeingResized.getY());
lastResizeZonePoint = new ZonePoint(zp.x, zp.y);
}
// theta is the rotation angle clockwise from the positive x-axis to compensate for the +ve y-axis
// pointing downwards in zone space and an unrotated token has facing of -90.
int theta = -tokenBeingResized.getFacing() - 90;
// can't handle snap to grid with rotated token when resizing because they have to be able to nudge.
if (tokenBeingResized.isSnapToGrid()) {
tokenBeingResized.setSnapToGrid(false);
}
Rectangle footprintBounds = tokenBeingResized.getBounds(renderer.getZone());
int changeX = (zp.x - lastResizeZonePoint.x) // zp = mouse location
* (tokenBeingResized.isSnapToGrid() && !tokenBeingResized.isBackgroundStamp() ? 2 : 1);
int changeY = (zp.y - lastResizeZonePoint.y) * (tokenBeingResized.isSnapToGrid() && !tokenBeingResized.isBackgroundStamp() ? 2 : 1);
double sinTheta = Math.sin(Math.toRadians(theta));
double cosTheta = Math.cos(Math.toRadians(theta));
// Calculate change in the stamp's height and width.
// Sine terms are negated from the standard rotation transform because the direction of theta
// is reversed (theta rotates clockwise)
double dw = changeX * cosTheta + changeY * sinTheta;
double dh = -changeX * sinTheta + changeY * cosTheta;
newWidth = (int) Math.max(1, footprintBounds.width + dw);
newHeight = (int) Math.max(1, footprintBounds.height + dh);
// Move the stamp to compensate for a change in the stamp's rotation anchor
// so that the stamp stays fixed in place while being resized
// change in stamp's rotation anchor due to resize
double dx = dw / 2;
double dy = dh / 2;
// change in rotated stamp's anchor due to resize. currently only works perfectly for clockwise 0-90
// needs fine tuning for the three other quadrants to prevent the stamp from creeping
double dxRot = dx * cosTheta - dy * sinTheta;
double dyRot = dx * sinTheta + dy * cosTheta;
// Resizing a stamp automatically adjusts its rotation anchor point, so only consider the
// adjustment required due to the rotation.
double stampAdjustX = dxRot - dx;
double stampAdjustY = dyRot - dy;
// prevent the stamp from moving around if a limit has been reached.
if (newWidth == 1 || newHeight == 1) {
newWidth = newWidth == 1 ? 1 : footprintBounds.width;
newHeight = newHeight == 1 ? 1 : footprintBounds.height;
} else {
// remembering the precise location prevents the stamp from drifting due to rounding to int
preciseStampZonePoint.x += stampAdjustX;
preciseStampZonePoint.y += stampAdjustY;
lastResizeZonePoint = (ZonePoint) zp.clone();
}
tokenBeingResized.setX((int) (preciseStampZonePoint.x));
tokenBeingResized.setY((int) (preciseStampZonePoint.y));
}
tokenBeingResized.setScaleX(newWidth / (double) image.getWidth());
tokenBeingResized.setScaleY(newHeight / (double) image.getHeight());
renderer.repaint();
return;
}
CellPoint cellUnderMouse = renderer.getCellAt(new ScreenPoint(e.getX(), e.getY()));
if (cellUnderMouse != null) {
TabletopTool.getFrame().getCoordinateStatusBar().update(cellUnderMouse.x, cellUnderMouse.y);
}
if (SwingUtilities.isLeftMouseButton(e) && !SwingUtilities.isRightMouseButton(e)) {
if (isDrawingSelectionBox) {
int x1 = dragStartX;
int y1 = dragStartY;
int x2 = e.getX();
int y2 = e.getY();
selectionBoundBox.x = Math.min(x1, x2);
selectionBoundBox.y = Math.min(y1, y2);
selectionBoundBox.width = Math.abs(x1 - x2);
selectionBoundBox.height = Math.abs(y1 - y2);
renderer.repaint();
return;
}
if (isDraggingToken) {
if (isMovingWithKeys) {
return;
}
ZonePoint zonePoint = new ScreenPoint(mouseX, mouseY).convertToZone(renderer);
handleDragToken(zonePoint);
return;
}
if (tokenUnderMouse == null || !renderer.getSelectedTokenSet().contains(tokenUnderMouse.getId())) {
return;
}
if (!isDraggingToken && renderer.isTokenMoving(tokenUnderMouse)) {
return;
}
if (isNewTokenSelected) {
renderer.clearSelectedTokens();
renderer.selectToken(tokenUnderMouse.getId());
}
isNewTokenSelected = false;
// Make user we're allowed
if (!TabletopTool.getPlayer().isGM() && TabletopTool.getServerPolicy().isMovementLocked()) {
return;
}
// Might be dragging a token
String playerId = TabletopTool.getPlayer().getName();
Set<GUID> selectedTokenSet = renderer.getSelectedTokenSet();
if (selectedTokenSet.size() > 0) {
// Make sure we can do this
if (!TabletopTool.getPlayer().isGM() && TabletopTool.getServerPolicy().useStrictTokenManagement()) {
for (GUID tokenGUID : selectedTokenSet) {
Token token = renderer.getZone().getToken(tokenGUID);
if (!token.isOwner(playerId)) {
return;
}
}
}
Point origin = new Point(tokenUnderMouse.getX(), tokenUnderMouse.getY());
origin.translate(dragOffsetX, dragOffsetY);
startTokenDrag(tokenUnderMouse);
isDraggingToken = true;
SwingUtil.hidePointer(renderer);
}
return;
}
super.mouseDragged(e);
}
public boolean isDraggingToken() {
return isDraggingToken;
}
/**
* Move the keytoken being dragged to this zone point
*
* @param zonePoint
* @return true if the move was successful
*/
public boolean handleDragToken(ZonePoint zonePoint) {
// TODO: Optimize this (combine with calling code)
if (tokenBeingDragged.isSnapToGrid()) {
zonePoint.translate(-dragOffsetX, -dragOffsetY);
CellPoint cellUnderMouse = renderer.getZone().getGrid().convert(zonePoint);
zonePoint = renderer.getZone().getGrid().convert(cellUnderMouse);
TabletopTool.getFrame().getCoordinateStatusBar().update(cellUnderMouse.x, cellUnderMouse.y);
} else {
zonePoint.translate(-dragOffsetX, -dragOffsetY);
}
// Don't bother if there isn't any movement
if (!renderer.hasMoveSelectionSetMoved(tokenBeingDragged.getId(), zonePoint)) {
return false;
}
dragStartX = zonePoint.x;
dragStartY = zonePoint.y;
renderer.updateMoveSelectionSet(tokenBeingDragged.getId(), zonePoint);
TabletopTool.serverCommand().updateTokenMove(renderer.getZone().getId(), tokenBeingDragged.getId(), zonePoint.x, zonePoint.y);
return true;
}
@Override
protected void installKeystrokes(Map<KeyStroke, Action> actionMap) {
super.installKeystrokes(actionMap);
actionMap.put(AppActions.CUT_TOKENS.getKeyStroke(), AppActions.CUT_TOKENS);
actionMap.put(AppActions.COPY_TOKENS.getKeyStroke(), AppActions.COPY_TOKENS);
actionMap.put(AppActions.PASTE_TOKENS.getKeyStroke(), AppActions.PASTE_TOKENS);
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, AppActions.menuShortcut), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (renderer.getSelectedTokenSet().isEmpty()) {
return;
}
Toolbox toolbox = TabletopTool.getFrame().getToolbox();
FacingTool tool = (FacingTool) toolbox.getTool(FacingTool.class);
tool.init(renderer.getZone().getToken(renderer.getSelectedTokenSet().iterator().next()), renderer.getSelectedTokenSet());
toolbox.setSelectedTool(FacingTool.class);
}
});
// TODO: Optimize this by making it non anonymous
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), new AbstractAction() {
@Override
public void actionPerformed(java.awt.event.ActionEvent e) {
ZoneRenderer renderer = (ZoneRenderer) e.getSource();
Set<GUID> selectedTokenSet = renderer.getSelectedTokenSet();
for (GUID tokenGUID : selectedTokenSet) {
Token token = renderer.getZone().getToken(tokenGUID);
if (AppUtil.playerOwns(token)) {
renderer.getZone().removeToken(tokenGUID);
TabletopTool.serverCommand().removeToken(renderer.getZone().getId(), tokenGUID);
}
}
renderer.clearSelectedTokens();
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (!isDraggingToken) {
return;
}
// Stop
stopTokenDrag();
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD5, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (!isDraggingToken) {
return;
}
// Stop
stopTokenDrag();
}
});
// TODO Should these keystrokes be based on the grid type, like they are in PointerTool?
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD6, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(1, 0, false);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD4, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(-1, 0, false);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD8, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(0, -1, false);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD2, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(0, 1, false);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD7, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(-1, -1, false);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD9, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(1, -1, false);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD1, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(-1, 1, false);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD3, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(1, 1, false);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD6, InputEvent.SHIFT_MASK), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(1, 0, true);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD4, InputEvent.SHIFT_MASK), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(-1, 0, true);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD8, InputEvent.SHIFT_MASK), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(0, -1, true);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD2, InputEvent.SHIFT_MASK), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(0, 1, true);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD7, InputEvent.SHIFT_MASK), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(-1, -1, true);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD9, InputEvent.SHIFT_MASK), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(1, -1, true);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD1, InputEvent.SHIFT_MASK), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(-1, 1, true);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD3, InputEvent.SHIFT_MASK), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(1, 1, true);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_T, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
cycleSelectedToken(1);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.SHIFT_DOWN_MASK), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
cycleSelectedToken(-1);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(0, 1, false);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(1, 0, false);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(-1, 0, false);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(0, -1, false);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_MASK), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(0, 1, true);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_MASK), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(1, 0, true);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_MASK), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(-1, 0, true);
}
});
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_MASK), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleKeyMove(0, -1, true);
}
});
}
private void cycleSelectedToken(int direction) {
List<Token> visibleTokens = renderer.getTokensOnScreen();
if (visibleTokens.size() == 0) {
return;
}
Set<GUID> selectedTokenSet = renderer.getSelectedTokenSet();
Integer newSelection = 0;
if (selectedTokenSet.size() != 0) {
// Find the first selected token on the screen
for (int i = 0; i < visibleTokens.size(); i++) {
Token token = visibleTokens.get(i);
if (!renderer.isTokenSelectable(token.getId())) {
continue;
}
if (renderer.getSelectedTokenSet().contains(token.getId())) {
newSelection = i;
break;
}
}
// Pick the next
newSelection += direction;
}
if (newSelection < 0) {
newSelection = visibleTokens.size() - 1;
}
if (newSelection >= visibleTokens.size()) {
newSelection = 0;
}
// Make the selection
renderer.clearSelectedTokens();
renderer.selectToken(visibleTokens.get(newSelection).getId());
}
private void handleKeyMove(int dx, int dy, boolean micro) {
if (!isDraggingToken) {
// Start
Set<GUID> selectedTokenSet = renderer.getSelectedTokenSet();
if (selectedTokenSet.size() != 1) {
// only allow one at a time
return;
}
Token token = renderer.getZone().getToken(selectedTokenSet.iterator().next());
if (token == null) {
return;
}
// Only one person at a time
if (renderer.isTokenMoving(token)) {
return;
}
dragStartX = token.getX();
dragStartY = token.getY();
startTokenDrag(token);
}
if (!isMovingWithKeys) {
dragOffsetX = 0;
dragOffsetY = 0;
}
ZonePoint zp = null;
if (tokenBeingDragged.isSnapToGrid()) {
CellPoint cp = renderer.getZone().getGrid().convert(new ZonePoint(dragStartX, dragStartY));
cp.x += dx;
cp.y += dy;
zp = renderer.getZone().getGrid().convert(cp);
} else {
Rectangle tokenSize = tokenBeingDragged.getBounds(renderer.getZone());
int x = dragStartX + (micro ? dx : (tokenSize.width * dx));
int y = dragStartY + (micro ? dy : (tokenSize.height * dy));
zp = new ZonePoint(x, y);
}
isMovingWithKeys = true;
handleDragToken(zp);
if (tokenBeingDragged.isBackgroundStamp()) {
stopTokenDrag();
}
}
// //
// ZoneOverlay
/*
* (non-Javadoc)
*
* @see com.t3.client.ZoneOverlay#paintOverlay(com.t3 .client.ZoneRenderer,
* java.awt.Graphics2D)
*/
@Override
public void paintOverlay(ZoneRenderer renderer, Graphics2D g) {
if (selectionBoundBox != null) {
Stroke stroke = g.getStroke();
g.setStroke(new BasicStroke(2));
Composite composite = g.getComposite();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, .25f));
g.setPaint(AppStyle.selectionBoxFill);
g.fillRoundRect(selectionBoundBox.x, selectionBoundBox.y, selectionBoundBox.width, selectionBoundBox.height, 10, 10);
g.setComposite(composite);
g.setColor(AppStyle.selectionBoxOutline);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.drawRoundRect(selectionBoundBox.x, selectionBoundBox.y, selectionBoundBox.width, selectionBoundBox.height, 10, 10);
g.setStroke(stroke);
}
if (isShowingTokenStackPopup) {
tokenStackPanel.paint(g);
} else {
resizeBoundsMap.clear();
//rotateBoundsMap.clear();
for (GUID tokenGUID : renderer.getSelectedTokenSet()) {
Token token = renderer.getZone().getToken(tokenGUID);
if (token == null) {
continue;
}
if (!token.isStamp()) {
return;
}
// Show sizing controls
// getTokenBounds() pulls the data from the tokenLocationCache in ZoneRenderer. That cache
// is populated inside renderer.renderTokens(). As long as the cache is created first, we should
// be good, right? This code relies on the order of operations in another class! Ugh! Double-ugh! :)
Area bounds = renderer.getTokenBounds(token);
if (bounds == null || renderer.isTokenMoving(token)) {
continue;
}
// Resize
if (!token.isSnapToScale()) {
Double scale = renderer.getScale();
Rectangle footprintBounds = token.getBounds(renderer.getZone());
double scaledWidth = (footprintBounds.width * scale);
double scaledHeight = (footprintBounds.height * scale);
ScreenPoint stampLocation = ScreenPoint.fromZonePoint(renderer, footprintBounds.x, footprintBounds.y);
// distance to place the resize image in the lower left corner of an unrotated stamp
double tx = stampLocation.x + scaledWidth - resizeImage.getWidth();
double ty = stampLocation.y + scaledHeight - resizeImage.getHeight();
Rectangle resizeBounds = new Rectangle(0, 0, resizeImage.getHeight(), resizeImage.getWidth());
Area resizeBoundsArea = new Area(resizeBounds);
AffineTransform at = new AffineTransform();
at.translate(tx, ty);
// Rotated
if (token.hasFacing() && token.getShape() == Token.TokenShape.TOP_DOWN) {
// untested when anchor != (0,0)
//rotate the resize image with the stamp.
double theta = Math.toRadians(-token.getFacing() - 90);
double anchorX = -scaledWidth / 2 + resizeImage.getWidth() - (token.getAnchor().x * scale);
double anchorY = -scaledHeight / 2 + resizeImage.getHeight() - (token.getAnchor().y * scale);
at.rotate(theta, anchorX, anchorY);
}
//place the map over the image.
resizeBoundsArea.transform(at);
resizeBoundsMap.put(resizeBoundsArea, token);
g.drawImage(resizeImage, at, renderer);
}
// g.setColor(Color.red);
// g.fillRect((int)(p.x-2), (int)(p.y-2), 4, 4);
//
// // Rotate
// int length = 35;
// int cx = bounds.x + bounds.width/2;
// int cy = bounds.y + bounds.height/2;
// int facing = token.getFacing() != null ? token.getFacing() : 0;
//
// int x = (int)(cx + Math.cos(Math.toRadians(facing)) * length);
// int y = (int)(cy - Math.sin(Math.toRadians(facing)) * length);
//
// Ellipse2D rotateBounds = new Ellipse2D.Float(x-5, y-5, 10, 10);
// rotateBoundsMap.put(rotateBounds, token);
//
// g.setColor(Color.black);
// g.drawLine(cx, cy, x, y);
// g.fill(rotateBounds);
//
// g.setColor(Color.gray);
// g.draw(rotateBounds);
}
}
}
}