/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.badlogic.gdx.scenes.scene2d.utils;
import com.badlogic.gdx.Input.Buttons;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.Touchable;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.ObjectMap.Entry;
/** Manages drag and drop operations through registered drag sources and drop targets.
* @author Nathan Sweet */
public class DragAndDrop {
static final Vector2 tmpVector = new Vector2();
Payload payload;
Actor dragActor;
Target target;
boolean isValidTarget;
Array<Target> targets = new Array();
ObjectMap<Source, DragListener> sourceListeners = new ObjectMap();
private float tapSquareSize = 8;
private int button;
float dragActorX = 0, dragActorY = 0;
float touchOffsetX, touchOffsetY;
long dragStartTime;
int dragTime = 250;
int activePointer = -1;
boolean cancelTouchFocus = true;
boolean keepWithinStage = true;
public void addSource (final Source source) {
DragListener listener = new DragListener() {
public void dragStart (InputEvent event, float x, float y, int pointer) {
if (activePointer != -1) {
event.stop();
return;
}
activePointer = pointer;
dragStartTime = System.currentTimeMillis();
payload = source.dragStart(event, getTouchDownX(), getTouchDownY(), pointer);
event.stop();
if (cancelTouchFocus && payload != null) source.getActor().getStage().cancelTouchFocusExcept(this, source.getActor());
}
public void drag (InputEvent event, float x, float y, int pointer) {
if (payload == null) return;
if (pointer != activePointer) return;
Stage stage = event.getStage();
Touchable dragActorTouchable = null;
if (dragActor != null) {
dragActorTouchable = dragActor.getTouchable();
dragActor.setTouchable(Touchable.disabled);
}
// Find target.
Target newTarget = null;
isValidTarget = false;
float stageX = event.getStageX() + touchOffsetX, stageY = event.getStageY() + touchOffsetY;
Actor hit = event.getStage().hit(stageX, stageY, true); // Prefer touchable actors.
if (hit == null) hit = event.getStage().hit(stageX, stageY, false);
if (hit != null) {
for (int i = 0, n = targets.size; i < n; i++) {
Target target = targets.get(i);
if (!target.actor.isAscendantOf(hit)) continue;
newTarget = target;
target.actor.stageToLocalCoordinates(tmpVector.set(stageX, stageY));
break;
}
}
// If over a new target, notify the former target that it's being left behind.
if (newTarget != target) {
if (target != null) target.reset(source, payload);
target = newTarget;
}
// Notify new target of drag.
if (newTarget != null) isValidTarget = newTarget.drag(source, payload, tmpVector.x, tmpVector.y, pointer);
if (dragActor != null) dragActor.setTouchable(dragActorTouchable);
// Add/remove and position the drag actor.
Actor actor = null;
if (target != null) actor = isValidTarget ? payload.validDragActor : payload.invalidDragActor;
if (actor == null) actor = payload.dragActor;
if (actor == null) return;
if (dragActor != actor) {
if (dragActor != null) dragActor.remove();
dragActor = actor;
stage.addActor(actor);
}
float actorX = event.getStageX() - actor.getWidth() + dragActorX;
float actorY = event.getStageY() + dragActorY;
if (keepWithinStage) {
if (actorX < 0) actorX = 0;
if (actorY < 0) actorY = 0;
if (actorX + actor.getWidth() > stage.getWidth()) actorX = stage.getWidth() - actor.getWidth();
if (actorY + actor.getHeight() > stage.getHeight()) actorY = stage.getHeight() - actor.getHeight();
}
actor.setPosition(actorX, actorY);
}
public void dragStop (InputEvent event, float x, float y, int pointer) {
if (pointer != activePointer) return;
activePointer = -1;
if (payload == null) return;
if (System.currentTimeMillis() - dragStartTime < dragTime) isValidTarget = false;
if (dragActor != null) dragActor.remove();
if (isValidTarget) {
float stageX = event.getStageX() + touchOffsetX, stageY = event.getStageY() + touchOffsetY;
target.actor.stageToLocalCoordinates(tmpVector.set(stageX, stageY));
target.drop(source, payload, tmpVector.x, tmpVector.y, pointer);
}
source.dragStop(event, x, y, pointer, payload, isValidTarget ? target : null);
if (target != null) target.reset(source, payload);
payload = null;
target = null;
isValidTarget = false;
dragActor = null;
}
};
listener.setTapSquareSize(tapSquareSize);
listener.setButton(button);
source.actor.addCaptureListener(listener);
sourceListeners.put(source, listener);
}
public void removeSource (Source source) {
DragListener dragListener = sourceListeners.remove(source);
source.actor.removeCaptureListener(dragListener);
}
public void addTarget (Target target) {
targets.add(target);
}
public void removeTarget (Target target) {
targets.removeValue(target, true);
}
/** Removes all targets and sources. */
public void clear () {
targets.clear();
for (Entry<Source, DragListener> entry : sourceListeners.entries())
entry.key.actor.removeCaptureListener(entry.value);
sourceListeners.clear();
}
/** Sets the distance a touch must travel before being considered a drag. */
public void setTapSquareSize (float halfTapSquareSize) {
tapSquareSize = halfTapSquareSize;
}
/** Sets the button to listen for, all other buttons are ignored. Default is {@link Buttons#LEFT}. Use -1 for any button. */
public void setButton (int button) {
this.button = button;
}
public void setDragActorPosition (float dragActorX, float dragActorY) {
this.dragActorX = dragActorX;
this.dragActorY = dragActorY;
}
/** Sets an offset in stage coordinates from the touch position which is used to determine the drop location. Default is
* 0,0. */
public void setTouchOffset (float touchOffsetX, float touchOffsetY) {
this.touchOffsetX = touchOffsetX;
this.touchOffsetY = touchOffsetY;
}
public boolean isDragging () {
return payload != null;
}
/** Returns the current drag actor, or null. */
public Actor getDragActor () {
return dragActor;
}
/** Time in milliseconds that a drag must take before a drop will be considered valid. This ignores an accidental drag and drop
* that was meant to be a click. Default is 250. */
public void setDragTime (int dragMillis) {
this.dragTime = dragMillis;
}
/** When true (default), the {@link Stage#cancelTouchFocus()} touch focus} is cancelled if
* {@link Source#dragStart(InputEvent, float, float, int) dragStart} returns non-null. This ensures the DragAndDrop is the only
* touch focus listener, eg when the source is inside a {@link ScrollPane} with flick scroll enabled. */
public void setCancelTouchFocus (boolean cancelTouchFocus) {
this.cancelTouchFocus = cancelTouchFocus;
}
public void setKeepWithinStage (boolean keepWithinStage) {
this.keepWithinStage = keepWithinStage;
}
/** A source where a payload can be dragged from.
* @author Nathan Sweet */
static abstract public class Source {
final Actor actor;
public Source (Actor actor) {
if (actor == null) throw new IllegalArgumentException("actor cannot be null.");
this.actor = actor;
}
/** Called when a drag is started on the source. The coordinates are in the source's local coordinate system.
* @return If null the drag will not affect any targets. */
abstract public Payload dragStart (InputEvent event, float x, float y, int pointer);
/** Called when a drag for the source is stopped. The coordinates are in the source's local coordinate system.
* @param payload null if dragStart returned null.
* @param target null if not dropped on a valid target. */
public void dragStop (InputEvent event, float x, float y, int pointer, Payload payload, Target target) {
}
public Actor getActor () {
return actor;
}
}
/** A target where a payload can be dropped to.
* @author Nathan Sweet */
static abstract public class Target {
final Actor actor;
public Target (Actor actor) {
if (actor == null) throw new IllegalArgumentException("actor cannot be null.");
this.actor = actor;
Stage stage = actor.getStage();
if (stage != null && actor == stage.getRoot())
throw new IllegalArgumentException("The stage root cannot be a drag and drop target.");
}
/** Called when the payload is dragged over the target. The coordinates are in the target's local coordinate system.
* @return true if this is a valid target for the payload. */
abstract public boolean drag (Source source, Payload payload, float x, float y, int pointer);
/** Called when the payload is no longer over the target, whether because the touch was moved or a drop occurred. */
public void reset (Source source, Payload payload) {
}
/** Called when the payload is dropped on the target. The coordinates are in the target's local coordinate system. */
abstract public void drop (Source source, Payload payload, float x, float y, int pointer);
public Actor getActor () {
return actor;
}
}
/** The payload of a drag and drop operation. Actors can be optionally provided to follow the cursor and change when over a
* target. Such Actors will be added and removed from the stage automatically during the drag operation. Care should be taken
* when using the source Actor as a payload drag actor. */
static public class Payload {
Actor dragActor, validDragActor, invalidDragActor;
Object object;
public void setDragActor (Actor dragActor) {
this.dragActor = dragActor;
}
public Actor getDragActor () {
return dragActor;
}
public void setValidDragActor (Actor validDragActor) {
this.validDragActor = validDragActor;
}
public Actor getValidDragActor () {
return validDragActor;
}
public void setInvalidDragActor (Actor invalidDragActor) {
this.invalidDragActor = invalidDragActor;
}
public Actor getInvalidDragActor () {
return invalidDragActor;
}
public Object getObject () {
return object;
}
public void setObject (Object object) {
this.object = object;
}
}
}