/*******************************************************************************
* 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.Gdx;
import com.badlogic.gdx.Input.Buttons;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.utils.TimeUtils;
/** Detects mouse over, mouse or finger touch presses, and clicks on an actor. A touch must go down over the actor and is
* considered pressed as long as it is over the actor or within the {@link #setTapSquareSize(float) tap square}. This behavior
* makes it easier to press buttons on a touch interface when the initial touch happens near the edge of the actor. Double clicks
* can be detected using {@link #getTapCount()}. Any touch (not just the first) will trigger this listener. While pressed, other
* touch downs are ignored.
* @author Nathan Sweet */
public class ClickListener extends InputListener {
/** Time in seconds {@link #isVisualPressed()} reports true after a press resulting in a click is released. */
static public float visualPressedDuration = 0.1f;
private float tapSquareSize = 14, touchDownX = -1, touchDownY = -1;
private int pressedPointer = -1;
private int pressedButton = -1;
private int button;
private boolean pressed, over, cancelled;
private long visualPressedTime;
private long tapCountInterval = (long)(0.4f * 1000000000l);
private int tapCount;
private long lastTapTime;
/** Create a listener where {@link #clicked(InputEvent, float, float)} is only called for left clicks.
* @see #ClickListener(int) */
public ClickListener () {
}
/** @see #setButton(int) */
public ClickListener (int button) {
this.button = button;
}
public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
if (pressed) return false;
if (pointer == 0 && this.button != -1 && button != this.button) return false;
pressed = true;
pressedPointer = pointer;
pressedButton = button;
touchDownX = x;
touchDownY = y;
visualPressedTime = TimeUtils.millis() + (long)(visualPressedDuration * 1000);
return true;
}
public void touchDragged (InputEvent event, float x, float y, int pointer) {
if (pointer != pressedPointer || cancelled) return;
pressed = isOver(event.getListenerActor(), x, y);
if (pressed && pointer == 0 && button != -1 && !Gdx.input.isButtonPressed(button)) pressed = false;
if (!pressed) {
// Once outside the tap square, don't use the tap square anymore.
invalidateTapSquare();
}
}
public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
if (pointer == pressedPointer) {
if (!cancelled) {
boolean touchUpOver = isOver(event.getListenerActor(), x, y);
// Ignore touch up if the wrong mouse button.
if (touchUpOver && pointer == 0 && this.button != -1 && button != this.button) touchUpOver = false;
if (touchUpOver) {
long time = TimeUtils.nanoTime();
if (time - lastTapTime > tapCountInterval) tapCount = 0;
tapCount++;
lastTapTime = time;
clicked(event, x, y);
}
}
pressed = false;
pressedPointer = -1;
pressedButton = -1;
cancelled = false;
}
}
public void enter (InputEvent event, float x, float y, int pointer, Actor fromActor) {
if (pointer == -1 && !cancelled) over = true;
}
public void exit (InputEvent event, float x, float y, int pointer, Actor toActor) {
if (pointer == -1 && !cancelled) over = false;
}
/** If a touch down is being monitored, the drag and touch up events are ignored until the next touch up. */
public void cancel () {
if (pressedPointer == -1) return;
cancelled = true;
pressed = false;
}
public void clicked (InputEvent event, float x, float y) {
}
/** Returns true if the specified position is over the specified actor or within the tap square. */
public boolean isOver (Actor actor, float x, float y) {
Actor hit = actor.hit(x, y, true);
if (hit == null || !hit.isDescendantOf(actor)) return inTapSquare(x, y);
return true;
}
public boolean inTapSquare (float x, float y) {
if (touchDownX == -1 && touchDownY == -1) return false;
return Math.abs(x - touchDownX) < tapSquareSize && Math.abs(y - touchDownY) < tapSquareSize;
}
/** Returns true if a touch is within the tap square. */
public boolean inTapSquare () {
return touchDownX != -1;
}
/** The tap square will not longer be used for the current touch. */
public void invalidateTapSquare () {
touchDownX = -1;
touchDownY = -1;
}
/** Returns true if a touch is over the actor or within the tap square. */
public boolean isPressed () {
return pressed;
}
/** Returns true if a touch is over the actor or within the tap square or has been very recently. This allows the UI to show a
* press and release that was so fast it occurred within a single frame. */
public boolean isVisualPressed () {
if (pressed) return true;
if (visualPressedTime <= 0) return false;
if (visualPressedTime > TimeUtils.millis()) return true;
visualPressedTime = 0;
return false;
}
/** Returns true if the mouse or touch is over the actor or pressed and within the tap square. */
public boolean isOver () {
return over || pressed;
}
public void setTapSquareSize (float halfTapSquareSize) {
tapSquareSize = halfTapSquareSize;
}
public float getTapSquareSize () {
return tapSquareSize;
}
/** @param tapCountInterval time in seconds that must pass for two touch down/up sequences to be detected as consecutive taps. */
public void setTapCountInterval (float tapCountInterval) {
this.tapCountInterval = (long)(tapCountInterval * 1000000000l);
}
/** Returns the number of taps within the tap count interval for the most recent click event. */
public int getTapCount () {
return tapCount;
}
public void setTapCount (int tapCount) {
this.tapCount = tapCount;
}
public float getTouchDownX () {
return touchDownX;
}
public float getTouchDownY () {
return touchDownY;
}
/** The button that initially pressed this button or -1 if the button is not pressed. */
public int getPressedButton () {
return pressedButton;
}
/** The pointer that initially pressed this button or -1 if the button is not pressed. */
public int getPressedPointer () {
return pressedPointer;
}
/** @see #setButton(int) */
public int getButton () {
return button;
}
/** 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;
}
}