/*
* This file is part of the Illarion project.
*
* Copyright © 2015 - Illarion e.V.
*
* Illarion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Illarion 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 General Public License for more details.
*/
package org.illarion.engine.backend.gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.Input.Buttons;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.InputProcessor;
import illarion.common.util.FastMath;
import org.illarion.engine.backend.shared.AbstractForwardingInput;
import org.illarion.engine.input.Button;
import org.illarion.engine.input.InputListener;
import org.illarion.engine.input.Key;
import org.lwjgl.input.Keyboard;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.awt.*;
import java.util.LinkedList;
import java.util.Queue;
/**
* This is the input system of the libGDX backend.
*
* @author Martin Karing <nitram@illarion.org>
*/
class GdxInput extends AbstractForwardingInput implements InputProcessor {
private static final Logger log = LoggerFactory.getLogger(GdxInput.class);
/**
* This is the ID of the pointer that is the only one used.
*/
private static final int USED_MOUSE_POINTER = 0;
/**
* This variable stores the size of the area the touch down and the touch up has to be,
* in order to accept these events as clicks.
*/
private static final int CLICK_TOLERANCE = 5;
/**
* The time in milliseconds between two clicks to recognise them as a double click.
*/
private final long doubleClickDelay;
/**
* The input listener that receives the input data when the polling function is called.
*/
@Nullable
private InputListener inputListener;
/**
* The events received since the last polling.
*/
@Nonnull
private final Queue<Runnable> events;
/**
* The libGDX input system that provides the updates.
*/
@Nonnull
private final Input gdxInput;
/**
* This variable stores the location where the mouse button was pressed down the last time.
*/
private int lastDragRelevantX;
/**
* This variable stores the location where the mouse button was pressed down the last time.
*/
private int lastDragRelevantY;
/**
* This variable stores the location where the mouse button was pressed down the last time.
*/
private int touchDownX;
/**
* This variable stores the location where the mouse button was pressed down the last time.
*/
private int touchDownY;
/**
* This variable stores the mouse pointer that was pressed last time.
*/
private int touchDownPointer;
/**
* This variable stores the button that was pressed down last time.
*/
@Nullable
private Button touchDownButton;
/**
* The button that was clicked at the first click.
*/
@Nullable
private Button clickButton;
/**
* The timestamp until the timeout for the next double click runs.
*/
private long clickTimeout;
/**
* The storage for the numbers that were typed in while a alt key was pressed.
*/
private char altKeyCode;
/**
* Create a new instance of the libGDX input system.
*
* @param gdxInput the input provider of libGDX that is supposed to be used
*/
GdxInput(@Nonnull Input gdxInput) {
@Nonnull Toolkit awtDefaultToolkit = Toolkit.getDefaultToolkit();
@Nullable Object doubleClick = awtDefaultToolkit.getDesktopProperty("awt.multiClickInterval");
if (doubleClick instanceof Number) {
doubleClickDelay = ((Number) doubleClick).longValue();
} else {
doubleClickDelay = 500L;
}
this.gdxInput = gdxInput;
gdxInput.setInputProcessor(this);
events = new LinkedList<>();
Keyboard.enableRepeatEvents(true);
}
/**
* Convert a libGDX button code to the engine button.
*
* @param button the libGDX button code
* @return the engine button or {@code null} in case the mapping failed
*/
@Nullable
private static Button getEngineButton(int button) {
switch (button) {
case Buttons.LEFT:
return Button.Left;
case Buttons.RIGHT:
return Button.Right;
case Buttons.MIDDLE:
return Button.Middle;
default:
return null;
}
}
/**
* Get the libGDX button code from a engine button.
*
* @param button the button
* @return the libGDX button code or {@code -1} in case the mapping failed
*/
private static int getGdxButton(@Nonnull Button button) {
switch (button) {
case Left:
return Buttons.LEFT;
case Right:
return Buttons.RIGHT;
case Middle:
return Buttons.MIDDLE;
}
return -1;
}
/**
* Convert a libGDX key code to a game engine key.
*
* @param gdxKeyCode the libGDX key code
* @return the game engine key or {@code null} in case the mapping was not possible
*/
@SuppressWarnings("SwitchStatementWithTooManyBranches")
@Nullable
private static Key getEngineKey(int gdxKeyCode) {
switch (gdxKeyCode) {
case Keys.A:
return Key.A;
case Keys.B:
return Key.B;
case Keys.C:
return Key.C;
case Keys.D:
return Key.D;
case Keys.E:
return Key.E;
case Keys.F:
return Key.F;
case Keys.G:
return Key.G;
case Keys.H:
return Key.H;
case Keys.I:
return Key.I;
case Keys.J:
return Key.J;
case Keys.K:
return Key.K;
case Keys.L:
return Key.L;
case Keys.M:
return Key.M;
case Keys.N:
return Key.N;
case Keys.O:
return Key.O;
case Keys.P:
return Key.P;
case Keys.Q:
return Key.Q;
case Keys.R:
return Key.R;
case Keys.S:
return Key.S;
case Keys.T:
return Key.T;
case Keys.U:
return Key.U;
case Keys.V:
return Key.V;
case Keys.W:
return Key.W;
case Keys.X:
return Key.X;
case Keys.Y:
return Key.Y;
case Keys.Z:
return Key.Z;
case Keys.SHIFT_LEFT:
return Key.LeftShift;
case Keys.SHIFT_RIGHT:
return Key.RightShift;
case Keys.ALT_LEFT:
return Key.LeftAlt;
case Keys.ALT_RIGHT:
return Key.RightAlt;
case Keys.CONTROL_LEFT:
return Key.LeftCtrl;
case Keys.CONTROL_RIGHT:
return Key.RightCtrl;
case Keys.LEFT:
return Key.CursorLeft;
case Keys.RIGHT:
return Key.CursorRight;
case Keys.UP:
return Key.CursorUp;
case Keys.DOWN:
return Key.CursorDown;
case Keys.ENTER:
return Key.Enter;
case Keys.BACKSPACE:
return Key.Backspace;
case Keys.SPACE:
return Key.Space;
case Keys.NUMPAD_0:
return Key.NumPad0;
case Keys.NUMPAD_1:
return Key.NumPad1;
case Keys.NUMPAD_2:
return Key.NumPad2;
case Keys.NUMPAD_3:
return Key.NumPad3;
case Keys.NUMPAD_4:
return Key.NumPad4;
case Keys.NUMPAD_5:
return Key.NumPad5;
case Keys.NUMPAD_6:
return Key.NumPad6;
case Keys.NUMPAD_7:
return Key.NumPad7;
case Keys.NUMPAD_8:
return Key.NumPad8;
case Keys.NUMPAD_9:
return Key.NumPad9;
case Keys.NUM:
return Key.NumLock;
case Keys.ESCAPE:
return Key.Escape;
case Keys.F1:
return Key.F1;
case Keys.F2:
return Key.F2;
case Keys.F3:
return Key.F3;
case Keys.F4:
return Key.F4;
case Keys.F5:
return Key.F5;
case Keys.F6:
return Key.F6;
case Keys.F7:
return Key.F7;
case Keys.F8:
return Key.F8;
case Keys.F9:
return Key.F9;
case Keys.F10:
return Key.F10;
case Keys.F11:
return Key.F11;
case Keys.F12:
return Key.F12;
case Keys.INSERT:
return Key.Insert;
case Keys.FORWARD_DEL:
return Key.Delete;
case Keys.HOME:
return Key.Home;
case Keys.END:
return Key.End;
case Keys.PAGE_UP:
return Key.PageUp;
case Keys.PAGE_DOWN:
return Key.PageDown;
case Keys.TAB:
return Key.Tab;
default:
return null;
}
}
/**
* Get the libGDX key code of a engine button.
*
* @param key the engine key
* @return the libGDX key code or {@link Keys#UNKNOWN} in case the mapping fails
*/
@SuppressWarnings("SwitchStatementWithTooManyBranches")
private static int getGdxKey(@Nonnull Key key) {
switch (key) {
case A:
return Keys.A;
case B:
return Keys.B;
case C:
return Keys.C;
case D:
return Keys.D;
case E:
return Keys.E;
case F:
return Keys.F;
case G:
return Keys.G;
case H:
return Keys.H;
case I:
return Keys.I;
case J:
return Keys.J;
case K:
return Keys.K;
case L:
return Keys.L;
case M:
return Keys.M;
case N:
return Keys.N;
case O:
return Keys.O;
case P:
return Keys.P;
case Q:
return Keys.Q;
case R:
return Keys.R;
case S:
return Keys.S;
case T:
return Keys.T;
case U:
return Keys.U;
case V:
return Keys.V;
case W:
return Keys.W;
case X:
return Keys.X;
case Y:
return Keys.Y;
case Z:
return Keys.Z;
case LeftShift:
return Keys.SHIFT_LEFT;
case RightShift:
return Keys.SHIFT_RIGHT;
case LeftAlt:
return Keys.ALT_LEFT;
case RightAlt:
return Keys.ALT_RIGHT;
case LeftCtrl:
return Keys.CONTROL_LEFT;
case RightCtrl:
return Keys.CONTROL_RIGHT;
case CursorLeft:
return Keys.LEFT;
case CursorRight:
return Keys.RIGHT;
case CursorUp:
return Keys.UP;
case CursorDown:
return Keys.DOWN;
case Enter:
return Keys.ENTER;
case Backspace:
return Keys.BACKSPACE;
case Space:
return Keys.SPACE;
case NumPad0:
return Keys.NUMPAD_0;
case NumPad1:
return Keys.NUMPAD_1;
case NumPad2:
return Keys.NUMPAD_2;
case NumPad3:
return Keys.NUMPAD_3;
case NumPad4:
return Keys.NUMPAD_4;
case NumPad5:
return Keys.NUMPAD_5;
case NumPad6:
return Keys.NUMPAD_6;
case NumPad7:
return Keys.NUMPAD_7;
case NumPad8:
return Keys.NUMPAD_8;
case NumPad9:
return Keys.NUMPAD_9;
case NumLock:
return Keys.NUM;
case Escape:
return Keys.ESCAPE;
case F1:
return Keys.F1;
case F2:
return Keys.F2;
case F3:
return Keys.F3;
case F4:
return Keys.F4;
case F5:
return Keys.F5;
case F6:
return Keys.F6;
case F7:
return Keys.F7;
case F8:
return Keys.F8;
case F9:
return Keys.F9;
case F10:
return Keys.F10;
case F11:
return Keys.F11;
case F12:
return Keys.F12;
case Insert:
return Keys.INSERT;
case Delete:
return Keys.FORWARD_DEL;
case Home:
return Keys.HOME;
case End:
return Keys.END;
case PageUp:
return Keys.PAGE_UP;
case PageDown:
return Keys.PAGE_DOWN;
case Tab:
return Keys.TAB;
}
return Keys.UNKNOWN;
}
@Override
public boolean keyDown(int keycode) {
Key pressedKey = getEngineKey(keycode);
if (pressedKey == null) {
log.debug("Received key down with code: {} that failed to translate to a key.", keycode);
return true;
}
if (isAnyKeyDown(Key.LeftAlt, Key.RightAlt) && isNumPadNumber(pressedKey)) {
addKeyToAltKeyCode(pressedKey);
}
log.debug("Received key down with code: {} that translated to key: {}", keycode, pressedKey);
events.offer(() -> {
assert inputListener != null;
inputListener.keyDown(pressedKey);
});
return true;
}
private void addKeyToAltKeyCode(@Nonnull Key key) {
int newNumber;
switch (key) {
case NumPad0:
newNumber = 0;
break;
case NumPad1:
newNumber = 1;
break;
case NumPad2:
newNumber = 2;
break;
case NumPad3:
newNumber = 3;
break;
case NumPad4:
newNumber = 4;
break;
case NumPad5:
newNumber = 5;
break;
case NumPad6:
newNumber = 6;
break;
case NumPad7:
newNumber = 7;
break;
case NumPad8:
newNumber = 8;
break;
case NumPad9:
newNumber = 9;
break;
default:
throw new IllegalArgumentException("Key is not a valid Numpad key: " + key);
}
altKeyCode *= 10;
altKeyCode += newNumber;
}
private boolean isNumPadNumber(@Nonnull Key key) {
return (key == Key.NumPad0) || (key == Key.NumPad1) || (key == Key.NumPad2) || (key == Key.NumPad3) ||
(key == Key.NumPad4) || (key == Key.NumPad5) || (key == Key.NumPad6) || (key == Key.NumPad7) ||
(key == Key.NumPad8) || (key == Key.NumPad9);
}
@Override
public boolean keyUp(int keycode) {
Key releasedKey = getEngineKey(keycode);
if (releasedKey == null) {
log.debug("Received key up with code: {} that failed to translate to a key.", keycode);
return true;
}
log.debug("Received key up with code: {} that translated to key: {}", keycode, releasedKey);
events.offer(() -> {
assert inputListener != null;
inputListener.keyUp(releasedKey);
});
if ((releasedKey == Key.LeftAlt) || (releasedKey == Key.RightAlt)) {
keyTyped(altKeyCode);
altKeyCode = 0;
}
return true;
}
@Override
public boolean keyTyped(char character) {
if (Character.isDefined(character) && (character != 0)) {
log.debug("Received key typed with character: {}", character);
events.offer(() -> {
assert inputListener != null;
inputListener.keyTyped(character);
});
return true;
} else {
return false;
}
}
@Override
public boolean touchDown(int x, int y, int pointer, int button) {
if (pointer != USED_MOUSE_POINTER) {
return false;
}
Button pressedButton = getEngineButton(button);
if (pressedButton == null) {
return true;
}
events.offer(() -> {
assert inputListener != null;
inputListener.buttonDown(x, y, pressedButton);
});
touchDownX = x;
touchDownY = y;
lastDragRelevantX = x;
lastDragRelevantY = y;
touchDownPointer = pointer;
touchDownButton = pressedButton;
return true;
}
@Override
public boolean touchUp(int x, int y, int pointer, int button) {
if (pointer != USED_MOUSE_POINTER) {
return false;
}
Button releasedButton = getEngineButton(button);
if (releasedButton == null) {
return true;
}
if ((touchDownButton == releasedButton) && (touchDownPointer == pointer) &&
(FastMath.abs(touchDownX - x) < CLICK_TOLERANCE) && (FastMath.abs(touchDownY - y) < CLICK_TOLERANCE)) {
publishClick(x, y, releasedButton);
}
events.offer(() -> {
assert inputListener != null;
inputListener.buttonUp(x, y, releasedButton);
});
return true;
}
/**
* Publish the event as mouse click event. This function also handles double clicks.
*
* @param x the x coordinate where the click happened
* @param y the y coordinate where the click happened
* @param button the button that was clicked
*/
private void publishClick(int x, int y, @Nonnull Button button) {
if ((clickTimeout == 0) || (clickButton != button) || (System.currentTimeMillis() > clickTimeout)) {
clickButton = button;
clickTimeout = System.currentTimeMillis() + doubleClickDelay;
events.offer(() -> {
assert inputListener != null;
inputListener.buttonClicked(x, y, button, 1);
});
} else {
clickTimeout = 0;
events.offer(() -> {
assert inputListener != null;
inputListener.buttonClicked(x, y, button, 2);
});
}
}
@Override
public boolean touchDragged(int x, int y, int pointer) {
if (pointer != USED_MOUSE_POINTER) {
return false;
}
int startX = lastDragRelevantX;
int startY = lastDragRelevantY;
lastDragRelevantX = x;
lastDragRelevantY = y;
for (@Nonnull Button button : Button.values()) {
if (isButtonDown(button)) {
events.offer(() -> {
assert inputListener != null;
inputListener.mouseDragged(button, startX, startY, x, y);
});
}
}
return true;
}
@Override
public boolean mouseMoved(int x, int y) {
events.offer(() -> {
assert inputListener != null;
inputListener.mouseMoved(x, y);
});
return true;
}
@Override
public boolean scrolled(int amount) {
events.offer(() -> {
assert inputListener != null;
inputListener.mouseWheelMoved(getMouseX(), getMouseY(), -amount);
});
return true;
}
@Override
public void poll() {
@Nullable Runnable task = events.poll();
while (task != null) {
task.run();
task = events.poll();
}
}
@Override
public void setListener(@Nonnull InputListener listener) {
inputListener = listener;
}
@Override
public boolean isButtonDown(@Nonnull Button button) {
int buttonCode = getGdxButton(button);
if (buttonCode == -1) {
return false;
}
return gdxInput.isButtonPressed(buttonCode);
}
@Override
public boolean isKeyDown(@Nonnull Key key) {
return gdxInput.isKeyPressed(getGdxKey(key));
}
@Override
public boolean isAnyButtonDown() {
return isAnyButtonDown(Button.values());
}
@Override
public boolean isAnyButtonDown(@Nonnull Button... buttons) {
for (@Nonnull Button button : buttons) {
if (isButtonDown(button)) {
return true;
}
}
return false;
}
@Override
public boolean isAnyKeyDown() {
return isAnyKeyDown(Key.values());
}
@Override
public boolean isAnyKeyDown(@Nonnull Key... keys) {
for (@Nonnull Key key : keys) {
if (isKeyDown(key)) {
return true;
}
}
return false;
}
@Override
public int getMouseX() {
return gdxInput.getX(USED_MOUSE_POINTER);
}
@Override
public int getMouseY() {
return gdxInput.getY(USED_MOUSE_POINTER);
}
@Override
public void setMouseLocation(int x, int y) {
gdxInput.setCursorPosition(x, y);
}
}