mirror of
https://github.com/izzy2lost/PSX2.git
synced 2026-07-05 15:18:36 -07:00
Added controller support
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
package com.izzy2lost.psx2;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.InputDevice;
|
||||
import android.util.Log;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Controller configuration and detection utility
|
||||
* Based on AetherSX2's controller support
|
||||
*/
|
||||
public class ControllerConfig {
|
||||
private static final String TAG = "ControllerConfig";
|
||||
|
||||
/**
|
||||
* Get list of connected controllers
|
||||
*/
|
||||
public static List<ControllerInfo> getConnectedControllers() {
|
||||
List<ControllerInfo> controllers = new ArrayList<>();
|
||||
|
||||
int[] deviceIds = InputDevice.getDeviceIds();
|
||||
for (int deviceId : deviceIds) {
|
||||
InputDevice device = InputDevice.getDevice(deviceId);
|
||||
if (device != null && isController(device)) {
|
||||
controllers.add(new ControllerInfo(deviceId, device.getName(), getControllerType(device)));
|
||||
}
|
||||
}
|
||||
|
||||
return controllers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a device is a controller
|
||||
*/
|
||||
private static boolean isController(InputDevice device) {
|
||||
int sources = device.getSources();
|
||||
return (sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
|
||||
(sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get controller type description
|
||||
*/
|
||||
private static String getControllerType(InputDevice device) {
|
||||
String name = device.getName().toLowerCase();
|
||||
|
||||
if (name.contains("xbox")) {
|
||||
return "Xbox Controller";
|
||||
} else if (name.contains("playstation") || name.contains("ps4") || name.contains("ps5") || name.contains("dualshock") || name.contains("dualsense")) {
|
||||
return "PlayStation Controller";
|
||||
} else if (name.contains("nintendo") || name.contains("pro controller")) {
|
||||
return "Nintendo Controller";
|
||||
} else if (name.contains("steam")) {
|
||||
return "Steam Controller";
|
||||
} else {
|
||||
return "Generic Controller";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log controller information for debugging
|
||||
*/
|
||||
public static void logControllerInfo(Context context) {
|
||||
List<ControllerInfo> controllers = getConnectedControllers();
|
||||
|
||||
Log.i(TAG, "=== Connected Controllers ===");
|
||||
if (controllers.isEmpty()) {
|
||||
Log.i(TAG, "No controllers detected");
|
||||
} else {
|
||||
for (ControllerInfo controller : controllers) {
|
||||
Log.i(TAG, "Controller: " + controller.name + " (ID: " + controller.deviceId + ", Type: " + controller.type + ")");
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "============================");
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller information holder
|
||||
*/
|
||||
public static class ControllerInfo {
|
||||
public final int deviceId;
|
||||
public final String name;
|
||||
public final String type;
|
||||
|
||||
public ControllerInfo(int deviceId, String name, String type) {
|
||||
this.deviceId = deviceId;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name + " (" + type + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button mapping description for user reference
|
||||
*/
|
||||
public static String getButtonMappingDescription() {
|
||||
return "Controller Button Mapping:\n\n" +
|
||||
"• A Button → Cross (X)\n" +
|
||||
"• B Button → Circle (O)\n" +
|
||||
"• X Button → Square (□)\n" +
|
||||
"• Y Button → Triangle (△)\n" +
|
||||
"• L1/LB → L1\n" +
|
||||
"• R1/RB → R1\n" +
|
||||
"• L2/LT → L2\n" +
|
||||
"• R2/RT → R2\n" +
|
||||
"• Left Stick Click → L3\n" +
|
||||
"• Right Stick Click → R3\n" +
|
||||
"• Select/Back → Select\n" +
|
||||
"• Start/Menu → Start\n" +
|
||||
"• D-Pad → D-Pad\n" +
|
||||
"• Left Stick → Left Analog\n" +
|
||||
"• Right Stick → Right Analog";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package com.izzy2lost.psx2;
|
||||
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
||||
/**
|
||||
* Controller input handler based on AetherSX2's PAD implementation
|
||||
* Maps Android controller inputs to PS2 controller buttons
|
||||
*/
|
||||
public class ControllerInputHandler {
|
||||
private static final String TAG = "ControllerInput";
|
||||
|
||||
// PS2 Controller button constants (using Android KeyEvent codes that MainActivity expects)
|
||||
public static final int PAD_L2 = KeyEvent.KEYCODE_BUTTON_L2;
|
||||
public static final int PAD_R2 = KeyEvent.KEYCODE_BUTTON_R2;
|
||||
public static final int PAD_L1 = KeyEvent.KEYCODE_BUTTON_L1;
|
||||
public static final int PAD_R1 = KeyEvent.KEYCODE_BUTTON_R1;
|
||||
public static final int PAD_TRIANGLE = KeyEvent.KEYCODE_BUTTON_Y;
|
||||
public static final int PAD_CIRCLE = KeyEvent.KEYCODE_BUTTON_B;
|
||||
public static final int PAD_CROSS = KeyEvent.KEYCODE_BUTTON_A;
|
||||
public static final int PAD_SQUARE = KeyEvent.KEYCODE_BUTTON_X;
|
||||
public static final int PAD_SELECT = KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||
public static final int PAD_L3 = KeyEvent.KEYCODE_BUTTON_THUMBL;
|
||||
public static final int PAD_R3 = KeyEvent.KEYCODE_BUTTON_THUMBR;
|
||||
public static final int PAD_START = KeyEvent.KEYCODE_BUTTON_START;
|
||||
public static final int PAD_UP = KeyEvent.KEYCODE_DPAD_UP;
|
||||
public static final int PAD_RIGHT = KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
public static final int PAD_DOWN = KeyEvent.KEYCODE_DPAD_DOWN;
|
||||
public static final int PAD_LEFT = KeyEvent.KEYCODE_DPAD_LEFT;
|
||||
// Analog stick mapping (using MainActivity's custom codes)
|
||||
public static final int PAD_L_UP = 110;
|
||||
public static final int PAD_L_RIGHT = 111;
|
||||
public static final int PAD_L_DOWN = 112;
|
||||
public static final int PAD_L_LEFT = 113;
|
||||
public static final int PAD_R_UP = 120;
|
||||
public static final int PAD_R_RIGHT = 121;
|
||||
public static final int PAD_R_DOWN = 122;
|
||||
public static final int PAD_R_LEFT = 123;
|
||||
|
||||
// Analog stick deadzone (matching AetherSX2's default)
|
||||
private static final float ANALOG_DEADZONE = 0.15f;
|
||||
|
||||
// Button combo detection
|
||||
private boolean mSelectPressed = false;
|
||||
private boolean mStartPressed = false;
|
||||
private long mComboDetectionTime = 0;
|
||||
private static final long COMBO_TIMEOUT_MS = 500; // 500ms window for combo
|
||||
|
||||
// Key mapping from Android KeyEvent to PS2 buttons
|
||||
private static final SparseArray<Integer> sKeyMapping = new SparseArray<>();
|
||||
|
||||
// Motion axis mapping from Android MotionEvent to PS2 analog inputs
|
||||
private static final SparseArray<Integer> sAxisMapping = new SparseArray<>();
|
||||
|
||||
static {
|
||||
// Initialize key mappings (Android KeyEvent -> PS2 button)
|
||||
// Since our constants now match Android keycodes, we can map directly
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_A, PAD_CROSS); // A -> Cross (X)
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_B, PAD_CIRCLE); // B -> Circle
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_X, PAD_SQUARE); // X -> Square
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_Y, PAD_TRIANGLE); // Y -> Triangle
|
||||
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_L1, PAD_L1);
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_R1, PAD_R1);
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_L2, PAD_L2);
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_R2, PAD_R2);
|
||||
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_THUMBL, PAD_L3);
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_THUMBR, PAD_R3);
|
||||
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_SELECT, PAD_SELECT);
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_BUTTON_START, PAD_START);
|
||||
|
||||
// D-Pad
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_DPAD_UP, PAD_UP);
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_DPAD_DOWN, PAD_DOWN);
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_DPAD_LEFT, PAD_LEFT);
|
||||
sKeyMapping.put(KeyEvent.KEYCODE_DPAD_RIGHT, PAD_RIGHT);
|
||||
|
||||
// Initialize axis mappings for analog sticks
|
||||
// Map to our custom analog codes that MainActivity expects
|
||||
sAxisMapping.put(MotionEvent.AXIS_X, PAD_L_LEFT); // Left stick X (negative = left)
|
||||
sAxisMapping.put(MotionEvent.AXIS_Y, PAD_L_UP); // Left stick Y (negative = up)
|
||||
sAxisMapping.put(MotionEvent.AXIS_Z, PAD_R_LEFT); // Right stick X (negative = left)
|
||||
sAxisMapping.put(MotionEvent.AXIS_RZ, PAD_R_UP); // Right stick Y (negative = up)
|
||||
sAxisMapping.put(MotionEvent.AXIS_LTRIGGER, PAD_L2); // Left trigger
|
||||
sAxisMapping.put(MotionEvent.AXIS_RTRIGGER, PAD_R2); // Right trigger
|
||||
}
|
||||
|
||||
public interface ControllerInputListener {
|
||||
void onControllerButtonPressed(int controllerId, int button, boolean pressed);
|
||||
void onControllerAnalogInput(int controllerId, int axis, float value);
|
||||
void onControllerCombo(int controllerId, String comboName);
|
||||
}
|
||||
|
||||
private ControllerInputListener mListener;
|
||||
|
||||
public ControllerInputHandler(ControllerInputListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key events from controllers
|
||||
*/
|
||||
public boolean handleKeyEvent(KeyEvent event) {
|
||||
if (!isFromController(event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int keyCode = event.getKeyCode();
|
||||
Integer ps2Button = sKeyMapping.get(keyCode);
|
||||
|
||||
if (ps2Button != null) {
|
||||
int controllerId = event.getDeviceId();
|
||||
boolean pressed = (event.getAction() == KeyEvent.ACTION_DOWN);
|
||||
|
||||
// Check for Select+Start combo
|
||||
if (handleButtonCombo(controllerId, ps2Button, pressed)) {
|
||||
return true; // Combo detected, don't send individual button presses
|
||||
}
|
||||
|
||||
Log.d(TAG, "Controller " + controllerId + " button " + ps2Button + " " + (pressed ? "pressed" : "released"));
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onControllerButtonPressed(controllerId, ps2Button, pressed);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle button combinations (like Select+Start)
|
||||
*/
|
||||
private boolean handleButtonCombo(int controllerId, int button, boolean pressed) {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
|
||||
// Track Select and Start button states
|
||||
if (button == PAD_SELECT) {
|
||||
mSelectPressed = pressed;
|
||||
if (pressed) mComboDetectionTime = currentTime;
|
||||
} else if (button == PAD_START) {
|
||||
mStartPressed = pressed;
|
||||
if (pressed) mComboDetectionTime = currentTime;
|
||||
}
|
||||
|
||||
// Check if both buttons are pressed within the timeout window
|
||||
if (mSelectPressed && mStartPressed &&
|
||||
(currentTime - mComboDetectionTime) < COMBO_TIMEOUT_MS) {
|
||||
|
||||
Log.d(TAG, "Select+Start combo detected!");
|
||||
|
||||
// Reset combo state
|
||||
mSelectPressed = false;
|
||||
mStartPressed = false;
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onControllerCombo(controllerId, "select_start");
|
||||
}
|
||||
return true; // Combo handled, don't process individual buttons
|
||||
}
|
||||
|
||||
// Reset combo state if timeout exceeded
|
||||
if ((currentTime - mComboDetectionTime) > COMBO_TIMEOUT_MS) {
|
||||
mSelectPressed = false;
|
||||
mStartPressed = false;
|
||||
}
|
||||
|
||||
return false; // No combo, process button normally
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle motion events from controllers (analog sticks, triggers)
|
||||
*/
|
||||
public boolean handleMotionEvent(MotionEvent event) {
|
||||
if (!isFromController(event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int controllerId = event.getDeviceId();
|
||||
|
||||
// Handle left stick X axis
|
||||
float leftX = event.getAxisValue(MotionEvent.AXIS_X);
|
||||
leftX = applyDeadzone(leftX, ANALOG_DEADZONE);
|
||||
if (mListener != null) {
|
||||
// Only send the direction that's actually being pressed
|
||||
if (leftX < 0) {
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_L_LEFT, -leftX); // Left direction (positive value)
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_L_RIGHT, 0); // Clear right
|
||||
} else if (leftX > 0) {
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_L_RIGHT, leftX); // Right direction
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_L_LEFT, 0); // Clear left
|
||||
} else {
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_L_LEFT, 0); // Clear both
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_L_RIGHT, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle left stick Y axis
|
||||
float leftY = event.getAxisValue(MotionEvent.AXIS_Y);
|
||||
leftY = applyDeadzone(leftY, ANALOG_DEADZONE);
|
||||
if (mListener != null) {
|
||||
if (leftY < 0) {
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_L_UP, -leftY); // Up direction (positive value)
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_L_DOWN, 0); // Clear down
|
||||
} else if (leftY > 0) {
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_L_DOWN, leftY); // Down direction
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_L_UP, 0); // Clear up
|
||||
} else {
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_L_UP, 0); // Clear both
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_L_DOWN, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle right stick X axis
|
||||
float rightX = event.getAxisValue(MotionEvent.AXIS_Z);
|
||||
rightX = applyDeadzone(rightX, ANALOG_DEADZONE);
|
||||
if (mListener != null) {
|
||||
if (rightX < 0) {
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_R_LEFT, -rightX); // Left direction (positive value)
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_R_RIGHT, 0); // Clear right
|
||||
} else if (rightX > 0) {
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_R_RIGHT, rightX); // Right direction
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_R_LEFT, 0); // Clear left
|
||||
} else {
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_R_LEFT, 0); // Clear both
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_R_RIGHT, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle right stick Y axis
|
||||
float rightY = event.getAxisValue(MotionEvent.AXIS_RZ);
|
||||
rightY = applyDeadzone(rightY, ANALOG_DEADZONE);
|
||||
if (mListener != null) {
|
||||
if (rightY < 0) {
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_R_UP, -rightY); // Up direction (positive value)
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_R_DOWN, 0); // Clear down
|
||||
} else if (rightY > 0) {
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_R_DOWN, rightY); // Down direction
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_R_UP, 0); // Clear up
|
||||
} else {
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_R_UP, 0); // Clear both
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_R_DOWN, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle triggers
|
||||
float leftTrigger = event.getAxisValue(MotionEvent.AXIS_LTRIGGER);
|
||||
if (mListener != null) {
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_L2, leftTrigger);
|
||||
}
|
||||
|
||||
float rightTrigger = event.getAxisValue(MotionEvent.AXIS_RTRIGGER);
|
||||
if (mListener != null) {
|
||||
mListener.onControllerAnalogInput(controllerId, PAD_R2, rightTrigger);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the input event is from a controller
|
||||
*/
|
||||
private boolean isFromController(android.view.InputEvent event) {
|
||||
InputDevice device = event.getDevice();
|
||||
if (device == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int sources = device.getSources();
|
||||
return (sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
|
||||
(sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply deadzone to analog input
|
||||
*/
|
||||
private float applyDeadzone(float value, float deadzone) {
|
||||
if (Math.abs(value) < deadzone) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// Scale the remaining range
|
||||
float sign = Math.signum(value);
|
||||
float scaledValue = (Math.abs(value) - deadzone) / (1.0f - deadzone);
|
||||
return sign * scaledValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the PS2 input is an analog stick axis
|
||||
*/
|
||||
private boolean isAnalogStickAxis(int ps2Input) {
|
||||
return ps2Input >= PAD_L_UP && ps2Input <= PAD_R_LEFT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Android input value to PS2 value range
|
||||
*/
|
||||
private int convertToPS2Value(int ps2Input, float value) {
|
||||
if (ps2Input == PAD_L2 || ps2Input == PAD_R2) {
|
||||
// Triggers: 0-255 range
|
||||
return Math.round(value * 255.0f);
|
||||
} else if (isAnalogStickAxis(ps2Input)) {
|
||||
// Analog sticks: -32768 to 32767 range
|
||||
return Math.round(value * 32767.0f);
|
||||
} else {
|
||||
// Digital buttons: 0 or 255
|
||||
return value > 0.5f ? 255 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get controller name for debugging
|
||||
*/
|
||||
public static String getControllerName(int deviceId) {
|
||||
InputDevice device = InputDevice.getDevice(deviceId);
|
||||
return device != null ? device.getName() : "Unknown Controller";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package com.izzy2lost.psx2;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Dialog for testing controller input
|
||||
*/
|
||||
public class ControllerTestDialogFragment extends DialogFragment implements ControllerInputHandler.ControllerInputListener {
|
||||
|
||||
private TextView mControllerListText;
|
||||
private TextView mInputLogText;
|
||||
private StringBuilder mInputLog = new StringBuilder();
|
||||
private ControllerInputHandler mControllerHandler;
|
||||
|
||||
public static ControllerTestDialogFragment newInstance() {
|
||||
return new ControllerTestDialogFragment();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_controller_test, null);
|
||||
|
||||
mControllerListText = view.findViewById(R.id.tv_controller_list);
|
||||
mInputLogText = view.findViewById(R.id.tv_input_log);
|
||||
|
||||
// Initialize controller handler for this dialog
|
||||
mControllerHandler = new ControllerInputHandler(this);
|
||||
|
||||
// Update controller list
|
||||
updateControllerList();
|
||||
|
||||
return new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Controller Test")
|
||||
.setView(view)
|
||||
.setPositiveButton("Close", null)
|
||||
.create();
|
||||
}
|
||||
|
||||
private void updateControllerList() {
|
||||
List<ControllerConfig.ControllerInfo> controllers = ControllerConfig.getConnectedControllers();
|
||||
|
||||
if (controllers.isEmpty()) {
|
||||
mControllerListText.setText("No controllers detected.\n\nMake sure your controller is connected and try pressing a button.");
|
||||
} else {
|
||||
StringBuilder sb = new StringBuilder("Connected Controllers:\n\n");
|
||||
for (ControllerConfig.ControllerInfo controller : controllers) {
|
||||
sb.append("• ").append(controller.toString()).append("\n");
|
||||
}
|
||||
sb.append("\n").append(ControllerConfig.getButtonMappingDescription());
|
||||
mControllerListText.setText(sb.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onControllerButtonPressed(int controllerId, int button, boolean pressed) {
|
||||
String buttonName = getButtonName(button);
|
||||
String action = pressed ? "PRESSED" : "RELEASED";
|
||||
String logEntry = String.format("Controller %d: %s %s\n", controllerId, buttonName, action);
|
||||
|
||||
mInputLog.append(logEntry);
|
||||
|
||||
// Keep only last 10 entries
|
||||
String[] lines = mInputLog.toString().split("\n");
|
||||
if (lines.length > 10) {
|
||||
mInputLog = new StringBuilder();
|
||||
for (int i = lines.length - 10; i < lines.length; i++) {
|
||||
mInputLog.append(lines[i]).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (mInputLogText != null) {
|
||||
mInputLogText.setText("Input Log:\n\n" + mInputLog.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onControllerAnalogInput(int controllerId, int axis, float value) {
|
||||
if (Math.abs(value) > 0.1f) { // Only log significant analog input
|
||||
String axisName = getAxisName(axis);
|
||||
String logEntry = String.format("Controller %d: %s %.2f\n", controllerId, axisName, value);
|
||||
|
||||
mInputLog.append(logEntry);
|
||||
|
||||
// Keep only last 10 entries
|
||||
String[] lines = mInputLog.toString().split("\n");
|
||||
if (lines.length > 10) {
|
||||
mInputLog = new StringBuilder();
|
||||
for (int i = lines.length - 10; i < lines.length; i++) {
|
||||
mInputLog.append(lines[i]).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (mInputLogText != null) {
|
||||
mInputLogText.setText("Input Log:\n\n" + mInputLog.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getButtonName(int button) {
|
||||
switch (button) {
|
||||
case ControllerInputHandler.PAD_CROSS: return "Cross (X)";
|
||||
case ControllerInputHandler.PAD_CIRCLE: return "Circle (O)";
|
||||
case ControllerInputHandler.PAD_SQUARE: return "Square (□)";
|
||||
case ControllerInputHandler.PAD_TRIANGLE: return "Triangle (△)";
|
||||
case ControllerInputHandler.PAD_L1: return "L1";
|
||||
case ControllerInputHandler.PAD_R1: return "R1";
|
||||
case ControllerInputHandler.PAD_L2: return "L2";
|
||||
case ControllerInputHandler.PAD_R2: return "R2";
|
||||
case ControllerInputHandler.PAD_L3: return "L3";
|
||||
case ControllerInputHandler.PAD_R3: return "R3";
|
||||
case ControllerInputHandler.PAD_SELECT: return "Select";
|
||||
case ControllerInputHandler.PAD_START: return "Start";
|
||||
case ControllerInputHandler.PAD_UP: return "D-Pad Up";
|
||||
case ControllerInputHandler.PAD_DOWN: return "D-Pad Down";
|
||||
case ControllerInputHandler.PAD_LEFT: return "D-Pad Left";
|
||||
case ControllerInputHandler.PAD_RIGHT: return "D-Pad Right";
|
||||
default: return "Button " + button;
|
||||
}
|
||||
}
|
||||
|
||||
private String getAxisName(int axis) {
|
||||
switch (axis) {
|
||||
case ControllerInputHandler.PAD_L_UP: return "Left Stick Up";
|
||||
case ControllerInputHandler.PAD_L_DOWN: return "Left Stick Down";
|
||||
case ControllerInputHandler.PAD_L_LEFT: return "Left Stick Left";
|
||||
case ControllerInputHandler.PAD_L_RIGHT: return "Left Stick Right";
|
||||
case ControllerInputHandler.PAD_R_UP: return "Right Stick Up";
|
||||
case ControllerInputHandler.PAD_R_DOWN: return "Right Stick Down";
|
||||
case ControllerInputHandler.PAD_R_LEFT: return "Right Stick Left";
|
||||
case ControllerInputHandler.PAD_R_RIGHT: return "Right Stick Right";
|
||||
default: return "Axis " + axis;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onControllerCombo(int controllerId, String comboName) {
|
||||
String logEntry = String.format("Controller %d: COMBO %s\n", controllerId, comboName);
|
||||
|
||||
mInputLog.append(logEntry);
|
||||
|
||||
// Keep only last 10 entries
|
||||
String[] lines = mInputLog.toString().split("\n");
|
||||
if (lines.length > 10) {
|
||||
mInputLog = new StringBuilder();
|
||||
for (int i = lines.length - 10; i < lines.length; i++) {
|
||||
mInputLog.append(lines[i]).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (mInputLogText != null) {
|
||||
mInputLogText.setText("Input Log:\n\n" + mInputLog.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,10 +50,11 @@ import org.json.JSONException;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import java.util.List;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements GamesCoverDialogFragment.OnGameSelectedListener {
|
||||
public class MainActivity extends AppCompatActivity implements GamesCoverDialogFragment.OnGameSelectedListener, ControllerInputHandler.ControllerInputListener {
|
||||
private String m_szGamefile = "";
|
||||
|
||||
private HIDDeviceManager mHIDDeviceManager;
|
||||
private ControllerInputHandler mControllerInputHandler;
|
||||
private Thread mEmulationThread = null;
|
||||
private boolean mHudVisible = false;
|
||||
|
||||
@@ -345,6 +346,12 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
|
||||
|
||||
Initialize();
|
||||
|
||||
// Initialize controller input handler
|
||||
mControllerInputHandler = new ControllerInputHandler(this);
|
||||
|
||||
// Log connected controllers for debugging
|
||||
ControllerConfig.logControllerInfo(this);
|
||||
|
||||
makeButtonTouch();
|
||||
|
||||
setSurfaceView(new SDLSurface(this));
|
||||
@@ -1153,6 +1160,8 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
|
||||
SettingsDialogFragment.loadAndApplySettings(this);
|
||||
|
||||
mHIDDeviceManager = HIDDeviceManager.acquire(this);
|
||||
// Initialize HID device manager for USB and Bluetooth controllers
|
||||
mHIDDeviceManager.initialize(true, true);
|
||||
|
||||
// Apply renderer setting again after a delay to override any automatic detection
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
|
||||
@@ -1217,38 +1226,40 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
if (SDLControllerManager.isDeviceSDLJoystick(event.getDeviceId())) {
|
||||
SDLControllerManager.handleJoystickMotionEvent(event);
|
||||
// Use only our controller handler - disable SDL fallback to avoid conflicts
|
||||
if (mControllerInputHandler != null && mControllerInputHandler.handleMotionEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip SDL controller manager to avoid mapping conflicts
|
||||
return super.onGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int p_keyCode, KeyEvent p_event) {
|
||||
if ((p_event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
|
||||
if (p_event.getRepeatCount() == 0) {
|
||||
SDLControllerManager.onNativePadDown(p_event.getDeviceId(), p_keyCode);
|
||||
return true;
|
||||
}
|
||||
// Use only our controller handler - disable SDL fallback to avoid conflicts
|
||||
if (mControllerInputHandler != null && mControllerInputHandler.handleKeyEvent(p_event)) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
if (p_keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
showExitDialog();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle back button for non-gamepad sources
|
||||
if (p_keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
showExitDialog();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip SDL controller manager to avoid mapping conflicts
|
||||
return super.onKeyDown(p_keyCode, p_event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int p_keyCode, KeyEvent p_event) {
|
||||
if ((p_event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
|
||||
if (p_event.getRepeatCount() == 0) {
|
||||
SDLControllerManager.onNativePadUp(p_event.getDeviceId(), p_keyCode);
|
||||
return true;
|
||||
}
|
||||
// Use only our controller handler - disable SDL fallback to avoid conflicts
|
||||
if (mControllerInputHandler != null && mControllerInputHandler.handleKeyEvent(p_event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip SDL controller manager to avoid mapping conflicts
|
||||
return super.onKeyUp(p_keyCode, p_event);
|
||||
}
|
||||
|
||||
@@ -1343,4 +1354,104 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ControllerInputHandler.ControllerInputListener implementation
|
||||
@Override
|
||||
public void onControllerButtonPressed(int controllerId, int button, boolean pressed) {
|
||||
android.util.Log.d("Controller", "Controller " + controllerId + " button " + button + " (" + getButtonName(button) + ") " + (pressed ? "pressed" : "released"));
|
||||
|
||||
// Send controller input to native code using existing setPadButton method
|
||||
// Range 255 for pressed, 0 for released (matching PS2 button range)
|
||||
int range = pressed ? 255 : 0;
|
||||
NativeApp.setPadButton(button, range, pressed);
|
||||
}
|
||||
|
||||
private String getButtonName(int button) {
|
||||
switch (button) {
|
||||
case KeyEvent.KEYCODE_BUTTON_A: return "Cross";
|
||||
case KeyEvent.KEYCODE_BUTTON_B: return "Circle";
|
||||
case KeyEvent.KEYCODE_BUTTON_X: return "Square";
|
||||
case KeyEvent.KEYCODE_BUTTON_Y: return "Triangle";
|
||||
case KeyEvent.KEYCODE_BUTTON_L1: return "L1";
|
||||
case KeyEvent.KEYCODE_BUTTON_R1: return "R1";
|
||||
case KeyEvent.KEYCODE_BUTTON_L2: return "L2";
|
||||
case KeyEvent.KEYCODE_BUTTON_R2: return "R2";
|
||||
case KeyEvent.KEYCODE_BUTTON_SELECT: return "Select";
|
||||
case KeyEvent.KEYCODE_BUTTON_START: return "Start";
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBL: return "L3";
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBR: return "R3";
|
||||
case KeyEvent.KEYCODE_DPAD_UP: return "D-Up";
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN: return "D-Down";
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT: return "D-Left";
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT: return "D-Right";
|
||||
default: return "Unknown(" + button + ")";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onControllerAnalogInput(int controllerId, int axis, float value) {
|
||||
android.util.Log.d("Controller", "Controller " + controllerId + " axis " + axis + " (" + getAxisName(axis) + ") value " + value);
|
||||
|
||||
// Send analog input to native code using setPadButton
|
||||
handleAnalogInput(axis, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onControllerCombo(int controllerId, String comboName) {
|
||||
android.util.Log.d("Controller", "Controller " + controllerId + " combo: " + comboName);
|
||||
|
||||
if ("select_start".equals(comboName)) {
|
||||
// Show quick actions dialog
|
||||
runOnUiThread(() -> {
|
||||
QuickActionsDialogFragment dialog = new QuickActionsDialogFragment();
|
||||
dialog.show(getSupportFragmentManager(), "quick_actions");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private String getAxisName(int axis) {
|
||||
switch (axis) {
|
||||
case 110: return "L-Up";
|
||||
case 111: return "L-Right";
|
||||
case 112: return "L-Down";
|
||||
case 113: return "L-Left";
|
||||
case 120: return "R-Up";
|
||||
case 121: return "R-Right";
|
||||
case 122: return "R-Down";
|
||||
case 123: return "R-Left";
|
||||
case KeyEvent.KEYCODE_BUTTON_L2: return "L2-Trigger";
|
||||
case KeyEvent.KEYCODE_BUTTON_R2: return "R2-Trigger";
|
||||
default: return "Unknown(" + axis + ")";
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAnalogInput(int axis, float value) {
|
||||
// Convert analog input to button presses for the native interface
|
||||
// This matches how AetherSX2 handles analog input
|
||||
|
||||
// For analog sticks, only send positive values (negative values are handled by opposite direction)
|
||||
int intensity = Math.max(0, Math.round(Math.abs(value) * 255));
|
||||
boolean pressed = Math.abs(value) > 0.1f;
|
||||
|
||||
switch (axis) {
|
||||
case ControllerInputHandler.PAD_L_UP:
|
||||
case ControllerInputHandler.PAD_L_DOWN:
|
||||
case ControllerInputHandler.PAD_L_LEFT:
|
||||
case ControllerInputHandler.PAD_L_RIGHT:
|
||||
case ControllerInputHandler.PAD_R_UP:
|
||||
case ControllerInputHandler.PAD_R_DOWN:
|
||||
case ControllerInputHandler.PAD_R_LEFT:
|
||||
case ControllerInputHandler.PAD_R_RIGHT:
|
||||
// Analog stick directions
|
||||
NativeApp.setPadButton(axis, intensity, pressed);
|
||||
break;
|
||||
case ControllerInputHandler.PAD_L2:
|
||||
case ControllerInputHandler.PAD_R2:
|
||||
// Triggers: 0-255 range
|
||||
NativeApp.setPadButton(axis, Math.round(value * 255), value > 0.1f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.izzy2lost.psx2;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
public class QuickActionsDialogFragment extends DialogFragment {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
View view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_quick_actions, null, false);
|
||||
|
||||
MaterialButton btnExitToMenu = view.findViewById(R.id.btn_exit_to_menu);
|
||||
MaterialButton btnRestartGame = view.findViewById(R.id.btn_restart_game);
|
||||
MaterialButton btnQuitApp = view.findViewById(R.id.btn_quit_app);
|
||||
MaterialButton btnCancel = view.findViewById(R.id.btn_cancel);
|
||||
|
||||
// Exit to Menu - not implemented yet, show placeholder
|
||||
if (btnExitToMenu != null) {
|
||||
btnExitToMenu.setOnClickListener(v -> {
|
||||
new MaterialAlertDialogBuilder(requireContext(),
|
||||
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
|
||||
.setTitle("Exit to Menu")
|
||||
.setMessage("This feature is not implemented yet. Would you like to quit the app instead?")
|
||||
.setNegativeButton("Cancel", null)
|
||||
.setPositiveButton("Quit App", (d, w) -> {
|
||||
quitApp();
|
||||
dismissAllowingStateLoss();
|
||||
})
|
||||
.show();
|
||||
});
|
||||
}
|
||||
|
||||
// Restart Game - use existing reboot functionality
|
||||
if (btnRestartGame != null) {
|
||||
btnRestartGame.setOnClickListener(v -> {
|
||||
new MaterialAlertDialogBuilder(requireContext(),
|
||||
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
|
||||
.setTitle("Restart Game")
|
||||
.setMessage("Restart the current game?")
|
||||
.setNegativeButton("Cancel", null)
|
||||
.setPositiveButton("Restart", (d, w) -> {
|
||||
if (requireActivity() instanceof MainActivity) {
|
||||
((MainActivity) requireActivity()).rebootEmu();
|
||||
} else {
|
||||
NativeApp.shutdown();
|
||||
}
|
||||
dismissAllowingStateLoss();
|
||||
})
|
||||
.show();
|
||||
});
|
||||
}
|
||||
|
||||
// Quit App - use existing power functionality
|
||||
if (btnQuitApp != null) {
|
||||
btnQuitApp.setOnClickListener(v -> {
|
||||
new MaterialAlertDialogBuilder(requireContext(),
|
||||
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
|
||||
.setTitle("Quit App")
|
||||
.setMessage("Quit PSX2?")
|
||||
.setNegativeButton("Cancel", null)
|
||||
.setPositiveButton("Quit", (d, w) -> {
|
||||
quitApp();
|
||||
dismissAllowingStateLoss();
|
||||
})
|
||||
.show();
|
||||
});
|
||||
}
|
||||
|
||||
// Cancel button
|
||||
if (btnCancel != null) {
|
||||
btnCancel.setOnClickListener(v -> dismissAllowingStateLoss());
|
||||
}
|
||||
|
||||
return new MaterialAlertDialogBuilder(requireContext(),
|
||||
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
|
||||
.setView(view)
|
||||
.create();
|
||||
}
|
||||
|
||||
private void quitApp() {
|
||||
// Stop emulator first
|
||||
NativeApp.shutdown();
|
||||
// Quit the whole app/activity task
|
||||
if (getActivity() != null) {
|
||||
getActivity().finishAffinity();
|
||||
getActivity().finishAndRemoveTask();
|
||||
}
|
||||
// As a fallback ensure process exit
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,7 @@ public class SettingsDialogFragment extends DialogFragment {
|
||||
Switch swDevHud = view.findViewById(R.id.sw_dev_hud);
|
||||
View btnPower = view.findViewById(R.id.btn_power);
|
||||
View btnReboot = view.findViewById(R.id.btn_reboot);
|
||||
View btnTestController = view.findViewById(R.id.btn_test_controller);
|
||||
|
||||
// Brand tints for checked/activated states to replace aqua
|
||||
int brand = ContextCompat.getColor(ctx, R.color.brand_primary);
|
||||
@@ -180,6 +181,13 @@ public class SettingsDialogFragment extends DialogFragment {
|
||||
});
|
||||
}
|
||||
|
||||
if (btnTestController != null) {
|
||||
btnTestController.setOnClickListener(v -> {
|
||||
ControllerTestDialogFragment controllerDialog = new ControllerTestDialogFragment();
|
||||
controllerDialog.show(getParentFragmentManager(), "controller_test");
|
||||
});
|
||||
}
|
||||
|
||||
// Populate scale spinner (1x..8x)
|
||||
ArrayAdapter<CharSequence> scaleAdapter = ArrayAdapter.createFromResource(ctx,
|
||||
R.array.scale_entries, android.R.layout.simple_spinner_item);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
|
||||
</vector>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fillViewport="true"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_controller_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Detecting controllers..."
|
||||
android:textSize="14sp"
|
||||
android:paddingBottom="16dp"/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/dividerHorizontal"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_input_log"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Input Log:\n\nPress any button or move analog sticks to test..."
|
||||
android:textSize="12sp"
|
||||
android:fontFamily="monospace"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:padding="8dp"
|
||||
android:minHeight="200dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Quick Actions"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_exit_to_menu"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Exit to Menu"
|
||||
android:textAllCaps="false"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:drawableStart="@drawable/ic_home"
|
||||
android:drawablePadding="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_restart_game"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Restart Game"
|
||||
android:textAllCaps="false"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:drawableStart="@drawable/ic_reboot"
|
||||
android:drawablePadding="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_quit_app"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Quit App"
|
||||
android:textAllCaps="false"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:drawableStart="@drawable/ic_power"
|
||||
android:drawablePadding="8dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_cancel"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Cancel"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -214,5 +214,24 @@
|
||||
android:text="Dev: HUD overlay"/>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="16dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Controller"
|
||||
android:textStyle="bold"
|
||||
android:paddingBottom="8dp"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_test_controller"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Test Controller Input"
|
||||
android:textAllCaps="false"/>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
Reference in New Issue
Block a user