/*
* $Id$
*
* Copyright (c) 2000-2007 by Rodney Kinney, Joel Uckelman
*
* 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.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import VASSAL.build.Buildable;
import VASSAL.build.GameModule;
import VASSAL.build.module.Map;
import VASSAL.counters.ColoredBorder;
import VASSAL.counters.Deck;
import VASSAL.counters.DeckVisitor;
import VASSAL.counters.EventFilter;
import VASSAL.counters.GamePiece;
import VASSAL.counters.Immobilized;
import VASSAL.counters.KeyBuffer;
import VASSAL.counters.PieceFinder;
import VASSAL.counters.PieceVisitorDispatcher;
import VASSAL.counters.Properties;
import VASSAL.counters.Stack;
/**
* This component listens for mouse clicks on a map and draws the selection
* rectangle.
*
* If the user clicks on a {@link GamePiece}, that piece is added to the
* {@link KeyBuffer}. {@link #draw(Graphics, Map)} is responsible for
* drawing the mouse selection rectangle, and
* {@link #mouseDragged(MouseEvent)} is responsible for triggering repaint
* events as the selection rectangle is moved.
*
* @see Map#addLocalMouseListener
*/
public class KeyBufferer extends MouseAdapter implements Buildable, MouseMotionListener, Drawable {
protected Map map;
protected Rectangle selection;
protected Point anchor;
protected Color color = Color.black;
protected int thickness = 3;
public void addTo(Buildable b) {
map = (Map) b;
map.addLocalMouseListenerFirst(this);
map.getView().addMouseMotionListener(this);
map.addDrawComponent(this);
}
public void add(Buildable b) {
}
public Element getBuildElement(Document doc) {
return doc.createElement(getClass().getName());
}
public void build(Element e) {
}
public void mousePressed(MouseEvent e) {
if (e.isConsumed()) {
return;
}
GamePiece p = map.findPiece(e.getPoint(), PieceFinder.PIECE_IN_STACK);
// Don't clear the buffer until we find the clicked-on piece
// Because selecting a piece affects its visibility
EventFilter filter = null;
if (p != null) {
filter = (EventFilter) p.getProperty(Properties.SELECT_EVENT_FILTER);
}
boolean ignoreEvent = filter != null && filter.rejectEvent(e);
if (p != null && !ignoreEvent) {
boolean movingStacksPickupUnits = ((Boolean) GameModule.getGameModule().getPrefs().getValue(Map.MOVING_STACKS_PICKUP_UNITS)).booleanValue();
if (!KeyBuffer.getBuffer().contains(p)) {
if (!e.isShiftDown() && !e.isControlDown()) {
KeyBuffer.getBuffer().clear();
}
// RFE 1629255 - If the top piece of an unexpanded stack is left-clicked
// while not selected, then select all of the pieces in the stack
// RFE 1659481 - Control clicking only deselects
if (!e.isControlDown()) {
if (movingStacksPickupUnits || p.getParent() == null || p.getParent().isExpanded() || e.isMetaDown()
|| Boolean.TRUE.equals(p.getProperty(Properties.SELECTED))) {
KeyBuffer.getBuffer().add(p);
}
else {
Stack s = p.getParent();
for (int i = 0; i < s.getPieceCount(); i++) {
KeyBuffer.getBuffer().add(s.getPieceAt(i));
}
}
}
// End RFE 1629255
}
else {
// RFE 1659481 Ctrl-click deselects clicked units
if (e.isControlDown() && Boolean.TRUE.equals(p.getProperty(Properties.SELECTED))) {
Stack s = p.getParent();
if (s == null) {
KeyBuffer.getBuffer().remove(p);
}
else if (!s.isExpanded()) {
for (int i = 0; i < s.getPieceCount(); i++) {
KeyBuffer.getBuffer().remove(s.getPieceAt(i));
}
}
}
// End RFE 1659481
}
if (p.getParent() != null) {
map.getPieceCollection().moveToFront(p.getParent());
}
else {
map.getPieceCollection().moveToFront(p);
}
}
else {
if (!e.isShiftDown() && !e.isControlDown()) { // No deselect if shift key down
KeyBuffer.getBuffer().clear();
}
anchor = map.componentCoordinates(e.getPoint());
selection = new Rectangle(anchor.x, anchor.y, 0, 0);
if (map.getHighlighter() instanceof ColoredBorder) {
ColoredBorder b = (ColoredBorder) map.getHighlighter();
color = b.getColor();
thickness = b.getThickness();
}
}
}
public void mouseReleased(MouseEvent evt) {
if (selection != null) {
selection.setLocation(map.mapCoordinates(selection.getLocation()));
selection.width /= map.getZoom();
selection.height /= map.getZoom();
PieceVisitorDispatcher d = createDragSelector(!evt.isControlDown(), evt.isAltDown());
// RFE 1659481 Don't clear the entire selection buffer if either shift
// or control is down - we select/deselect lassoed counters instead
if (!evt.isShiftDown() && !evt.isControlDown()) {
KeyBuffer.getBuffer().clear();
}
map.apply(d);
repaintSelectionRect();
}
selection = null;
}
/**
* This PieceVisitorDispatcher determines what to do with pieces on the
* map when the player finished dragging a rectangle to select pieces
*
* @return
*/
protected PieceVisitorDispatcher createDragSelector(boolean selecting, boolean altDown) {
return new PieceVisitorDispatcher(new KBDeckVisitor(selecting, altDown));
}
public class KBDeckVisitor implements DeckVisitor {
boolean selecting = false;
boolean altDown = false;
public KBDeckVisitor(boolean b, boolean c) {
selecting = b;
altDown = c;
}
public Object visitDeck(Deck d) {
return null;
}
public Object visitStack(Stack s) {
if (s.topPiece() != null) {
if (s.isExpanded()) {
Point[] pos = new Point[s.getPieceCount()];
map.getStackMetrics().getContents(s, pos, null, null, s.getPosition().x, s.getPosition().y);
for (int i = 0; i < pos.length; ++i) {
if (selection.contains(pos[i])) {
if (selecting) {
KeyBuffer.getBuffer().add(s.getPieceAt(i));
}
else {
KeyBuffer.getBuffer().remove(s.getPieceAt(i));
}
}
}
}
else if (selection.contains(s.getPosition())) {
for (int i = 0, n = s.getPieceCount(); i < n; ++i) {
if (selecting) {
KeyBuffer.getBuffer().add(s.getPieceAt(i));
}
else {
KeyBuffer.getBuffer().remove(s.getPieceAt(i));
}
}
}
}
return null;
}
// Handle non-stacked units, including Does Not Stack units
// Does Not Stack units deselect normally once selected
public Object visitDefault(GamePiece p) {
if (selection.contains(p.getPosition()) && !Boolean.TRUE.equals(p.getProperty(Properties.INVISIBLE_TO_ME))) {
if (selecting) {
final EventFilter filter = (EventFilter) p.getProperty(Properties.SELECT_EVENT_FILTER);
final boolean altSelect = (altDown && filter instanceof Immobilized.UseAlt);
if (filter == null || altSelect) {
KeyBuffer.getBuffer().add(p);
}
}
else {
KeyBuffer.getBuffer().remove(p);
}
}
return null;
}
}
protected void repaintSelectionRect() {
/*
* Repaint strategy: There is no reason to repaint the interior of
* the selection rectangle, as we didn't paint over it in the first
* place. Instead, we repaint only the four (slender) rectangles
* which the stroke of the selection rectangle filled. We have to
* call a repaint on both the old selection rectangle and the new
* in order to prevent drawing artifacts. The KeyBuffer handles
* repainting pieces which have been (de)selected, so we don't
* worry about those.
*
* Area drawn:
* selection.x
* |
* ___________________
* selection.y __ |__|__|_____|__|__| __
* |__|__|_____|__|__| |
* | | | | | | |
* | | | | | | | selection.height
* |__|__|_____|__|__| |
* ~thickness/2 --{ |__|__|_____|__|__| __|
* ~thickness/2 --{ |__|__|_____|__|__|
*
* |___________|
* selection.width
*/
final int ht = thickness / 2 + thickness % 2;
final int ht2 = 2*ht;
// left
map.getView().repaint(selection.x - ht,
selection.y - ht,
ht2,
selection.height + ht2);
// right
map.getView().repaint(selection.x + selection.width - ht,
selection.y - ht,
ht2,
selection.height + ht2);
// top
map.getView().repaint(selection.x - ht,
selection.y - ht,
selection.width + ht2,
ht2);
// bottom
map.getView().repaint(selection.x - ht,
selection.y + selection.width - ht,
selection.width + ht2,
ht2);
}
/**
* Sets the new location of the selection rectangle.
*/
public void mouseDragged(MouseEvent e) {
if (selection != null) {
repaintSelectionRect();
selection.x = Math.min(e.getX(), anchor.x);
selection.y = Math.min(e.getY(), anchor.y);
selection.width = Math.abs(e.getX() - anchor.x);
selection.height = Math.abs(e.getY() - anchor.y);
repaintSelectionRect();
}
}
public void mouseMoved(MouseEvent e) {
}
public void draw(Graphics g, Map map) {
if (selection != null) {
final Graphics2D g2d = (Graphics2D) g;
final Stroke str = g2d.getStroke();
g2d.setStroke(new BasicStroke(thickness));
g2d.setColor(color);
g2d.drawRect(selection.x, selection.y, selection.width, selection.height);
g2d.setStroke(str);
}
}
public boolean drawAboveCounters() {
return true;
}
}