/* * 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.Color; import java.awt.Composite; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.GradientPaint; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.TexturePaint; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.Area; import java.awt.image.BufferedImage; import java.awt.image.ImageObserver; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; 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.JComponent; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import com.t3.MD5Key; import com.t3.client.AppActions; import com.t3.client.AppConstants; import com.t3.client.AppPreferences; import com.t3.client.AppStyle; import com.t3.client.AppUtil; import com.t3.client.ScreenPoint; import com.t3.client.TabletopTool; import com.t3.client.swing.HTMLPanelRenderer; 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.FogUtil; import com.t3.client.ui.zone.PlayerView; 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.ExposedAreaMetaData; import com.t3.model.MovementKey; import com.t3.model.Player; import com.t3.model.Pointer; import com.t3.model.Token; import com.t3.model.TokenFootprint; import com.t3.model.Zone; import com.t3.model.Zone.Layer; import com.t3.model.Zone.VisionType; import com.t3.model.ZonePoint; import com.t3.model.campaign.TokenProperty; import com.t3.model.grid.Grid; import com.t3.swing.SwingUtil; import com.t3.util.GraphicsUtil; import com.t3.util.ImageManager; import com.t3.util.StringUtil; import com.t3.util.TokenUtil; /** * This is the pointer tool from the top-level of the toolbar. It allows tokens to be selected and moved, it triggers * the statsheet to be displayed, it handles keystroke movement of tokens using the NumPad keys, and it handles * positioning the Speech and Thought bubbles when the Spacebar is held down (possibly in combination with Shift or * Ctrl). */ public class PointerTool extends DefaultTool implements ZoneOverlay { private static final long serialVersionUID = 8606021718606275084L; private boolean isShowingTokenStackPopup; private boolean isShowingPointer; private boolean isDraggingToken; private boolean isNewTokenSelected; private boolean isDrawingSelectionBox; private boolean isSpaceDown; private boolean isMovingWithKeys; private Rectangle selectionBoundBox; // Hovers private boolean isShowingHover; private Area hoverTokenBounds; private String hoverTokenNotes; private Token tokenBeingDragged; private Token tokenUnderMouse; private Token markerUnderMouse; private int keysDown; // used to record whether Shift/Ctrl/Meta keys are down private final TokenStackPanel tokenStackPanel = new TokenStackPanel(); private final HTMLPanelRenderer htmlRenderer = new HTMLPanelRenderer(); private final Font boldFont = AppStyle.labelFont.deriveFont(Font.BOLD); private final LayerSelectionDialog layerSelectionDialog; private BufferedImage statSheet; private Token tokenOnStatSheet; private static int PADDING = 7; // Offset from token's X,Y when dragging. Values are in zone coordinates. private int dragOffsetX; private int dragOffsetY; private int dragStartX; private int dragStartY; public PointerTool() { try { setIcon(new ImageIcon(ImageUtil.getImage("com/t3/client/image/tool/pointer-blue.png"))); } catch (IOException ioe) { ioe.printStackTrace(); } htmlRenderer.setBackground(new Color(0, 0, 0, 200)); htmlRenderer.setForeground(Color.black); htmlRenderer.setOpaque(false); htmlRenderer.addStyleSheetRule("body{color:black}"); htmlRenderer.addStyleSheetRule(".title{font-size: 14pt}"); 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(StampTool.class); } } } }); } @Override protected void attachTo(ZoneRenderer renderer) { super.attachTo(renderer); // XXX Can we simply remove this? Would that leave the layer the same when the PointerTool is made active after using another tool? renderer.setActiveLayer(Zone.Layer.TOKEN); if (TabletopTool.getPlayer().isGM()) { TabletopTool.getFrame().showControlPanel(layerSelectionDialog); } htmlRenderer.attach(renderer); layerSelectionDialog.updateViewList(); } /** * When implementation is completed, this method will accept a ZoneRenderer parameter and determine that zone's grid * style, then query the grid for the keystroke movement it wants to use. Those keystrokes are then added to the * InputMap and ActionMap for the component by calling the superclass's addListeners() method. * * @param comp */ protected void addListeners_NOT_USED(JComponent comp) { if (comp != null && comp instanceof ZoneRenderer) { Grid grid = ((ZoneRenderer) comp).getZone().getGrid(); addGridBasedKeys(grid, true); } super.addListeners(comp); } /** * Let the grid decide which keys perform which kind of movement. This allows hex grids to handle the six-sided * shapes intelligently depending on whether the grid is a vertical or horizontal grid. This also moves us one step * closer to defining the keys in an external file... * <p> * Boy, this is ugly. As I pin down fixes for code leading up to MT1.4 I find myself performing criminal acts on the * code base. :( */ @Override protected void addGridBasedKeys(Grid grid, boolean enable) { // XXX Currently not called from anywhere try { if (enable) { grid.installMovementKeys(this, keyActionMap); } else { grid.uninstallMovementKeys(keyActionMap); } } catch (Exception e) { // If there was an exception just ignore those keystrokes... TabletopTool.showError("exception adding grid-based keys; shouldn't get here!", e); // this gives me a hook to set a breakpoint } } @Override protected void detachFrom(ZoneRenderer renderer) { super.detachFrom(renderer); TabletopTool.getFrame().hideControlPanel(); htmlRenderer.detach(renderer); } @Override public String getInstructions() { return "tool.pointer.instructions"; } @Override public String getTooltip() { return "tool.pointer.tooltip"; } public void startTokenDrag(Token keyToken) { tokenBeingDragged = keyToken; Player p = TabletopTool.getPlayer(); if (!p.isGM() && (TabletopTool.getServerPolicy().isMovementLocked() || TabletopTool.getFrame().getInitiativePanel().isMovementLocked(keyToken))) { // Not allowed return; } renderer.addMoveSelectionSet(p.getName(), tokenBeingDragged.getId(), renderer.getOwnedTokens(renderer.getSelectedTokenSet()), false); TabletopTool.serverCommand().startTokenMove(p.getName(), renderer.getZone().getId(), tokenBeingDragged.getId(), renderer.getOwnedTokens(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; // if has fog(required) // and ((isGM with pref set) OR serverPolicy allows auto reveal by players) if (renderer.getZone().hasFog() && ((AppPreferences.getAutoRevealVisionOnGMMovement() && TabletopTool.getPlayer().isGM()) || TabletopTool.getServerPolicy().isAutoRevealOnMovement())) { Set<GUID> exposeSet = new HashSet<GUID>(); Zone zone = renderer.getZone(); for (GUID tokenGUID : renderer.getOwnedTokens(renderer.getSelectedTokenSet())) { Token token = zone.getToken(tokenGUID); if (token == null) { continue; } if (token.getType() == Token.Type.PC) { exposeSet.add(tokenGUID); } } FogUtil.exposeLastPath(renderer, exposeSet); } } 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(); FontMetrics fm = getFontMetrics(getFont()); return new Dimension(tokenList.size() * (gridSize + PADDING) + PADDING, gridSize + PADDING * 2 + fm.getHeight() + 10); } public void handleMouseReleased(MouseEvent event) { } public void handleMousePressed(MouseEvent event) { if (event.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(event)) { Token token = getTokenAt(event.getX(), event.getY()); if (token == null || !AppUtil.playerOwns(token)) { return; } tokenUnderMouse = token; // TODO: Combine this with the code just like it below EditTokenDialog tokenPropertiesDialog = TabletopTool.getFrame().getTokenPropertiesDialog(); tokenPropertiesDialog.showDialog(tokenUnderMouse); if (tokenPropertiesDialog.isTokenSaved()) { TabletopTool.serverCommand().putToken(renderer.getZone().getId(), token); renderer.getZone().putToken(token); TabletopTool.getFrame().resetTokenPanels(); renderer.repaint(); renderer.flush(token); } } if (SwingUtilities.isRightMouseButton(event)) { Token token = getTokenAt(event.getX(), event.getY()); if (token == null || !AppUtil.playerOwns(token)) { return; } tokenUnderMouse = token; Set<GUID> selectedSet = new HashSet<GUID>(); selectedSet.add(token.getId()); new TokenPopupMenu(selectedSet, event.getX(), event.getY(), renderer, tokenUnderMouse).showPopup(renderer); } } public void handleMouseMotionEvent(MouseEvent event) { Token token = getTokenAt(event.getX(), event.getY()); if (token == null || !AppUtil.playerOwns(token)) { return; } renderer.clearSelectedTokens(); boolean selected = renderer.selectToken(token.getId()); if (selected) { Tool tool = TabletopTool.getFrame().getToolbox().getSelectedTool(); if (!(tool instanceof PointerTool)) { return; } tokenUnderMouse = token; ((PointerTool) tool).startTokenDrag(token); } } public void paint(Graphics g) { Dimension size = getSize(); int gridSize = (int) renderer.getScaledGridSize(); FontMetrics fm = g.getFontMetrics(); // Background ((Graphics2D) g).setPaint(new GradientPaint(x, y, Color.white, x + size.width, y + size.height, Color.gray)); 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); GraphicsUtil.drawBoxedString((Graphics2D) g, token.getName(), bounds.x + bounds.width / 2, bounds.y + bounds.height + fm.getAscent()); tokenLocationList.add(new TokenLocation(bounds, token)); } } public Token getTokenAt(int x, int y) { for (TokenLocation location : tokenLocationList) { if (location.getBounds().contains(x, y)) { return location.getToken(); } } return null; } 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 (isShowingHover) { isShowingHover = false; hoverTokenBounds = null; hoverTokenNotes = null; markerUnderMouse = renderer.getMarkerAt(e.getX(), e.getY()); repaint(); } if (isShowingTokenStackPopup) { if (tokenStackPanel.contains(e.getX(), e.getY())) { tokenStackPanel.handleMousePressed(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(); // These same two lines are in super.mousePressed(). Why do them here? dragStartY = e.getY(); // 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 = renderer.getTokenAt(e.getX(), e.getY()); if (token != null) { if (!AppUtil.playerOwns(token)) { return; } EditTokenDialog tokenPropertiesDialog = TabletopTool.getFrame().getTokenPropertiesDialog(); tokenPropertiesDialog.showDialog(token); if (tokenPropertiesDialog.isTokenSaved()) { renderer.repaint(); renderer.flush(token); TabletopTool.serverCommand().putToken(renderer.getZone().getId(), token); renderer.getZone().putToken(token); TabletopTool.getFrame().resetTokenPanels(); } } } return; } // SELECTION Token token = renderer.getTokenAt(e.getX(), e.getY()); if (token != null && !isDraggingToken && SwingUtilities.isLeftMouseButton(e)) { // 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(); } // XXX Isn't Windows standard to use Ctrl-click to add one element and Shift-click to extend? // XXX Similarly, OSX uses Cmd-click to add one element and Shift-click to extend... Change it later. 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); Rectangle tokenBounds = token.getBounds(renderer.getZone()); if (token.isSnapToGrid()) { dragOffsetX = (pos.x - tokenBounds.x) - (tokenBounds.width / 2); dragOffsetY = (pos.y - tokenBounds.y) - (tokenBounds.height / 2); } else { dragOffsetX = pos.x - tokenBounds.x; dragOffsetY = pos.y - tokenBounds.y; } } } 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.handleMouseReleased(e); return; } else { isShowingTokenStackPopup = false; repaint(); } } if (SwingUtilities.isLeftMouseButton(e)) { try { // MARKER renderer.setCursor(Cursor.getPredefinedCursor(markerUnderMouse != null ? Cursor.HAND_CURSOR : Cursor.DEFAULT_CURSOR)); if (tokenUnderMouse == null && markerUnderMouse != null && !isShowingHover && !isDraggingToken) { isShowingHover = true; hoverTokenBounds = renderer.getMarkerBounds(markerUnderMouse); hoverTokenNotes = createHoverNote(markerUnderMouse); if (hoverTokenBounds == null) { // Uhhhh, where's the token ? isShowingHover = false; } repaint(); } // 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) { SwingUtil.showPointer(renderer); stopTokenDrag(); } else { // SELECT SINGLE TOKEN if (SwingUtilities.isLeftMouseButton(e) && !SwingUtil.isShiftDown(e)) { Token token = renderer.getTokenAt(e.getX(), e.getY()); // Only if it isn't already being moved if (token != null && !renderer.isTokenMoving(token)) { renderer.clearSelectedTokens(); renderer.selectToken(token.getId()); } } } } finally { isDraggingToken = false; isDrawingSelectionBox = false; } return; } // WAYPOINT if (SwingUtilities.isMiddleMouseButton(e) && isDraggingToken) { setWaypoint(); } // 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().isEmpty()) { if (tokenUnderMouse.isStamp()) { new StampPopupMenu(renderer.getSelectedTokenSet(), e.getX(), e.getY(), renderer, tokenUnderMouse).showPopup(renderer); } else if (AppUtil.playerOwns(tokenUnderMouse)) { // FIXME Every once in awhile we get a report on the forum of the following exception: // java.awt.IllegalComponentStateException: component must be showing on the screen to determine its location // It's thrown as a result of the showPopup() call on the next line. For the life of me, I can't figure out why the // "renderer" component might not be "showing on the screen"??? Maybe it has something to do with a dual-monitor // configuration? Or a monitor added after Java was started and then MT dragged to that monitor? new TokenPopupMenu(renderer.getSelectedTokenSet(), e.getX(), e.getY(), renderer, tokenUnderMouse).showPopup(renderer); } return; } } super.mouseReleased(e); } // // // MouseMotion @Override public void mouseMoved(MouseEvent e) { if (renderer == null) { return; } super.mouseMoved(e); // mouseX = e.getX(); // done by super.mouseMoved() // mouseY = e.getY(); if (isShowingPointer) { ZonePoint zp = new ScreenPoint(mouseX, mouseY).convertToZone(renderer); Pointer pointer = TabletopTool.getFrame().getPointerOverlay().getPointer(TabletopTool.getPlayer().getName()); if (pointer != null) { pointer.setX(zp.x); pointer.setY(zp.y); renderer.repaint(); TabletopTool.serverCommand().movePointer(TabletopTool.getPlayer().getName(), zp.x, zp.y); } return; } if (isShowingTokenStackPopup) { if (tokenStackPanel.contains(e.getX(), e.getY())) { return; } // Turn it off isShowingTokenStackPopup = false; repaint(); return; } if (isDraggingToken) { // FJE If we're dragging the token, wouldn't mouseDragged() be called instead? Can this code ever be executed? if (isMovingWithKeys) { return; } ZonePoint zp = new ScreenPoint(mouseX, mouseY).convertToZone(renderer); ZonePoint last; if (tokenUnderMouse == null) last = zp; else { last = renderer.getLastWaypoint(tokenUnderMouse.getId()); // XXX This shouldn't be possible, but it happens?! if (last == null) last = zp; } handleDragToken(zp, zp.x - last.x, zp.y - last.y); return; } tokenUnderMouse = renderer.getTokenAt(mouseX, mouseY); keysDown = e.getModifiersEx(); renderer.setMouseOver(tokenUnderMouse); if (tokenUnderMouse == null) { statSheet = null; } Token marker = renderer.getMarkerAt(mouseX, mouseY); if (!AppUtil.tokenIsVisible(renderer.getZone(), marker, renderer.getPlayerView())) { marker = null; } if (marker != markerUnderMouse && marker != null) { markerUnderMouse = marker; renderer.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); TabletopTool.getFrame().setStatusMessage(markerUnderMouse.getName()); } else if (marker == null && markerUnderMouse != null) { markerUnderMouse = null; renderer.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); TabletopTool.getFrame().setStatusMessage(""); } } @Override public void mouseDragged(MouseEvent e) { mouseX = e.getX(); mouseY = e.getY(); if (isShowingTokenStackPopup) { isShowingTokenStackPopup = false; if (tokenStackPanel.contains(mouseX, mouseY)) { tokenStackPanel.handleMouseMotionEvent(e); return; } else { renderer.repaint(); } } // XXX Updating the status bar is done in super.mouseDragged() -- maybe just call that here? But it also causes repaint events... CellPoint cellUnderMouse = renderer.getCellAt(new ScreenPoint(mouseX, mouseY)); 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 = mouseX; int y2 = mouseY; selectionBoundBox.x = Math.min(x1, x2); selectionBoundBox.y = Math.min(y1, y2); selectionBoundBox.width = Math.abs(x1 - x2); selectionBoundBox.height = Math.abs(y1 - y2); /* * NOTE: This is a weird one that has to do with the order of the mouseReleased event. If the selection * box started the drag while hovering over a marker, we need to tell it to not show the marker after * the drag is complete. */ markerUnderMouse = null; renderer.repaint(); return; } if (tokenUnderMouse == null || !renderer.getSelectedTokenSet().contains(tokenUnderMouse.getId())) { return; } if (isDraggingToken) { if (isMovingWithKeys) { return; } Grid grid = getZone().getGrid(); TokenFootprint tf = tokenUnderMouse.getFootprint(grid); Rectangle r = tf.getBounds(grid); ZonePoint last = renderer.getLastWaypoint(tokenUnderMouse.getId()); if (last == null) last = new ZonePoint(tokenUnderMouse.getX() + r.width / 2, tokenUnderMouse.getY() + r.height / 2); ZonePoint zp = new ScreenPoint(mouseX, mouseY).convertToZone(renderer); if (tokenUnderMouse.isSnapToGrid() && grid.getCapabilities().isSnapToGridSupported()) { zp.translate(-r.width / 2, -r.height / 2); last.translate(-r.width / 2, -r.height / 2); } zp.translate(-dragOffsetX, -dragOffsetY); int dx = zp.x - last.x; int dy = zp.y - last.y; handleDragToken(zp, dx, dy); return; } if (!isDraggingToken && renderer.isTokenMoving(tokenUnderMouse)) { return; } if (isNewTokenSelected) { renderer.clearSelectedTokens(); renderer.selectToken(tokenUnderMouse.getId()); } isNewTokenSelected = false; // Make sure 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.getOwnedTokens(renderer.getSelectedTokenSet()); if (!selectedTokenSet.isEmpty()) { // Make sure we can do this // Possibly let unowned tokens be moved? if (!TabletopTool.getPlayer().isGM() && TabletopTool.getServerPolicy().useStrictTokenManagement()) { for (GUID tokenGUID : selectedTokenSet) { Token token = renderer.getZone().getToken(tokenGUID); if (!token.isOwner(playerId)) { return; } } } 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 * The new ZonePoint for the token. * @param dx * The amount being moved in the X direction * @param dy * The amount being moved in the Y direction * @return true if the move was successful */ public boolean handleDragToken(ZonePoint zonePoint, int dx, int dy) { Grid grid = renderer.getZone().getGrid(); if (tokenBeingDragged.isSnapToGrid() && grid.getCapabilities().isSnapToGridSupported()) { // cellUnderMouse is actually token position if the token is being dragged with keys. CellPoint cellUnderMouse = grid.convert(zonePoint); zonePoint.translate(grid.getCellOffset().width / 2, grid.getCellOffset().height / 2); // Convert the zone point to a cell point and back to force the snap to grid on drag zonePoint = grid.convert(grid.convert(zonePoint)); TabletopTool.getFrame().getCoordinateStatusBar().update(cellUnderMouse.x, cellUnderMouse.y); } else { // Nothing } // Don't bother if there isn't any movement if (!renderer.hasMoveSelectionSetMoved(tokenBeingDragged.getId(), zonePoint)) { return false; } // Make sure it's a valid move boolean isValid; if (grid.getSize() >= 9) isValid = validateMove(tokenBeingDragged, renderer.getSelectedTokenSet(), zonePoint, dx, dy); else isValid = validateMove_legacy(tokenBeingDragged, renderer.getSelectedTokenSet(), zonePoint); if (!isValid) { 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; } private boolean validateMove(Token leadToken, Set<GUID> tokenSet, ZonePoint point, int dirx, int diry) { if (TabletopTool.getPlayer().isGM()) { return true; } boolean isBlocked = false; Zone zone = renderer.getZone(); if (zone.hasFog()) { // Check that the new position for each token is within the exposed area Area zoneFog = zone.getExposedArea(); if (zoneFog == null) zoneFog = new Area(); boolean useTokenExposedArea = TabletopTool.getServerPolicy().isUseIndividualFOW() && zone.getVisionType() != VisionType.OFF; int deltaX = point.x - leadToken.getX(); int deltaY = point.y - leadToken.getY(); Grid grid = zone.getGrid(); // Loop through all tokens. As soon as one of them is blocked, stop processing and return false. for (Iterator<GUID> iter = tokenSet.iterator(); !isBlocked && iter.hasNext();) { Area tokenFog = new Area(zoneFog); GUID tokenGUID = iter.next(); Token token = zone.getToken(tokenGUID); if (token == null) { continue; } if (useTokenExposedArea) { ExposedAreaMetaData meta = zone.getExposedAreaMetaData(token.getExposedAreaGUID()); tokenFog.add(meta.getExposedAreaHistory()); } Rectangle tokenSize = token.getBounds(zone); Rectangle destination = new Rectangle(tokenSize.x + deltaX, tokenSize.y + deltaY, tokenSize.width, tokenSize.height); isBlocked = !grid.validateMove(token, destination, dirx, diry, tokenFog); } } return !isBlocked; } private boolean validateMove_legacy(Token leadToken, Set<GUID> tokenSet, ZonePoint point) { Zone zone = renderer.getZone(); if (TabletopTool.getPlayer().isGM()) { return true; } boolean isVisible = true; if (zone.hasFog()) { // Check that the new position for each token is within the exposed area Area fow = zone.getExposedArea(); if (fow == null) { return true; } isVisible = false; int fudgeSize = Math.max(Math.min((zone.getGrid().getSize() - 2) / 3 - 1, 8), 0); int deltaX = point.x - leadToken.getX(); int deltaY = point.y - leadToken.getY(); Rectangle bounds = new Rectangle(); for (GUID tokenGUID : tokenSet) { Token token = zone.getToken(tokenGUID); if (token == null) { continue; } int x = token.getX() + deltaX; int y = token.getY() + deltaY; Rectangle tokenSize = token.getBounds(zone); /* * Perhaps create a counter and count the number of times that the contains() check returns true? There * are currently 9 rectangular areas checked by this code (note the "/3" in the two 'interval' * variables) so checking for 5 or more would mean more than 55%+ of the destination was visible... */ int intervalX = tokenSize.width - fudgeSize * 2; int intervalY = tokenSize.height - fudgeSize * 2; int counter = 0; for (int dy = 0; dy < 3; dy++) { for (int dx = 0; dx < 3; dx++) { int by = y + fudgeSize + (intervalY * dy / 3); int bx = x + fudgeSize + (intervalX * dx / 3); bounds.x = bx; bounds.y = by; bounds.width = intervalY * (dy + 1) / 3 - intervalY * dy / 3; // No, this isn't the same as intervalY*1/3 because of integer arithmetic bounds.height = intervalX * (dx + 1) / 3 - intervalX * dx / 3; if (!TabletopTool.getServerPolicy().isUseIndividualFOW() || zone.getVisionType() == VisionType.OFF) { if (fow.contains(bounds)) { counter++; } } else { ExposedAreaMetaData meta = zone.getExposedAreaMetaData(token.getExposedAreaGUID()); if (meta.getExposedAreaHistory().contains(bounds)) { counter++; } } } } isVisible = (counter >= 6); } } return isVisible; } /** * These keystrokes are currently hard-coded and should be exported to the i18n.properties file in a perfect * universe. :) * <p> * <table> * <tr> * <td>Meta R * <td>Select the FacingTool (to allow rotating with the left/right arrows) * <tr> * <td>DELETE * <td>Allow deletion of owned tokens * <tr> * <td>Space * <td>Show arrow pointer on map * <tr> * <td>Ctrl Space * <td>Show speech bubble on map * <tr> * <td>Shift Space * <td>Show thought bubble on map * <tr> * <td>D * <td>Stop dragging token * <tr> * <td>T * <td>Cycle forward through tokens * <tr> * <td>Shift T * <td>Cycle backward through tokens * <tr> * <td>Meta I * <td>Expose fog from visible area * <tr> * <td>Meta P * <td>Expose fog from last path * <tr> * <td>Meta Shift O * <td>Expose only PC area (reinsert other fog) * <tr> * <td>NumPad digits * <td>Move token (specifics based on the grid type are not implemented yet):<br> * <table> * <tr> * <td>7 (up/left) * <td>8 (up) * <td>9 (up/right) * <tr> * <td>4 (left) * <td>5 (stop) * <td>6(right) * <tr> * <td>1 (down/left) * <td>2 (down) * <td>3 (down/right) * </table> * <tr> * <td>Down * <td>Move token down * <tr> * <td>Up * <td>Move token up * <tr> * <td>Right * <td>Move token right * <tr> * <td>Shift Right * <td>Rotate token right by facing amount (depends on grid) * <tr> * <td>Ctrl Shift Right * <td>Rotate token right by 5� increments * <tr> * <td>Left * <td>Move token left * <tr> * <td>Shift Left * <td>Rotate token left by facing amount (depends on grid) * <tr> * <td>Ctrl Shift Left * <td>Rotate token left by 5� increments * </table> */ @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() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { // TODO: Combine all this crap with the Stamp tool 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() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(java.awt.event.ActionEvent e) { ZoneRenderer renderer = (ZoneRenderer) e.getSource(); // Check to see if this is the required action if (!TabletopTool.confirmTokenDelete()) { return; } 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_SPACE, 0, true), new StopPointerActionListener()); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, ActionEvent.CTRL_MASK, true), new StopPointerActionListener()); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, ActionEvent.SHIFT_MASK, true), new StopPointerActionListener()); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, false), new PointerActionListener(Pointer.Type.ARROW)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, ActionEvent.CTRL_MASK, false), new PointerActionListener(Pointer.Type.SPEECH_BUBBLE)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, ActionEvent.SHIFT_MASK, false), new PointerActionListener(Pointer.Type.THOUGHT_BUBBLE)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0), new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { if (!isDraggingToken) { return; } // Stop stopTokenDrag(); } }); // Other NumPad keys are handled by individual grid types actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD5, 0), new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { if (!isDraggingToken) { return; } // Stop stopTokenDrag(); } }); int size = 1; actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD7, 0), new MovementKey(this, -size, -size)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD8, 0), new MovementKey(this, 0, -size)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD9, 0), new MovementKey(this, size, -size)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD4, 0), new MovementKey(this, -size, 0)); // actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD5, 0), new MovementKey(this, 0, 0)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD6, 0), new MovementKey(this, size, 0)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD1, 0), new MovementKey(this, -size, size)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD2, 0), new MovementKey(this, 0, size)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD3, 0), new MovementKey(this, size, size)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), new MovementKey(this, -size, 0)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), new MovementKey(this, size, 0)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), new MovementKey(this, 0, -size)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), new MovementKey(this, 0, size)); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_DOWN_MASK), new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { handleKeyRotate(-1, false); // clockwise } }); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_DOWN_MASK | InputEvent.CTRL_DOWN_MASK), new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { handleKeyRotate(-1, true); // clockwise } }); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_DOWN_MASK), new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { handleKeyRotate(1, false); // counter-clockwise } }); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_DOWN_MASK | InputEvent.CTRL_DOWN_MASK), new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { handleKeyRotate(1, true); // counter-clockwise } }); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_T, 0), new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { renderer.cycleSelectedToken(1); } }); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.SHIFT_DOWN_MASK), new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { renderer.cycleSelectedToken(-1); } }); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_I, AppActions.menuShortcut), new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { if (TabletopTool.getPlayer().isGM() || TabletopTool.getServerPolicy().getPlayersCanRevealVision()) { FogUtil.exposeVisibleArea(renderer, renderer.getOwnedTokens(renderer.getSelectedTokenSet())); } } }); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, AppActions.menuShortcut | InputEvent.SHIFT_DOWN_MASK), new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { // Only let the GM's do this if (TabletopTool.getPlayer().isGM()) { FogUtil.exposePCArea(renderer); TabletopTool.serverCommand().exposePCArea(renderer.getZone().getId()); } } }); actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_P, AppActions.menuShortcut), new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { if (TabletopTool.getPlayer().isGM() || TabletopTool.getServerPolicy().getPlayersCanRevealVision()) { FogUtil.exposeLastPath(renderer, renderer.getOwnedTokens(renderer.getSelectedTokenSet())); } } }); } /** * Handle token rotations when using the arrow keys. * * @param direction * -1 is cw & 1 is ccw */ private void handleKeyRotate(int direction, boolean freeRotate) { Set<GUID> tokenGUIDSet = renderer.getSelectedTokenSet(); if (tokenGUIDSet.isEmpty()) { return; } for (GUID tokenGUID : tokenGUIDSet) { Token token = renderer.getZone().getToken(tokenGUID); if (token == null) { continue; } if (!AppUtil.playerOwns(token)) { continue; } Integer facing = token.getFacing(); // TODO: this should really be a per grid setting if (facing == null) { facing = -90; // natural alignment } if (freeRotate) { facing += direction * 5; } else { int[] facingArray = renderer.getZone().getGrid().getFacingAngles(); int facingIndex = TokenUtil.getIndexNearestTo(facingArray, facing); facingIndex += direction; if (facingIndex < 0) { facingIndex = facingArray.length - 1; } if (facingIndex == facingArray.length) { facingIndex = 0; } facing = facingArray[facingIndex]; } token.setFacing(facing); renderer.flush(token); TabletopTool.serverCommand().putToken(renderer.getZone().getId(), token); } renderer.repaint(); } /** * Handle the movement of tokens by keypresses. * * @param dx * The X movement in Cell units * @param dy * The Y movement in Cell units */ public void handleKeyMove(double dx, double dy) { Token keyToken = null; if (!isDraggingToken) { // Start Set<GUID> selectedTokenSet = renderer.getOwnedTokens(renderer.getSelectedTokenSet()); for (GUID tokenId : selectedTokenSet) { Token token = renderer.getZone().getToken(tokenId); if (token == null) { return; } // Need a key token to orient the move from, just arbitraily // pick the first one if (keyToken == null) { keyToken = token; } // Only one person at a time if (renderer.isTokenMoving(token)) { return; } } if (keyToken == null) { return; } // Note these are zone space coordinates dragStartX = keyToken.getX(); dragStartY = keyToken.getY(); startTokenDrag(keyToken); } if (!isMovingWithKeys) { dragOffsetX = 0; dragOffsetY = 0; } // The zone point the token will be moved to after adjusting for dx/dy ZonePoint zp = new ZonePoint(dragStartX, dragStartY); Grid grid = renderer.getZone().getGrid(); if (tokenBeingDragged.isSnapToGrid() && grid.getCapabilities().isSnapToGridSupported()) { CellPoint cp = grid.convert(zp); cp.x += dx; cp.y += dy; zp = grid.convert(cp); dx = zp.x - tokenBeingDragged.getX(); dy = zp.y - tokenBeingDragged.getY(); } else { // Scalar for dx/dy in zone space. Defaulting to essentially 1 pixel. int moveFactor = 1; if (tokenBeingDragged.isSnapToGrid()) { // Move in grid size increments. Allows tokens set snap-to-grid on gridless maps // to move in whole cell size increments. moveFactor = grid.getSize(); } int x = dragStartX + (int) (dx * moveFactor); int y = dragStartY + (int) (dy * moveFactor); zp = new ZonePoint(x, y); } isMovingWithKeys = true; handleDragToken(zp, (int) dx, (int) dy); } private void setWaypoint() { ZonePoint p = new ZonePoint(dragStartX, dragStartY); renderer.toggleMoveSelectionSetWaypoint(tokenBeingDragged.getId(), p); TabletopTool.serverCommand().toggleTokenMoveWaypoint(renderer.getZone().getId(), tokenBeingDragged.getId(), p); } // // // POINTER KEY ACTION private class PointerActionListener extends AbstractAction { private static final long serialVersionUID = 8348513388262364724L; Pointer.Type type; public PointerActionListener(Pointer.Type type) { this.type = type; } @Override public void actionPerformed(ActionEvent e) { if (isSpaceDown) { return; } if (isDraggingToken) { setWaypoint(); } else { // Pointer isShowingPointer = true; ZonePoint zp = new ScreenPoint(mouseX, mouseY).convertToZone(renderer); Pointer pointer = new Pointer(renderer.getZone(), zp.x, zp.y, 0, type); TabletopTool.serverCommand().showPointer(TabletopTool.getPlayer().getName(), pointer); } isSpaceDown = true; } } // // // STOP POINTER ACTION private class StopPointerActionListener extends AbstractAction { private static final long serialVersionUID = -8508019800264211345L; @Override public void actionPerformed(ActionEvent e) { if (isShowingPointer) { isShowingPointer = false; TabletopTool.serverCommand().hidePointer(TabletopTool.getPlayer().getName()); } isSpaceDown = false; } } // // // ZoneOverlay /* * (non-Javadoc) * * @see com.t3.client.ZoneOverlay#paintOverlay(com.t3 .client.ZoneRenderer, * java.awt.Graphics2D) */ @Override public void paintOverlay(final ZoneRenderer renderer, Graphics2D g) { Dimension viewSize = renderer.getSize(); Composite composite = g.getComposite(); if (selectionBoundBox != null) { Stroke stroke = g.getStroke(); g.setStroke(new BasicStroke(2)); if (AppPreferences.getFillSelectionBox()) { 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); } // Statsheet if (tokenUnderMouse != null && !isDraggingToken && AppUtil.tokenIsVisible(renderer.getZone(), tokenUnderMouse, new PlayerView(TabletopTool.getPlayer().getRole()))) { if (AppPreferences.getPortraitSize() > 0 && !SwingUtil.isShiftDown(keysDown) && (tokenOnStatSheet == null || !tokenOnStatSheet.equals(tokenUnderMouse) || statSheet == null)) { tokenOnStatSheet = tokenUnderMouse; // Portrait MD5Key portraitId = tokenUnderMouse.getPortraitImage() != null ? tokenUnderMouse.getPortraitImage() : tokenUnderMouse.getImageAssetId(); BufferedImage image = ImageManager.getImage(portraitId, new ImageObserver() { @Override public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { // The image was loading, so now rebuild the portrait panel with the real image statSheet = null; renderer.repaint(); return true; } }); Dimension imgSize = new Dimension(image.getWidth(), image.getHeight()); // Size SwingUtil.constrainTo(imgSize, AppPreferences.getPortraitSize()); // Stats Map<String, String> propertyMap = new LinkedHashMap<String, String>(); if (AppPreferences.getShowStatSheet()) { for (TokenProperty property : TabletopTool.getCampaign().getTokenPropertyList(tokenUnderMouse.getPropertyType())) { if (property.isShowOnStatSheet()) { if (property.isGMOnly() && !TabletopTool.getPlayer().isGM()) { continue; } if (property.isOwnerOnly() && !AppUtil.playerOwns(tokenUnderMouse)) { continue; } Object propertyValue = tokenUnderMouse.getProperty(property.getName()); if (propertyValue != null) { if (propertyValue.toString().length() > 0) { String propName; if (property.getShortName() != null) propName = property.getShortName(); else propName= property.getName(); propertyMap.put(propName, property.getType().toStatsheetString(propertyValue)); } } } } } Dimension statSize = null; int rm = AppStyle.miniMapBorder.getRightMargin(); int lm = AppStyle.miniMapBorder.getLeftMargin(); int tm = AppStyle.miniMapBorder.getTopMargin(); int bm = AppStyle.miniMapBorder.getBottomMargin(); if (tokenUnderMouse.getPortraitImage() != null || !propertyMap.isEmpty()) { Font font = AppStyle.labelFont; FontMetrics valueFM = g.getFontMetrics(font); FontMetrics keyFM = g.getFontMetrics(boldFont); int rowHeight = Math.max(valueFM.getHeight(), keyFM.getHeight()); if (!propertyMap.isEmpty()) { // Figure out size requirements int height = propertyMap.size() * (rowHeight + PADDING); int width = -1; for (Entry<String, String> entry : propertyMap.entrySet()) { int lineWidth = SwingUtilities.computeStringWidth(keyFM, entry.getKey()) + SwingUtilities.computeStringWidth(valueFM, " " + entry.getValue()); if (width < 0 || lineWidth > width) { width = lineWidth; } } statSize = new Dimension(width + PADDING * 3, height); } // Create the space for the image int width = imgSize.width + (statSize != null ? statSize.width + rm : 0) + lm + rm; int height = Math.max(imgSize.height, (statSize != null ? statSize.height + bm : 0)) + tm + bm; statSheet = new BufferedImage(width, height, BufferedImage.BITMASK); Graphics2D statsG = statSheet.createGraphics(); statsG.setClip(new Rectangle(0, 0, width, height)); statsG.setFont(font); SwingUtil.useAntiAliasing(statsG); // Draw the stats first, right aligned if (statSize != null) { Rectangle bounds = new Rectangle(width - statSize.width - rm, statSize.height == height ? 0 : height - statSize.height - bm, statSize.width, statSize.height); statsG.setPaint(new TexturePaint(AppStyle.panelTexture, new Rectangle(0, 0, AppStyle.panelTexture.getWidth(), AppStyle.panelTexture.getHeight()))); statsG.fill(bounds); AppStyle.miniMapBorder.paintAround(statsG, bounds); AppStyle.shadowBorder.paintWithin(statsG, bounds); // Stats int y = bounds.y + rowHeight; for (Entry<String, String> entry : propertyMap.entrySet()) { // Box statsG.setColor(new Color(249, 241, 230, 140)); statsG.fillRect(bounds.x, y - keyFM.getAscent(), bounds.width - PADDING / 2, rowHeight); statsG.setColor(new Color(175, 163, 149)); statsG.drawRect(bounds.x, y - keyFM.getAscent(), bounds.width - PADDING / 2, rowHeight); // Values statsG.setColor(Color.black); statsG.setFont(boldFont); statsG.drawString(entry.getKey(), bounds.x + PADDING * 2, y); statsG.setFont(font); int strw = SwingUtilities.computeStringWidth(valueFM, entry.getValue()); statsG.drawString(entry.getValue(), bounds.x + bounds.width - strw - PADDING, y); y += PADDING + rowHeight; } } // Draw the portrait Rectangle bounds = new Rectangle(lm, height - imgSize.height - bm, imgSize.width, imgSize.height); statsG.setPaint(new TexturePaint(AppStyle.panelTexture, new Rectangle(0, 0, AppStyle.panelTexture.getWidth(), AppStyle.panelTexture.getHeight()))); statsG.fill(bounds); statsG.drawImage(image, bounds.x, bounds.y, imgSize.width, imgSize.height, this); AppStyle.miniMapBorder.paintAround(statsG, bounds); AppStyle.shadowBorder.paintWithin(statsG, bounds); // Label GraphicsUtil.drawBoxedString(statsG, tokenUnderMouse.getName(), bounds.width / 2 + lm, height - 15); statsG.dispose(); } } if (statSheet != null) { g.drawImage(statSheet, 5, viewSize.height - statSheet.getHeight() - 5, this); } } // Hovers if (isShowingHover) { // Anchor next to the token Dimension size = htmlRenderer.setText(hoverTokenNotes, (int) (renderer.getWidth() * .75), (int) (renderer.getHeight() * .75)); Point location = new Point(hoverTokenBounds.getBounds().x + hoverTokenBounds.getBounds().width / 2 - size.width / 2, hoverTokenBounds.getBounds().y); // Anchor in the bottom left corner location.x = 4 + PADDING; location.y = viewSize.height - size.height - 4 - PADDING; // Keep it on screen if (location.x + size.width > viewSize.width) { location.x = viewSize.width - size.width; } if (location.x < 4) { location.x = 4; } if (location.y + size.height > viewSize.height - 4) { location.y = viewSize.height - size.height - 4; } if (location.y < 4) { location.y = 4; } // Background // g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, .5f)); // g.setColor(Color.black); // g.fillRect(location.x, location.y, size.width, size.height); // g.setComposite(composite); g.setPaint(new TexturePaint(AppStyle.panelTexture, new Rectangle(0, 0, AppStyle.panelTexture.getWidth(), AppStyle.panelTexture.getHeight()))); g.fillRect(location.x, location.y, size.width, size.height); // Content htmlRenderer.render(g, location.x, location.y); // Border AppStyle.miniMapBorder.paintAround(g, location.x, location.y, size.width, size.height); AppStyle.shadowBorder.paintWithin(g, location.x, location.y, size.width, size.height); // AppStyle.border.paintAround(g, location.x, location.y, // size.width, size.height); } } private String createHoverNote(Token marker) { boolean showGMNotes = TabletopTool.getPlayer().isGM() && !StringUtil.isEmpty(marker.getGMNotes()); StringBuilder builder = new StringBuilder(); if (marker.getPortraitImage() != null) { builder.append("<table><tr><td valign=top>"); } if (!StringUtil.isEmpty(marker.getNotes())) { builder.append("<b><span class='title'>").append(marker.getName()).append("</span></b><br>"); builder.append(markerUnderMouse.getNotes()); // add a gap between player and gmNotes if (showGMNotes) { builder.append("\n\n"); } } if (showGMNotes) { builder.append("<b><span class='title'>GM Notes"); if (!StringUtil.isEmpty(marker.getGMName())) { builder.append(" - ").append(marker.getGMName()); } builder.append("</span></b><br>"); builder.append(marker.getGMNotes()); } if (marker.getPortraitImage() != null) { BufferedImage image = ImageManager.getImageAndWait(marker.getPortraitImage()); Dimension imgSize = new Dimension(image.getWidth(), image.getHeight()); if (imgSize.width > AppConstants.NOTE_PORTRAIT_SIZE || imgSize.height > AppConstants.NOTE_PORTRAIT_SIZE) { SwingUtil.constrainTo(imgSize, AppConstants.NOTE_PORTRAIT_SIZE); } builder.append("</td><td valign=top>"); builder.append("<img src='asset://") .append(marker.getPortraitImage()) .append("' width=") .append(imgSize.width) .append(" height=") .append(imgSize.height) .append("></tr></table>"); } String notes = builder.toString(); notes = notes.replaceAll("\n", "<br>"); return notes; } }