/*
* Copyright (c) 2008-2009, Piccolo2D project, http://piccolo2d.org
* Copyright (c) 1998-2008, University of Maryland
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided
* that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this list of conditions
* and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions
* and the following disclaimer in the documentation and/or other materials provided with the
* distribution.
*
* None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
* contributors may be used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package edu.umd.cs.piccolox.event;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Paint;
import java.awt.Stroke;
import java.awt.event.KeyEvent;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import edu.umd.cs.piccolo.PCamera;
import edu.umd.cs.piccolo.PLayer;
import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.event.PDragSequenceEventHandler;
import edu.umd.cs.piccolo.event.PInputEvent;
import edu.umd.cs.piccolo.nodes.PPath;
import edu.umd.cs.piccolo.util.PBounds;
import edu.umd.cs.piccolo.util.PDimension;
import edu.umd.cs.piccolo.util.PNodeFilter;
import edu.umd.cs.piccolox.handles.PBoundsHandle;
/**
* <code>PSelectionEventHandler</code> provides standard interaction for
* selection. Clicking selects the object under the cursor. Shift-clicking
* allows multiple objects to be selected. Dragging offers marquee selection.
* Pressing the delete key deletes the selection by default.
*
* @version 1.0
* @author Ben Bederson
*/
public class PSelectionEventHandler extends PDragSequenceEventHandler {
public static final String SELECTION_CHANGED_NOTIFICATION = "SELECTION_CHANGED_NOTIFICATION";
final static int DASH_WIDTH = 5;
final static int NUM_STROKES = 10;
// The current selection
private HashMap selection = null;
// List of nodes whose children can be selected
private List selectableParents = null;
private PPath marquee = null;
// Node that marquee is added to as a child
private PNode marqueeParent = null;
private Point2D presspt = null;
private Point2D canvasPressPt = null;
private float strokeNum = 0;
private Stroke[] strokes = null;
// Used within drag handler temporarily
private HashMap allItems = null;
// Used within drag handler temporarily
private ArrayList unselectList = null;
private HashMap marqueeMap = null;
// Node pressed on (or null if none)
private PNode pressNode = null;
// True if DELETE key should delete selection
private boolean deleteKeyActive = true;
private Paint marqueePaint;
private float marqueePaintTransparency = 1.0f;
/**
* Creates a selection event handler.
*
* @param marqueeParent The node to which the event handler dynamically adds
* a marquee (temporarily) to represent the area being selected.
* @param selectableParent The node whose children will be selected by this
* event handler.
*/
public PSelectionEventHandler(PNode marqueeParent, PNode selectableParent) {
this.marqueeParent = marqueeParent;
this.selectableParents = new ArrayList();
this.selectableParents.add(selectableParent);
init();
}
/**
* Creates a selection event handler.
*
* @param marqueeParent The node to which the event handler dynamically adds
* a marquee (temporarily) to represent the area being selected.
* @param selectableParents A list of nodes whose children will be selected
* by this event handler.
*/
public PSelectionEventHandler(PNode marqueeParent, List selectableParents) {
this.marqueeParent = marqueeParent;
this.selectableParents = selectableParents;
init();
}
protected void init() {
float[] dash = { DASH_WIDTH, DASH_WIDTH };
strokes = new Stroke[NUM_STROKES];
for (int i = 0; i < NUM_STROKES; i++) {
strokes[i] = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, dash, i);
}
selection = new HashMap();
allItems = new HashMap();
unselectList = new ArrayList();
marqueeMap = new HashMap();
}
// /////////////////////////////////////////////////////
// Public static methods for manipulating the selection
// /////////////////////////////////////////////////////
public void select(Collection items) {
boolean changes = false;
Iterator itemIt = items.iterator();
while (itemIt.hasNext()) {
PNode node = (PNode) itemIt.next();
changes |= internalSelect(node);
}
if (changes) {
postSelectionChanged();
}
}
public void select(Map items) {
select(items.keySet());
}
private boolean internalSelect(PNode node) {
if (isSelected(node)) {
return false;
}
selection.put(node, Boolean.TRUE);
decorateSelectedNode(node);
return true;
}
private void postSelectionChanged() {
PNotificationCenter.defaultCenter().postNotification(SELECTION_CHANGED_NOTIFICATION, this);
}
public void select(PNode node) {
if (internalSelect(node)) {
postSelectionChanged();
}
}
public void decorateSelectedNode(PNode node) {
PBoundsHandle.addBoundsHandlesTo(node);
}
public void unselect(Collection items) {
boolean changes = false;
Iterator itemIt = items.iterator();
while (itemIt.hasNext()) {
PNode node = (PNode) itemIt.next();
changes |= internalUnselect(node);
}
if (changes) {
postSelectionChanged();
}
}
private boolean internalUnselect(PNode node) {
if (!isSelected(node)) {
return false;
}
undecorateSelectedNode(node);
selection.remove(node);
return true;
}
public void unselect(PNode node) {
if (internalUnselect(node)) {
postSelectionChanged();
}
}
public void undecorateSelectedNode(PNode node) {
PBoundsHandle.removeBoundsHandlesFrom(node);
}
public void unselectAll() {
// Because unselect() removes from selection, we need to
// take a copy of it first so it isn't changed while we're iterating
ArrayList sel = new ArrayList(selection.keySet());
unselect(sel);
}
public boolean isSelected(PNode node) {
if ((node != null) && (selection.containsKey(node))) {
return true;
}
else {
return false;
}
}
/**
* Returns a copy of the currently selected nodes.
*/
public Collection getSelection() {
ArrayList sel = new ArrayList(selection.keySet());
return sel;
}
/**
* Gets a reference to the currently selected nodes. You should not modify
* or store this collection.
*/
public Collection getSelectionReference() {
return Collections.unmodifiableCollection(selection.keySet());
}
/**
* Determine if the specified node is selectable (i.e., if it is a child of
* the one the list of selectable parents.
*/
protected boolean isSelectable(PNode node) {
boolean selectable = false;
Iterator parentsIt = selectableParents.iterator();
while (parentsIt.hasNext()) {
PNode parent = (PNode) parentsIt.next();
if (parent.getChildrenReference().contains(node)) {
selectable = true;
break;
}
else if (parent instanceof PCamera) {
for (int i = 0; i < ((PCamera) parent).getLayerCount(); i++) {
PLayer layer = ((PCamera) parent).getLayer(i);
if (layer.getChildrenReference().contains(node)) {
selectable = true;
break;
}
}
}
}
return selectable;
}
// ////////////////////////////////////////////////////
// Methods for modifying the set of selectable parents
// ////////////////////////////////////////////////////
public void addSelectableParent(PNode node) {
selectableParents.add(node);
}
public void removeSelectableParent(PNode node) {
selectableParents.remove(node);
}
public void setSelectableParent(PNode node) {
selectableParents.clear();
selectableParents.add(node);
}
public void setSelectableParents(Collection c) {
selectableParents.clear();
selectableParents.addAll(c);
}
public Collection getSelectableParents() {
return new ArrayList(selectableParents);
}
// //////////////////////////////////////////////////////
// The overridden methods from PDragSequenceEventHandler
// //////////////////////////////////////////////////////
protected void startDrag(PInputEvent e) {
super.startDrag(e);
initializeSelection(e);
if (isMarqueeSelection(e)) {
initializeMarquee(e);
if (!isOptionSelection(e)) {
startMarqueeSelection(e);
}
else {
startOptionMarqueeSelection(e);
}
}
else {
if (!isOptionSelection(e)) {
startStandardSelection(e);
}
else {
startStandardOptionSelection(e);
}
}
}
protected void drag(PInputEvent e) {
super.drag(e);
if (isMarqueeSelection(e)) {
updateMarquee(e);
if (!isOptionSelection(e)) {
computeMarqueeSelection(e);
}
else {
computeOptionMarqueeSelection(e);
}
}
else {
dragStandardSelection(e);
}
}
protected void endDrag(PInputEvent e) {
super.endDrag(e);
if (isMarqueeSelection(e)) {
endMarqueeSelection(e);
}
else {
endStandardSelection(e);
}
}
// //////////////////////////
// Additional methods
// //////////////////////////
public boolean isOptionSelection(PInputEvent pie) {
return pie.isShiftDown();
}
protected boolean isMarqueeSelection(PInputEvent pie) {
return (pressNode == null);
}
protected void initializeSelection(PInputEvent pie) {
canvasPressPt = pie.getCanvasPosition();
presspt = pie.getPosition();
pressNode = pie.getPath().getPickedNode();
if (pressNode instanceof PCamera) {
pressNode = null;
}
}
protected void initializeMarquee(PInputEvent e) {
marquee = PPath.createRectangle((float) presspt.getX(), (float) presspt.getY(), 0, 0);
marquee.setPaint(marqueePaint);
marquee.setTransparency(marqueePaintTransparency);
marquee.setStrokePaint(Color.black);
marquee.setStroke(strokes[0]);
marqueeParent.addChild(marquee);
marqueeMap.clear();
}
protected void startOptionMarqueeSelection(PInputEvent e) {
}
protected void startMarqueeSelection(PInputEvent e) {
unselectAll();
}
protected void startStandardSelection(PInputEvent pie) {
// Option indicator not down - clear selection, and start fresh
if (!isSelected(pressNode)) {
unselectAll();
if (isSelectable(pressNode)) {
select(pressNode);
}
}
}
protected void startStandardOptionSelection(PInputEvent pie) {
// Option indicator is down, toggle selection
if (isSelectable(pressNode)) {
if (isSelected(pressNode)) {
unselect(pressNode);
}
else {
select(pressNode);
}
}
}
protected void updateMarquee(PInputEvent pie) {
PBounds b = new PBounds();
if (marqueeParent instanceof PCamera) {
b.add(canvasPressPt);
b.add(pie.getCanvasPosition());
}
else {
b.add(presspt);
b.add(pie.getPosition());
}
marquee.globalToLocal(b);
marquee.setPathToRectangle((float) b.x, (float) b.y, (float) b.width, (float) b.height);
b.reset();
b.add(presspt);
b.add(pie.getPosition());
allItems.clear();
PNodeFilter filter = createNodeFilter(b);
Iterator parentsIt = selectableParents.iterator();
while (parentsIt.hasNext()) {
PNode parent = (PNode) parentsIt.next();
Collection items;
if (parent instanceof PCamera) {
items = new ArrayList();
for (int i = 0; i < ((PCamera) parent).getLayerCount(); i++) {
((PCamera) parent).getLayer(i).getAllNodes(filter, items);
}
}
else {
items = parent.getAllNodes(filter, null);
}
Iterator itemsIt = items.iterator();
while (itemsIt.hasNext()) {
allItems.put(itemsIt.next(), Boolean.TRUE);
}
}
}
protected void computeMarqueeSelection(PInputEvent pie) {
unselectList.clear();
// Make just the items in the list selected
// Do this efficiently by first unselecting things not in the list
Iterator selectionEn = selection.keySet().iterator();
while (selectionEn.hasNext()) {
PNode node = (PNode) selectionEn.next();
if (!allItems.containsKey(node)) {
unselectList.add(node);
}
}
unselect(unselectList);
// Then select the rest
selectionEn = allItems.keySet().iterator();
while (selectionEn.hasNext()) {
PNode node = (PNode) selectionEn.next();
if (!selection.containsKey(node) && !marqueeMap.containsKey(node) && isSelectable(node)) {
marqueeMap.put(node, Boolean.TRUE);
}
else if (!isSelectable(node)) {
selectionEn.remove();
}
}
select(allItems);
}
protected void computeOptionMarqueeSelection(PInputEvent pie) {
unselectList.clear();
Iterator selectionEn = selection.keySet().iterator();
while (selectionEn.hasNext()) {
PNode node = (PNode) selectionEn.next();
if (!allItems.containsKey(node) && marqueeMap.containsKey(node)) {
marqueeMap.remove(node);
unselectList.add(node);
}
}
unselect(unselectList);
// Then select the rest
selectionEn = allItems.keySet().iterator();
while (selectionEn.hasNext()) {
PNode node = (PNode) selectionEn.next();
if (!selection.containsKey(node) && !marqueeMap.containsKey(node) && isSelectable(node)) {
marqueeMap.put(node, Boolean.TRUE);
}
else if (!isSelectable(node)) {
selectionEn.remove();
}
}
select(allItems);
}
protected PNodeFilter createNodeFilter(PBounds bounds) {
return new BoundsFilter(bounds);
}
protected PBounds getMarqueeBounds() {
if (marquee != null) {
return marquee.getBounds();
}
return new PBounds();
}
protected void dragStandardSelection(PInputEvent e) {
// There was a press node, so drag selection
PDimension d = e.getCanvasDelta();
e.getTopCamera().localToView(d);
PDimension gDist = new PDimension();
Iterator selectionEn = getSelection().iterator();
while (selectionEn.hasNext()) {
PNode node = (PNode) selectionEn.next();
gDist.setSize(d);
node.getParent().globalToLocal(gDist);
node.offset(gDist.getWidth(), gDist.getHeight());
}
}
protected void endMarqueeSelection(PInputEvent e) {
// Remove marquee
allItems.clear();
marqueeMap.clear();
marquee.removeFromParent();
marquee = null;
}
protected void endStandardSelection(PInputEvent e) {
pressNode = null;
}
/**
* This gets called continuously during the drag, and is used to animate the
* marquee
*/
protected void dragActivityStep(PInputEvent aEvent) {
if (marquee != null) {
float origStrokeNum = strokeNum;
strokeNum = (strokeNum + 0.5f) % NUM_STROKES; // Increment by
// partial steps to
// slow down animation
if ((int) strokeNum != (int) origStrokeNum) {
marquee.setStroke(strokes[(int) strokeNum]);
}
}
}
/**
* Delete selection when delete key is pressed (if enabled)
*/
public void keyPressed(PInputEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_DELETE:
if (deleteKeyActive) {
Iterator selectionEn = selection.keySet().iterator();
while (selectionEn.hasNext()) {
PNode node = (PNode) selectionEn.next();
node.removeFromParent();
}
selection.clear();
}
}
}
public boolean getSupportDeleteKey() {
return deleteKeyActive;
}
public boolean isDeleteKeyActive() {
return deleteKeyActive;
}
/**
* Specifies if the DELETE key should delete the selection
*/
public void setDeleteKeyActive(boolean deleteKeyActive) {
this.deleteKeyActive = deleteKeyActive;
}
// ////////////////////
// Inner classes
// ////////////////////
protected class BoundsFilter implements PNodeFilter {
PBounds localBounds = new PBounds();
PBounds bounds;
protected BoundsFilter(PBounds bounds) {
this.bounds = bounds;
}
public boolean accept(PNode node) {
localBounds.setRect(bounds);
node.globalToLocal(localBounds);
boolean boundsIntersects = node.intersects(localBounds);
boolean isMarquee = (node == marquee);
return (node.getPickable() && boundsIntersects && !isMarquee && !selectableParents.contains(node) && !isCameraLayer(node));
}
public boolean acceptChildrenOf(PNode node) {
return selectableParents.contains(node) || isCameraLayer(node);
}
public boolean isCameraLayer(PNode node) {
if (node instanceof PLayer) {
for (Iterator i = selectableParents.iterator(); i.hasNext();) {
PNode parent = (PNode) i.next();
if (parent instanceof PCamera) {
if (((PCamera) parent).indexOfLayer((PLayer) node) != -1) {
return true;
}
}
}
}
return false;
}
}
/**
* Indicates the color used to paint the marquee.
*
* @return the paint for interior of the marquee
*/
public Paint getMarqueePaint() {
return marqueePaint;
}
/**
* Sets the color used to paint the marquee.
*
* @param paint the paint color
*/
public void setMarqueePaint(Paint paint) {
this.marqueePaint = paint;
}
/**
* Indicates the transparency level for the interior of the marquee.
*
* @return Returns the marquee paint transparency, zero to one
*/
public float getMarqueePaintTransparency() {
return marqueePaintTransparency;
}
/**
* Sets the transparency level for the interior of the marquee.
*
* @param marqueePaintTransparency The marquee paint transparency to set.
*/
public void setMarqueePaintTransparency(float marqueePaintTransparency) {
this.marqueePaintTransparency = marqueePaintTransparency;
}
}