/* * $Id$ * * Copyright (c) 2000-2003 by Rodney Kinney, Jim Urbas * Refactoring of DragHandler Copyright 2011 Pieter Geerkens * * 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.build.module.map; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.datatransfer.StringSelection; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.dnd.DragSourceDragEvent; import java.awt.dnd.DragSourceDropEvent; import java.awt.dnd.DragSourceEvent; import java.awt.dnd.DragSourceListener; import java.awt.dnd.DragSourceMotionListener; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetEvent; import java.awt.dnd.DropTargetListener; import java.awt.dnd.InvalidDnDOperationException; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JLayeredPane; import javax.swing.JRootPane; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import org.apache.commons.lang.SystemUtils; import VASSAL.build.AbstractBuildable; import VASSAL.build.Buildable; import VASSAL.build.GameModule; import VASSAL.build.module.GameComponent; import VASSAL.build.module.GlobalOptions; import VASSAL.build.module.Map; import VASSAL.build.module.map.boardPicker.Board; import VASSAL.command.ChangeTracker; import VASSAL.command.Command; import VASSAL.command.NullCommand; import VASSAL.configure.BooleanConfigurer; import VASSAL.counters.BasicPiece; import VASSAL.counters.BoundsTracker; import VASSAL.counters.Deck; import VASSAL.counters.DeckVisitor; import VASSAL.counters.DeckVisitorDispatcher; import VASSAL.counters.Decorator; import VASSAL.counters.DragBuffer; import VASSAL.counters.EventFilter; import VASSAL.counters.GamePiece; import VASSAL.counters.Highlighter; import VASSAL.counters.KeyBuffer; import VASSAL.counters.PieceCloner; import VASSAL.counters.PieceFinder; import VASSAL.counters.PieceIterator; import VASSAL.counters.PieceSorter; import VASSAL.counters.PieceVisitorDispatcher; import VASSAL.counters.Properties; import VASSAL.counters.Stack; import VASSAL.tools.LaunchButton; import VASSAL.tools.image.ImageUtils; import VASSAL.tools.imageop.Op; /** * This is a MouseListener that moves pieces onto a Map window */ public class PieceMover extends AbstractBuildable implements MouseListener, GameComponent, Comparator<GamePiece> { /** The Preferences key for autoreporting moves. */ public static final String AUTO_REPORT = "autoReport"; //$NON-NLS-1$ public static final String NAME = "name"; public static final String HOTKEY = "hotkey"; protected Map map; protected Point dragBegin; protected GamePiece dragging; protected LaunchButton markUnmovedButton; protected String markUnmovedText; protected String markUnmovedIcon; public static final String ICON_NAME = "icon"; //$NON-NLS-1$ protected String iconName; // Selects drag target from mouse click on the Map protected PieceFinder dragTargetSelector; // Selects piece to merge with at the drop destination protected PieceFinder dropTargetSelector; // Processes drag target after having been selected protected PieceVisitorDispatcher selectionProcessor; protected Comparator<GamePiece> pieceSorter = new PieceSorter(); public void addTo(Buildable b) { dragTargetSelector = createDragTargetSelector(); dropTargetSelector = createDropTargetSelector(); selectionProcessor = createSelectionProcessor(); map = (Map) b; map.addLocalMouseListener(this); GameModule.getGameModule().getGameState().addGameComponent(this); map.setDragGestureListener(DragHandler.getTheDragHandler()); map.setPieceMover(this); setAttribute(Map.MARK_UNMOVED_TEXT, map.getAttributeValueString(Map.MARK_UNMOVED_TEXT)); setAttribute(Map.MARK_UNMOVED_ICON, map.getAttributeValueString(Map.MARK_UNMOVED_ICON)); } protected MovementReporter createMovementReporter(Command c) { return new MovementReporter(c); } /** * When the user completes a drag-drop operation, the pieces being * dragged will either be combined with an existing piece on the map * or else placed on the map without stack. This method returns a * {@link PieceFinder} instance that determines which {@link GamePiece} * (if any) to combine the being-dragged pieces with. * * @return */ protected PieceFinder createDropTargetSelector() { return new PieceFinder.Movable() { public Object visitDeck(Deck d) { final Point pos = d.getPosition(); final Point p = new Point(pt.x - pos.x, pt.y - pos.y); if (d.getShape().contains(p)) { return d; } else { return null; } } public Object visitDefault(GamePiece piece) { GamePiece selected = null; if (this.map.getStackMetrics().isStackingEnabled() && this.map.getPieceCollection().canMerge(dragging, piece)) { if (this.map.isLocationRestricted(pt)) { final Point snap = this.map.snapTo(pt); if (piece.getPosition().equals(snap)) { selected = piece; } } else { selected = (GamePiece) super.visitDefault(piece); } } if (selected != null && DragBuffer.getBuffer().contains(selected) && selected.getParent() != null && selected.getParent().topPiece() == selected) { selected = null; } return selected; } public Object visitStack(Stack s) { GamePiece selected = null; if (this.map.getStackMetrics().isStackingEnabled() && this.map.getPieceCollection().canMerge(dragging, s) && !DragBuffer.getBuffer().contains(s) && s.topPiece() != null) { if (this.map.isLocationRestricted(pt) && !s.isExpanded()) { if (s.getPosition().equals(this.map.snapTo(pt))) { selected = s; } } else { selected = (GamePiece) super.visitStack(s); } } return selected; } }; } /** * When the user clicks on the map, a piece from the map is selected by * the dragTargetSelector. What happens to that piece is determined by * the {@link PieceVisitorDispatcher} instance returned by this method. * The default implementation does the following: If a Deck, add the top * piece to the drag buffer If a stack, add it to the drag buffer. * Otherwise, add the piece and any other multi-selected pieces to the * drag buffer. * * @see #createDragTargetSelector * @return */ protected PieceVisitorDispatcher createSelectionProcessor() { return new DeckVisitorDispatcher(new DeckVisitor() { public Object visitDeck(Deck d) { DragBuffer.getBuffer().clear(); for (PieceIterator it = d.drawCards(); it.hasMoreElements();) { DragBuffer.getBuffer().add(it.nextPiece()); } return null; } public Object visitStack(Stack s) { DragBuffer.getBuffer().clear(); // RFE 1629255 - Only add selected pieces within the stack to the DragBuffer // Add whole stack if all pieces are selected - better drag cursor int selectedCount = 0; for (int i = 0; i < s.getPieceCount(); i++) { if (Boolean.TRUE.equals(s.getPieceAt(i) .getProperty(Properties.SELECTED))) { selectedCount++; } } if (((Boolean) GameModule.getGameModule().getPrefs().getValue(Map.MOVING_STACKS_PICKUP_UNITS)).booleanValue() || s.getPieceCount() == 1 || s.getPieceCount() == selectedCount) { DragBuffer.getBuffer().add(s); } else { for (int i = 0; i < s.getPieceCount(); i++) { final GamePiece p = s.getPieceAt(i); if (Boolean.TRUE.equals(p.getProperty(Properties.SELECTED))) { DragBuffer.getBuffer().add(p); } } } // End RFE 1629255 if (KeyBuffer.getBuffer().containsChild(s)) { // If clicking on a stack with a selected piece, put all selected // pieces in other stacks into the drag buffer KeyBuffer.getBuffer().sort(PieceMover.this); for (Iterator<GamePiece> i = KeyBuffer.getBuffer().getPiecesIterator(); i.hasNext();) { final GamePiece piece = i.next(); if (piece.getParent() != s) { DragBuffer.getBuffer().add(piece); } } } return null; } public Object visitDefault(GamePiece selected) { DragBuffer.getBuffer().clear(); if (KeyBuffer.getBuffer().contains(selected)) { // If clicking on a selected piece, put all selected pieces into the // drag buffer KeyBuffer.getBuffer().sort(PieceMover.this); for (Iterator<GamePiece> i = KeyBuffer.getBuffer().getPiecesIterator(); i.hasNext();) { DragBuffer.getBuffer().add(i.next()); } } else { DragBuffer.getBuffer().clear(); DragBuffer.getBuffer().add(selected); } return null; } }); } /** * Returns the {@link PieceFinder} instance that will select a * {@link GamePiece} for processing when the user clicks on the map. * The default implementation is to return the first piece whose shape * contains the point clicked on. * * @return */ protected PieceFinder createDragTargetSelector() { return new PieceFinder.Movable() { public Object visitDeck(Deck d) { final Point pos = d.getPosition(); final Point p = new Point(pt.x - pos.x, pt.y - pos.y); if (d.boundingBox().contains(p) && d.getPieceCount() > 0) { return d; } else { return null; } } }; } public void setup(boolean gameStarting) { if (gameStarting) { initButton(); } } public Command getRestoreCommand() { return null; } private Image loadIcon(String name) { if (name == null || name.length() == 0) return null; return Op.load(name).getImage(); } protected void initButton() { final String value = getMarkOption(); if (GlobalOptions.PROMPT.equals(value)) { BooleanConfigurer config = new BooleanConfigurer( Map.MARK_MOVED, "Mark Moved Pieces", Boolean.TRUE); GameModule.getGameModule().getPrefs().addOption(config); } if (!GlobalOptions.NEVER.equals(value)) { if (markUnmovedButton == null) { final ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e) { final GamePiece[] p = map.getAllPieces(); final Command c = new NullCommand(); for (int i = 0; i < p.length; ++i) { c.append(markMoved(p[i], false)); } GameModule.getGameModule().sendAndLog(c); map.repaint(); } }; markUnmovedButton = new LaunchButton("", NAME, HOTKEY, Map.MARK_UNMOVED_ICON, al); Image img = null; if (iconName != null && iconName.length() > 0) { img = loadIcon(iconName); if (img != null) { markUnmovedButton.setAttribute(Map.MARK_UNMOVED_ICON, iconName); } } if (img == null) { img = loadIcon(markUnmovedIcon); if (img != null) { markUnmovedButton.setAttribute(Map.MARK_UNMOVED_ICON, markUnmovedIcon); } } markUnmovedButton.setAlignmentY(0.0F); markUnmovedButton.setText(markUnmovedText); markUnmovedButton.setToolTipText( map.getAttributeValueString(Map.MARK_UNMOVED_TOOLTIP)); map.getToolBar().add(markUnmovedButton); } } else if (markUnmovedButton != null) { map.getToolBar().remove(markUnmovedButton); markUnmovedButton = null; } } private String getMarkOption() { String value = map.getAttributeValueString(Map.MARK_MOVED); if (value == null) { value = GlobalOptions.getInstance() .getAttributeValueString(GlobalOptions.MARK_MOVED); } return value; } public String[] getAttributeNames() { return new String[]{ICON_NAME}; } public String getAttributeValueString(String key) { return ICON_NAME.equals(key) ? iconName : null; } public void setAttribute(String key, Object value) { if (ICON_NAME.equals(key)) { iconName = (String) value; } else if (Map.MARK_UNMOVED_TEXT.equals(key)) { if (markUnmovedButton != null) { markUnmovedButton.setAttribute(NAME, value); } markUnmovedText = (String) value; } else if (Map.MARK_UNMOVED_ICON.equals(key)) { if (markUnmovedButton != null) { markUnmovedButton.setAttribute(Map.MARK_UNMOVED_ICON, value); } markUnmovedIcon = (String) value; } } protected boolean isMultipleSelectionEvent(MouseEvent e) { return e.isShiftDown(); } /** Invoked just before a piece is moved */ protected Command movedPiece(GamePiece p, Point loc) { setOldLocation(p); Command c = null; if (!loc.equals(p.getPosition())) { c = markMoved(p, true); } if (p.getParent() != null) { final Command removedCommand = p.getParent().pieceRemoved(p); c = c == null ? removedCommand : c.append(removedCommand); } return c; } protected void setOldLocation(GamePiece p) { if (p instanceof Stack) { for (int i = 0; i < ((Stack) p).getPieceCount(); i++) { Decorator.setOldProperties(((Stack) p).getPieceAt(i)); } } else Decorator.setOldProperties(p); } public Command markMoved(GamePiece p, boolean hasMoved) { if (GlobalOptions.NEVER.equals(getMarkOption())) { hasMoved = false; } Command c = new NullCommand(); if (!hasMoved || shouldMarkMoved()) { if (p instanceof Stack) { for (Iterator<GamePiece> i = ((Stack) p).getPiecesIterator(); i.hasNext();) { c.append(markMoved(i.next(), hasMoved)); } } else if (p.getProperty(Properties.MOVED) != null) { if (p.getId() != null) { final ChangeTracker comm = new ChangeTracker(p); p.setProperty(Properties.MOVED, hasMoved ? Boolean.TRUE : Boolean.FALSE); c = comm.getChangeCommand(); } } } return c; } protected boolean shouldMarkMoved() { final String option = getMarkOption(); if (GlobalOptions.ALWAYS.equals(option)) { return true; } else if (GlobalOptions.NEVER.equals(option)) { return false; } else { return Boolean.TRUE.equals( GameModule.getGameModule().getPrefs().getValue(Map.MARK_MOVED)); } } /** * Moves pieces in DragBuffer to point p by generating a Command for * each element in DragBuffer * * @param map * Map * @param p * Point mouse released */ public Command movePieces(Map map, Point p) { final List<GamePiece> allDraggedPieces = new ArrayList<GamePiece>(); final PieceIterator it = DragBuffer.getBuffer().getIterator(); if (!it.hasMoreElements()) return null; Point offset = null; Command comm = new NullCommand(); final BoundsTracker tracker = new BoundsTracker(); // Map of Point->List<GamePiece> of pieces to merge with at a given // location. There is potentially one piece for each Game Piece Layer. final HashMap<Point,List<GamePiece>> mergeTargets = new HashMap<Point,List<GamePiece>>(); while (it.hasMoreElements()) { dragging = it.nextPiece(); tracker.addPiece(dragging); /* * Take a copy of the pieces in dragging. * If it is a stack, it is cleared by the merging process. */ final ArrayList<GamePiece> draggedPieces = new ArrayList<GamePiece>(0); if (dragging instanceof Stack) { int size = ((Stack) dragging).getPieceCount(); for (int i = 0; i < size; i++) { draggedPieces.add(((Stack) dragging).getPieceAt(i)); } } else { draggedPieces.add(dragging); } if (offset != null) { p = new Point(dragging.getPosition().x + offset.x, dragging.getPosition().y + offset.y); } List<GamePiece> mergeCandidates = mergeTargets.get(p); GamePiece mergeWith = null; // Find an already-moved piece that we can merge with at the destination // point if (mergeCandidates != null) { for (int i = 0, n = mergeCandidates.size(); i < n; ++i) { final GamePiece candidate = mergeCandidates.get(i); if (map.getPieceCollection().canMerge(candidate, dragging)) { mergeWith = candidate; mergeCandidates.set(i, dragging); break; } } } // Now look for an already-existing piece at the destination point if (mergeWith == null) { mergeWith = map.findAnyPiece(p, dropTargetSelector); if (mergeWith == null && !Boolean.TRUE.equals( dragging.getProperty(Properties.IGNORE_GRID))) { p = map.snapTo(p); } if (offset == null) { offset = new Point(p.x - dragging.getPosition().x, p.y - dragging.getPosition().y); } if (mergeWith != null && map.getStackMetrics().isStackingEnabled()) { mergeCandidates = new ArrayList<GamePiece>(); mergeCandidates.add(dragging); mergeCandidates.add(mergeWith); mergeTargets.put(p, mergeCandidates); } } if (mergeWith == null) { comm = comm.append(movedPiece(dragging, p)); comm = comm.append(map.placeAt(dragging, p)); if (!(dragging instanceof Stack) && !Boolean.TRUE.equals(dragging.getProperty(Properties.NO_STACK))) { final Stack parent = map.getStackMetrics().createStack(dragging); if (parent != null) { comm = comm.append(map.placeAt(parent, p)); } } } else { // Do not add pieces to the Deck that are Obscured to us, or that // the Deck does not want to contain. Removing them from the // draggedPieces list will cause them to be left behind where the // drag started. NB. Pieces that have been dragged from a face-down // Deck will be be Obscued to us, but will be Obscured by the dummy // user Deck.NO_USER if (mergeWith instanceof Deck) { final ArrayList<GamePiece> newList = new ArrayList<GamePiece>(0); for (GamePiece piece : draggedPieces) { if (((Deck) mergeWith).mayContain(piece)) { final boolean isObscuredToMe = Boolean.TRUE.equals(piece.getProperty(Properties.OBSCURED_TO_ME)); if (!isObscuredToMe || (isObscuredToMe && Deck.NO_USER.equals(piece.getProperty(Properties.OBSCURED_BY)))) { newList.add(piece); } } } if (newList.size() != draggedPieces.size()) { draggedPieces.clear(); for (GamePiece piece : newList) { draggedPieces.add(piece); } } } // Add the remaining dragged counters to the target. // If mergeWith is a single piece (not a Stack), then we are merging // into an expanded Stack and the merge order must be reversed to // maintain the order of the merging pieces. if (mergeWith instanceof Stack) { for (int i = 0; i < draggedPieces.size(); ++i) { comm = comm.append(movedPiece(draggedPieces.get(i), mergeWith.getPosition())); comm = comm.append(map.getStackMetrics().merge(mergeWith, draggedPieces.get(i))); } } else { for (int i = draggedPieces.size()-1; i >= 0; --i) { comm = comm.append(movedPiece(draggedPieces.get(i), mergeWith.getPosition())); comm = comm.append(map.getStackMetrics().merge(mergeWith, draggedPieces.get(i))); } } } for (GamePiece piece : draggedPieces) { KeyBuffer.getBuffer().add(piece); } // Record each individual piece moved for (GamePiece piece : draggedPieces) { allDraggedPieces.add(piece); } tracker.addPiece(dragging); } if (GlobalOptions.getInstance().autoReportEnabled()) { final Command report = createMovementReporter(comm).getReportCommand().append(new MovementReporter.HiddenMovementReporter(comm).getReportCommand()); report.execute(); comm = comm.append(report); } // Apply key after move to each moved piece if (map.getMoveKey() != null) { applyKeyAfterMove(allDraggedPieces, comm, map.getMoveKey()); } tracker.repaint(); return comm; } protected void applyKeyAfterMove(List<GamePiece> pieces, Command comm, KeyStroke key) { for (GamePiece piece : pieces) { if (piece.getProperty(Properties.SNAPSHOT) == null) { piece.setProperty(Properties.SNAPSHOT, PieceCloner.getInstance().clonePiece(piece)); } comm.append(piece.keyEvent(key)); } } /** * This listener is used for faking drag-and-drop on Java 1.1 systems * * @param e */ public void mousePressed(MouseEvent e) { if (canHandleEvent(e)) { selectMovablePieces(e); } } /** Place the clicked-on piece into the {@link DragBuffer} */ protected void selectMovablePieces(MouseEvent e) { final GamePiece p = map.findPiece(e.getPoint(), dragTargetSelector); dragBegin = e.getPoint(); if (p != null) { final EventFilter filter = (EventFilter) p.getProperty(Properties.MOVE_EVENT_FILTER); if (filter == null || !filter.rejectEvent(e)) { selectionProcessor.accept(p); } else { DragBuffer.getBuffer().clear(); } } else { DragBuffer.getBuffer().clear(); } // show/hide selection boxes map.repaint(); } /** @deprecated Use {@link #selectMovablePieces(MouseEvent)}. */ @Deprecated protected void selectMovablePieces(Point point) { final GamePiece p = map.findPiece(point, dragTargetSelector); dragBegin = point; selectionProcessor.accept(p); // show/hide selection boxes map.repaint(); } protected boolean canHandleEvent(MouseEvent e) { return !e.isShiftDown() && !e.isControlDown() && !e.isMetaDown() && e.getClickCount() < 2 && !e.isConsumed(); } /** * Return true if this point is "close enough" to the point at which * the user pressed the mouse to be considered a mouse click (such * that no moves are done) */ public boolean isClick(Point pt) { boolean isClick = false; if (dragBegin != null) { final Board b = map.findBoard(pt); boolean useGrid = b != null && b.getGrid() != null; if (useGrid) { final PieceIterator it = DragBuffer.getBuffer().getIterator(); final GamePiece dragging = it.hasMoreElements() ? it.nextPiece() : null; useGrid = dragging != null && !Boolean.TRUE.equals(dragging.getProperty(Properties.IGNORE_GRID)) && (dragging.getParent() == null || !dragging.getParent().isExpanded()); } if (useGrid) { if (map.equals(DragBuffer.getBuffer().getFromMap())) { if (map.snapTo(pt).equals(map.snapTo(dragBegin))) { isClick = true; } } } else { if (Math.abs(pt.x - dragBegin.x) <= 5 && Math.abs(pt.y - dragBegin.y) <= 5) { isClick = true; } } } return isClick; } public void mouseReleased(MouseEvent e) { if (canHandleEvent(e)) { if (!isClick(e.getPoint())) { performDrop(e.getPoint()); } } dragBegin = null; map.getView().setCursor(null); } protected void performDrop(Point p) { final Command move = movePieces(map, p); GameModule.getGameModule().sendAndLog(move); if (move != null) { DragBuffer.getBuffer().clear(); } } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mouseClicked(MouseEvent e) { } /** * Implement Comparator to sort the contents of the drag buffer before * completing the drag. This sorts the contents to be in the same order * as the pieces were in their original parent stack. */ public int compare(GamePiece p1, GamePiece p2) { return pieceSorter.compare(p1, p2); } // We force the loading of these classes because otherwise they would // be loaded when the user initiates the first drag, which makes the // start of the drag choppy. static { try { Class.forName(MovementReporter.class.getName()); Class.forName(KeyBuffer.class.getName()); } catch (ClassNotFoundException e) { throw new IllegalStateException(e); // impossible } } /** Common functionality for DragHandler for cases with and without * drag image support. <p> * NOTE: DragSource.isDragImageSupported() returns false for j2sdk1.4.2_02 on * Windows 2000 * * @author Pieter Geerkens */ abstract static public class AbstractDragHandler implements DragGestureListener, DragSourceListener, DragSourceMotionListener, DropTargetListener { static private AbstractDragHandler theDragHandler = DragSource.isDragImageSupported() ? (SystemUtils.IS_OS_MAC_OSX ? new DragHandlerMacOSX() : new DragHandler()) : new DragHandlerNoImage(); /** returns the singleton DragHandler instance */ static public AbstractDragHandler getTheDragHandler() { return theDragHandler; } static public void setTheDragHandler(AbstractDragHandler myHandler) { theDragHandler = myHandler; } final static int CURSOR_ALPHA = 127; // psuedo cursor is 50% transparent final static int EXTRA_BORDER = 4; // psuedo cursor is includes a 4 pixel border protected JLabel dragCursor; // An image label. Lives on current DropTarget's // LayeredPane. // private BufferedImage dragImage; // An image label. Lives on current DropTarget's LayeredPane. private Point drawOffset = new Point(); // translates event coords to local // drawing coords private Rectangle boundingBox; // image bounds private int originalPieceOffsetX; // How far drag STARTED from gamepiece's // center private int originalPieceOffsetY; // I.e. on original map protected double dragPieceOffCenterZoom = 1.0; // zoom at start of drag private int currentPieceOffsetX; // How far cursor is CURRENTLY off-center, // a function of // dragPieceOffCenter{X,Y,Zoom} private int currentPieceOffsetY; // I.e. on current map (which may have // different zoom protected double dragCursorZoom = 1.0; // Current cursor scale (zoom) Component dragWin; // the component that initiated the drag operation Component dropWin; // the drop target the mouse is currently over JLayeredPane drawWin; // the component that owns our psuedo-cursor // Seems there can be only one DropTargetListener a drop target. After we // process a drop target // event, we manually pass the event on to this listener. java.util.Map<Component,DropTargetListener> dropTargetListeners = new HashMap<Component,DropTargetListener>(); abstract protected int getOffsetMult(); /** * Creates a new DropTarget and hooks us into the beginning of a * DropTargetListener chain. DropTarget events are not multicast; * there can be only one "true" listener. */ static public DropTarget makeDropTarget(Component theComponent, int dndContants, DropTargetListener dropTargetListener) { if (dropTargetListener != null) { DragHandler.getTheDragHandler() .dropTargetListeners.put(theComponent, dropTargetListener); } return new DropTarget(theComponent, dndContants, DragHandler.getTheDragHandler()); } static public void removeDropTarget(Component theComponent) { DragHandler.getTheDragHandler().dropTargetListeners.remove(theComponent); } protected DropTargetListener getListener(DropTargetEvent e) { final Component component = e.getDropTargetContext().getComponent(); return dropTargetListeners.get(component); } /** Moves the drag cursor on the current draw window */ protected void moveDragCursor(int dragX, int dragY) { if (drawWin != null) { dragCursor.setLocation(dragX - drawOffset.x, dragY - drawOffset.y); } } /** Removes the drag cursor from the current draw window */ protected void removeDragCursor() { if (drawWin != null) { if (dragCursor != null) { dragCursor.setVisible(false); drawWin.remove(dragCursor); } drawWin = null; } } /** calculates the offset between cursor dragCursor positions */ private void calcDrawOffset() { if (drawWin != null) { // drawOffset is the offset between the mouse location during a drag // and the upper-left corner of the cursor // accounts for difference between event point (screen coords) // and Layered Pane position, boundingBox and off-center drag drawOffset.x = -boundingBox.x - currentPieceOffsetX + EXTRA_BORDER; drawOffset.y = -boundingBox.y - currentPieceOffsetY + EXTRA_BORDER; SwingUtilities.convertPointToScreen(drawOffset, drawWin); } } /** * creates or moves cursor object to given JLayeredPane. Usually called by setDrawWinToOwnerOf() */ private void setDrawWin(JLayeredPane newDrawWin) { if (newDrawWin != drawWin) { // remove cursor from old window if (dragCursor.getParent() != null) { dragCursor.getParent().remove(dragCursor); } if (drawWin != null) { drawWin.repaint(dragCursor.getBounds()); } drawWin = newDrawWin; calcDrawOffset(); dragCursor.setVisible(false); drawWin.add(dragCursor, JLayeredPane.DRAG_LAYER); } } /** * creates or moves cursor object to given window. Called when drag operation begins in a window or the cursor is * dragged over a new drop-target window */ public void setDrawWinToOwnerOf(Component newDropWin) { if (newDropWin != null) { final JRootPane rootWin = SwingUtilities.getRootPane(newDropWin); if (rootWin != null) { setDrawWin(rootWin.getLayeredPane()); } } } /** Common functionality abstracted from makeDragImage and makeDragCursor * * @param zoom * @param doOffset * @param target * @param setSize * @return */ BufferedImage makeDragImageCursorCommon(double zoom, boolean doOffset, Component target, boolean setSize) { // FIXME: Should be an ImageOp. dragCursorZoom = zoom; final List<Point> relativePositions = buildBoundingBox(zoom, doOffset); final int w = boundingBox.width + EXTRA_BORDER * 2; final int h = boundingBox.height + EXTRA_BORDER * 2; BufferedImage image = ImageUtils.createCompatibleTranslucentImage(w, h); drawDragImage(image, target, relativePositions, zoom); if (setSize) dragCursor.setSize(w, h); image = featherDragImage(image, w, h, EXTRA_BORDER); return image; } /** * Creates the image to use when dragging based on the zoom factor * passed in. * * @param zoom DragBuffer.getBuffer * @return dragImage */ private BufferedImage makeDragImage(double zoom) { return makeDragImageCursorCommon(zoom, false, null, false); } /** * Installs the cursor image into our dragCursor JLabel. * Sets current zoom. Should be called at beginning of drag * and whenever zoom changes. INPUT: DragBuffer.getBuffer OUTPUT: * dragCursorZoom cursorOffCenterX cursorOffCenterY boundingBox * @param zoom DragBuffer.getBuffer * */ protected void makeDragCursor(double zoom) { // create the cursor if necessary if (dragCursor == null) { dragCursor = new JLabel(); dragCursor.setVisible(false); } dragCursor.setIcon(new ImageIcon( makeDragImageCursorCommon(zoom, true, dragCursor, true))); } private List<Point> buildBoundingBox(double zoom, boolean doOffset) { final ArrayList<Point> relativePositions = new ArrayList<Point>(); final PieceIterator dragContents = DragBuffer.getBuffer().getIterator(); final GamePiece firstPiece = dragContents.nextPiece(); GamePiece lastPiece = firstPiece; currentPieceOffsetX = (int) (originalPieceOffsetX / dragPieceOffCenterZoom * zoom + 0.5); currentPieceOffsetY = (int) (originalPieceOffsetY / dragPieceOffCenterZoom * zoom + 0.5); boundingBox = firstPiece.getShape().getBounds(); boundingBox.width *= zoom; boundingBox.height *= zoom; boundingBox.x *= zoom; boundingBox.y *= zoom; if (doOffset) { calcDrawOffset(); } relativePositions.add(new Point(0,0)); int stackCount = 0; while (dragContents.hasMoreElements()) { final GamePiece nextPiece = dragContents.nextPiece(); final Rectangle r = nextPiece.getShape().getBounds(); r.width *= zoom; r.height *= zoom; r.x *= zoom; r.y *= zoom; final Point p = new Point( (int) Math.round( zoom * (nextPiece.getPosition().x - firstPiece.getPosition().x)), (int) Math.round( zoom * (nextPiece.getPosition().y - firstPiece.getPosition().y))); r.translate(p.x, p.y); if (nextPiece.getPosition().equals(lastPiece.getPosition())) { stackCount++; final StackMetrics sm = getStackMetrics(nextPiece); r.translate(sm.unexSepX*stackCount,-sm.unexSepY*stackCount); } boundingBox.add(r); relativePositions.add(p); lastPiece = nextPiece; } return relativePositions; } private void drawDragImage(BufferedImage image, Component target, List<Point> relativePositions, double zoom) { final Graphics2D g = image.createGraphics(); int index = 0; Point lastPos = null; int stackCount = 0; for (PieceIterator dragContents = DragBuffer.getBuffer().getIterator(); dragContents.hasMoreElements(); ) { final GamePiece piece = dragContents.nextPiece(); final Point pos = relativePositions.get(index++); final Map map = piece.getMap(); if (piece instanceof Stack){ stackCount = 0; piece.draw(g, EXTRA_BORDER - boundingBox.x + pos.x, EXTRA_BORDER - boundingBox.y + pos.y, map == null ? target : map.getView(), zoom); } else { final Point offset = new Point(0,0); if (pos.equals(lastPos)) { stackCount++; final StackMetrics sm = getStackMetrics(piece); offset.x = sm.unexSepX * stackCount; offset.y = sm.unexSepY * stackCount; } else { stackCount = 0; } final int x = EXTRA_BORDER - boundingBox.x + pos.x + offset.x; final int y = EXTRA_BORDER - boundingBox.y + pos.y - offset.y; piece.draw(g, x, y, map == null ? target : map.getView(), zoom); final Highlighter highlighter = map == null ? BasicPiece.getHighlighter() : map.getHighlighter(); highlighter.draw(piece, g, x, y, null, zoom); } lastPos = pos; } g.dispose(); } private StackMetrics getStackMetrics(GamePiece piece) { StackMetrics sm = null; final Map map = piece.getMap(); if (map != null) { sm = map.getStackMetrics(); } if (sm == null) { sm = new StackMetrics(); } return sm; } private BufferedImage featherDragImage(BufferedImage src, int w, int h, int b) { // FIXME: This should be redone so that we draw the feathering onto the // destination first, and then pass the Graphics2D on to draw the pieces // directly over it. Presently this doesn't work because some of the // pieces screw up the Graphics2D when passed it... The advantage to doing // it this way is that we create only one BufferedImage instead of two. final BufferedImage dst = ImageUtils.createCompatibleTranslucentImage(w, h); final Graphics2D g = dst.createGraphics(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // paint the rectangle occupied by the piece at specified alpha g.setColor(new Color(0xff, 0xff, 0xff, CURSOR_ALPHA)); g.fillRect(0, 0, w, h); // feather outwards for (int f = 0; f < b; ++f) { final int alpha = CURSOR_ALPHA * (f + 1) / b; g.setColor(new Color(0xff, 0xff, 0xff, alpha)); g.drawRect(f, f, w-2*f, h-2*f); } // paint in the source image g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_IN)); g.drawImage(src, 0, 0, null); g.dispose(); return dst; } /////////////////////////////////////////////////////////////////////////// // DRAG GESTURE LISTENER INTERFACE // // EVENT uses SCALED, DRAG-SOURCE coordinate system. // PIECE uses SCALED, OWNER (arbitrary) coordinate system // /////////////////////////////////////////////////////////////////////////// /** Fires after user begins moving the mouse several pixels over a map. */ public void dragGestureRecognized(DragGestureEvent dge) { try { beginDragging(dge); } // FIXME: Fix by replacing AWT Drag 'n Drop with Swing DnD. // Catch and ignore spurious DragGestures catch (InvalidDnDOperationException e) { } } protected Point dragGestureRecognizedPrep(DragGestureEvent dge) { // Ensure the user has dragged on a counter before starting the drag. final DragBuffer db = DragBuffer.getBuffer(); if (db.isEmpty()) return null; // Remove any Immovable pieces from the DragBuffer that were // selected in a selection rectangle, unless they are being // dragged from a piece palette (i.e., getMap() == null). final List<GamePiece> pieces = new ArrayList<GamePiece>(); for (PieceIterator i = db.getIterator(); i.hasMoreElements(); pieces.add(i.nextPiece())); for (GamePiece piece : pieces) { if (piece.getMap() != null && Boolean.TRUE.equals(piece.getProperty(Properties.NON_MOVABLE))) { db.remove(piece); } } // Bail out if this leaves no pieces to drag. if (db.isEmpty()) return null; final GamePiece piece = db.getIterator().nextPiece(); final Map map = dge.getComponent() instanceof Map.View ? ((Map.View) dge.getComponent()).getMap() : null; final Point mousePosition = (map == null) ? dge.getDragOrigin() : map.componentCoordinates(dge.getDragOrigin()); Point piecePosition = (map == null) ? piece.getPosition() : map.componentCoordinates(piece.getPosition()); // If DragBuffer holds a piece with invalid coordinates (for example, a // card drawn from a deck), drag from center of piece if (piecePosition.x <= 0 || piecePosition.y <= 0) { piecePosition = mousePosition; } // Account for offset of piece within stack // We do this even for un-expanded stacks, since the offset can // still be significant if the stack is large dragPieceOffCenterZoom = map == null ? 1.0 : map.getZoom(); if (piece.getParent() != null && map != null) { final Point offset = piece.getParent() .getStackMetrics() .relativePosition(piece.getParent(), piece); piecePosition.translate( (int) Math.round(offset.x * dragPieceOffCenterZoom), (int) Math.round(offset.y * dragPieceOffCenterZoom)); } // dragging from UL results in positive offsets originalPieceOffsetX = piecePosition.x - mousePosition.x; originalPieceOffsetY = piecePosition.y - mousePosition.y; dragWin = dge.getComponent(); drawWin = null; dropWin = null; return mousePosition; } protected void beginDragging(DragGestureEvent dge) { // this call is needed to instantiate the boundingBox object final BufferedImage bImage = makeDragImage(dragPieceOffCenterZoom); final Point dragPointOffset = new Point( getOffsetMult() * (boundingBox.x + currentPieceOffsetX - EXTRA_BORDER), getOffsetMult() * (boundingBox.y + currentPieceOffsetY - EXTRA_BORDER) ); dge.startDrag( Cursor.getPredefinedCursor(Cursor.HAND_CURSOR), bImage, dragPointOffset, new StringSelection(""), this ); dge.getDragSource().addDragSourceMotionListener(this); } /////////////////////////////////////////////////////////////////////////// // DRAG SOURCE LISTENER INTERFACE // /////////////////////////////////////////////////////////////////////////// public void dragDropEnd(DragSourceDropEvent e) { final DragSource ds = e.getDragSourceContext().getDragSource(); ds.removeDragSourceMotionListener(this); } public void dragEnter(DragSourceDragEvent e) {} public void dragExit(DragSourceEvent e) {} public void dragOver(DragSourceDragEvent e) {} public void dropActionChanged(DragSourceDragEvent e) {} /////////////////////////////////////////////////////////////////////////// // DRAG SOURCE MOTION LISTENER INTERFACE // // EVENT uses UNSCALED, SCREEN coordinate system // /////////////////////////////////////////////////////////////////////////// // Used to check for real mouse movement. // Warning: dragMouseMoved fires 8 times for each point on development // system (Win2k) protected Point lastDragLocation = new Point(); /** Moves cursor after mouse */ abstract public void dragMouseMoved(DragSourceDragEvent e); /////////////////////////////////////////////////////////////////////////// // DROP TARGET INTERFACE // // EVENT uses UNSCALED, DROP-TARGET coordinate system /////////////////////////////////////////////////////////////////////////// /** switches current drawWin when mouse enters a new DropTarget */ public void dragEnter(DropTargetDragEvent e) { final DropTargetListener forward = getListener(e); if (forward != null) forward.dragEnter(e); } /** * Last event of the drop operation. We adjust the drop point for * off-center drag, remove the cursor, and pass the event along * listener chain. */ public void drop(DropTargetDropEvent e) { // EVENT uses UNSCALED, DROP-TARGET coordinate system e.getLocation().translate(currentPieceOffsetX, currentPieceOffsetY); final DropTargetListener forward = getListener(e); if (forward != null) forward.drop(e); } /** ineffectual. Passes event along listener chain */ public void dragExit(DropTargetEvent e) { final DropTargetListener forward = getListener(e); if (forward != null) forward.dragExit(e); } /** ineffectual. Passes event along listener chain */ public void dragOver(DropTargetDragEvent e) { final DropTargetListener forward = getListener(e); if (forward != null) forward.dragOver(e); } /** ineffectual. Passes event along listener chain */ public void dropActionChanged(DropTargetDragEvent e) { final DropTargetListener forward = getListener(e); if (forward != null) forward.dropActionChanged(e); } } /** * Implements a psudo-cursor that follows the mouse cursor when user * drags gamepieces. Supports map zoom by resizing cursor when it enters * a drop target of type Map.View. * * @author Jim Urbas * @version 0.4.2 * */ static public class DragHandlerNoImage extends AbstractDragHandler { @Override public void dragGestureRecognized(DragGestureEvent dge) { final Point mousePosition = dragGestureRecognizedPrep(dge); if (mousePosition == null) return; makeDragCursor(dragPieceOffCenterZoom); setDrawWinToOwnerOf(dragWin); SwingUtilities.convertPointToScreen(mousePosition, drawWin); moveDragCursor(mousePosition.x, mousePosition.y); super.dragGestureRecognized(dge); } protected int getOffsetMult() { return 1; } @Override public void dragDropEnd(DragSourceDropEvent e) { removeDragCursor(); super.dragDropEnd(e); } public void dragMouseMoved(DragSourceDragEvent e) { if (!e.getLocation().equals(lastDragLocation)) { lastDragLocation = e.getLocation(); moveDragCursor(e.getX(), e.getY()); if (dragCursor != null && !dragCursor.isVisible()) { dragCursor.setVisible(true); } } } public void dragEnter(DropTargetDragEvent e) { final Component newDropWin = e.getDropTargetContext().getComponent(); if (newDropWin != dropWin) { final double newZoom = newDropWin instanceof Map.View ? ((Map.View) newDropWin).getMap().getZoom() : 1.0; if (Math.abs(newZoom - dragCursorZoom) > 0.01) { makeDragCursor(newZoom); } setDrawWinToOwnerOf(e.getDropTargetContext().getComponent()); dropWin = newDropWin; } super.dragEnter(e); } public void drop(DropTargetDropEvent e) { removeDragCursor(); super.drop(e); } } /** Implementation of AbstractDragHandler when DragImage is supported by JRE * * @Author Pieter Geerkens */ static public class DragHandler extends AbstractDragHandler { public void dragGestureRecognized(DragGestureEvent dge) { if (dragGestureRecognizedPrep(dge) == null) return; super.dragGestureRecognized(dge); } protected int getOffsetMult() { return -1; } public void dragMouseMoved(DragSourceDragEvent e) {} } static public class DragHandlerMacOSX extends DragHandler { @Override protected int getOffsetMult() { return 1; } } }