Android: Controller binding support
This commit is contained in:
parent
6bf37f51be
commit
0aa3b6b399
|
@ -1,4 +1,5 @@
|
|||
#include "android_host_interface.h"
|
||||
#include "android_controller_interface.h"
|
||||
#include "android_progress_callback.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/audio_stream.h"
|
||||
|
@ -503,6 +504,27 @@ std::unique_ptr<AudioStream> AndroidHostInterface::CreateAudioStream(AudioBacken
|
|||
return CommonHostInterface::CreateAudioStream(backend);
|
||||
}
|
||||
|
||||
void AndroidHostInterface::UpdateControllerInterface()
|
||||
{
|
||||
if (m_controller_interface)
|
||||
{
|
||||
m_controller_interface->Shutdown();
|
||||
m_controller_interface.reset();
|
||||
}
|
||||
|
||||
m_controller_interface = std::make_unique<AndroidControllerInterface>();
|
||||
if (!m_controller_interface || !m_controller_interface->Initialize(this))
|
||||
{
|
||||
Log_WarningPrintf("Failed to initialize controller interface, bindings are not possible.");
|
||||
if (m_controller_interface)
|
||||
{
|
||||
m_controller_interface->Shutdown();
|
||||
m_controller_interface.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void AndroidHostInterface::OnSystemPaused(bool paused)
|
||||
{
|
||||
CommonHostInterface::OnSystemPaused(paused);
|
||||
|
@ -637,6 +659,32 @@ void AndroidHostInterface::SetControllerAxisState(u32 index, s32 button_code, fl
|
|||
false);
|
||||
}
|
||||
|
||||
void AndroidHostInterface::HandleControllerButtonEvent(u32 controller_index, u32 button_index, bool pressed)
|
||||
{
|
||||
if (!IsEmulationThreadRunning())
|
||||
return;
|
||||
|
||||
RunOnEmulationThread(
|
||||
[this, controller_index, button_index, pressed]() {
|
||||
AndroidControllerInterface* ci = static_cast<AndroidControllerInterface*>(m_controller_interface.get());
|
||||
if (ci)
|
||||
ci->HandleButtonEvent(controller_index, button_index, pressed);
|
||||
});
|
||||
}
|
||||
|
||||
void AndroidHostInterface::HandleControllerAxisEvent(u32 controller_index, u32 axis_index, float value)
|
||||
{
|
||||
if (!IsEmulationThreadRunning())
|
||||
return;
|
||||
|
||||
RunOnEmulationThread(
|
||||
[this, controller_index, axis_index, value]() {
|
||||
AndroidControllerInterface* ci = static_cast<AndroidControllerInterface*>(m_controller_interface.get());
|
||||
if (ci)
|
||||
ci->HandleAxisEvent(controller_index, axis_index, value);
|
||||
});
|
||||
}
|
||||
|
||||
void AndroidHostInterface::SetFastForwardEnabled(bool enabled)
|
||||
{
|
||||
m_fast_forward_enabled = enabled;
|
||||
|
@ -656,6 +704,7 @@ void AndroidHostInterface::ApplySettings(bool display_osd_messages)
|
|||
LoadAndConvertSettings();
|
||||
CommonHostInterface::ApplyGameSettings(display_osd_messages);
|
||||
CommonHostInterface::FixIncompatibleSettings(display_osd_messages);
|
||||
UpdateInputMap();
|
||||
|
||||
// Defer renderer changes, the app really doesn't like it.
|
||||
if (System::IsValid() && g_settings.gpu_renderer != old_settings.gpu_renderer)
|
||||
|
@ -743,6 +792,45 @@ void AndroidHostInterface::UpdateVibration()
|
|||
SetVibration(vibration_state);
|
||||
}
|
||||
|
||||
jobjectArray AndroidHostInterface::GetInputProfileNames(JNIEnv* env) const
|
||||
{
|
||||
const InputProfileList profile_list(GetInputProfileList());
|
||||
if (profile_list.empty())
|
||||
return nullptr;
|
||||
|
||||
jobjectArray name_array = env->NewObjectArray(static_cast<u32>(profile_list.size()), s_String_class, nullptr);
|
||||
u32 name_array_index = 0;
|
||||
Assert(name_array != nullptr);
|
||||
for (const InputProfileEntry& e : profile_list)
|
||||
{
|
||||
jstring axis_name_jstr = env->NewStringUTF(e.name.c_str());
|
||||
env->SetObjectArrayElement(name_array, name_array_index++, axis_name_jstr);
|
||||
env->DeleteLocalRef(axis_name_jstr);
|
||||
}
|
||||
|
||||
return name_array;
|
||||
}
|
||||
|
||||
bool AndroidHostInterface::ApplyInputProfile(const char *profile_name)
|
||||
{
|
||||
const std::string path(GetInputProfilePath(profile_name));
|
||||
if (path.empty())
|
||||
return false;
|
||||
|
||||
Assert(!IsEmulationThreadRunning() || IsEmulationThreadPaused());
|
||||
CommonHostInterface::ApplyInputProfile(path.c_str(), m_settings_interface);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AndroidHostInterface::SaveInputProfile(const char *profile_name)
|
||||
{
|
||||
const std::string path(GetSavePathForInputProfile(profile_name));
|
||||
if (path.empty())
|
||||
return false;
|
||||
|
||||
return CommonHostInterface::SaveInputProfile(path.c_str(), m_settings_interface);
|
||||
}
|
||||
|
||||
extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
|
||||
{
|
||||
Log::SetDebugOutputParams(true, nullptr, LOGLEVEL_DEV);
|
||||
|
@ -938,6 +1026,82 @@ DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerAxisCode, jobject
|
|||
return code.value_or(-1);
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerButtonNames, jobject unused, jstring controller_type)
|
||||
{
|
||||
std::optional<ControllerType> type =
|
||||
Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str());
|
||||
if (!type)
|
||||
return nullptr;
|
||||
|
||||
const Controller::ButtonList buttons(Controller::GetButtonNames(type.value()));
|
||||
if (buttons.empty())
|
||||
return nullptr;
|
||||
|
||||
jobjectArray name_array = env->NewObjectArray(static_cast<u32>(buttons.size()), s_String_class, nullptr);
|
||||
u32 name_array_index = 0;
|
||||
Assert(name_array != nullptr);
|
||||
for (const auto& [button_name, button_code] : buttons)
|
||||
{
|
||||
jstring button_name_jstr = env->NewStringUTF(button_name.c_str());
|
||||
env->SetObjectArrayElement(name_array, name_array_index++, button_name_jstr);
|
||||
env->DeleteLocalRef(button_name_jstr);
|
||||
}
|
||||
|
||||
return name_array;
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerAxisNames, jobject unused, jstring controller_type)
|
||||
{
|
||||
std::optional<ControllerType> type =
|
||||
Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str());
|
||||
if (!type)
|
||||
return nullptr;
|
||||
|
||||
const Controller::AxisList axes(Controller::GetAxisNames(type.value()));
|
||||
if (axes.empty())
|
||||
return nullptr;
|
||||
|
||||
jobjectArray name_array = env->NewObjectArray(static_cast<u32>(axes.size()), s_String_class, nullptr);
|
||||
u32 name_array_index = 0;
|
||||
Assert(name_array != nullptr);
|
||||
for (const auto& [axis_name, axis_code, axis_type] : axes)
|
||||
{
|
||||
jstring axis_name_jstr = env->NewStringUTF(axis_name.c_str());
|
||||
env->SetObjectArrayElement(name_array, name_array_index++, axis_name_jstr);
|
||||
env->DeleteLocalRef(axis_name_jstr);
|
||||
}
|
||||
|
||||
return name_array;
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerButtonEvent, jobject obj, jint controller_index, jint button_index, jboolean pressed)
|
||||
{
|
||||
AndroidHelpers::GetNativeClass(env, obj)->HandleControllerButtonEvent(controller_index, button_index, pressed);
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerAxisEvent, jobject obj, jint controller_index, jint axis_index, jfloat value)
|
||||
{
|
||||
AndroidHelpers::GetNativeClass(env, obj)->HandleControllerAxisEvent(controller_index, axis_index, value);
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getInputProfileNames, jobject obj)
|
||||
{
|
||||
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
|
||||
return hi->GetInputProfileNames(env);
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_loadInputProfile, jobject obj, jstring name)
|
||||
{
|
||||
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
|
||||
return hi->ApplyInputProfile(AndroidHelpers::JStringToString(env, name).c_str());
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_saveInputProfile, jobject obj, jstring name)
|
||||
{
|
||||
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
|
||||
return hi->SaveInputProfile(AndroidHelpers::JStringToString(env, name).c_str());
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_refreshGameList, jobject obj, jboolean invalidate_cache,
|
||||
jboolean invalidate_database, jobject progress_callback)
|
||||
{
|
||||
|
|
|
@ -51,6 +51,8 @@ public:
|
|||
void SetControllerType(u32 index, std::string_view type_name);
|
||||
void SetControllerButtonState(u32 index, s32 button_code, bool pressed);
|
||||
void SetControllerAxisState(u32 index, s32 button_code, float value);
|
||||
void HandleControllerButtonEvent(u32 controller_index, u32 button_index, bool pressed);
|
||||
void HandleControllerAxisEvent(u32 controller_index, u32 axis_index, float value);
|
||||
void SetFastForwardEnabled(bool enabled);
|
||||
|
||||
void RefreshGameList(bool invalidate_cache, bool invalidate_database, ProgressCallback* progress_callback);
|
||||
|
@ -58,6 +60,10 @@ public:
|
|||
|
||||
bool ImportPatchCodesFromString(const std::string& str);
|
||||
|
||||
jobjectArray GetInputProfileNames(JNIEnv* env) const;
|
||||
bool ApplyInputProfile(const char* profile_name);
|
||||
bool SaveInputProfile(const char* profile_name);
|
||||
|
||||
protected:
|
||||
void SetUserDirectory() override;
|
||||
void LoadSettings() override;
|
||||
|
@ -66,6 +72,7 @@ protected:
|
|||
bool AcquireHostDisplay() override;
|
||||
void ReleaseHostDisplay() override;
|
||||
std::unique_ptr<AudioStream> CreateAudioStream(AudioBackend backend) override;
|
||||
void UpdateControllerInterface() override;
|
||||
|
||||
void OnSystemPaused(bool paused) override;
|
||||
void OnSystemDestroyed() override;
|
||||
|
|
|
@ -308,6 +308,20 @@ std::vector<std::string> AndroidSettingsInterface::GetStringList(const char* sec
|
|||
env, env->CallObjectMethod(m_java_shared_preferences, m_get_string_set, key_string.Get(), nullptr));
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
env->ExceptionClear();
|
||||
|
||||
// this might just be a string, not a string set
|
||||
LocalRefHolder<jstring> string_object(
|
||||
env, reinterpret_cast<jstring>(env->CallObjectMethod(m_java_shared_preferences, m_get_string, key_string.Get(), nullptr)));
|
||||
|
||||
if (!env->ExceptionCheck()) {
|
||||
std::vector<std::string> ret;
|
||||
if (string_object)
|
||||
ret.push_back(AndroidHelpers::JStringToString(env, string_object));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
env->ExceptionClear();
|
||||
return {};
|
||||
}
|
||||
|
|
|
@ -34,6 +34,15 @@
|
|||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.github.stenzek.duckstation.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ControllerMappingActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.github.stenzek.duckstation.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
|
|
|
@ -71,6 +71,16 @@ public class AndroidHostInterface {
|
|||
|
||||
public static native int getControllerAxisCode(String controllerType, String axisName);
|
||||
|
||||
public static native String[] getControllerButtonNames(String controllerType);
|
||||
public static native String[] getControllerAxisNames(String controllerType);
|
||||
|
||||
public native void handleControllerButtonEvent(int controllerIndex, int buttonIndex, boolean pressed);
|
||||
public native void handleControllerAxisEvent(int controllerIndex, int axisIndex, float value);
|
||||
|
||||
public native String[] getInputProfileNames();
|
||||
public native boolean loadInputProfile(String name);
|
||||
public native boolean saveInputProfile(String name);
|
||||
|
||||
public native void refreshGameList(boolean invalidateCache, boolean invalidateDatabase, AndroidProgressCallback progressCallback);
|
||||
|
||||
public native GameListEntry[] getGameListEntries();
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class ControllerBindingDialog extends AlertDialog {
|
||||
private boolean mIsAxis;
|
||||
private String mSettingKey;
|
||||
private String mCurrentBinding;
|
||||
|
||||
public ControllerBindingDialog(Context context, String buttonName, String settingKey, String currentBinding, boolean isAxis) {
|
||||
super(context);
|
||||
|
||||
mIsAxis = isAxis;
|
||||
mSettingKey = settingKey;
|
||||
mCurrentBinding = currentBinding;
|
||||
if (mCurrentBinding == null)
|
||||
mCurrentBinding = getContext().getString(R.string.controller_binding_dialog_no_binding);
|
||||
|
||||
setTitle(buttonName);
|
||||
updateMessage();
|
||||
setButton(BUTTON_POSITIVE, context.getString(R.string.controller_binding_dialog_cancel), (dialogInterface, button) -> dismiss());
|
||||
setButton(BUTTON_NEGATIVE, context.getString(R.string.controller_binding_dialog_clear), (dialogInterface, button) -> {
|
||||
mCurrentBinding = null;
|
||||
updateBinding();
|
||||
dismiss();
|
||||
});
|
||||
|
||||
setOnKeyListener(new DialogInterface.OnKeyListener() {
|
||||
@Override
|
||||
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
|
||||
if (onKeyDown(keyCode, event))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateMessage() {
|
||||
setMessage(String.format(getContext().getString(R.string.controller_binding_dialog_message), mCurrentBinding));
|
||||
}
|
||||
|
||||
private void updateBinding() {
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
|
||||
if (mCurrentBinding != null) {
|
||||
ArraySet<String> values = new ArraySet<>();
|
||||
values.add(mCurrentBinding);
|
||||
editor.putStringSet(mSettingKey, values);
|
||||
} else {
|
||||
try {
|
||||
editor.remove(mSettingKey);
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (mIsAxis || !EmulationSurfaceView.isDPadOrButtonEvent(event))
|
||||
return super.onKeyUp(keyCode, event);
|
||||
|
||||
int buttonIndex = EmulationSurfaceView.getButtonIndexForKeyCode(keyCode);
|
||||
if (buttonIndex < 0)
|
||||
return super.onKeyUp(keyCode, event);
|
||||
|
||||
// TODO: Multiple controllers
|
||||
final int controllerIndex = 0;
|
||||
mCurrentBinding = String.format("Controller%d/Button%d", controllerIndex, buttonIndex);
|
||||
updateMessage();
|
||||
updateBinding();
|
||||
dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
private int mUpdatedAxisCode = -1;
|
||||
|
||||
private void setAxisCode(int axisCode, boolean positive) {
|
||||
final int axisIndex = EmulationSurfaceView.getAxisIndexForAxisCode(axisCode);
|
||||
if (mUpdatedAxisCode >= 0 || axisIndex < 0)
|
||||
return;
|
||||
|
||||
mUpdatedAxisCode = axisCode;
|
||||
|
||||
final int controllerIndex = 0;
|
||||
if (mIsAxis)
|
||||
mCurrentBinding = String.format("Controller%d/Axis%d", controllerIndex, axisIndex);
|
||||
else
|
||||
mCurrentBinding = String.format("Controller%d/%cAxis%d", controllerIndex, (positive) ? '+' : '-', axisIndex);
|
||||
|
||||
updateBinding();
|
||||
updateMessage();
|
||||
dismiss();
|
||||
}
|
||||
|
||||
final static float DETECT_THRESHOLD = 0.25f;
|
||||
|
||||
private HashMap<Integer, float[]> mStartingAxisValues = new HashMap<>();
|
||||
|
||||
private boolean doAxisDetection(MotionEvent event) {
|
||||
if ((event.getSource() & (InputDevice.SOURCE_JOYSTICK | InputDevice.SOURCE_GAMEPAD | InputDevice.SOURCE_DPAD)) == 0)
|
||||
return false;
|
||||
|
||||
final int[] axisCodes = EmulationSurfaceView.getKnownAxisCodes();
|
||||
final int deviceId = event.getDeviceId();
|
||||
|
||||
if (!mStartingAxisValues.containsKey(deviceId)) {
|
||||
final float[] axisValues = new float[axisCodes.length];
|
||||
for (int axisIndex = 0; axisIndex < axisCodes.length; axisIndex++) {
|
||||
final int axisCode = axisCodes[axisIndex];
|
||||
|
||||
// these are binary, so start at zero
|
||||
if (axisCode == MotionEvent.AXIS_HAT_X || axisCode == MotionEvent.AXIS_HAT_Y)
|
||||
axisValues[axisIndex] = 0.0f;
|
||||
else
|
||||
axisValues[axisIndex] = event.getAxisValue(axisCode);
|
||||
}
|
||||
|
||||
mStartingAxisValues.put(deviceId, axisValues);
|
||||
}
|
||||
|
||||
final float[] axisValues = mStartingAxisValues.get(deviceId);
|
||||
for (int axisIndex = 0; axisIndex < axisCodes.length; axisIndex++) {
|
||||
final float newValue = event.getAxisValue(axisCodes[axisIndex]);
|
||||
if (Math.abs(newValue - axisValues[axisIndex]) >= DETECT_THRESHOLD) {
|
||||
setAxisCode(axisCodes[axisIndex], newValue >= 0.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
|
||||
if (doAxisDetection(event))
|
||||
return true;
|
||||
|
||||
return super.onGenericMotionEvent(event);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class ControllerBindingPreference extends Preference {
|
||||
private boolean mIsAxis;
|
||||
private String mBindingName;
|
||||
private String mValue;
|
||||
private TextView mValueView;
|
||||
|
||||
private static int getIconForButton(String buttonName) {
|
||||
if (buttonName.equals("Up")) {
|
||||
return R.drawable.ic_controller_up_button_pressed;
|
||||
} else if (buttonName.equals("Right")) {
|
||||
return R.drawable.ic_controller_right_button_pressed;
|
||||
} else if (buttonName.equals("Down")) {
|
||||
return R.drawable.ic_controller_down_button_pressed;
|
||||
} else if (buttonName.equals("Left")) {
|
||||
return R.drawable.ic_controller_left_button_pressed;
|
||||
} else if (buttonName.equals("Triangle")) {
|
||||
return R.drawable.ic_controller_triangle_button_pressed;
|
||||
} else if (buttonName.equals("Circle")) {
|
||||
return R.drawable.ic_controller_circle_button_pressed;
|
||||
} else if (buttonName.equals("Cross")) {
|
||||
return R.drawable.ic_controller_cross_button_pressed;
|
||||
} else if (buttonName.equals("Square")) {
|
||||
return R.drawable.ic_controller_square_button_pressed;
|
||||
} else if (buttonName.equals("Start")) {
|
||||
return R.drawable.ic_controller_start_button_pressed;
|
||||
} else if (buttonName.equals("Select")) {
|
||||
return R.drawable.ic_controller_select_button_pressed;
|
||||
} else if (buttonName.equals("L1")) {
|
||||
return R.drawable.ic_controller_l1_button_pressed;
|
||||
} else if (buttonName.equals("L2")) {
|
||||
return R.drawable.ic_controller_l2_button_pressed;
|
||||
} else if (buttonName.equals("R1")) {
|
||||
return R.drawable.ic_controller_r1_button_pressed;
|
||||
} else if (buttonName.equals("R2")) {
|
||||
return R.drawable.ic_controller_r2_button_pressed;
|
||||
}
|
||||
|
||||
return R.drawable.ic_baseline_radio_button_unchecked_24;
|
||||
}
|
||||
|
||||
private static int getIconForAxis(String axisName) {
|
||||
return R.drawable.ic_baseline_radio_button_checked_24;
|
||||
}
|
||||
|
||||
public ControllerBindingPreference(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
setWidgetLayoutResource(R.layout.layout_controller_binding_preference);
|
||||
setIconSpaceReserved(false);
|
||||
}
|
||||
|
||||
public ControllerBindingPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setWidgetLayoutResource(R.layout.layout_controller_binding_preference);
|
||||
setIconSpaceReserved(false);
|
||||
}
|
||||
|
||||
public ControllerBindingPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
setWidgetLayoutResource(R.layout.layout_controller_binding_preference);
|
||||
setIconSpaceReserved(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
|
||||
ImageView iconView = ((ImageView)holder.findViewById(R.id.controller_binding_icon));
|
||||
TextView nameView = ((TextView)holder.findViewById(R.id.controller_binding_name));
|
||||
mValueView = ((TextView)holder.findViewById(R.id.controller_binding_value));
|
||||
|
||||
iconView.setImageDrawable(ContextCompat.getDrawable(getContext(), getIconForButton(mBindingName)));
|
||||
nameView.setText(mBindingName);
|
||||
updateValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
ControllerBindingDialog dialog = new ControllerBindingDialog(getContext(), mBindingName, getKey(), mValue, mIsAxis);
|
||||
dialog.setOnDismissListener((dismissedDialog) -> updateValue());
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public void initButton(int controllerIndex, String buttonName) {
|
||||
mBindingName = buttonName;
|
||||
mIsAxis = false;
|
||||
setKey(String.format("Controller%d/Button%s", controllerIndex, buttonName));
|
||||
updateValue();
|
||||
}
|
||||
|
||||
public void initAxis(int controllerIndex, String axisName) {
|
||||
mBindingName = axisName;
|
||||
mIsAxis = true;
|
||||
setKey(String.format("Controller%d/Axis%s", controllerIndex, axisName));
|
||||
updateValue();
|
||||
}
|
||||
|
||||
private void updateValue(String value) {
|
||||
mValue = value;
|
||||
if (mValueView != null) {
|
||||
if (value != null)
|
||||
mValueView.setText(value);
|
||||
else
|
||||
mValueView.setText(getContext().getString(R.string.controller_binding_dialog_no_binding));
|
||||
}
|
||||
}
|
||||
|
||||
public void updateValue() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
Set<String> values = PreferenceHelpers.getStringSet(prefs, getKey());
|
||||
if (values != null) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String value : values) {
|
||||
if (sb.length() > 0)
|
||||
sb.append(", ");
|
||||
sb.append(value);
|
||||
}
|
||||
|
||||
updateValue(sb.toString());
|
||||
} else {
|
||||
updateValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearBinding(SharedPreferences.Editor prefEditor) {
|
||||
try {
|
||||
prefEditor.remove(getKey());
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
|
||||
updateValue(null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ControllerMappingActivity extends AppCompatActivity {
|
||||
|
||||
private static final int NUM_CONTROLLER_PORTS = 2;
|
||||
|
||||
private ArrayList<ControllerBindingPreference> mPreferences = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.settings_activity);
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, new SettingsCollectionFragment(this))
|
||||
.commit();
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.controller_mapping_activity_title);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.menu_controller_mapping, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
final int id = item.getItemId();
|
||||
|
||||
//noinspection SimplifiableIfStatement
|
||||
if (id == R.id.action_load_profile) {
|
||||
doLoadProfile();
|
||||
return true;
|
||||
} else if (id == R.id.action_save_profile) {
|
||||
doSaveProfile();
|
||||
return true;
|
||||
} else if (id == R.id.action_clear_bindings) {
|
||||
doClearBindings();
|
||||
return true;
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void displayError(String text) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.emulation_activity_error)
|
||||
.setMessage(text)
|
||||
.setNegativeButton(R.string.main_activity_ok, ((dialog, which) -> dialog.dismiss()))
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void doLoadProfile() {
|
||||
final String[] profileNames = AndroidHostInterface.getInstance().getInputProfileNames();
|
||||
if (profileNames == null) {
|
||||
displayError(getString(R.string.controller_mapping_activity_no_profiles_found));
|
||||
return;
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.controller_mapping_activity_select_input_profile)
|
||||
.setItems(profileNames, (dialog, choice) -> {
|
||||
doLoadProfile(profileNames[choice]);
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton("Cancel", ((dialog, which) -> dialog.dismiss()))
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void doLoadProfile(String profileName) {
|
||||
if (!AndroidHostInterface.getInstance().loadInputProfile(profileName)) {
|
||||
displayError(String.format(getString(R.string.controller_mapping_activity_failed_to_load_profile), profileName));
|
||||
return;
|
||||
}
|
||||
|
||||
updateAllBindings();
|
||||
}
|
||||
|
||||
private void doSaveProfile() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
final EditText input = new EditText(this);
|
||||
builder.setTitle(R.string.controller_mapping_activity_input_profile_name);
|
||||
builder.setView(input);
|
||||
builder.setPositiveButton(R.string.controller_mapping_activity_save, (dialog, which) -> {
|
||||
final String name = input.getText().toString();
|
||||
if (name.isEmpty()) {
|
||||
displayError(getString(R.string.controller_mapping_activity_name_must_be_provided));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AndroidHostInterface.getInstance().saveInputProfile(name)) {
|
||||
displayError(getString(R.string.controller_mapping_activity_failed_to_save_input_profile));
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.makeText(ControllerMappingActivity.this, String.format(ControllerMappingActivity.this.getString(R.string.controller_mapping_activity_input_profile_saved), name),
|
||||
Toast.LENGTH_LONG).show();
|
||||
});
|
||||
builder.setNegativeButton(R.string.controller_mapping_activity_cancel, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void doClearBindings() {
|
||||
SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(this).edit();
|
||||
for (ControllerBindingPreference pref : mPreferences)
|
||||
pref.clearBinding(prefEdit);
|
||||
prefEdit.commit();
|
||||
}
|
||||
|
||||
private void updateAllBindings() {
|
||||
for (ControllerBindingPreference pref : mPreferences)
|
||||
pref.updateValue();
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragmentCompat {
|
||||
private ControllerMappingActivity activity;
|
||||
private int controllerIndex;
|
||||
|
||||
public SettingsFragment(ControllerMappingActivity activity, int controllerIndex) {
|
||||
this.activity = activity;
|
||||
this.controllerIndex = controllerIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
|
||||
String controllerType = sp.getString(String.format("Controller%d/Type", controllerIndex), "None");
|
||||
String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType);
|
||||
String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType);
|
||||
|
||||
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
||||
if (controllerButtons != null) {
|
||||
for (String buttonName : controllerButtons) {
|
||||
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
||||
cbp.initButton(controllerIndex, buttonName);
|
||||
ps.addPreference(cbp);
|
||||
activity.mPreferences.add(cbp);
|
||||
}
|
||||
}
|
||||
if (axisButtons != null) {
|
||||
for (String axisName : axisButtons) {
|
||||
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
||||
cbp.initAxis(controllerIndex, axisName);
|
||||
ps.addPreference(cbp);
|
||||
activity.mPreferences.add(cbp);
|
||||
}
|
||||
}
|
||||
|
||||
setPreferenceScreen(ps);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsCollectionFragment extends Fragment {
|
||||
private ControllerMappingActivity activity;
|
||||
private SettingsCollectionAdapter adapter;
|
||||
private ViewPager2 viewPager;
|
||||
|
||||
public SettingsCollectionFragment(ControllerMappingActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_controller_mapping, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
adapter = new SettingsCollectionAdapter(activity, this);
|
||||
viewPager = view.findViewById(R.id.view_pager);
|
||||
viewPager.setAdapter(adapter);
|
||||
|
||||
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
|
||||
new TabLayoutMediator(tabLayout, viewPager,
|
||||
(tab, position) -> tab.setText(String.format("Port %d", position + 1))
|
||||
).attach();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsCollectionAdapter extends FragmentStateAdapter {
|
||||
private ControllerMappingActivity activity;
|
||||
|
||||
public SettingsCollectionAdapter(@NonNull ControllerMappingActivity activity, @NonNull Fragment fragment) {
|
||||
super(fragment);
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
return new SettingsFragment(activity, position + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return NUM_CONTROLLER_PORTS;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
|
@ -8,7 +9,10 @@ import android.view.KeyEvent;
|
|||
import android.view.MotionEvent;
|
||||
import android.view.SurfaceView;
|
||||
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class EmulationSurfaceView extends SurfaceView {
|
||||
|
@ -24,7 +28,7 @@ public class EmulationSurfaceView extends SurfaceView {
|
|||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
private boolean isDPadOrButtonEvent(KeyEvent event) {
|
||||
public static boolean isDPadOrButtonEvent(KeyEvent event) {
|
||||
final int source = event.getSource();
|
||||
return (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
|
||||
(source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD ||
|
||||
|
@ -51,66 +55,65 @@ public class EmulationSurfaceView extends SurfaceView {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (!isDPadOrButtonEvent(event) || isExternalKeyCode(keyCode))
|
||||
return false;
|
||||
private static final int[] buttonKeyCodes = new int[] {
|
||||
KeyEvent.KEYCODE_BUTTON_A, // 0/Cross
|
||||
KeyEvent.KEYCODE_BUTTON_B, // 1/Circle
|
||||
KeyEvent.KEYCODE_BUTTON_X, // 2/Square
|
||||
KeyEvent.KEYCODE_BUTTON_Y, // 3/Triangle
|
||||
KeyEvent.KEYCODE_BUTTON_SELECT, // 4/Select
|
||||
KeyEvent.KEYCODE_BUTTON_MODE, // 5/Analog
|
||||
KeyEvent.KEYCODE_BUTTON_START, // 6/Start
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBL, // 7/L3
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBR, // 8/R3
|
||||
KeyEvent.KEYCODE_BUTTON_L1, // 9/L1
|
||||
KeyEvent.KEYCODE_BUTTON_R1, // 10/R1
|
||||
KeyEvent.KEYCODE_DPAD_UP, // 11/Up
|
||||
KeyEvent.KEYCODE_DPAD_DOWN, // 12/Down
|
||||
KeyEvent.KEYCODE_DPAD_LEFT, // 13/Left
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT, // 14/Right
|
||||
KeyEvent.KEYCODE_BUTTON_L2, // 15
|
||||
KeyEvent.KEYCODE_BUTTON_R2, // 16
|
||||
KeyEvent.KEYCODE_BUTTON_C, // 17
|
||||
KeyEvent.KEYCODE_BUTTON_Z, // 18
|
||||
};
|
||||
private static final int[] axisCodes = new int[] {
|
||||
MotionEvent.AXIS_X, // 0/LeftX
|
||||
MotionEvent.AXIS_Y, // 1/LeftY
|
||||
MotionEvent.AXIS_Z, // 2/RightX
|
||||
MotionEvent.AXIS_RZ, // 3/RightY
|
||||
MotionEvent.AXIS_LTRIGGER, // 4/L2
|
||||
MotionEvent.AXIS_RTRIGGER, // 5/R2
|
||||
MotionEvent.AXIS_RX, // 6
|
||||
MotionEvent.AXIS_RY, // 7
|
||||
MotionEvent.AXIS_HAT_X, // 8
|
||||
MotionEvent.AXIS_HAT_Y, // 9
|
||||
};
|
||||
|
||||
if (event.getRepeatCount() == 0)
|
||||
handleControllerKey(event.getDeviceId(), keyCode, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (!isDPadOrButtonEvent(event) || isExternalKeyCode(keyCode))
|
||||
return false;
|
||||
|
||||
if (event.getRepeatCount() == 0)
|
||||
handleControllerKey(event.getDeviceId(), keyCode, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
final int source = event.getSource();
|
||||
if ((source & (InputDevice.SOURCE_JOYSTICK | InputDevice.SOURCE_GAMEPAD | InputDevice.SOURCE_DPAD)) == 0)
|
||||
return false;
|
||||
|
||||
final int deviceId = event.getDeviceId();
|
||||
for (AxisMapping mapping : mControllerAxisMapping) {
|
||||
if (mapping.deviceId != deviceId)
|
||||
continue;
|
||||
|
||||
final float axisValue = event.getAxisValue(mapping.deviceAxisOrButton);
|
||||
float emuValue;
|
||||
|
||||
if (mapping.deviceMotionRange != null) {
|
||||
final float transformedValue = (axisValue - mapping.deviceMotionRange.getMin()) / mapping.deviceMotionRange.getRange();
|
||||
emuValue = (transformedValue * 2.0f) - 1.0f;
|
||||
} else {
|
||||
emuValue = axisValue;
|
||||
}
|
||||
Log.d("EmulationSurfaceView", String.format("axis %d value %f emuvalue %f", mapping.deviceAxisOrButton, axisValue, emuValue));
|
||||
|
||||
if (mapping.axisMapping >= 0) {
|
||||
AndroidHostInterface.getInstance().setControllerAxisState(0, mapping.axisMapping, emuValue);
|
||||
}
|
||||
|
||||
final float DEAD_ZONE = 0.25f;
|
||||
if (mapping.negativeButton >= 0) {
|
||||
AndroidHostInterface.getInstance().setControllerButtonState(0, mapping.negativeButton, (emuValue <= -DEAD_ZONE));
|
||||
}
|
||||
if (mapping.positiveButton >= 0) {
|
||||
AndroidHostInterface.getInstance().setControllerButtonState(0, mapping.positiveButton, (emuValue >= DEAD_ZONE));
|
||||
}
|
||||
public static int getButtonIndexForKeyCode(int keyCode) {
|
||||
for (int buttonIndex = 0; buttonIndex < buttonKeyCodes.length; buttonIndex++) {
|
||||
if (buttonKeyCodes[buttonIndex] == keyCode)
|
||||
return buttonIndex;
|
||||
}
|
||||
|
||||
return true;
|
||||
Log.e("EmulationSurfaceView", String.format("Button code %d not found", keyCode));
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static int[] getKnownAxisCodes() {
|
||||
return axisCodes;
|
||||
}
|
||||
|
||||
public static int getAxisIndexForAxisCode(int axisCode) {
|
||||
for (int axisIndex = 0; axisIndex < axisCodes.length; axisIndex++) {
|
||||
if (axisCodes[axisIndex] == axisCode)
|
||||
return axisIndex;
|
||||
}
|
||||
|
||||
Log.e("EmulationSurfaceView", String.format("Axis code %d not found", axisCode));
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
private class ButtonMapping {
|
||||
public ButtonMapping(int deviceId, int deviceButton, int controllerIndex, int button) {
|
||||
this.deviceId = deviceId;
|
||||
|
@ -158,16 +161,92 @@ public class EmulationSurfaceView extends SurfaceView {
|
|||
private ArrayList<ButtonMapping> mControllerKeyMapping;
|
||||
private ArrayList<AxisMapping> mControllerAxisMapping;
|
||||
|
||||
private void addControllerKeyMapping(int deviceId, int keyCode, int controllerIndex, String controllerType, String buttonName) {
|
||||
int mapping = AndroidHostInterface.getControllerButtonCode(controllerType, buttonName);
|
||||
Log.i("EmulationSurfaceView", String.format("Map %d to %d (%s)", keyCode, mapping,
|
||||
buttonName));
|
||||
if (mapping >= 0) {
|
||||
mControllerKeyMapping.add(new ButtonMapping(deviceId, keyCode, controllerIndex, mapping));
|
||||
private boolean handleControllerKey(int deviceId, int keyCode, boolean pressed) {
|
||||
boolean result = false;
|
||||
for (ButtonMapping mapping : mControllerKeyMapping) {
|
||||
if (mapping.deviceId != deviceId || mapping.deviceAxisOrButton != keyCode)
|
||||
continue;
|
||||
|
||||
AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.buttonMapping, pressed);
|
||||
Log.d("EmulationSurfaceView", String.format("handleControllerKey %d -> %d %d", keyCode, mapping.buttonMapping, pressed ? 1 : 0));
|
||||
result = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addControllerAxisMapping(int deviceId, List<InputDevice.MotionRange> motionRanges, int axis, int controllerIndex, String controllerType, String axisName, String negativeButtonName, String positiveButtonName) {
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (!isDPadOrButtonEvent(event) || isExternalKeyCode(keyCode))
|
||||
return false;
|
||||
|
||||
if (event.getRepeatCount() == 0)
|
||||
handleControllerKey(event.getDeviceId(), keyCode, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (!isDPadOrButtonEvent(event) || isExternalKeyCode(keyCode))
|
||||
return false;
|
||||
|
||||
if (event.getRepeatCount() == 0)
|
||||
handleControllerKey(event.getDeviceId(), keyCode, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
final int source = event.getSource();
|
||||
if ((source & (InputDevice.SOURCE_JOYSTICK | InputDevice.SOURCE_GAMEPAD | InputDevice.SOURCE_DPAD)) == 0)
|
||||
return false;
|
||||
|
||||
final int deviceId = event.getDeviceId();
|
||||
for (AxisMapping mapping : mControllerAxisMapping) {
|
||||
if (mapping.deviceId != deviceId)
|
||||
continue;
|
||||
|
||||
final float axisValue = event.getAxisValue(mapping.deviceAxisOrButton);
|
||||
float emuValue;
|
||||
|
||||
if (mapping.deviceMotionRange != null) {
|
||||
final float transformedValue = (axisValue - mapping.deviceMotionRange.getMin()) / mapping.deviceMotionRange.getRange();
|
||||
emuValue = (transformedValue * 2.0f) - 1.0f;
|
||||
} else {
|
||||
emuValue = axisValue;
|
||||
}
|
||||
Log.d("EmulationSurfaceView", String.format("axis %d value %f emuvalue %f", mapping.deviceAxisOrButton, axisValue, emuValue));
|
||||
|
||||
if (mapping.axisMapping >= 0) {
|
||||
AndroidHostInterface.getInstance().handleControllerAxisEvent(0, mapping.axisMapping, emuValue);
|
||||
}
|
||||
|
||||
final float DEAD_ZONE = 0.25f;
|
||||
if (mapping.negativeButton >= 0) {
|
||||
AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.negativeButton, (emuValue <= -DEAD_ZONE));
|
||||
}
|
||||
if (mapping.positiveButton >= 0) {
|
||||
AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.positiveButton, (emuValue >= DEAD_ZONE));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean addControllerKeyMapping(int deviceId, int keyCode, int controllerIndex) {
|
||||
int mapping = getButtonIndexForKeyCode(keyCode);
|
||||
Log.i("EmulationSurfaceView", String.format("Map %d to %d", keyCode, mapping));
|
||||
if (mapping >= 0) {
|
||||
mControllerKeyMapping.add(new ButtonMapping(deviceId, keyCode, controllerIndex, mapping));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean addControllerAxisMapping(int deviceId, List<InputDevice.MotionRange> motionRanges, int axis, int controllerIndex) {
|
||||
InputDevice.MotionRange range = null;
|
||||
for (InputDevice.MotionRange curRange : motionRanges) {
|
||||
if (curRange.getAxis() == axis) {
|
||||
|
@ -176,26 +255,26 @@ public class EmulationSurfaceView extends SurfaceView {
|
|||
}
|
||||
}
|
||||
if (range == null)
|
||||
return;
|
||||
return false;
|
||||
|
||||
if (axisName != null) {
|
||||
int mapping = AndroidHostInterface.getControllerAxisCode(controllerType, axisName);
|
||||
Log.i("EmulationSurfaceView", String.format("Map axis %d to %d (%s)", axis, mapping, axisName));
|
||||
if (mapping >= 0) {
|
||||
mControllerAxisMapping.add(new AxisMapping(deviceId, axis, range, controllerIndex, mapping));
|
||||
return;
|
||||
}
|
||||
int mapping = getAxisIndexForAxisCode(axis);
|
||||
int negativeButton = -1;
|
||||
int positiveButton = -1;
|
||||
|
||||
if (mapping >= 0) {
|
||||
Log.i("EmulationSurfaceView", String.format("Map axis %d to %d", axis, mapping));
|
||||
mControllerAxisMapping.add(new AxisMapping(deviceId, axis, range, controllerIndex, mapping));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (negativeButtonName != null && positiveButtonName != null) {
|
||||
final int negativeMapping = AndroidHostInterface.getControllerButtonCode(controllerType, negativeButtonName);
|
||||
final int positiveMapping = AndroidHostInterface.getControllerButtonCode(controllerType, positiveButtonName);
|
||||
Log.i("EmulationSurfaceView", String.format("Map axis %d to %d %d (button %s %s)", axis, negativeMapping, positiveMapping,
|
||||
negativeButtonName, positiveButtonName));
|
||||
if (negativeMapping >= 0 && positiveMapping >= 0) {
|
||||
mControllerAxisMapping.add(new AxisMapping(deviceId, axis, range, controllerIndex, positiveMapping, negativeMapping));
|
||||
}
|
||||
if (negativeButton >= 0 && negativeButton >= 0) {
|
||||
Log.i("EmulationSurfaceView", String.format("Map axis %d to buttons %d %d", axis, negativeButton, positiveButton));
|
||||
mControllerAxisMapping.add(new AxisMapping(deviceId, axis, range, controllerIndex, positiveButton, negativeButton));
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.w("EmulationSurfaceView", String.format("Axis %d was not mapped", axis));
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isJoystickDevice(int deviceId) {
|
||||
|
@ -229,48 +308,18 @@ public class EmulationSurfaceView extends SurfaceView {
|
|||
List<InputDevice.MotionRange> motionRanges = device.getMotionRanges();
|
||||
int controllerIndex = 0;
|
||||
|
||||
addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_DPAD_UP, controllerIndex, controllerType, "Up");
|
||||
addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_DPAD_RIGHT, controllerIndex, controllerType, "Right");
|
||||
addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_DPAD_DOWN, controllerIndex, controllerType, "Down");
|
||||
addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_DPAD_LEFT, controllerIndex, controllerType, "Left");
|
||||
addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_L1, controllerIndex, controllerType, "L1");
|
||||
addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_L2, controllerIndex, controllerType, "L2");
|
||||
addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_SELECT, controllerIndex, controllerType, "Select");
|
||||
addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_START, controllerIndex, controllerType, "Start");
|
||||
addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_Y, controllerIndex, controllerType, "Triangle");
|
||||
addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_B, controllerIndex, controllerType, "Circle");
|
||||
addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_A, controllerIndex, controllerType, "Cross");
|
||||
addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_X, controllerIndex, controllerType, "Square");
|
||||
addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_R1, controllerIndex, controllerType, "R1");
|
||||
addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_R2, controllerIndex, controllerType, "R2");
|
||||
for (int keyCode : buttonKeyCodes) {
|
||||
addControllerKeyMapping(deviceId, keyCode, controllerIndex);
|
||||
}
|
||||
|
||||
if (motionRanges != null) {
|
||||
addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_X, controllerIndex, controllerType, "LeftX", null, null);
|
||||
addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_Y, controllerIndex, controllerType, "LeftY", null, null);
|
||||
addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_RX, controllerIndex, controllerType, "RightX", null, null);
|
||||
addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_RY, controllerIndex, controllerType, "RightY", null, null);
|
||||
addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_Z, controllerIndex, controllerType, "RightX", null, null);
|
||||
addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_RZ, controllerIndex, controllerType, "RightY", null, null);
|
||||
addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_LTRIGGER, controllerIndex, controllerType, "L2", "L2", "L2");
|
||||
addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_RTRIGGER, controllerIndex, controllerType, "R2", "R2", "R2");
|
||||
addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_HAT_X, controllerIndex, controllerType, null, "Left", "Right");
|
||||
addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_HAT_Y, controllerIndex, controllerType, null, "Up", "Down");
|
||||
for (int axisCode : axisCodes) {
|
||||
addControllerAxisMapping(deviceId, motionRanges, axisCode, controllerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !mControllerKeyMapping.isEmpty() || !mControllerKeyMapping.isEmpty();
|
||||
}
|
||||
|
||||
private boolean handleControllerKey(int deviceId, int keyCode, boolean pressed) {
|
||||
boolean result = false;
|
||||
for (ButtonMapping mapping : mControllerKeyMapping) {
|
||||
if (mapping.deviceId != deviceId || mapping.deviceAxisOrButton != keyCode)
|
||||
continue;
|
||||
|
||||
AndroidHostInterface.getInstance().setControllerButtonState(0, mapping.buttonMapping, pressed);
|
||||
Log.d("EmulationSurfaceView", String.format("handleControllerKey %d -> %d %d", keyCode, mapping.buttonMapping, pressed ? 1 : 0));
|
||||
result = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -218,6 +218,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivityForResult(intent, REQUEST_SETTINGS);
|
||||
return true;
|
||||
} else if (id == R.id.action_controller_mapping) {
|
||||
Intent intent = new Intent(this, ControllerMappingActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
} else if (id == R.id.action_show_version) {
|
||||
showVersion();
|
||||
return true;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,6h-8l-2,-2L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8h16v10z"/>
|
||||
</vector>
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15,7.5V2H9v5.5l3,3 3,-3zM7.5,9H2v6h5.5l3,-3 -3,-3zM9,16.5V22h6v-5.5l-3,-3 -3,3zM16.5,9l-3,3 3,3H22V9h-5.5z"/>
|
||||
</vector>
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5 5,-2.24 5,-5 -2.24,-5 -5,-5zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
|
||||
</vector>
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
|
||||
</vector>
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabTextAppearance="@style/TabTextAppearance"
|
||||
app:tabMode="fixed" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/view_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/controller_binding_icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_alignParentTop="true"
|
||||
tools:srcCompat="@drawable/ic_media_cdrom" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/controller_binding_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:text="Up"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_toRightOf="@id/controller_binding_icon" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/controller_binding_value"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:text="Controller0/Button0"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:layout_toRightOf="@id/controller_binding_icon"
|
||||
android:layout_below="@id/controller_binding_name" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_load_profile"
|
||||
android:title="Load Profile"
|
||||
android:icon="@drawable/ic_baseline_folder_open_24"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_save_profile"
|
||||
android:title="Save Profile"
|
||||
android:icon="@drawable/ic_baseline_save_24"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_clear_bindings"
|
||||
android:title="Clear Bindings"/>
|
||||
</menu>
|
|
@ -36,6 +36,11 @@
|
|||
android:id="@+id/action_discord_server"
|
||||
android:title="@string/menu_main_discord_server" />
|
||||
</group>
|
||||
<item
|
||||
android:id="@+id/action_controller_mapping"
|
||||
android:icon="@drawable/ic_baseline_gamepad_24"
|
||||
android:orderInCategory="100"
|
||||
android:title="@string/action_controller_mapping" />
|
||||
<item
|
||||
android:id="@+id/action_settings"
|
||||
android:icon="@drawable/ic_baseline_settings_24"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<resources>
|
||||
<string name="app_name">DuckStation</string>
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="action_controller_mapping">Controller Mapping</string>
|
||||
<string name="title_activity_settings">Settings</string>
|
||||
<string name="settings_console_region">Console Region</string>
|
||||
<string name="settings_console_tty_output">Enable TTY Output</string>
|
||||
|
@ -160,4 +161,20 @@
|
|||
<string name="touchscreen_controller_reset_layout">Reset Layout</string>
|
||||
<string name="emulation_activity_touchscreen_controller_not_active">Touchscreen controller is not active.</string>
|
||||
<string name="settings_theme">Theme</string>
|
||||
<string name="settings_controller_mapping_summary">Allows you bind external controller buttons/axises to the emulated controller.</string>
|
||||
<string name="settings_controller_mapping">Controller Mapping</string>
|
||||
<string name="controller_binding_dialog_message">Press button on controller to set new binding.\n\nCurrent Binding: %s</string>
|
||||
<string name="controller_binding_dialog_no_binding"><![CDATA[<No Binding>]]></string>
|
||||
<string name="controller_binding_dialog_cancel">Cancel</string>
|
||||
<string name="controller_binding_dialog_clear">Clear</string>
|
||||
<string name="controller_mapping_activity_title">Controller Mapping</string>
|
||||
<string name="controller_mapping_activity_no_profiles_found">No profiles found.</string>
|
||||
<string name="controller_mapping_activity_select_input_profile">Select Input Profile</string>
|
||||
<string name="controller_mapping_activity_failed_to_load_profile">Failed to load profile \'%s\'</string>
|
||||
<string name="controller_mapping_activity_input_profile_name">Input Profile Name:</string>
|
||||
<string name="controller_mapping_activity_save">Save</string>
|
||||
<string name="controller_mapping_activity_name_must_be_provided">A name must be provided.</string>
|
||||
<string name="controller_mapping_activity_failed_to_save_input_profile">Failed to save input profile.</string>
|
||||
<string name="controller_mapping_activity_input_profile_saved">Input profile \'%s\' saved.</string>
|
||||
<string name="controller_mapping_activity_cancel">Cancel</string>
|
||||
</resources>
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<ListPreference
|
||||
app:key="Controller1/Type"
|
||||
|
@ -24,6 +25,14 @@
|
|||
app:defaultValue="DigitalController"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:iconSpaceReserved="false" />
|
||||
<Preference
|
||||
app:title="@string/settings_controller_mapping"
|
||||
app:summary="@string/settings_controller_mapping_summary"
|
||||
app:iconSpaceReserved="false">
|
||||
<intent android:action="android.intent.action.VIEW"
|
||||
android:targetClass="com.github.stenzek.duckstation.ControllerMappingActivity"
|
||||
android:targetPackage="com.github.stenzek.duckstation" />
|
||||
</Preference>
|
||||
<SwitchPreferenceCompat
|
||||
app:key="Controller1/ForceAnalogOnReset"
|
||||
app:title="@string/settings_enable_analog_mode_on_reset"
|
||||
|
|
Loading…
Reference in New Issue