diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 5bb384d26b..657b410505 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1 @@ -open_collective: pojavlauncher patreon: pojavlauncher diff --git a/app_pojavlauncher/build.gradle b/app_pojavlauncher/build.gradle index d7104fca8d..d19a53d646 100644 --- a/app_pojavlauncher/build.gradle +++ b/app_pojavlauncher/build.gradle @@ -90,7 +90,7 @@ configurations { android { namespace 'net.kdt.pojavlaunch' - compileSdk = 33 + compileSdk = 34 lintOptions { abortOnError false @@ -114,7 +114,7 @@ android { defaultConfig { applicationId "net.kdt.pojavlaunch" minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode getDateSeconds() versionName getVersionName() multiDexEnabled true //important diff --git a/app_pojavlauncher/src/main/AndroidManifest.xml b/app_pojavlauncher/src/main/AndroidManifest.xml index 4147e1f3a7..98628756a7 100644 --- a/app_pojavlauncher/src/main/AndroidManifest.xml +++ b/app_pojavlauncher/src/main/AndroidManifest.xml @@ -18,6 +18,8 @@ + + - - + + + + diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/EfficientAndroidLWJGLKeycode.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/EfficientAndroidLWJGLKeycode.java index c2ff62248f..5fce0dcbc4 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/EfficientAndroidLWJGLKeycode.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/EfficientAndroidLWJGLKeycode.java @@ -198,7 +198,7 @@ public static void execKeyIndex(int index){ sendKeyPress(getValueByIndex(index)); } - public static int getValueByIndex(int index) { + public static short getValueByIndex(int index) { return sLwjglKeycodes[index]; } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java index 7ea6225126..3c4a26f0ee 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java @@ -27,6 +27,7 @@ import androidx.annotation.RequiresApi; import net.kdt.pojavlaunch.customcontrols.ControlLayout; +import net.kdt.pojavlaunch.customcontrols.gamepad.DefaultDataProvider; import net.kdt.pojavlaunch.customcontrols.gamepad.Gamepad; import net.kdt.pojavlaunch.customcontrols.mouse.AbstractTouchpad; import net.kdt.pojavlaunch.customcontrols.mouse.AndroidPointerCapture; @@ -202,6 +203,10 @@ public boolean onTouchEvent(MotionEvent e) { return mCurrentTouchProcessor.processTouchEvent(e); } + private void createGamepad(View contextView, InputDevice inputDevice) { + mGamepad = new Gamepad(contextView, inputDevice, DefaultDataProvider.INSTANCE, true); + } + /** * The event for mouse/joystick movements */ @@ -211,9 +216,7 @@ public boolean dispatchGenericMotionEvent(MotionEvent event) { int mouseCursorIndex = -1; if(Gamepad.isGamepadEvent(event)){ - if(mGamepad == null){ - mGamepad = new Gamepad(this, event.getDevice()); - } + if(mGamepad == null) createGamepad(this, event.getDevice()); mInputManager.handleMotionEventInput(getContext(), event, mGamepad); return true; @@ -285,9 +288,7 @@ public boolean processKeyEvent(KeyEvent event) { } if(Gamepad.isGamepadEvent(event)){ - if(mGamepad == null){ - mGamepad = new Gamepad(this, event.getDevice()); - } + if(mGamepad == null) createGamepad(this, event.getDevice()); mInputManager.handleKeyEventInput(getContext(), event, mGamepad); return true; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/DefaultDataProvider.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/DefaultDataProvider.java new file mode 100644 index 0000000000..f16c97115e --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/DefaultDataProvider.java @@ -0,0 +1,33 @@ +package net.kdt.pojavlaunch.customcontrols.gamepad; + +import net.kdt.pojavlaunch.GrabListener; + +import org.lwjgl.glfw.CallbackBridge; + +public class DefaultDataProvider implements GamepadDataProvider { + public static final DefaultDataProvider INSTANCE = new DefaultDataProvider(); + + // Cannot instantiate this class publicly + private DefaultDataProvider() {} + + @Override + public GamepadMap getGameMap() { + return GamepadMapStore.getGameMap(); + } + + + @Override + public GamepadMap getMenuMap() { + return GamepadMapStore.getMenuMap(); + } + + @Override + public boolean isGrabbing() { + return CallbackBridge.isGrabbing(); + } + + @Override + public void attachGrabListener(GrabListener grabListener) { + CallbackBridge.addGrabListener(grabListener); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/Gamepad.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/Gamepad.java index dad6a443fd..4d3a7b135a 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/Gamepad.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/Gamepad.java @@ -19,7 +19,6 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; -import android.widget.Toast; import androidx.core.content.res.ResourcesCompat; import androidx.core.math.MathUtils; @@ -27,7 +26,6 @@ import net.kdt.pojavlaunch.GrabListener; import net.kdt.pojavlaunch.LwjglGlfwKeycode; import net.kdt.pojavlaunch.R; -import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.prefs.LauncherPreferences; import net.kdt.pojavlaunch.utils.MCOptionUtils; @@ -75,12 +73,11 @@ public class Gamepad implements GrabListener, GamepadHandler { private double mMouseAngle; private double mMouseSensitivity = 19; - private final GamepadMap mGameMap = GamepadMap.getDefaultGameMap(); - private final GamepadMap mMenuMap = GamepadMap.getDefaultMenuMap(); - private GamepadMap mCurrentMap = mGameMap; + private GamepadMap mGameMap; + private GamepadMap mMenuMap; + private GamepadMap mCurrentMap; - // The negation is to force trigger the onGrabState - private boolean isGrabbing = !CallbackBridge.isGrabbing(); + private boolean isGrabbing; /* Choreographer with time to compute delta on ticking */ @@ -91,7 +88,9 @@ public class Gamepad implements GrabListener, GamepadHandler { @SuppressWarnings("FieldCanBeLocal") //the field is used in a WeakReference private final MCOptionUtils.MCOptionListener mGuiScaleListener = () -> notifyGUISizeChange(getMcScale()); - public Gamepad(View contextView, InputDevice inputDevice){ + private final GamepadDataProvider mMapProvider; + + public Gamepad(View contextView, InputDevice inputDevice, GamepadDataProvider mapProvider, boolean showCursor){ Settings.setDeadzoneScale(PREF_DEADZONE_SCALE); mScreenChoreographer = Choreographer.getInstance(); @@ -120,16 +119,34 @@ public void doFrame(long frameTimeNanos) { int size = (int) ((22 * getMcScale()) / mScaleFactor); mPointerImageView.setLayoutParams(new FrameLayout.LayoutParams(size, size)); + mMapProvider = mapProvider; + CallbackBridge.sendCursorPos(CallbackBridge.windowWidth/2f, CallbackBridge.windowHeight/2f); - ((ViewGroup)contextView.getParent()).addView(mPointerImageView); + + if(showCursor) { + ((ViewGroup)contextView.getParent()).addView(mPointerImageView); + } + placePointerView(CallbackBridge.physicalWidth/2, CallbackBridge.physicalHeight/2); - CallbackBridge.addGrabListener(this); + reloadGamepadMaps(); + mMapProvider.attachGrabListener(this); } - + public void reloadGamepadMaps() { + if(mGameMap != null) mGameMap.resetPressedState(); + if(mMenuMap != null) mMenuMap.resetPressedState(); + GamepadMapStore.load(); + mGameMap = mMapProvider.getGameMap(); + mMenuMap = mMapProvider.getMenuMap(); + mCurrentMap = mGameMap; + // Force state refresh + boolean currentGrab = CallbackBridge.isGrabbing(); + isGrabbing = !currentGrab; + onGrabState(currentGrab); + } public void updateJoysticks(){ updateDirectionalJoystick(); @@ -144,8 +161,8 @@ public void notifyGUISizeChange(int newSize){ } - public static void sendInput(int[] keycodes, boolean isDown){ - for(int keycode : keycodes){ + public static void sendInput(short[] keycodes, boolean isDown){ + for(short keycode : keycodes){ switch (keycode){ case GamepadMap.MOUSE_SCROLL_DOWN: if(isDown) CallbackBridge.sendScroll(0, -1); @@ -153,20 +170,23 @@ public static void sendInput(int[] keycodes, boolean isDown){ case GamepadMap.MOUSE_SCROLL_UP: if(isDown) CallbackBridge.sendScroll(0, 1); break; - - case LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT: + case GamepadMap.MOUSE_LEFT: + sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT, isDown); + break; + case GamepadMap.MOUSE_MIDDLE: + sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_MIDDLE, isDown); + break; + case GamepadMap.MOUSE_RIGHT: sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, isDown); break; - case LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT: - sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT, isDown); + case GamepadMap.UNSPECIFIED: break; - default: sendKeyPress(keycode, CallbackBridge.getCurrentMods(), isDown); + CallbackBridge.setModifiers(keycode, isDown); break; } - CallbackBridge.setModifiers(keycode, isDown); } } @@ -261,32 +281,32 @@ private GamepadMap getCurrentMap(){ private static void sendDirectionalKeycode(int direction, boolean isDown, GamepadMap map){ switch (direction){ case DIRECTION_NORTH: - sendInput(map.DIRECTION_FORWARD, isDown); + map.DIRECTION_FORWARD.update(isDown); break; case DIRECTION_NORTH_EAST: - sendInput(map.DIRECTION_FORWARD, isDown); - sendInput(map.DIRECTION_RIGHT, isDown); + map.DIRECTION_FORWARD.update(isDown); + map.DIRECTION_RIGHT.update(isDown); break; case DIRECTION_EAST: - sendInput(map.DIRECTION_RIGHT, isDown); + map.DIRECTION_RIGHT.update(isDown); break; case DIRECTION_SOUTH_EAST: - sendInput(map.DIRECTION_RIGHT, isDown); - sendInput(map.DIRECTION_BACKWARD, isDown); + map.DIRECTION_RIGHT.update(isDown); + map.DIRECTION_BACKWARD.update(isDown); break; case DIRECTION_SOUTH: - sendInput(map.DIRECTION_BACKWARD, isDown); + map.DIRECTION_BACKWARD.update(isDown); break; case DIRECTION_SOUTH_WEST: - sendInput(map.DIRECTION_BACKWARD, isDown); - sendInput(map.DIRECTION_LEFT, isDown); + map.DIRECTION_BACKWARD.update(isDown); + map.DIRECTION_LEFT.update(isDown); break; case DIRECTION_WEST: - sendInput(map.DIRECTION_LEFT, isDown); + map.DIRECTION_LEFT.update(isDown); break; case DIRECTION_NORTH_WEST: - sendInput(map.DIRECTION_FORWARD, isDown); - sendInput(map.DIRECTION_LEFT, isDown); + map.DIRECTION_FORWARD.update(isDown); + map.DIRECTION_LEFT.update(isDown); break; } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadButton.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadButton.java index a6c7cc1e79..4d2d9aaba5 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadButton.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadButton.java @@ -1,46 +1,30 @@ package net.kdt.pojavlaunch.customcontrols.gamepad; -import android.view.KeyEvent; - /** - * Simple button able to store its state and some properties + * This class corresponds to a button that does exist on the gamepad */ -public class GamepadButton { - - public int[] keycodes; +public class GamepadButton extends GamepadEmulatedButton { public boolean isToggleable = false; - private boolean mIsDown = false; private boolean mIsToggled = false; - public void update(KeyEvent event){ - boolean isKeyDown = (event.getAction() == KeyEvent.ACTION_DOWN); - update(isKeyDown); - } - public void update(boolean isKeyDown){ - if(isKeyDown != mIsDown){ - mIsDown = isKeyDown; - if(isToggleable){ - if(isKeyDown){ - mIsToggled = !mIsToggled; - Gamepad.sendInput(keycodes, mIsToggled); - } - return; - } - Gamepad.sendInput(keycodes, mIsDown); + @Override + protected void onDownStateChanged(boolean isDown) { + if(isToggleable && isDown){ + mIsToggled = !mIsToggled; + Gamepad.sendInput(keycodes, mIsToggled); + return; } + super.onDownStateChanged(isDown); } - public void resetButtonState(){ - if(mIsDown || mIsToggled){ + @Override + public void resetButtonState() { + if(!mIsDown && mIsToggled) { Gamepad.sendInput(keycodes, false); + mIsToggled = false; + } else { + super.resetButtonState(); } - mIsDown = false; - mIsToggled = false; } - - public boolean isDown(){ - return isToggleable ? mIsToggled : mIsDown; - } - } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadDataProvider.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadDataProvider.java new file mode 100644 index 0000000000..37dcbaab5d --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadDataProvider.java @@ -0,0 +1,10 @@ +package net.kdt.pojavlaunch.customcontrols.gamepad; + +import net.kdt.pojavlaunch.GrabListener; + +public interface GamepadDataProvider { + GamepadMap getMenuMap(); + GamepadMap getGameMap(); + boolean isGrabbing(); + void attachGrabListener(GrabListener grabListener); +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadEmulatedButton.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadEmulatedButton.java new file mode 100644 index 0000000000..a05095b80a --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadEmulatedButton.java @@ -0,0 +1,33 @@ +package net.kdt.pojavlaunch.customcontrols.gamepad; + +import android.view.KeyEvent; + +/** + * This class corresponds to a button that does not physically exist on the gamepad, but is + * emulated from other inputs on it (like WASD directional keys) + */ +public class GamepadEmulatedButton { + public short[] keycodes; + protected boolean mIsDown = false; + + public void update(KeyEvent event) { + boolean isKeyDown = (event.getAction() == KeyEvent.ACTION_DOWN); + update(isKeyDown); + } + + public void update(boolean isKeyDown){ + if(isKeyDown != mIsDown){ + mIsDown = isKeyDown; + onDownStateChanged(mIsDown); + } + } + + public void resetButtonState() { + if(mIsDown) Gamepad.sendInput(keycodes, false); + mIsDown = false; + } + + protected void onDownStateChanged(boolean isDown) { + Gamepad.sendInput(keycodes, mIsDown); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadMap.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadMap.java index 4d7fbb1c15..c18c5dc38e 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadMap.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadMap.java @@ -4,8 +4,16 @@ public class GamepadMap { - public static final int MOUSE_SCROLL_DOWN = -1; - public static final int MOUSE_SCROLL_UP = -2; + + public static final short MOUSE_SCROLL_DOWN = -1; + public static final short MOUSE_SCROLL_UP = -2; + // Made mouse keycodes their own specials because managing special keycodes above 0 + // proved to be complicated + public static final short MOUSE_LEFT = -3; + public static final short MOUSE_MIDDLE = -4; + public static final short MOUSE_RIGHT = -5; + // Workaround, because GLFW_KEY_UNKNOWN and GLFW_MOUSE_BUTTON_LEFT are both 0 + public static final short UNSPECIFIED = -6; /* This class is just here to store the mapping @@ -14,33 +22,11 @@ public class GamepadMap { Be warned, you should define ALL keys if you want to avoid a non defined exception */ - public final GamepadButton BUTTON_A = new GamepadButton(); - public final GamepadButton BUTTON_B = new GamepadButton(); - public final GamepadButton BUTTON_X = new GamepadButton(); - public final GamepadButton BUTTON_Y = new GamepadButton(); - - public final GamepadButton BUTTON_START = new GamepadButton(); - public final GamepadButton BUTTON_SELECT = new GamepadButton(); - - public final GamepadButton TRIGGER_RIGHT = new GamepadButton(); //R2 - public final GamepadButton TRIGGER_LEFT = new GamepadButton(); //L2 - - public final GamepadButton SHOULDER_RIGHT = new GamepadButton(); //R1 - public final GamepadButton SHOULDER_LEFT = new GamepadButton(); //L1 - - public int[] DIRECTION_FORWARD; - public int[] DIRECTION_BACKWARD; - public int[] DIRECTION_RIGHT; - public int[] DIRECTION_LEFT; - - public final GamepadButton THUMBSTICK_RIGHT = new GamepadButton(); //R3 - public final GamepadButton THUMBSTICK_LEFT = new GamepadButton(); //L3 - - public final GamepadButton DPAD_UP = new GamepadButton(); - public final GamepadButton DPAD_RIGHT = new GamepadButton(); - public final GamepadButton DPAD_DOWN = new GamepadButton(); - public final GamepadButton DPAD_LEFT = new GamepadButton(); + public GamepadButton BUTTON_A, BUTTON_B, BUTTON_X, BUTTON_Y, BUTTON_START, BUTTON_SELECT, + TRIGGER_RIGHT, TRIGGER_LEFT, SHOULDER_RIGHT, SHOULDER_LEFT, THUMBSTICK_RIGHT, + THUMBSTICK_LEFT, DPAD_UP, DPAD_DOWN, DPAD_RIGHT, DPAD_LEFT; + public GamepadEmulatedButton DIRECTION_FORWARD, DIRECTION_BACKWARD, DIRECTION_RIGHT, DIRECTION_LEFT; /* * Sets all buttons to a not pressed state, sending an input if needed @@ -70,39 +56,70 @@ public void resetPressedState(){ } + private static GamepadMap createAndInitializeButtons() { + GamepadMap gamepadMap = new GamepadMap(); + gamepadMap.BUTTON_A = new GamepadButton(); + gamepadMap.BUTTON_B = new GamepadButton(); + gamepadMap.BUTTON_X = new GamepadButton(); + gamepadMap.BUTTON_Y = new GamepadButton(); + + gamepadMap.BUTTON_START = new GamepadButton(); + gamepadMap.BUTTON_SELECT = new GamepadButton(); + + gamepadMap.TRIGGER_RIGHT = new GamepadButton(); + gamepadMap.TRIGGER_LEFT = new GamepadButton(); + + gamepadMap.SHOULDER_RIGHT = new GamepadButton(); + gamepadMap.SHOULDER_LEFT = new GamepadButton(); + + gamepadMap.DIRECTION_FORWARD = new GamepadEmulatedButton(); + gamepadMap.DIRECTION_BACKWARD = new GamepadEmulatedButton(); + gamepadMap.DIRECTION_RIGHT = new GamepadEmulatedButton(); + gamepadMap.DIRECTION_LEFT = new GamepadEmulatedButton(); + + gamepadMap.THUMBSTICK_RIGHT = new GamepadButton(); + gamepadMap.THUMBSTICK_LEFT = new GamepadButton(); + + gamepadMap.DPAD_UP = new GamepadButton(); + gamepadMap.DPAD_RIGHT = new GamepadButton(); + gamepadMap.DPAD_DOWN = new GamepadButton(); + gamepadMap.DPAD_LEFT = new GamepadButton(); + return gamepadMap; + } + /* * Returns a pre-done mapping used when the mouse is grabbed by the game. */ public static GamepadMap getDefaultGameMap(){ - GamepadMap gameMap = new GamepadMap(); + GamepadMap gameMap = GamepadMap.createEmptyMap(); - gameMap.BUTTON_A.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_SPACE}; - gameMap.BUTTON_B.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_Q}; - gameMap.BUTTON_X.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_E}; - gameMap.BUTTON_Y.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_F}; + gameMap.BUTTON_A.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_SPACE; + gameMap.BUTTON_B.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_Q; + gameMap.BUTTON_X.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_E; + gameMap.BUTTON_Y.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_F; - gameMap.DIRECTION_FORWARD = new int[]{LwjglGlfwKeycode.GLFW_KEY_W}; - gameMap.DIRECTION_BACKWARD = new int[]{LwjglGlfwKeycode.GLFW_KEY_S}; - gameMap.DIRECTION_RIGHT = new int[]{LwjglGlfwKeycode.GLFW_KEY_D}; - gameMap.DIRECTION_LEFT = new int[]{LwjglGlfwKeycode.GLFW_KEY_A}; + gameMap.DIRECTION_FORWARD.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_W; + gameMap.DIRECTION_BACKWARD.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_S; + gameMap.DIRECTION_RIGHT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_D; + gameMap.DIRECTION_LEFT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_A; - gameMap.DPAD_UP.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_LEFT_SHIFT}; - gameMap.DPAD_DOWN.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_O}; //For mods ? - gameMap.DPAD_RIGHT.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_K}; //For mods ? - gameMap.DPAD_LEFT.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_J}; //For mods ? + gameMap.DPAD_UP.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_LEFT_SHIFT; + gameMap.DPAD_DOWN.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_O; //For mods ? + gameMap.DPAD_RIGHT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_K; //For mods ? + gameMap.DPAD_LEFT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_J; //For mods ? - gameMap.SHOULDER_LEFT.keycodes = new int[]{GamepadMap.MOUSE_SCROLL_UP}; - gameMap.SHOULDER_RIGHT.keycodes = new int[]{GamepadMap.MOUSE_SCROLL_DOWN}; + gameMap.SHOULDER_LEFT.keycodes[0] = GamepadMap.MOUSE_SCROLL_UP; + gameMap.SHOULDER_RIGHT.keycodes[0] = GamepadMap.MOUSE_SCROLL_DOWN; - gameMap.TRIGGER_LEFT.keycodes = new int[]{LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT}; - gameMap.TRIGGER_RIGHT.keycodes = new int[]{LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT}; + gameMap.TRIGGER_LEFT.keycodes[0] = GamepadMap.MOUSE_RIGHT; + gameMap.TRIGGER_RIGHT.keycodes[0] = GamepadMap.MOUSE_LEFT; - gameMap.THUMBSTICK_LEFT.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_LEFT_CONTROL}; - gameMap.THUMBSTICK_RIGHT.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_LEFT_SHIFT}; + gameMap.THUMBSTICK_LEFT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_LEFT_CONTROL; + gameMap.THUMBSTICK_RIGHT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_LEFT_SHIFT; gameMap.THUMBSTICK_RIGHT.isToggleable = true; - gameMap.BUTTON_START.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_ESCAPE}; - gameMap.BUTTON_SELECT.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_TAB}; + gameMap.BUTTON_START.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_ESCAPE; + gameMap.BUTTON_SELECT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_TAB; return gameMap; } @@ -111,64 +128,63 @@ public static GamepadMap getDefaultGameMap(){ * Returns a pre-done mapping used when the mouse is NOT grabbed by the game. */ public static GamepadMap getDefaultMenuMap(){ - GamepadMap menuMap = new GamepadMap(); - - menuMap.BUTTON_A.keycodes = new int[]{LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT}; - menuMap.BUTTON_B.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_ESCAPE}; - menuMap.BUTTON_X.keycodes = new int[]{LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT}; - menuMap.BUTTON_Y.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_LEFT_SHIFT, LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT}; //Oops, doesn't work since left shift isn't properly applied. - - menuMap.DIRECTION_FORWARD = new int[]{GamepadMap.MOUSE_SCROLL_UP, GamepadMap.MOUSE_SCROLL_UP, GamepadMap.MOUSE_SCROLL_UP, GamepadMap.MOUSE_SCROLL_UP, GamepadMap.MOUSE_SCROLL_UP}; - menuMap.DIRECTION_BACKWARD = new int[]{GamepadMap.MOUSE_SCROLL_DOWN, GamepadMap.MOUSE_SCROLL_DOWN, GamepadMap.MOUSE_SCROLL_DOWN, GamepadMap.MOUSE_SCROLL_DOWN, GamepadMap.MOUSE_SCROLL_DOWN}; - menuMap.DIRECTION_RIGHT = new int[]{}; - menuMap.DIRECTION_LEFT = new int[]{}; - - menuMap.DPAD_UP.keycodes = new int[]{}; - menuMap.DPAD_DOWN.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_O}; //For mods ? - menuMap.DPAD_RIGHT.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_K}; //For mods ? - menuMap.DPAD_LEFT.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_J}; //For mods ? - - menuMap.SHOULDER_LEFT.keycodes = new int[]{GamepadMap.MOUSE_SCROLL_UP}; - menuMap.SHOULDER_RIGHT.keycodes = new int[]{GamepadMap.MOUSE_SCROLL_DOWN}; - - menuMap.TRIGGER_LEFT.keycodes = new int[]{}; - menuMap.TRIGGER_RIGHT.keycodes = new int[]{}; - - menuMap.THUMBSTICK_LEFT.keycodes = new int[]{}; - menuMap.THUMBSTICK_RIGHT.keycodes = new int[]{}; - - menuMap.BUTTON_START.keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_ESCAPE}; - menuMap.BUTTON_SELECT.keycodes = new int[]{}; + GamepadMap menuMap = GamepadMap.createEmptyMap(); + + menuMap.BUTTON_A.keycodes[0] = GamepadMap.MOUSE_LEFT; + menuMap.BUTTON_B.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_ESCAPE; + menuMap.BUTTON_X.keycodes[0] = GamepadMap.MOUSE_RIGHT; + { + short[] keycodes = menuMap.BUTTON_Y.keycodes; + keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_LEFT_SHIFT; + keycodes[1] = GamepadMap.MOUSE_RIGHT; + } + + { + short[] keycodes = menuMap.DIRECTION_FORWARD.keycodes; + keycodes[0] = keycodes[1] = keycodes[2] = keycodes[3] = GamepadMap.MOUSE_SCROLL_UP; + } + { + short[] keycodes = menuMap.DIRECTION_BACKWARD.keycodes; + keycodes[0] = keycodes[1] = keycodes[2] = keycodes[3] = GamepadMap.MOUSE_SCROLL_DOWN; + } + + menuMap.DPAD_DOWN.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_O; //For mods ? + menuMap.DPAD_RIGHT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_K; //For mods ? + menuMap.DPAD_LEFT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_J; //For mods ? + + menuMap.SHOULDER_LEFT.keycodes[0] = GamepadMap.MOUSE_SCROLL_UP; + menuMap.SHOULDER_RIGHT.keycodes[0] = GamepadMap.MOUSE_SCROLL_DOWN; + + menuMap.BUTTON_SELECT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_ESCAPE; return menuMap; } /* - * Returns all GamepadButtons, does not include directional keys + * Returns all GamepadEmulatedButtons of the controller key map. */ - public GamepadButton[] getButtons(){ - return new GamepadButton[]{ BUTTON_A, BUTTON_B, BUTTON_X, BUTTON_Y, + public GamepadEmulatedButton[] getButtons(){ + return new GamepadEmulatedButton[]{ BUTTON_A, BUTTON_B, BUTTON_X, BUTTON_Y, BUTTON_SELECT, BUTTON_START, TRIGGER_LEFT, TRIGGER_RIGHT, SHOULDER_LEFT, SHOULDER_RIGHT, THUMBSTICK_LEFT, THUMBSTICK_RIGHT, - DPAD_UP, DPAD_RIGHT, DPAD_DOWN, DPAD_LEFT}; + DPAD_UP, DPAD_RIGHT, DPAD_DOWN, DPAD_LEFT, + DIRECTION_FORWARD, DIRECTION_BACKWARD, + DIRECTION_LEFT, DIRECTION_RIGHT}; } /* * Returns an pre-initialized GamepadMap with only empty keycodes */ - @SuppressWarnings("unused") public static GamepadMap getEmptyMap(){ - GamepadMap emptyMap = new GamepadMap(); - for(GamepadButton button : emptyMap.getButtons()) - button.keycodes = new int[]{}; - - emptyMap.DIRECTION_LEFT = new int[]{}; - emptyMap.DIRECTION_FORWARD = new int[]{}; - emptyMap.DIRECTION_RIGHT = new int[]{}; - emptyMap.DIRECTION_BACKWARD = new int[]{}; - + @SuppressWarnings("unused") public static GamepadMap createEmptyMap(){ + GamepadMap emptyMap = createAndInitializeButtons(); + for(GamepadEmulatedButton button : emptyMap.getButtons()) + button.keycodes = new short[] {UNSPECIFIED, UNSPECIFIED, UNSPECIFIED, UNSPECIFIED}; return emptyMap; } + public static String[] getSpecialKeycodeNames() { + return new String[] {"UNSPECIFIED", "MOUSE_RIGHT", "MOUSE_MIDDLE", "MOUSE_LEFT", "SCROLL_UP", "SCROLL_DOWN"}; + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadMapStore.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadMapStore.java new file mode 100644 index 0000000000..85af2404cc --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadMapStore.java @@ -0,0 +1,60 @@ +package net.kdt.pojavlaunch.customcontrols.gamepad; + +import android.util.Log; + +import com.google.gson.JsonParseException; + +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.utils.FileUtils; + +import java.io.File; +import java.io.IOException; + +public class GamepadMapStore { + private static final File STORE_FILE = new File(Tools.DIR_DATA, "gamepad_map.json"); + private static GamepadMapStore sMapStore; + private GamepadMap mInMenuMap; + private GamepadMap mInGameMap; + private static GamepadMapStore createDefault() { + GamepadMapStore mapStore = new GamepadMapStore(); + mapStore.mInGameMap = GamepadMap.getDefaultGameMap(); + mapStore.mInMenuMap = GamepadMap.getDefaultMenuMap(); + return mapStore; + } + + private static void loadIfNecessary() { + if(sMapStore == null) return; + load(); + } + + public static void load() { + GamepadMapStore mapStore = null; + if(STORE_FILE.exists() && STORE_FILE.canRead()) { + try { + String storeFileContent = Tools.read(STORE_FILE); + mapStore = Tools.GLOBAL_GSON.fromJson(storeFileContent, GamepadMapStore.class); + } catch (JsonParseException | IOException e) { + Log.w("GamepadMapStore", "Map store failed to load!", e); + } + } + if(mapStore == null) mapStore = createDefault(); + sMapStore = mapStore; + } + + public static void save() throws IOException { + if(sMapStore == null) throw new RuntimeException("Must load map store first!"); + FileUtils.ensureParentDirectory(STORE_FILE); + String jsonData = Tools.GLOBAL_GSON.toJson(sMapStore); + Tools.write(STORE_FILE.getAbsolutePath(), jsonData); + } + + public static GamepadMap getGameMap() { + loadIfNecessary(); + return sMapStore.mInGameMap; + } + + public static GamepadMap getMenuMap() { + loadIfNecessary(); + return sMapStore.mInMenuMap; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadMapperAdapter.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadMapperAdapter.java new file mode 100644 index 0000000000..e590bd3a44 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/GamepadMapperAdapter.java @@ -0,0 +1,313 @@ +package net.kdt.pojavlaunch.customcontrols.gamepad; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.Spinner; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import net.kdt.pojavlaunch.EfficientAndroidLWJGLKeycode; +import net.kdt.pojavlaunch.GrabListener; +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; + +import android.widget.TextView; + +public class GamepadMapperAdapter extends RecyclerView.Adapter implements GamepadDataProvider { + private static final int BUTTON_COUNT = 20; + + private GamepadMap mSimulatedGamepadMap; + private RebinderButton[] mRebinderButtons; + private GamepadEmulatedButton[] mRealButtons; + private final ArrayAdapter mKeyAdapter; + private final int mSpecialKeycodeCount; + private GrabListener mGamepadGrabListener; + private boolean mGrabState = false; + private boolean mOldState = false; + + public GamepadMapperAdapter(Context context) { + GamepadMapStore.load(); + mKeyAdapter = new ArrayAdapter<>(context, R.layout.item_centered_textview_large); + String[] specialKeycodeNames = GamepadMap.getSpecialKeycodeNames(); + mSpecialKeycodeCount = specialKeycodeNames.length; + mKeyAdapter.addAll(specialKeycodeNames); + mKeyAdapter.addAll(EfficientAndroidLWJGLKeycode.generateKeyName()); + createRebinderMap(); + updateRealButtons(); + } + + private void createRebinderMap() { + mRebinderButtons = new RebinderButton[BUTTON_COUNT]; + mRealButtons = new GamepadEmulatedButton[BUTTON_COUNT]; + mSimulatedGamepadMap = new GamepadMap(); + int index = 0; + mSimulatedGamepadMap.BUTTON_A = mRebinderButtons[index++] = new RebinderButton(R.drawable.button_a, R.string.controller_button_a); + mSimulatedGamepadMap.BUTTON_B = mRebinderButtons[index++] = new RebinderButton(R.drawable.button_b, R.string.controller_button_b); + mSimulatedGamepadMap.BUTTON_X = mRebinderButtons[index++] = new RebinderButton(R.drawable.button_x, R.string.controller_button_x); + mSimulatedGamepadMap.BUTTON_Y = mRebinderButtons[index++] = new RebinderButton(R.drawable.button_y, R.string.controller_button_y); + mSimulatedGamepadMap.BUTTON_START = mRebinderButtons[index++] = new RebinderButton(R.drawable.button_start, R.string.controller_button_start); + mSimulatedGamepadMap.BUTTON_SELECT = mRebinderButtons[index++] = new RebinderButton(R.drawable.button_select, R.string.controller_button_select); + mSimulatedGamepadMap.TRIGGER_RIGHT = mRebinderButtons[index++] = new RebinderButton(R.drawable.trigger_right, R.string.controller_button_trigger_right); + mSimulatedGamepadMap.TRIGGER_LEFT = mRebinderButtons[index++] = new RebinderButton(R.drawable.trigger_left, R.string.controller_button_trigger_left); + mSimulatedGamepadMap.SHOULDER_RIGHT = mRebinderButtons[index++] = new RebinderButton(R.drawable.shoulder_right, R.string.controller_button_shoulder_right); + mSimulatedGamepadMap.SHOULDER_LEFT = mRebinderButtons[index++] = new RebinderButton(R.drawable.shoulder_left, R.string.controller_button_shoulder_left); + mSimulatedGamepadMap.DIRECTION_FORWARD = mRebinderButtons[index++] = new RebinderButton(R.drawable.stick_right, R.string.controller_direction_forward); + mSimulatedGamepadMap.DIRECTION_RIGHT = mRebinderButtons[index++] = new RebinderButton(R.drawable.stick_right, R.string.controller_direction_right); + mSimulatedGamepadMap.DIRECTION_LEFT = mRebinderButtons[index++] = new RebinderButton(R.drawable.stick_right, R.string.controller_direction_left); + mSimulatedGamepadMap.DIRECTION_BACKWARD = mRebinderButtons[index++] = new RebinderButton(R.drawable.stick_right, R.string.controller_direction_backward); + mSimulatedGamepadMap.THUMBSTICK_RIGHT = mRebinderButtons[index++] = new RebinderButton(R.drawable.stick_right_click, R.string.controller_stick_press_r); + mSimulatedGamepadMap.THUMBSTICK_LEFT = mRebinderButtons[index++] = new RebinderButton(R.drawable.stick_left_click, R.string.controller_stick_press_l); + mSimulatedGamepadMap.DPAD_UP = mRebinderButtons[index++] = new RebinderButton(R.drawable.dpad_up, R.string.controller_dpad_up); + mSimulatedGamepadMap.DPAD_DOWN = mRebinderButtons[index++] = new RebinderButton(R.drawable.dpad_down, R.string.controller_dpad_down); + mSimulatedGamepadMap.DPAD_RIGHT = mRebinderButtons[index++] = new RebinderButton(R.drawable.dpad_right, R.string.controller_dpad_right); + mSimulatedGamepadMap.DPAD_LEFT = mRebinderButtons[index] = new RebinderButton(R.drawable.dpad_left, R.string.controller_dpad_left); + } + + private void updateRealButtons() { + GamepadMap currentRealMap = mGrabState ? GamepadMapStore.getGameMap() : GamepadMapStore.getMenuMap(); + int index = 0; + mRealButtons[index++] = currentRealMap.BUTTON_A; + mRealButtons[index++] = currentRealMap.BUTTON_B; + mRealButtons[index++] = currentRealMap.BUTTON_X; + mRealButtons[index++] = currentRealMap.BUTTON_Y; + mRealButtons[index++] = currentRealMap.BUTTON_START; + mRealButtons[index++] = currentRealMap.BUTTON_SELECT; + mRealButtons[index++] = currentRealMap.TRIGGER_RIGHT; + mRealButtons[index++] = currentRealMap.TRIGGER_LEFT; + mRealButtons[index++] = currentRealMap.SHOULDER_RIGHT; + mRealButtons[index++] = currentRealMap.SHOULDER_LEFT; + mRealButtons[index++] = currentRealMap.DIRECTION_FORWARD; + mRealButtons[index++] = currentRealMap.DIRECTION_RIGHT; + mRealButtons[index++] = currentRealMap.DIRECTION_LEFT; + mRealButtons[index++] = currentRealMap.DIRECTION_BACKWARD; + mRealButtons[index++] = currentRealMap.THUMBSTICK_RIGHT; + mRealButtons[index++] = currentRealMap.THUMBSTICK_LEFT; + mRealButtons[index++] = currentRealMap.DPAD_UP; + mRealButtons[index++] = currentRealMap.DPAD_DOWN; + mRealButtons[index++] = currentRealMap.DPAD_RIGHT; + mRealButtons[index] = currentRealMap.DPAD_LEFT; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + View view = layoutInflater.inflate(R.layout.item_controller_mapping, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + holder.attach(position); + } + + @Override + public void onViewRecycled(@NonNull ViewHolder holder) { + super.onViewRecycled(holder); + holder.detach(); + } + + @Override + public int getItemCount() { + return mRebinderButtons.length; + } + + private void updateStickIcons() { + // Which stick is used for keyboard emulation depends on grab state, so we need + // to update the mapper UI icons accordingly + int stickIcon = mGrabState ? R.drawable.stick_left : R.drawable.stick_right; + ((RebinderButton)mSimulatedGamepadMap.DIRECTION_FORWARD).iconResourceId = stickIcon; + ((RebinderButton)mSimulatedGamepadMap.DIRECTION_BACKWARD).iconResourceId = stickIcon; + ((RebinderButton)mSimulatedGamepadMap.DIRECTION_RIGHT).iconResourceId = stickIcon; + ((RebinderButton)mSimulatedGamepadMap.DIRECTION_LEFT).iconResourceId = stickIcon; + } + + private static class RebinderButton extends GamepadButton { + public int iconResourceId; + public final int localeResourceId; + private GamepadMapperAdapter.ViewHolder mButtonHolder; + + public RebinderButton(int iconResourceId, int localeResourceId) { + this.iconResourceId = iconResourceId; + this.localeResourceId = localeResourceId; + } + + public void changeViewHolder(GamepadMapperAdapter.ViewHolder viewHolder) { + mButtonHolder = viewHolder; + if(mButtonHolder != null) mButtonHolder.setPressed(mIsDown); + } + + @Override + protected void onDownStateChanged(boolean isDown) { + if(mButtonHolder == null) return; + mButtonHolder.setPressed(isDown); + } + } + + public class ViewHolder extends RecyclerView.ViewHolder implements AdapterView.OnItemSelectedListener, View.OnClickListener { + private static final int COLOR_ACTIVE_BUTTON = 0x2000FF00; + private final Context mContext; + private final ImageView mButtonIcon; + private final ImageView mExpansionIndicator; + private final Spinner[] mKeySpinners; + private final View mExpandedView; + private final TextView mKeycodeLabel; + private int mAttachedPosition = -1; + private short[] mKeycodes; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + mContext = itemView.getContext(); + mButtonIcon = itemView.findViewById(R.id.controller_mapper_button); + mExpandedView = itemView.findViewById(R.id.controller_mapper_expanded_view); + mExpansionIndicator = itemView.findViewById(R.id.controller_mapper_expand_button); + mKeycodeLabel = itemView.findViewById(R.id.controller_mapper_keycode_label); + View defaultView = itemView.findViewById(R.id.controller_mapper_default_view); + defaultView.setOnClickListener(this); + mKeySpinners = new Spinner[4]; + mKeySpinners[0] = itemView.findViewById(R.id.controller_mapper_key_spinner1); + mKeySpinners[1] = itemView.findViewById(R.id.controller_mapper_key_spinner2); + mKeySpinners[2] = itemView.findViewById(R.id.controller_mapper_key_spinner3); + mKeySpinners[3] = itemView.findViewById(R.id.controller_mapper_key_spinner4); + for(Spinner spinner : mKeySpinners) { + spinner.setAdapter(mKeyAdapter); + spinner.setOnItemSelectedListener(this); + } + } + private void attach(int index) { + RebinderButton rebinderButton = mRebinderButtons[index]; + mExpandedView.setVisibility(View.GONE); + mButtonIcon.setImageResource(rebinderButton.iconResourceId); + String buttonName = mContext.getString(rebinderButton.localeResourceId); + mButtonIcon.setContentDescription(buttonName); + rebinderButton.changeViewHolder(this); + + GamepadEmulatedButton realButton = mRealButtons[index]; + + mKeycodes = realButton.keycodes; + + int spinnerIndex; + + // Populate spinners with known keycodes until we run out of keycodes + for(spinnerIndex = 0; spinnerIndex < mKeycodes.length; spinnerIndex++) { + Spinner keySpinner = mKeySpinners[spinnerIndex]; + keySpinner.setEnabled(true); + short keyCode = mKeycodes[spinnerIndex]; + int selected; + if(keyCode < 0) selected = keyCode + mSpecialKeycodeCount; + else selected = EfficientAndroidLWJGLKeycode.getIndexByValue(keyCode) + mSpecialKeycodeCount; + keySpinner.setSelection(selected); + } + // In case if there is too much spinners, disable the rest of them + for(;spinnerIndex < mKeySpinners.length; spinnerIndex++) { + mKeySpinners[spinnerIndex].setEnabled(false); + } + updateKeycodeLabel(); + + mAttachedPosition = index; + } + private void detach() { + mRebinderButtons[mAttachedPosition].changeViewHolder(null); + mAttachedPosition = -1; + } + private void setPressed(boolean pressed) { + itemView.setBackgroundColor(pressed ? COLOR_ACTIVE_BUTTON : Color.TRANSPARENT); + } + + private void updateKeycodeLabel() { + StringBuilder labelBuilder = new StringBuilder(); + boolean first = true; + int unspecifiedPosition = GamepadMap.UNSPECIFIED + mSpecialKeycodeCount; + for (Spinner keySpinner : mKeySpinners) { + if (keySpinner.getSelectedItemPosition() == unspecifiedPosition) continue; + if (!first) labelBuilder.append(" + "); + else first = false; + labelBuilder.append(keySpinner.getSelectedItem().toString()); + } + if(labelBuilder.length() == 0) labelBuilder.append(mKeyAdapter.getItem(unspecifiedPosition)); + mKeycodeLabel.setText(labelBuilder.toString()); + } + + @Override + public void onItemSelected(AdapterView adapterView, View view, int selectionIndex, long selectionId) { + if(mAttachedPosition == -1) return; + int editedKeycodeIndex = -1; + for(int i = 0; i < mKeySpinners.length && i < mKeycodes.length; i++) { + if(!adapterView.equals(mKeySpinners[i])) continue; + editedKeycodeIndex = i; + break; + } + if(editedKeycodeIndex == -1) return; + int keycode_offset = selectionIndex - mSpecialKeycodeCount; + if(selectionIndex <= mSpecialKeycodeCount) mKeycodes[editedKeycodeIndex] = (short) (keycode_offset); + else mKeycodes[editedKeycodeIndex] = EfficientAndroidLWJGLKeycode.getValueByIndex(keycode_offset); + updateKeycodeLabel(); + try { + GamepadMapStore.save(); + }catch (Exception e) { + Tools.showError(adapterView.getContext(), e); + } + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + + } + + @Override + public void onClick(View view) { + int visibility = mExpandedView.getVisibility(); + switch (visibility) { + case View.INVISIBLE: + case View.GONE: + mExpansionIndicator.setRotation(0); + mExpandedView.setVisibility(View.VISIBLE); + break; + case View.VISIBLE: + mExpansionIndicator.setRotation(180); + mExpandedView.setVisibility(View.GONE); + } + } + } + + @Override + public GamepadMap getMenuMap() { + return mSimulatedGamepadMap; + } + + @Override + public GamepadMap getGameMap() { + return mSimulatedGamepadMap; + } + + @Override + public boolean isGrabbing() { + return mGrabState; + } + + @Override + public void attachGrabListener(GrabListener grabListener) { + mGamepadGrabListener = grabListener; + grabListener.onGrabState(mGrabState); + } + + // Cannot do it another way + @SuppressLint("NotifyDataSetChanged") + public void setGrabState(boolean newState) { + mGrabState = newState; + if(mGamepadGrabListener != null) mGamepadGrabListener.onGrabState(newState); + if(mGrabState == mOldState) return; + updateRealButtons(); + updateStickIcons(); + notifyDataSetChanged(); + mOldState = mGrabState; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/GamepadMapperFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/GamepadMapperFragment.java new file mode 100644 index 0000000000..4f60e46f52 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/GamepadMapperFragment.java @@ -0,0 +1,121 @@ +package net.kdt.pojavlaunch.fragments; + +import android.app.Activity; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.customcontrols.gamepad.Gamepad; +import net.kdt.pojavlaunch.customcontrols.gamepad.GamepadMapperAdapter; + +import fr.spse.gamepad_remapper.RemapperManager; +import fr.spse.gamepad_remapper.RemapperView; + +public class GamepadMapperFragment extends Fragment implements + View.OnKeyListener, View.OnGenericMotionListener, AdapterView.OnItemSelectedListener { + public static final String TAG = "GamepadMapperFragment"; + private final RemapperView.Builder mRemapperViewBuilder = new RemapperView.Builder(null) + .remapA(true) + .remapB(true) + .remapX(true) + .remapY(true) + .remapLeftJoystick(true) + .remapRightJoystick(true) + .remapStart(true) + .remapSelect(true) + .remapLeftShoulder(true) + .remapRightShoulder(true) + .remapLeftTrigger(true) + .remapRightTrigger(true); + private final Handler mExitHandler = new Handler(Looper.getMainLooper()); + private final Runnable mExitRunnable = () -> { + Activity activity = getActivity(); + if(activity == null) return; + activity.onBackPressed(); + }; + private RemapperManager mInputManager; + private GamepadMapperAdapter mMapperAdapter; + private Gamepad mGamepad; + public GamepadMapperFragment() { + super(R.layout.fragment_controller_remapper); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + RecyclerView buttonRecyclerView = view.findViewById(R.id.gamepad_remapper_recycler); + mMapperAdapter = new GamepadMapperAdapter(view.getContext()); + buttonRecyclerView.setLayoutManager(new LinearLayoutManager(view.getContext())); + buttonRecyclerView.setAdapter(mMapperAdapter); + buttonRecyclerView.setOnKeyListener(this); + buttonRecyclerView.setOnGenericMotionListener(this); + buttonRecyclerView.requestFocus(); + mInputManager = new RemapperManager(view.getContext(), mRemapperViewBuilder); + Spinner grabStateSpinner = view.findViewById(R.id.gamepad_remapper_mode_spinner); + ArrayAdapter mGrabStateAdapter = new ArrayAdapter<>(view.getContext(), R.layout.support_simple_spinner_dropdown_item); + mGrabStateAdapter.addAll(getString(R.string.customctrl_visibility_in_menus), getString(R.string.customctrl_visibility_ingame)); + grabStateSpinner.setAdapter(mGrabStateAdapter); + grabStateSpinner.setSelection(0); + grabStateSpinner.setOnItemSelectedListener(this); + } + + private void createGamepad(View mainView, InputDevice inputDevice) { + mGamepad = new Gamepad(mainView, inputDevice, mMapperAdapter, false) { + @Override + public void handleGamepadInput(int keycode, float value) { + if(keycode == KeyEvent.KEYCODE_BUTTON_SELECT) { + handleExitButton(value > 0.5); + } + super.handleGamepadInput(keycode, value); + } + }; + } + + private void handleExitButton(boolean isPressed) { + if(isPressed) mExitHandler.postDelayed(mExitRunnable, 400); + else mExitHandler.removeCallbacks(mExitRunnable); + } + + @Override + public boolean onKey(View view, int i, KeyEvent keyEvent) { + View mainView = getView(); + if(!Gamepad.isGamepadEvent(keyEvent) || mainView == null) return false; + if(mGamepad == null) createGamepad(mainView, keyEvent.getDevice()); + mInputManager.handleKeyEventInput(mainView.getContext(), keyEvent, mGamepad); + return true; + } + + @Override + public boolean onGenericMotion(View view, MotionEvent motionEvent) { + View mainView = getView(); + if(!Gamepad.isGamepadEvent(motionEvent) || mainView == null) return false; + if(mGamepad == null) createGamepad(mainView, motionEvent.getDevice()); + mInputManager.handleMotionEventInput(mainView.getContext(), motionEvent, mGamepad); + return true; + } + + @Override + public void onItemSelected(AdapterView adapterView, View view, int i, long l) { + boolean grab = i == 1; + mMapperAdapter.setGrabState(grab); + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java index beea56e1d8..3c827ec715 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java @@ -56,7 +56,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat mShareLogsButton.setOnClickListener((v) -> shareLog(requireContext())); mNewsButton.setOnLongClickListener((v)->{ - Tools.swapFragment(requireActivity(), SearchModFragment.class, SearchModFragment.TAG, null); + Tools.swapFragment(requireActivity(), GamepadMapperFragment.class, GamepadMapperFragment.TAG, null); return true; }); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java index d259490f09..43ab1c0879 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java @@ -1,8 +1,10 @@ package net.kdt.pojavlaunch.services; +import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; +import android.content.pm.ServiceInfo; import android.os.Binder; import android.os.Build; import android.os.IBinder; @@ -43,7 +45,13 @@ public int onStartCommand(Intent intent, int flags, int startId) { .addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.notification_terminate), pendingKillIntent) .setSmallIcon(R.drawable.notif_icon) .setNotificationSilent(); - startForeground(NotificationUtils.NOTIFICATION_ID_GAME_SERVICE, notificationBuilder.build()); + + Notification notification = notificationBuilder.build(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(NotificationUtils.NOTIFICATION_ID_GAME_SERVICE, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST); + } else { + startForeground(NotificationUtils.NOTIFICATION_ID_GAME_SERVICE, notification); + } return START_NOT_STICKY; // non-sticky so android wont try restarting the game after the user uses the "Quit" button } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java index fa005f0161..4678c3fb13 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java @@ -1,10 +1,12 @@ package net.kdt.pojavlaunch.services; import android.annotation.SuppressLint; +import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; +import android.content.pm.ServiceInfo; import android.os.Build; import android.os.IBinder; import android.os.Process; @@ -64,7 +66,12 @@ public int onStartCommand(Intent intent, int flags, int startId) { } Log.d("ProgressService", "Started!"); mNotificationBuilder.setContentText(getString(R.string.progresslayout_tasks_in_progress, ProgressKeeper.getTaskCount())); - startForeground(NotificationUtils.NOTIFICATION_ID_PROGRESS_SERVICE, mNotificationBuilder.build()); + Notification notification = mNotificationBuilder.build(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(NotificationUtils.NOTIFICATION_ID_PROGRESS_SERVICE, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST); + } else { + startForeground(NotificationUtils.NOTIFICATION_ID_PROGRESS_SERVICE, notification); + } if(ProgressKeeper.getTaskCount() < 1) stopSelf(); else ProgressKeeper.addTaskCountListener(this, false); diff --git a/app_pojavlauncher/src/main/res/layout/dialog_control_button_setting.xml b/app_pojavlauncher/src/main/res/layout/dialog_control_button_setting.xml index a3185552dd..08508c24a4 100644 --- a/app_pojavlauncher/src/main/res/layout/dialog_control_button_setting.xml +++ b/app_pojavlauncher/src/main/res/layout/dialog_control_button_setting.xml @@ -483,7 +483,7 @@ android:id="@+id/visibility_textview" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="Visibility" + android:text="@string/customctrl_visibility_title" app:layout_constraintTop_toBottomOf="@+id/editButtonOpacity_seekbar" tools:layout_editor_absoluteX="6dp" /> @@ -491,14 +491,14 @@ android:id="@+id/visibility_game_checkbox" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="In game" + android:text="@string/customctrl_visibility_ingame" app:layout_constraintTop_toBottomOf="@+id/visibility_textview" /> diff --git a/app_pojavlauncher/src/main/res/layout/fragment_controller_remapper.xml b/app_pojavlauncher/src/main/res/layout/fragment_controller_remapper.xml new file mode 100644 index 0000000000..bf5a97dbf9 --- /dev/null +++ b/app_pojavlauncher/src/main/res/layout/fragment_controller_remapper.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app_pojavlauncher/src/main/res/layout/item_centered_textview_large.xml b/app_pojavlauncher/src/main/res/layout/item_centered_textview_large.xml new file mode 100644 index 0000000000..ac329d6e3e --- /dev/null +++ b/app_pojavlauncher/src/main/res/layout/item_centered_textview_large.xml @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/app_pojavlauncher/src/main/res/layout/item_controller_mapping.xml b/app_pojavlauncher/src/main/res/layout/item_controller_mapping.xml new file mode 100644 index 0000000000..af6ff587b3 --- /dev/null +++ b/app_pojavlauncher/src/main/res/layout/item_controller_mapping.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml index 993218ff1a..9265f64b62 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - + SolCraftLauncher (SolDev PojavLancher fork) SolCraftLauncher @@ -394,4 +394,33 @@ Log output: Failed to read the .jar file Execute .jar feature is not compatible with Java %d + A + B + X + Y + Start + Select + Left Trigger + Right Trigger + Left Shoulder + Right Shoulder + Stick Up + Stick Down + Stick Left + Stick Right + Left Thumbstick (click) + Right Thumbstick (click) + D-Pad Up + D-Pad Down + D-Pad Left + D-Pad Right + Hold + to exit + Current mode + Visibility + In-game + In menus + Expand to change keycodes + Change controller key bindings + Allows you to modify the keyboard keys bound to each controller button diff --git a/app_pojavlauncher/src/main/res/xml/pref_control.xml b/app_pojavlauncher/src/main/res/xml/pref_control.xml index 550439aeaf..f6300e7c32 100644 --- a/app_pojavlauncher/src/main/res/xml/pref_control.xml +++ b/app_pojavlauncher/src/main/res/xml/pref_control.xml @@ -132,6 +132,10 @@ + + diff --git a/gradle.properties b/gradle.properties index 2a84a3a9fe..7be9b6b006 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,5 @@ org.gradle.configureondemand=true android.useAndroidX=true android.bundle.language.enableSplit=false -org.gradle.jvmargs=-Xmx8G +# Increase Gradle daemon RAM allocation +org.gradle.jvmargs=-Xmx4096M