mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-11-23 06:15:38 +00:00
Merge pull request #1806 from stenzek/android-multi-controllers
Android: Add >1 controller, multitap, external controller vibration
This commit is contained in:
commit
dea713e243
|
@ -1,4 +1,5 @@
|
||||||
#include "android_controller_interface.h"
|
#include "android_controller_interface.h"
|
||||||
|
#include "android_host_interface.h"
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/file_system.h"
|
#include "common/file_system.h"
|
||||||
#include "common/log.h"
|
#include "common/log.h"
|
||||||
|
@ -34,36 +35,45 @@ void AndroidControllerInterface::PollEvents() {}
|
||||||
|
|
||||||
void AndroidControllerInterface::ClearBindings()
|
void AndroidControllerInterface::ClearBindings()
|
||||||
{
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||||
for (ControllerData& cd : m_controllers)
|
for (ControllerData& cd : m_controllers)
|
||||||
{
|
{
|
||||||
cd.axis_mapping.fill({});
|
cd.axis_mapping.clear();
|
||||||
cd.button_mapping.fill({});
|
cd.button_mapping.clear();
|
||||||
cd.axis_button_mapping.fill({});
|
cd.axis_button_mapping.clear();
|
||||||
cd.button_axis_mapping.fill({});
|
cd.button_axis_mapping.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<int> AndroidControllerInterface::GetControllerIndex(const std::string_view& device)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||||
|
for (u32 i = 0; i < static_cast<u32>(m_device_names.size()); i++)
|
||||||
|
{
|
||||||
|
if (device == m_device_names[i])
|
||||||
|
return static_cast<int>(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
bool AndroidControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
|
bool AndroidControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
|
||||||
AxisCallback callback)
|
AxisCallback callback)
|
||||||
{
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (axis_number < 0 || axis_number >= NUM_AXISES)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
m_controllers[controller_index].axis_mapping[axis_number][axis_side] = std::move(callback);
|
m_controllers[controller_index].axis_mapping[axis_number][axis_side] = std::move(callback);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AndroidControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback)
|
bool AndroidControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback)
|
||||||
{
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (button_number < 0 || button_number >= NUM_BUTTONS)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
m_controllers[controller_index].button_mapping[button_number] = std::move(callback);
|
m_controllers[controller_index].button_mapping[button_number] = std::move(callback);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -71,12 +81,10 @@ bool AndroidControllerInterface::BindControllerButton(int controller_index, int
|
||||||
bool AndroidControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
bool AndroidControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||||
ButtonCallback callback)
|
ButtonCallback callback)
|
||||||
{
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (axis_number < 0 || axis_number >= NUM_AXISES)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
m_controllers[controller_index].axis_button_mapping[axis_number][BoolToUInt8(direction)] = std::move(callback);
|
m_controllers[controller_index].axis_button_mapping[axis_number][BoolToUInt8(direction)] = std::move(callback);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -84,99 +92,145 @@ bool AndroidControllerInterface::BindControllerAxisToButton(int controller_index
|
||||||
bool AndroidControllerInterface::BindControllerHatToButton(int controller_index, int hat_number,
|
bool AndroidControllerInterface::BindControllerHatToButton(int controller_index, int hat_number,
|
||||||
std::string_view hat_position, ButtonCallback callback)
|
std::string_view hat_position, ButtonCallback callback)
|
||||||
{
|
{
|
||||||
// Hats don't exist in XInput
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AndroidControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number,
|
bool AndroidControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number,
|
||||||
AxisCallback callback)
|
AxisCallback callback)
|
||||||
{
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (button_number < 0 || button_number >= NUM_BUTTONS)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
m_controllers[controller_index].button_axis_mapping[button_number] = std::move(callback);
|
m_controllers[controller_index].button_axis_mapping[button_number] = std::move(callback);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AndroidControllerInterface::HandleAxisEvent(u32 index, u32 axis, float value)
|
void AndroidControllerInterface::SetDeviceNames(std::vector<std::string> device_names)
|
||||||
{
|
{
|
||||||
Log_DevPrintf("controller %u axis %u %f", index, static_cast<u32>(axis), value);
|
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||||
DebugAssert(index < NUM_CONTROLLERS);
|
m_device_names = std::move(device_names);
|
||||||
|
m_controllers.resize(m_device_names.size());
|
||||||
|
}
|
||||||
|
|
||||||
if (DoEventHook(Hook::Type::Axis, index, static_cast<u32>(axis), value))
|
void AndroidControllerInterface::SetDeviceRumble(u32 index, bool has_vibrator)
|
||||||
return true;
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||||
|
if (index >= m_controllers.size())
|
||||||
|
return;
|
||||||
|
|
||||||
const AxisCallback& cb = m_controllers[index].axis_mapping[static_cast<u32>(axis)][AxisSide::Full];
|
m_controllers[index].has_rumble = has_vibrator;
|
||||||
if (cb)
|
}
|
||||||
|
|
||||||
|
void AndroidControllerInterface::HandleAxisEvent(u32 index, u32 axis, float value)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||||
|
if (index >= m_controllers.size())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Log_DevPrintf("controller %u axis %u %f", index, axis, value);
|
||||||
|
if (DoEventHook(Hook::Type::Axis, index, axis, value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const ControllerData& cd = m_controllers[index];
|
||||||
|
const auto am_iter = cd.axis_mapping.find(axis);
|
||||||
|
if (am_iter != cd.axis_mapping.end())
|
||||||
{
|
{
|
||||||
cb(value);
|
const AxisCallback& cb = am_iter->second[AxisSide::Full];
|
||||||
return true;
|
if (cb)
|
||||||
|
{
|
||||||
|
cb(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the other direction to false so large movements don't leave the opposite on
|
// set the other direction to false so large movements don't leave the opposite on
|
||||||
const bool outside_deadzone = (std::abs(value) >= m_controllers[index].deadzone);
|
const bool outside_deadzone = (std::abs(value) >= cd.deadzone);
|
||||||
const bool positive = (value >= 0.0f);
|
const bool positive = (value >= 0.0f);
|
||||||
const ButtonCallback& other_button_cb =
|
const auto bm_iter = cd.axis_button_mapping.find(axis);
|
||||||
m_controllers[index].axis_button_mapping[static_cast<u32>(axis)][BoolToUInt8(!positive)];
|
if (bm_iter != cd.axis_button_mapping.end())
|
||||||
const ButtonCallback& button_cb =
|
|
||||||
m_controllers[index].axis_button_mapping[static_cast<u32>(axis)][BoolToUInt8(positive)];
|
|
||||||
if (button_cb)
|
|
||||||
{
|
{
|
||||||
button_cb(outside_deadzone);
|
const ButtonCallback& other_button_cb = bm_iter->second[BoolToUInt8(!positive)];
|
||||||
if (other_button_cb)
|
const ButtonCallback& button_cb = bm_iter->second[BoolToUInt8(positive)];
|
||||||
|
if (button_cb)
|
||||||
|
{
|
||||||
|
button_cb(outside_deadzone);
|
||||||
|
if (other_button_cb)
|
||||||
|
other_button_cb(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (other_button_cb)
|
||||||
|
{
|
||||||
other_button_cb(false);
|
other_button_cb(false);
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
else if (other_button_cb)
|
|
||||||
{
|
|
||||||
other_button_cb(false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AndroidControllerInterface::HandleButtonEvent(u32 index, u32 button, bool pressed)
|
void AndroidControllerInterface::HandleButtonEvent(u32 index, u32 button, bool pressed)
|
||||||
{
|
{
|
||||||
Log_DevPrintf("controller %u button %u %s", index, button, pressed ? "pressed" : "released");
|
Log_DevPrintf("controller %u button %u %s", index, button, pressed ? "pressed" : "released");
|
||||||
DebugAssert(index < NUM_CONTROLLERS);
|
|
||||||
|
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||||
|
if (index >= m_controllers.size())
|
||||||
|
return;
|
||||||
|
|
||||||
if (DoEventHook(Hook::Type::Button, index, button, pressed ? 1.0f : 0.0f))
|
if (DoEventHook(Hook::Type::Button, index, button, pressed ? 1.0f : 0.0f))
|
||||||
return true;
|
return;
|
||||||
|
|
||||||
const ButtonCallback& cb = m_controllers[index].button_mapping[button];
|
const ControllerData& cd = m_controllers[index];
|
||||||
if (cb)
|
const auto button_iter = cd.button_mapping.find(button);
|
||||||
|
if (button_iter != cd.button_mapping.end() && button_iter->second)
|
||||||
{
|
{
|
||||||
cb(pressed);
|
button_iter->second(pressed);
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AxisCallback& axis_cb = m_controllers[index].button_axis_mapping[button];
|
const auto axis_iter = cd.button_axis_mapping.find(button);
|
||||||
if (axis_cb)
|
if (axis_iter != cd.button_axis_mapping.end() && axis_iter->second)
|
||||||
{
|
{
|
||||||
axis_cb(pressed ? 1.0f : -1.0f);
|
axis_iter->second(pressed ? 1.0f : -1.0f);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return true;
|
}
|
||||||
|
|
||||||
|
bool AndroidControllerInterface::HasButtonBinding(u32 index, u32 button)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||||
|
if (index >= m_controllers.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const ControllerData& cd = m_controllers[index];
|
||||||
|
return (cd.button_mapping.find(button) != cd.button_mapping.end() ||
|
||||||
|
cd.button_axis_mapping.find(button) != cd.button_axis_mapping.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 AndroidControllerInterface::GetControllerRumbleMotorCount(int controller_index)
|
u32 AndroidControllerInterface::GetControllerRumbleMotorCount(int controller_index)
|
||||||
{
|
{
|
||||||
return 0;
|
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||||
|
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return m_controllers[static_cast<u32>(controller_index)].has_rumble ? NUM_RUMBLE_MOTORS : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AndroidControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths,
|
void AndroidControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths,
|
||||||
u32 num_motors)
|
u32 num_motors)
|
||||||
{
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||||
|
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const float small_motor = strengths[0];
|
||||||
|
const float large_motor = strengths[1];
|
||||||
|
static_cast<AndroidHostInterface*>(m_host_interface)
|
||||||
|
->SetControllerVibration(static_cast<u32>(controller_index), small_motor, large_motor);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AndroidControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */)
|
bool AndroidControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */)
|
||||||
{
|
{
|
||||||
if (static_cast<u32>(controller_index) >= NUM_CONTROLLERS)
|
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||||
|
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
m_controllers[static_cast<u32>(controller_index)].deadzone = std::clamp(std::abs(size), 0.01f, 0.99f);
|
m_controllers[static_cast<u32>(controller_index)].deadzone = std::clamp(std::abs(size), 0.01f, 0.99f);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#include "frontend-common/controller_interface.h"
|
#include "frontend-common/controller_interface.h"
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
@ -12,6 +13,8 @@ public:
|
||||||
AndroidControllerInterface();
|
AndroidControllerInterface();
|
||||||
~AndroidControllerInterface() override;
|
~AndroidControllerInterface() override;
|
||||||
|
|
||||||
|
ALWAYS_INLINE u32 GetControllerCount() const { return static_cast<u32>(m_controllers.size()); }
|
||||||
|
|
||||||
Backend GetBackend() const override;
|
Backend GetBackend() const override;
|
||||||
bool Initialize(CommonHostInterface* host_interface) override;
|
bool Initialize(CommonHostInterface* host_interface) override;
|
||||||
void Shutdown() override;
|
void Shutdown() override;
|
||||||
|
@ -20,6 +23,7 @@ public:
|
||||||
void ClearBindings() override;
|
void ClearBindings() override;
|
||||||
|
|
||||||
// Binding to events. If a binding for this axis/button already exists, returns false.
|
// Binding to events. If a binding for this axis/button already exists, returns false.
|
||||||
|
std::optional<int> GetControllerIndex(const std::string_view& device) override;
|
||||||
bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override;
|
bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override;
|
||||||
bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override;
|
bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override;
|
||||||
bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||||
|
@ -37,30 +41,32 @@ public:
|
||||||
|
|
||||||
void PollEvents() override;
|
void PollEvents() override;
|
||||||
|
|
||||||
bool HandleAxisEvent(u32 index, u32 axis, float value);
|
void SetDeviceNames(std::vector<std::string> device_names);
|
||||||
bool HandleButtonEvent(u32 index, u32 button, bool pressed);
|
void SetDeviceRumble(u32 index, bool has_vibrator);
|
||||||
|
void HandleAxisEvent(u32 index, u32 axis, float value);
|
||||||
|
void HandleButtonEvent(u32 index, u32 button, bool pressed);
|
||||||
|
bool HasButtonBinding(u32 index, u32 button);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum : u32
|
enum : u32
|
||||||
{
|
{
|
||||||
NUM_CONTROLLERS = 1,
|
NUM_RUMBLE_MOTORS = 2
|
||||||
NUM_AXISES = 12,
|
|
||||||
NUM_BUTTONS = 23
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ControllerData
|
struct ControllerData
|
||||||
{
|
{
|
||||||
float deadzone = 0.25f;
|
float deadzone = 0.25f;
|
||||||
|
|
||||||
std::array<std::array<AxisCallback, 3>, NUM_AXISES> axis_mapping;
|
std::map<u32, std::array<AxisCallback, 3>> axis_mapping;
|
||||||
std::array<ButtonCallback, NUM_BUTTONS> button_mapping;
|
std::map<u32, ButtonCallback> button_mapping;
|
||||||
std::array<std::array<ButtonCallback, 2>, NUM_AXISES> axis_button_mapping;
|
std::map<u32, std::array<ButtonCallback, 2>> axis_button_mapping;
|
||||||
std::array<AxisCallback, NUM_BUTTONS> button_axis_mapping;
|
std::map<u32, AxisCallback> button_axis_mapping;
|
||||||
|
bool has_rumble = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
using ControllerDataArray = std::array<ControllerData, NUM_CONTROLLERS>;
|
std::vector<std::string> m_device_names;
|
||||||
|
std::vector<ControllerData> m_controllers;
|
||||||
ControllerDataArray m_controllers;
|
std::mutex m_controllers_mutex;
|
||||||
|
|
||||||
std::mutex m_event_intercept_mutex;
|
std::mutex m_event_intercept_mutex;
|
||||||
Hook::Callback m_event_intercept_callback;
|
Hook::Callback m_event_intercept_callback;
|
||||||
|
|
|
@ -49,6 +49,9 @@ static jmethodID s_EmulationActivity_method_onGameTitleChanged;
|
||||||
static jmethodID s_EmulationActivity_method_setVibration;
|
static jmethodID s_EmulationActivity_method_setVibration;
|
||||||
static jmethodID s_EmulationActivity_method_getRefreshRate;
|
static jmethodID s_EmulationActivity_method_getRefreshRate;
|
||||||
static jmethodID s_EmulationActivity_method_openPauseMenu;
|
static jmethodID s_EmulationActivity_method_openPauseMenu;
|
||||||
|
static jmethodID s_EmulationActivity_method_getInputDeviceNames;
|
||||||
|
static jmethodID s_EmulationActivity_method_hasInputDeviceVibration;
|
||||||
|
static jmethodID s_EmulationActivity_method_setInputDeviceVibration;
|
||||||
static jclass s_PatchCode_class;
|
static jclass s_PatchCode_class;
|
||||||
static jmethodID s_PatchCode_constructor;
|
static jmethodID s_PatchCode_constructor;
|
||||||
static jclass s_GameListEntry_class;
|
static jclass s_GameListEntry_class;
|
||||||
|
@ -284,6 +287,48 @@ void AndroidHostInterface::LoadSettings(SettingsInterface& si)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AndroidHostInterface::UpdateInputMap(SettingsInterface& si)
|
||||||
|
{
|
||||||
|
if (m_emulation_activity_object)
|
||||||
|
{
|
||||||
|
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||||
|
DebugAssert(env);
|
||||||
|
|
||||||
|
std::vector<std::string> device_names;
|
||||||
|
|
||||||
|
jobjectArray const java_names = reinterpret_cast<jobjectArray>(
|
||||||
|
env->CallObjectMethod(m_emulation_activity_object, s_EmulationActivity_method_getInputDeviceNames));
|
||||||
|
if (java_names)
|
||||||
|
{
|
||||||
|
const u32 count = static_cast<u32>(env->GetArrayLength(java_names));
|
||||||
|
for (u32 i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
device_names.push_back(
|
||||||
|
AndroidHelpers::JStringToString(env, reinterpret_cast<jstring>(env->GetObjectArrayElement(java_names, i))));
|
||||||
|
}
|
||||||
|
|
||||||
|
env->DeleteLocalRef(java_names);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_controller_interface)
|
||||||
|
{
|
||||||
|
AndroidControllerInterface* ci = static_cast<AndroidControllerInterface*>(m_controller_interface.get());
|
||||||
|
if (ci)
|
||||||
|
{
|
||||||
|
ci->SetDeviceNames(std::move(device_names));
|
||||||
|
for (u32 i = 0; i < ci->GetControllerCount(); i++)
|
||||||
|
{
|
||||||
|
const bool has_vibration = env->CallBooleanMethod(
|
||||||
|
m_emulation_activity_object, s_EmulationActivity_method_hasInputDeviceVibration, static_cast<jint>(i));
|
||||||
|
ci->SetDeviceRumble(i, has_vibration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonHostInterface::UpdateInputMap(si);
|
||||||
|
}
|
||||||
|
|
||||||
bool AndroidHostInterface::IsEmulationThreadPaused() const
|
bool AndroidHostInterface::IsEmulationThreadPaused() const
|
||||||
{
|
{
|
||||||
return System::IsValid() && System::IsPaused();
|
return System::IsValid() && System::IsPaused();
|
||||||
|
@ -461,6 +506,7 @@ void AndroidHostInterface::EmulationThreadLoop(JNIEnv* env)
|
||||||
else
|
else
|
||||||
System::RunFrame();
|
System::RunFrame();
|
||||||
|
|
||||||
|
UpdateControllerRumble();
|
||||||
if (m_vibration_enabled)
|
if (m_vibration_enabled)
|
||||||
UpdateVibration();
|
UpdateVibration();
|
||||||
}
|
}
|
||||||
|
@ -720,6 +766,28 @@ void AndroidHostInterface::HandleControllerAxisEvent(u32 controller_index, u32 a
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AndroidHostInterface::HasControllerButtonBinding(u32 controller_index, u32 button)
|
||||||
|
{
|
||||||
|
AndroidControllerInterface* ci = static_cast<AndroidControllerInterface*>(m_controller_interface.get());
|
||||||
|
if (!ci)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return ci->HasButtonBinding(controller_index, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidHostInterface::SetControllerVibration(u32 controller_index, float small_motor, float large_motor)
|
||||||
|
{
|
||||||
|
if (!m_emulation_activity_object)
|
||||||
|
return;
|
||||||
|
|
||||||
|
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||||
|
DebugAssert(env);
|
||||||
|
|
||||||
|
env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_setInputDeviceVibration,
|
||||||
|
static_cast<jint>(controller_index), static_cast<jfloat>(small_motor),
|
||||||
|
static_cast<jfloat>(large_motor));
|
||||||
|
}
|
||||||
|
|
||||||
void AndroidHostInterface::SetFastForwardEnabled(bool enabled)
|
void AndroidHostInterface::SetFastForwardEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
m_fast_forward_enabled = enabled;
|
m_fast_forward_enabled = enabled;
|
||||||
|
@ -885,6 +953,12 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
|
||||||
env->GetMethodID(emulation_activity_class, "getRefreshRate", "()F")) == nullptr ||
|
env->GetMethodID(emulation_activity_class, "getRefreshRate", "()F")) == nullptr ||
|
||||||
(s_EmulationActivity_method_openPauseMenu = env->GetMethodID(emulation_activity_class, "openPauseMenu", "()V")) ==
|
(s_EmulationActivity_method_openPauseMenu = env->GetMethodID(emulation_activity_class, "openPauseMenu", "()V")) ==
|
||||||
nullptr ||
|
nullptr ||
|
||||||
|
(s_EmulationActivity_method_getInputDeviceNames =
|
||||||
|
env->GetMethodID(s_EmulationActivity_class, "getInputDeviceNames", "()[Ljava/lang/String;")) == nullptr ||
|
||||||
|
(s_EmulationActivity_method_hasInputDeviceVibration =
|
||||||
|
env->GetMethodID(s_EmulationActivity_class, "hasInputDeviceVibration", "(I)Z")) == nullptr ||
|
||||||
|
(s_EmulationActivity_method_setInputDeviceVibration =
|
||||||
|
env->GetMethodID(s_EmulationActivity_class, "setInputDeviceVibration", "(IFF)V")) == nullptr ||
|
||||||
(s_PatchCode_constructor = env->GetMethodID(s_PatchCode_class, "<init>", "(ILjava/lang/String;Z)V")) == nullptr ||
|
(s_PatchCode_constructor = env->GetMethodID(s_PatchCode_class, "<init>", "(ILjava/lang/String;Z)V")) == nullptr ||
|
||||||
(s_GameListEntry_constructor = env->GetMethodID(
|
(s_GameListEntry_constructor = env->GetMethodID(
|
||||||
s_GameListEntry_class, "<init>",
|
s_GameListEntry_class, "<init>",
|
||||||
|
@ -1119,16 +1193,35 @@ DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerAxisNames
|
||||||
return name_array;
|
return name_array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerVibrationMotorCount, jobject unused, jstring controller_type)
|
||||||
|
{
|
||||||
|
std::optional<ControllerType> type =
|
||||||
|
Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str());
|
||||||
|
if (!type)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return static_cast<jint>(Controller::GetVibrationMotorCount(type.value()));
|
||||||
|
}
|
||||||
|
|
||||||
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerButtonEvent, jobject obj, jint controller_index,
|
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerButtonEvent, jobject obj, jint controller_index,
|
||||||
jint button_index, jboolean pressed)
|
jint button_index, jboolean pressed)
|
||||||
{
|
{
|
||||||
AndroidHelpers::GetNativeClass(env, obj)->HandleControllerButtonEvent(controller_index, button_index, pressed);
|
AndroidHelpers::GetNativeClass(env, obj)->HandleControllerButtonEvent(static_cast<u32>(controller_index),
|
||||||
|
static_cast<u32>(button_index), pressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerAxisEvent, jobject obj, jint controller_index,
|
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerAxisEvent, jobject obj, jint controller_index,
|
||||||
jint axis_index, jfloat value)
|
jint axis_index, jfloat value)
|
||||||
{
|
{
|
||||||
AndroidHelpers::GetNativeClass(env, obj)->HandleControllerAxisEvent(controller_index, axis_index, value);
|
AndroidHelpers::GetNativeClass(env, obj)->HandleControllerAxisEvent(static_cast<u32>(controller_index),
|
||||||
|
static_cast<u32>(axis_index), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_hasControllerButtonBinding, jobject obj, jint controller_index,
|
||||||
|
jint button_index)
|
||||||
|
{
|
||||||
|
return AndroidHelpers::GetNativeClass(env, obj)->HasControllerButtonBinding(static_cast<u32>(controller_index),
|
||||||
|
static_cast<u32>(button_index));
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getInputProfileNames, jobject obj)
|
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getInputProfileNames, jobject obj)
|
||||||
|
@ -1330,6 +1423,19 @@ DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_applySettings, jobject obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_updateInputMap, jobject obj)
|
||||||
|
{
|
||||||
|
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
|
||||||
|
if (hi->IsEmulationThreadRunning())
|
||||||
|
{
|
||||||
|
hi->RunOnEmulationThread([hi]() { hi->UpdateInputMap(); });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hi->UpdateInputMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_resetSystem, jobject obj, jboolean global, jint slot)
|
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_resetSystem, jobject obj, jboolean global, jint slot)
|
||||||
{
|
{
|
||||||
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
|
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
|
||||||
|
|
|
@ -21,6 +21,8 @@ class Controller;
|
||||||
class AndroidHostInterface final : public CommonHostInterface
|
class AndroidHostInterface final : public CommonHostInterface
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
using CommonHostInterface::UpdateInputMap;
|
||||||
|
|
||||||
AndroidHostInterface(jobject java_object, jobject context_object, std::string user_directory);
|
AndroidHostInterface(jobject java_object, jobject context_object, std::string user_directory);
|
||||||
~AndroidHostInterface() override;
|
~AndroidHostInterface() override;
|
||||||
|
|
||||||
|
@ -57,6 +59,8 @@ public:
|
||||||
void SetControllerAxisState(u32 index, s32 button_code, float value);
|
void SetControllerAxisState(u32 index, s32 button_code, float value);
|
||||||
void HandleControllerButtonEvent(u32 controller_index, u32 button_index, bool pressed);
|
void HandleControllerButtonEvent(u32 controller_index, u32 button_index, bool pressed);
|
||||||
void HandleControllerAxisEvent(u32 controller_index, u32 axis_index, float value);
|
void HandleControllerAxisEvent(u32 controller_index, u32 axis_index, float value);
|
||||||
|
bool HasControllerButtonBinding(u32 controller_index, u32 button);
|
||||||
|
void SetControllerVibration(u32 controller_index, float small_motor, float large_motor);
|
||||||
void SetFastForwardEnabled(bool enabled);
|
void SetFastForwardEnabled(bool enabled);
|
||||||
|
|
||||||
void RefreshGameList(bool invalidate_cache, bool invalidate_database, ProgressCallback* progress_callback);
|
void RefreshGameList(bool invalidate_cache, bool invalidate_database, ProgressCallback* progress_callback);
|
||||||
|
@ -83,6 +87,7 @@ private:
|
||||||
void EmulationThreadLoop(JNIEnv* env);
|
void EmulationThreadLoop(JNIEnv* env);
|
||||||
|
|
||||||
void LoadSettings(SettingsInterface& si) override;
|
void LoadSettings(SettingsInterface& si) override;
|
||||||
|
void UpdateInputMap(SettingsInterface& si) override;
|
||||||
void SetVibration(bool enabled);
|
void SetVibration(bool enabled);
|
||||||
void UpdateVibration();
|
void UpdateVibration();
|
||||||
|
|
||||||
|
|
|
@ -43,9 +43,9 @@
|
||||||
android:value="com.github.stenzek.duckstation.MainActivity" />
|
android:value="com.github.stenzek.duckstation.MainActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ControllerMappingActivity"
|
android:name=".ControllerSettingsActivity"
|
||||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||||
android:label="@string/title_activity_settings"
|
android:label="@string/controller_mapping_activity_title"
|
||||||
android:parentActivityName=".MainActivity">
|
android:parentActivityName=".MainActivity">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
|
|
@ -82,10 +82,14 @@ public class AndroidHostInterface {
|
||||||
|
|
||||||
public static native String[] getControllerAxisNames(String controllerType);
|
public static native String[] getControllerAxisNames(String controllerType);
|
||||||
|
|
||||||
|
public static native int getControllerVibrationMotorCount(String controllerType);
|
||||||
|
|
||||||
public native void handleControllerButtonEvent(int controllerIndex, int buttonIndex, boolean pressed);
|
public native void handleControllerButtonEvent(int controllerIndex, int buttonIndex, boolean pressed);
|
||||||
|
|
||||||
public native void handleControllerAxisEvent(int controllerIndex, int axisIndex, float value);
|
public native void handleControllerAxisEvent(int controllerIndex, int axisIndex, float value);
|
||||||
|
|
||||||
|
public native boolean hasControllerButtonBinding(int controllerIndex, int buttonIndex);
|
||||||
|
|
||||||
public native void toggleControllerAnalogMode();
|
public native void toggleControllerAnalogMode();
|
||||||
|
|
||||||
public native String[] getInputProfileNames();
|
public native String[] getInputProfileNames();
|
||||||
|
@ -115,6 +119,7 @@ public class AndroidHostInterface {
|
||||||
public native void saveResumeState(boolean waitForCompletion);
|
public native void saveResumeState(boolean waitForCompletion);
|
||||||
|
|
||||||
public native void applySettings();
|
public native void applySettings();
|
||||||
|
public native void updateInputMap();
|
||||||
|
|
||||||
public native void setDisplayAlignment(int alignment);
|
public native void setDisplayAlignment(int alignment);
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
package com.github.stenzek.duckstation;
|
package com.github.stenzek.duckstation;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.util.ArraySet;
|
import android.util.ArraySet;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
@ -15,16 +13,20 @@ import android.view.MotionEvent;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class ControllerBindingDialog extends AlertDialog {
|
public class ControllerBindingDialog extends AlertDialog {
|
||||||
private boolean mIsAxis;
|
final static float DETECT_THRESHOLD = 0.25f;
|
||||||
private String mSettingKey;
|
private final ControllerBindingPreference.Type mType;
|
||||||
|
private final String mSettingKey;
|
||||||
private String mCurrentBinding;
|
private String mCurrentBinding;
|
||||||
|
private int mUpdatedAxisCode = -1;
|
||||||
|
private final HashMap<Integer, float[]> mStartingAxisValues = new HashMap<>();
|
||||||
|
|
||||||
public ControllerBindingDialog(Context context, String buttonName, String settingKey, String currentBinding, boolean isAxis) {
|
public ControllerBindingDialog(Context context, String buttonName, String settingKey, String currentBinding, ControllerBindingPreference.Type type) {
|
||||||
super(context);
|
super(context);
|
||||||
|
|
||||||
mIsAxis = isAxis;
|
mType = type;
|
||||||
mSettingKey = settingKey;
|
mSettingKey = settingKey;
|
||||||
mCurrentBinding = currentBinding;
|
mCurrentBinding = currentBinding;
|
||||||
if (mCurrentBinding == null)
|
if (mCurrentBinding == null)
|
||||||
|
@ -42,10 +44,7 @@ public class ControllerBindingDialog extends AlertDialog {
|
||||||
setOnKeyListener(new DialogInterface.OnKeyListener() {
|
setOnKeyListener(new DialogInterface.OnKeyListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
|
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
|
||||||
if (onKeyDown(keyCode, event))
|
return onKeyDown(keyCode, event);
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -73,57 +72,53 @@ public class ControllerBindingDialog extends AlertDialog {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||||
if (mIsAxis || !EmulationSurfaceView.isDPadOrButtonEvent(event))
|
if (!EmulationSurfaceView.isBindableDevice(event.getDevice()) || !EmulationSurfaceView.isBindableKeyCode(event.getKeyCode())) {
|
||||||
|
return super.onKeyUp(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mType == ControllerBindingPreference.Type.BUTTON)
|
||||||
|
mCurrentBinding = String.format("%s/Button%d", event.getDevice().getDescriptor(), event.getKeyCode());
|
||||||
|
else if (mType == ControllerBindingPreference.Type.VIBRATION)
|
||||||
|
mCurrentBinding = event.getDevice().getDescriptor();
|
||||||
|
else
|
||||||
return super.onKeyUp(keyCode, 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();
|
updateMessage();
|
||||||
updateBinding();
|
updateBinding();
|
||||||
dismiss();
|
dismiss();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int mUpdatedAxisCode = -1;
|
private void setAxisCode(InputDevice device, int axisCode, boolean positive) {
|
||||||
|
if (mUpdatedAxisCode >= 0)
|
||||||
private void setAxisCode(int axisCode, boolean positive) {
|
|
||||||
final int axisIndex = EmulationSurfaceView.getAxisIndexForAxisCode(axisCode);
|
|
||||||
if (mUpdatedAxisCode >= 0 || axisIndex < 0)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
mUpdatedAxisCode = axisCode;
|
mUpdatedAxisCode = axisCode;
|
||||||
|
|
||||||
final int controllerIndex = 0;
|
final int controllerIndex = 0;
|
||||||
if (mIsAxis)
|
if (mType == ControllerBindingPreference.Type.AXIS)
|
||||||
mCurrentBinding = String.format("Controller%d/Axis%d", controllerIndex, axisIndex);
|
mCurrentBinding = String.format("%s/Axis%d", device.getDescriptor(), axisCode);
|
||||||
else
|
else
|
||||||
mCurrentBinding = String.format("Controller%d/%cAxis%d", controllerIndex, (positive) ? '+' : '-', axisIndex);
|
mCurrentBinding = String.format("%s/%cAxis%d", device.getDescriptor(), (positive) ? '+' : '-', axisCode);
|
||||||
|
|
||||||
updateBinding();
|
updateBinding();
|
||||||
updateMessage();
|
updateMessage();
|
||||||
dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
final static float DETECT_THRESHOLD = 0.25f;
|
|
||||||
|
|
||||||
private HashMap<Integer, float[]> mStartingAxisValues = new HashMap<>();
|
|
||||||
|
|
||||||
private boolean doAxisDetection(MotionEvent event) {
|
private boolean doAxisDetection(MotionEvent event) {
|
||||||
if ((event.getSource() & (InputDevice.SOURCE_JOYSTICK | InputDevice.SOURCE_GAMEPAD | InputDevice.SOURCE_DPAD)) == 0)
|
if (!EmulationSurfaceView.isBindableDevice(event.getDevice()))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
final int[] axisCodes = EmulationSurfaceView.getKnownAxisCodes();
|
final List<InputDevice.MotionRange> motionEventList = event.getDevice().getMotionRanges();
|
||||||
final int deviceId = event.getDeviceId();
|
if (motionEventList == null || motionEventList.isEmpty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
final int deviceId = event.getDeviceId();
|
||||||
if (!mStartingAxisValues.containsKey(deviceId)) {
|
if (!mStartingAxisValues.containsKey(deviceId)) {
|
||||||
final float[] axisValues = new float[axisCodes.length];
|
final float[] axisValues = new float[motionEventList.size()];
|
||||||
for (int axisIndex = 0; axisIndex < axisCodes.length; axisIndex++) {
|
for (int axisIndex = 0; axisIndex < motionEventList.size(); axisIndex++) {
|
||||||
final int axisCode = axisCodes[axisIndex];
|
final int axisCode = motionEventList.get(axisIndex).getAxis();
|
||||||
|
|
||||||
// these are binary, so start at zero
|
// these are binary, so start at zero
|
||||||
if (axisCode == MotionEvent.AXIS_HAT_X || axisCode == MotionEvent.AXIS_HAT_Y)
|
if (axisCode == MotionEvent.AXIS_HAT_X || axisCode == MotionEvent.AXIS_HAT_Y)
|
||||||
|
@ -133,13 +128,15 @@ public class ControllerBindingDialog extends AlertDialog {
|
||||||
}
|
}
|
||||||
|
|
||||||
mStartingAxisValues.put(deviceId, axisValues);
|
mStartingAxisValues.put(deviceId, axisValues);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final float[] axisValues = mStartingAxisValues.get(deviceId);
|
final float[] axisValues = mStartingAxisValues.get(deviceId);
|
||||||
for (int axisIndex = 0; axisIndex < axisCodes.length; axisIndex++) {
|
for (int axisIndex = 0; axisIndex < motionEventList.size(); axisIndex++) {
|
||||||
final float newValue = event.getAxisValue(axisCodes[axisIndex]);
|
final int axisCode = motionEventList.get(axisIndex).getAxis();
|
||||||
|
final float newValue = event.getAxisValue(axisCode);
|
||||||
if (Math.abs(newValue - axisValues[axisIndex]) >= DETECT_THRESHOLD) {
|
if (Math.abs(newValue - axisValues[axisIndex]) >= DETECT_THRESHOLD) {
|
||||||
setAxisCode(axisCodes[axisIndex], newValue >= 0.0f);
|
setAxisCode(event.getDevice(), axisCode, newValue >= 0.0f);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,6 +146,10 @@ public class ControllerBindingDialog extends AlertDialog {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
|
public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
|
||||||
|
if (mType != ControllerBindingPreference.Type.AXIS && mType != ControllerBindingPreference.Type.BUTTON) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (doAxisDetection(event))
|
if (doAxisDetection(event))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.github.stenzek.duckstation;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.view.InputDevice;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
@ -14,16 +15,25 @@ import androidx.preference.PreferenceViewHolder;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class ControllerBindingPreference extends Preference {
|
public class ControllerBindingPreference extends Preference {
|
||||||
private enum Type {
|
public enum Type {
|
||||||
BUTTON,
|
BUTTON,
|
||||||
AXIS,
|
AXIS,
|
||||||
|
VIBRATION
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum VisualType {
|
||||||
|
BUTTON,
|
||||||
|
AXIS,
|
||||||
|
VIBRATION,
|
||||||
HOTKEY
|
HOTKEY
|
||||||
}
|
}
|
||||||
|
|
||||||
private String mBindingName;
|
private String mBindingName;
|
||||||
|
private String mDisplayName;
|
||||||
private String mValue;
|
private String mValue;
|
||||||
private TextView mValueView;
|
private TextView mValueView;
|
||||||
private Type mType = Type.BUTTON;
|
private Type mType = Type.BUTTON;
|
||||||
|
private VisualType mVisualType = VisualType.BUTTON;
|
||||||
|
|
||||||
private static int getIconForButton(String buttonName) {
|
private static int getIconForButton(String buttonName) {
|
||||||
if (buttonName.equals("Up")) {
|
if (buttonName.equals("Up")) {
|
||||||
|
@ -64,7 +74,16 @@ public class ControllerBindingPreference extends Preference {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getIconForHotkey(String hotkeyDisplayName) {
|
private static int getIconForHotkey(String hotkeyDisplayName) {
|
||||||
return R.drawable.ic_baseline_category_24;
|
switch (hotkeyDisplayName) {
|
||||||
|
case "FastForward":
|
||||||
|
case "ToggleFastForward":
|
||||||
|
case "Turbo":
|
||||||
|
case "ToggleTurbo":
|
||||||
|
return R.drawable.ic_controller_fast_forward;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return R.drawable.ic_baseline_category_24;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ControllerBindingPreference(Context context, AttributeSet attrs) {
|
public ControllerBindingPreference(Context context, AttributeSet attrs) {
|
||||||
|
@ -94,7 +113,7 @@ public class ControllerBindingPreference extends Preference {
|
||||||
mValueView = ((TextView) holder.findViewById(R.id.controller_binding_value));
|
mValueView = ((TextView) holder.findViewById(R.id.controller_binding_value));
|
||||||
|
|
||||||
int drawableId = R.drawable.ic_baseline_radio_button_checked_24;
|
int drawableId = R.drawable.ic_baseline_radio_button_checked_24;
|
||||||
switch (mType) {
|
switch (mVisualType) {
|
||||||
case BUTTON:
|
case BUTTON:
|
||||||
drawableId = getIconForButton(mBindingName);
|
drawableId = getIconForButton(mBindingName);
|
||||||
break;
|
break;
|
||||||
|
@ -104,41 +123,102 @@ public class ControllerBindingPreference extends Preference {
|
||||||
case HOTKEY:
|
case HOTKEY:
|
||||||
drawableId = getIconForHotkey(mBindingName);
|
drawableId = getIconForHotkey(mBindingName);
|
||||||
break;
|
break;
|
||||||
|
case VIBRATION:
|
||||||
|
drawableId = R.drawable.ic_baseline_vibration_24;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
iconView.setImageDrawable(ContextCompat.getDrawable(getContext(), drawableId));
|
iconView.setImageDrawable(ContextCompat.getDrawable(getContext(), drawableId));
|
||||||
nameView.setText(mBindingName);
|
nameView.setText(mDisplayName);
|
||||||
updateValue();
|
updateValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onClick() {
|
protected void onClick() {
|
||||||
ControllerBindingDialog dialog = new ControllerBindingDialog(getContext(), mBindingName, getKey(), mValue, (mType == Type.AXIS));
|
ControllerBindingDialog dialog = new ControllerBindingDialog(getContext(), mBindingName, getKey(), mValue, mType);
|
||||||
dialog.setOnDismissListener((dismissedDialog) -> updateValue());
|
dialog.setOnDismissListener((dismissedDialog) -> updateValue());
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initButton(int controllerIndex, String buttonName) {
|
public void initButton(int controllerIndex, String buttonName) {
|
||||||
mBindingName = buttonName;
|
mBindingName = buttonName;
|
||||||
|
mDisplayName = buttonName;
|
||||||
mType = Type.BUTTON;
|
mType = Type.BUTTON;
|
||||||
|
mVisualType = VisualType.BUTTON;
|
||||||
setKey(String.format("Controller%d/Button%s", controllerIndex, buttonName));
|
setKey(String.format("Controller%d/Button%s", controllerIndex, buttonName));
|
||||||
updateValue();
|
updateValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initAxis(int controllerIndex, String axisName) {
|
public void initAxis(int controllerIndex, String axisName) {
|
||||||
mBindingName = axisName;
|
mBindingName = axisName;
|
||||||
|
mDisplayName = axisName;
|
||||||
mType = Type.AXIS;
|
mType = Type.AXIS;
|
||||||
|
mVisualType = VisualType.AXIS;
|
||||||
setKey(String.format("Controller%d/Axis%s", controllerIndex, axisName));
|
setKey(String.format("Controller%d/Axis%s", controllerIndex, axisName));
|
||||||
updateValue();
|
updateValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void initVibration(int controllerIndex) {
|
||||||
|
mBindingName = "Rumble";
|
||||||
|
mDisplayName = getContext().getString(R.string.controller_binding_device_for_vibration);
|
||||||
|
mType = Type.VIBRATION;
|
||||||
|
mVisualType = VisualType.VIBRATION;
|
||||||
|
setKey(String.format("Controller%d/Rumble", controllerIndex));
|
||||||
|
updateValue();
|
||||||
|
}
|
||||||
|
|
||||||
public void initHotkey(HotkeyInfo hotkeyInfo) {
|
public void initHotkey(HotkeyInfo hotkeyInfo) {
|
||||||
mBindingName = hotkeyInfo.getDisplayName();
|
mBindingName = hotkeyInfo.getName();
|
||||||
mType = Type.HOTKEY;
|
mDisplayName = hotkeyInfo.getDisplayName();
|
||||||
|
mType = Type.BUTTON;
|
||||||
|
mVisualType = VisualType.HOTKEY;
|
||||||
setKey(hotkeyInfo.getBindingConfigKey());
|
setKey(hotkeyInfo.getBindingConfigKey());
|
||||||
updateValue();
|
updateValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String prettyPrintBinding(String value) {
|
||||||
|
final int index = value.indexOf('/');
|
||||||
|
String device, binding;
|
||||||
|
if (index >= 0) {
|
||||||
|
device = value.substring(0, index);
|
||||||
|
binding = value.substring(index);
|
||||||
|
} else {
|
||||||
|
device = value;
|
||||||
|
binding = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
String humanName = device;
|
||||||
|
int deviceIndex = -1;
|
||||||
|
|
||||||
|
final int[] deviceIds = InputDevice.getDeviceIds();
|
||||||
|
for (int i = 0; i < deviceIds.length; i++) {
|
||||||
|
final InputDevice inputDevice = InputDevice.getDevice(deviceIds[i]);
|
||||||
|
if (inputDevice == null || !inputDevice.getDescriptor().equals(device)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
humanName = inputDevice.getName();
|
||||||
|
deviceIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int MAX_LENGTH = 40;
|
||||||
|
if (humanName.length() > MAX_LENGTH) {
|
||||||
|
final StringBuilder shortenedName = new StringBuilder();
|
||||||
|
shortenedName.append(humanName, 0, MAX_LENGTH / 2);
|
||||||
|
shortenedName.append("...");
|
||||||
|
shortenedName.append(humanName, humanName.length() - (MAX_LENGTH / 2),
|
||||||
|
humanName.length());
|
||||||
|
|
||||||
|
humanName = shortenedName.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceIndex < 0)
|
||||||
|
return String.format("%s[??]%s", humanName, binding);
|
||||||
|
else
|
||||||
|
return String.format("%s[%d]%s", humanName, deviceIndex, binding);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateValue(String value) {
|
private void updateValue(String value) {
|
||||||
mValue = value;
|
mValue = value;
|
||||||
if (mValueView != null) {
|
if (mValueView != null) {
|
||||||
|
@ -157,7 +237,7 @@ public class ControllerBindingPreference extends Preference {
|
||||||
for (String value : values) {
|
for (String value : values) {
|
||||||
if (sb.length() > 0)
|
if (sb.length() > 0)
|
||||||
sb.append(", ");
|
sb.append(", ");
|
||||||
sb.append(value);
|
sb.append(prettyPrintBinding(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateValue(sb.toString());
|
updateValue(sb.toString());
|
||||||
|
|
|
@ -1,279 +0,0 @@
|
||||||
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.fragment.app.FragmentActivity;
|
|
||||||
import androidx.fragment.app.FragmentFactory;
|
|
||||||
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
|
|
||||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.remove("android:support:fragments");
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 ControllerPortFragment extends PreferenceFragmentCompat {
|
|
||||||
private ControllerMappingActivity activity;
|
|
||||||
private int controllerIndex;
|
|
||||||
|
|
||||||
public ControllerPortFragment(ControllerMappingActivity activity, int controllerIndex) {
|
|
||||||
this.activity = activity;
|
|
||||||
this.controllerIndex = controllerIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
|
||||||
final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
|
|
||||||
final String defaultControllerType = controllerIndex == 0 ? "DigitalController" : "None";
|
|
||||||
String controllerType = sp.getString(String.format("Controller%d/Type", controllerIndex), defaultControllerType);
|
|
||||||
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 HotkeyFragment extends PreferenceFragmentCompat {
|
|
||||||
private ControllerMappingActivity activity;
|
|
||||||
private HotkeyInfo[] mHotkeyInfo;
|
|
||||||
|
|
||||||
public HotkeyFragment(ControllerMappingActivity activity) {
|
|
||||||
this.activity = activity;
|
|
||||||
this.mHotkeyInfo = AndroidHostInterface.getInstance().getHotkeyInfoList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
|
||||||
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
|
||||||
if (mHotkeyInfo != null) {
|
|
||||||
for (HotkeyInfo hotkeyInfo : mHotkeyInfo) {
|
|
||||||
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
|
||||||
cbp.initHotkey(hotkeyInfo);
|
|
||||||
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) -> {
|
|
||||||
if (position == NUM_CONTROLLER_PORTS)
|
|
||||||
tab.setText("Hotkeys");
|
|
||||||
else
|
|
||||||
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) {
|
|
||||||
if (position != NUM_CONTROLLER_PORTS)
|
|
||||||
return new ControllerPortFragment(activity, position + 1);
|
|
||||||
else
|
|
||||||
return new HotkeyFragment(activity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return NUM_CONTROLLER_PORTS + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,445 @@
|
||||||
|
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.ListPreference;
|
||||||
|
import androidx.preference.Preference;
|
||||||
|
import androidx.preference.PreferenceCategory;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
import androidx.preference.PreferenceScreen;
|
||||||
|
import androidx.preference.SwitchPreferenceCompat;
|
||||||
|
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;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
public class ControllerSettingsActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private static final int NUM_CONTROLLER_PORTS = 2;
|
||||||
|
public static final String MULTITAP_MODE_SETTINGS_KEY = "ControllerPorts/MultitapMode";
|
||||||
|
|
||||||
|
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
|
||||||
|
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
outState.remove("android:support:fragments");
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(R.string.controller_mapping_activity_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(ControllerSettingsActivity.this, String.format(ControllerSettingsActivity.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 {
|
||||||
|
ControllerSettingsActivity parent;
|
||||||
|
|
||||||
|
public SettingsFragment(ControllerSettingsActivity parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
setPreferencesFromResource(R.xml.controllers_preferences, rootKey);
|
||||||
|
|
||||||
|
final Preference multitapModePreference = getPreferenceScreen().findPreference(MULTITAP_MODE_SETTINGS_KEY);
|
||||||
|
if (multitapModePreference != null) {
|
||||||
|
multitapModePreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||||
|
parent.recreate();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ControllerPortFragment extends PreferenceFragmentCompat {
|
||||||
|
private ControllerSettingsActivity activity;
|
||||||
|
private int controllerIndex;
|
||||||
|
private PreferenceCategory mButtonsCategory;
|
||||||
|
private PreferenceCategory mAxisCategory;
|
||||||
|
private PreferenceCategory mSettingsCategory;
|
||||||
|
|
||||||
|
public ControllerPortFragment(ControllerSettingsActivity activity, int controllerIndex) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.controllerIndex = controllerIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
||||||
|
setPreferenceScreen(ps);
|
||||||
|
createPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SwitchPreferenceCompat createTogglePreference(String key, int title, int summary, boolean defaultValue) {
|
||||||
|
final SwitchPreferenceCompat pref = new SwitchPreferenceCompat(getContext());
|
||||||
|
pref.setKey(key);
|
||||||
|
pref.setTitle(title);
|
||||||
|
pref.setSummary(summary);
|
||||||
|
pref.setIconSpaceReserved(false);
|
||||||
|
pref.setDefaultValue(defaultValue);
|
||||||
|
return pref;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createPreferences() {
|
||||||
|
final PreferenceScreen ps = getPreferenceScreen();
|
||||||
|
final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
|
||||||
|
final String defaultControllerType = controllerIndex == 0 ? "DigitalController" : "None";
|
||||||
|
final String controllerTypeKey = String.format("Controller%d/Type", controllerIndex);
|
||||||
|
final String controllerType = sp.getString(controllerTypeKey, defaultControllerType);
|
||||||
|
final String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType);
|
||||||
|
final String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType);
|
||||||
|
final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType);
|
||||||
|
|
||||||
|
final ListPreference typePreference = new ListPreference(getContext());
|
||||||
|
typePreference.setEntries(R.array.settings_controller_type_entries);
|
||||||
|
typePreference.setEntryValues(R.array.settings_controller_type_values);
|
||||||
|
typePreference.setKey(controllerTypeKey);
|
||||||
|
typePreference.setTitle(R.string.settings_controller_type);
|
||||||
|
typePreference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance());
|
||||||
|
typePreference.setIconSpaceReserved(false);
|
||||||
|
typePreference.setOnPreferenceChangeListener((pref, value) -> {
|
||||||
|
removePreferences();
|
||||||
|
createPreferences(value.toString());
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
ps.addPreference(typePreference);
|
||||||
|
|
||||||
|
mButtonsCategory = new PreferenceCategory(getContext());
|
||||||
|
mButtonsCategory.setTitle(getContext().getString(R.string.controller_settings_category_button_bindings));
|
||||||
|
mButtonsCategory.setIconSpaceReserved(false);
|
||||||
|
ps.addPreference(mButtonsCategory);
|
||||||
|
|
||||||
|
mAxisCategory = new PreferenceCategory(getContext());
|
||||||
|
mAxisCategory.setTitle(getContext().getString(R.string.controller_settings_category_axis_bindings));
|
||||||
|
mAxisCategory.setIconSpaceReserved(false);
|
||||||
|
ps.addPreference(mAxisCategory);
|
||||||
|
|
||||||
|
mSettingsCategory = new PreferenceCategory(getContext());
|
||||||
|
mSettingsCategory.setTitle(getContext().getString(R.string.controller_settings_category_settings));
|
||||||
|
mSettingsCategory.setIconSpaceReserved(false);
|
||||||
|
ps.addPreference(mSettingsCategory);
|
||||||
|
|
||||||
|
createPreferences(controllerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createPreferences(String controllerType) {
|
||||||
|
final PreferenceScreen ps = getPreferenceScreen();
|
||||||
|
final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
|
||||||
|
final String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType);
|
||||||
|
final String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType);
|
||||||
|
final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType);
|
||||||
|
|
||||||
|
if (controllerButtons != null) {
|
||||||
|
for (String buttonName : controllerButtons) {
|
||||||
|
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
||||||
|
cbp.initButton(controllerIndex, buttonName);
|
||||||
|
mButtonsCategory.addPreference(cbp);
|
||||||
|
activity.mPreferences.add(cbp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (axisButtons != null) {
|
||||||
|
for (String axisName : axisButtons) {
|
||||||
|
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
||||||
|
cbp.initAxis(controllerIndex, axisName);
|
||||||
|
mAxisCategory.addPreference(cbp);
|
||||||
|
activity.mPreferences.add(cbp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vibrationMotors > 0) {
|
||||||
|
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
||||||
|
cbp.initVibration(controllerIndex);
|
||||||
|
mSettingsCategory.addPreference(cbp);
|
||||||
|
activity.mPreferences.add(cbp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controllerType.equals("AnalogController")) {
|
||||||
|
mSettingsCategory.addPreference(
|
||||||
|
createTogglePreference(String.format("Controller%d/ForceAnalogOnReset", controllerIndex),
|
||||||
|
R.string.settings_use_analog_sticks_for_dpad, R.string.settings_summary_enable_analog_mode_on_reset, true));
|
||||||
|
|
||||||
|
mSettingsCategory.addPreference(
|
||||||
|
createTogglePreference(String.format("Controller%d/AnalogDPadInDigitalMode", controllerIndex),
|
||||||
|
R.string.settings_enable_analog_mode_on_reset, R.string.settings_summary_use_analog_sticks_for_dpad, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removePreferences() {
|
||||||
|
for (int i = 0; i < mButtonsCategory.getPreferenceCount(); i++) {
|
||||||
|
activity.mPreferences.remove(mButtonsCategory.getPreference(i));
|
||||||
|
}
|
||||||
|
mButtonsCategory.removeAll();
|
||||||
|
|
||||||
|
for (int i = 0; i < mAxisCategory.getPreferenceCount(); i++) {
|
||||||
|
activity.mPreferences.remove(mAxisCategory.getPreference(i));
|
||||||
|
}
|
||||||
|
mAxisCategory.removeAll();
|
||||||
|
|
||||||
|
for (int i = 0; i < mSettingsCategory.getPreferenceCount(); i++) {
|
||||||
|
activity.mPreferences.remove(mSettingsCategory.getPreference(i));
|
||||||
|
}
|
||||||
|
mSettingsCategory.removeAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class HotkeyFragment extends PreferenceFragmentCompat {
|
||||||
|
private ControllerSettingsActivity activity;
|
||||||
|
private HotkeyInfo[] mHotkeyInfo;
|
||||||
|
|
||||||
|
public HotkeyFragment(ControllerSettingsActivity activity) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.mHotkeyInfo = AndroidHostInterface.getInstance().getHotkeyInfoList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
||||||
|
if (mHotkeyInfo != null) {
|
||||||
|
final HashMap<String, PreferenceCategory> categoryMap = new HashMap<>();
|
||||||
|
|
||||||
|
for (HotkeyInfo hotkeyInfo : mHotkeyInfo) {
|
||||||
|
PreferenceCategory category = categoryMap.containsKey(hotkeyInfo.getCategory()) ?
|
||||||
|
categoryMap.get(hotkeyInfo.getCategory()) : null;
|
||||||
|
if (category == null) {
|
||||||
|
category = new PreferenceCategory(getContext());
|
||||||
|
category.setTitle(hotkeyInfo.getCategory());
|
||||||
|
category.setIconSpaceReserved(false);
|
||||||
|
categoryMap.put(hotkeyInfo.getCategory(), category);
|
||||||
|
ps.addPreference(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
||||||
|
cbp.initHotkey(hotkeyInfo);
|
||||||
|
category.addPreference(cbp);
|
||||||
|
activity.mPreferences.add(cbp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreferenceScreen(ps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SettingsCollectionFragment extends Fragment {
|
||||||
|
private ControllerSettingsActivity activity;
|
||||||
|
private SettingsCollectionAdapter adapter;
|
||||||
|
private ViewPager2 viewPager;
|
||||||
|
private String[] controllerPortNames;
|
||||||
|
|
||||||
|
private static final int NUM_MAIN_CONTROLLER_PORTS = 2;
|
||||||
|
private static final int NUM_SUB_CONTROLLER_PORTS = 4;
|
||||||
|
private static final char[] SUB_CONTROLLER_PORT_NAMES = new char[] {'A', 'B', 'C', 'D'};
|
||||||
|
|
||||||
|
public SettingsCollectionFragment(ControllerSettingsActivity activity) {
|
||||||
|
this.activity = activity;
|
||||||
|
|
||||||
|
final String multitapMode = PreferenceManager.getDefaultSharedPreferences(activity).getString(
|
||||||
|
MULTITAP_MODE_SETTINGS_KEY, "Disabled");
|
||||||
|
|
||||||
|
final ArrayList<String> portNames = new ArrayList<>();
|
||||||
|
for (int i = 0; i < NUM_MAIN_CONTROLLER_PORTS; i++) {
|
||||||
|
final boolean isMultitap = (multitapMode.equals("BothPorts") ||
|
||||||
|
(i == 0 && multitapMode.equals("Port1Only")) ||
|
||||||
|
(i == 1 && multitapMode.equals("Port2Only")));
|
||||||
|
|
||||||
|
if (isMultitap) {
|
||||||
|
for (int j = 0; j < NUM_SUB_CONTROLLER_PORTS; j++) {
|
||||||
|
portNames.add(activity.getString(
|
||||||
|
R.string.controller_settings_sub_port_format,
|
||||||
|
i + 1, SUB_CONTROLLER_PORT_NAMES[j]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
portNames.add(activity.getString(
|
||||||
|
R.string.controller_settings_main_port_format,
|
||||||
|
i + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controllerPortNames = new String[portNames.size()];
|
||||||
|
portNames.toArray(controllerPortNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_controller_settings, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
adapter = new SettingsCollectionAdapter(activity, this, controllerPortNames.length);
|
||||||
|
viewPager = view.findViewById(R.id.view_pager);
|
||||||
|
viewPager.setAdapter(adapter);
|
||||||
|
|
||||||
|
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
|
||||||
|
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
|
||||||
|
if (position == 0)
|
||||||
|
tab.setText(R.string.controller_settings_tab_settings);
|
||||||
|
else if (position <= controllerPortNames.length)
|
||||||
|
tab.setText(controllerPortNames[position - 1]);
|
||||||
|
else
|
||||||
|
tab.setText(R.string.controller_settings_tab_hotkeys);
|
||||||
|
}).attach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SettingsCollectionAdapter extends FragmentStateAdapter {
|
||||||
|
private ControllerSettingsActivity activity;
|
||||||
|
private int controllerPorts;
|
||||||
|
|
||||||
|
public SettingsCollectionAdapter(@NonNull ControllerSettingsActivity activity, @NonNull Fragment fragment, int controllerPorts) {
|
||||||
|
super(fragment);
|
||||||
|
this.activity = activity;
|
||||||
|
this.controllerPorts = controllerPorts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Fragment createFragment(int position) {
|
||||||
|
if (position == 0)
|
||||||
|
return new SettingsFragment(activity);
|
||||||
|
else if (position <= controllerPorts)
|
||||||
|
return new ControllerPortFragment(activity, position);
|
||||||
|
else
|
||||||
|
return new HotkeyFragment(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return controllerPorts + 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package com.github.stenzek.duckstation;
|
package com.github.stenzek.duckstation;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.ActivityInfo;
|
import android.content.pm.ActivityInfo;
|
||||||
|
@ -17,17 +16,14 @@ import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.Window;
|
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -157,6 +153,19 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String[] getInputDeviceNames() {
|
||||||
|
return (mContentView != null) ? mContentView.getInputDeviceNames() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasInputDeviceVibration(int controllerIndex) {
|
||||||
|
return (mContentView != null) ? mContentView.hasInputDeviceVibration(controllerIndex) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInputDeviceVibration(int controllerIndex, float smallMotor, float largeMotor) {
|
||||||
|
if (mContentView != null)
|
||||||
|
mContentView.setInputDeviceVibration(controllerIndex, smallMotor, largeMotor);
|
||||||
|
}
|
||||||
|
|
||||||
private void doApplySettings() {
|
private void doApplySettings() {
|
||||||
AndroidHostInterface.getInstance().applySettings();
|
AndroidHostInterface.getInstance().applySettings();
|
||||||
updateRequestedOrientation();
|
updateRequestedOrientation();
|
||||||
|
@ -731,8 +740,10 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||||
Log.i("EmulationActivity", "Controller type: " + controllerType);
|
Log.i("EmulationActivity", "Controller type: " + controllerType);
|
||||||
Log.i("EmulationActivity", "View type: " + viewType);
|
Log.i("EmulationActivity", "View type: " + viewType);
|
||||||
|
|
||||||
final boolean hasAnyControllers = mContentView.initControllerMapping(controllerType);
|
mContentView.updateInputDevices();
|
||||||
|
AndroidHostInterface.getInstance().updateInputMap();
|
||||||
|
|
||||||
|
final boolean hasAnyControllers = mContentView.hasAnyGamePads();
|
||||||
if (controllerType.equals("none") || viewType.equals("none") || (hasAnyControllers && autoHideTouchscreenController)) {
|
if (controllerType.equals("none") || viewType.equals("none") || (hasAnyControllers && autoHideTouchscreenController)) {
|
||||||
if (mTouchscreenController != null) {
|
if (mTouchscreenController != null) {
|
||||||
activityLayout.removeView(mTouchscreenController);
|
activityLayout.removeView(mTouchscreenController);
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.github.stenzek.duckstation;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Vibrator;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
|
@ -28,14 +29,43 @@ public class EmulationSurfaceView extends SurfaceView {
|
||||||
super(context, attrs, defStyle);
|
super(context, attrs, defStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isDPadOrButtonEvent(KeyEvent event) {
|
public static boolean isBindableDevice(InputDevice inputDevice) {
|
||||||
final int source = event.getSource();
|
if (inputDevice == null)
|
||||||
return (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
|
return false;
|
||||||
(source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD ||
|
|
||||||
(source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK;
|
final int sources = inputDevice.getSources();
|
||||||
|
|
||||||
|
// Prevent binding pointer devices such as a mouse.
|
||||||
|
if ((sources & InputDevice.SOURCE_CLASS_POINTER) == InputDevice.SOURCE_CLASS_POINTER)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_CLASS_JOYSTICK) ||
|
||||||
|
((sources & InputDevice.SOURCE_CLASS_BUTTON) == InputDevice.SOURCE_CLASS_BUTTON);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isExternalKeyCode(int keyCode) {
|
public static boolean isGamepadDevice(InputDevice inputDevice) {
|
||||||
|
final int sources = inputDevice.getSources();
|
||||||
|
return ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isBindableKeyCode(int keyCode) {
|
||||||
|
switch (keyCode) {
|
||||||
|
case KeyEvent.KEYCODE_BACK:
|
||||||
|
case KeyEvent.KEYCODE_HOME:
|
||||||
|
case KeyEvent.KEYCODE_MENU:
|
||||||
|
case KeyEvent.KEYCODE_POWER:
|
||||||
|
case KeyEvent.KEYCODE_CAMERA:
|
||||||
|
case KeyEvent.KEYCODE_CALL:
|
||||||
|
case KeyEvent.KEYCODE_ENDCALL:
|
||||||
|
case KeyEvent.KEYCODE_VOICE_ASSIST:
|
||||||
|
return false;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isExternalKeyCode(int keyCode) {
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case KeyEvent.KEYCODE_BACK:
|
case KeyEvent.KEYCODE_BACK:
|
||||||
case KeyEvent.KEYCODE_HOME:
|
case KeyEvent.KEYCODE_HOME:
|
||||||
|
@ -55,157 +85,146 @@ public class EmulationSurfaceView extends SurfaceView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int[] buttonKeyCodes = new int[]{
|
private class InputDeviceData {
|
||||||
KeyEvent.KEYCODE_BUTTON_A, // 0/Cross
|
private int deviceId;
|
||||||
KeyEvent.KEYCODE_BUTTON_B, // 1/Circle
|
private String descriptor;
|
||||||
KeyEvent.KEYCODE_BUTTON_X, // 2/Square
|
private int[] axes;
|
||||||
KeyEvent.KEYCODE_BUTTON_Y, // 3/Triangle
|
private float[] axisValues;
|
||||||
KeyEvent.KEYCODE_BUTTON_SELECT, // 4/Select
|
private int controllerIndex;
|
||||||
KeyEvent.KEYCODE_BUTTON_MODE, // 5/Analog
|
private Vibrator vibrator;
|
||||||
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
|
|
||||||
KeyEvent.KEYCODE_VOLUME_DOWN, // 19
|
|
||||||
KeyEvent.KEYCODE_VOLUME_UP, // 20
|
|
||||||
KeyEvent.KEYCODE_MENU, // 21
|
|
||||||
KeyEvent.KEYCODE_CAMERA, // 22
|
|
||||||
};
|
|
||||||
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
|
|
||||||
MotionEvent.AXIS_GAS, // 10
|
|
||||||
MotionEvent.AXIS_BRAKE, // 11
|
|
||||||
};
|
|
||||||
|
|
||||||
public static int getButtonIndexForKeyCode(int keyCode) {
|
public InputDeviceData(InputDevice device, int controllerIndex) {
|
||||||
for (int buttonIndex = 0; buttonIndex < buttonKeyCodes.length; buttonIndex++) {
|
deviceId = device.getId();
|
||||||
if (buttonKeyCodes[buttonIndex] == keyCode)
|
descriptor = device.getDescriptor();
|
||||||
return buttonIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
this.deviceAxisOrButton = deviceButton;
|
|
||||||
this.controllerIndex = controllerIndex;
|
this.controllerIndex = controllerIndex;
|
||||||
this.buttonMapping = button;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int deviceId;
|
List<InputDevice.MotionRange> motionRanges = device.getMotionRanges();
|
||||||
public int deviceAxisOrButton;
|
if (motionRanges != null && !motionRanges.isEmpty()) {
|
||||||
public int controllerIndex;
|
axes = new int[motionRanges.size()];
|
||||||
public int buttonMapping;
|
axisValues = new float[motionRanges.size()];
|
||||||
}
|
for (int i = 0; i < motionRanges.size(); i++)
|
||||||
|
axes[i] = motionRanges.get(i).getAxis();
|
||||||
private class AxisMapping {
|
|
||||||
public AxisMapping(int deviceId, int deviceAxis, InputDevice.MotionRange motionRange, int controllerIndex, int axis) {
|
|
||||||
this.deviceId = deviceId;
|
|
||||||
this.deviceAxisOrButton = deviceAxis;
|
|
||||||
this.deviceMotionRange = motionRange;
|
|
||||||
this.controllerIndex = controllerIndex;
|
|
||||||
this.axisMapping = axis;
|
|
||||||
this.positiveButton = -1;
|
|
||||||
this.negativeButton = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AxisMapping(int deviceId, int deviceAxis, InputDevice.MotionRange motionRange, int controllerIndex, int positiveButton, int negativeButton) {
|
|
||||||
this.deviceId = deviceId;
|
|
||||||
this.deviceAxisOrButton = deviceAxis;
|
|
||||||
this.deviceMotionRange = motionRange;
|
|
||||||
this.controllerIndex = controllerIndex;
|
|
||||||
this.axisMapping = -1;
|
|
||||||
this.positiveButton = positiveButton;
|
|
||||||
this.negativeButton = negativeButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int deviceId;
|
|
||||||
public int deviceAxisOrButton;
|
|
||||||
public InputDevice.MotionRange deviceMotionRange;
|
|
||||||
public int controllerIndex;
|
|
||||||
public int axisMapping;
|
|
||||||
public int positiveButton;
|
|
||||||
public int negativeButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArrayList<ButtonMapping> mControllerKeyMapping;
|
|
||||||
private ArrayList<AxisMapping> mControllerAxisMapping;
|
|
||||||
|
|
||||||
private boolean handleControllerKey(int deviceId, int keyCode, int repeatCount, boolean pressed) {
|
|
||||||
boolean result = false;
|
|
||||||
for (ButtonMapping mapping : mControllerKeyMapping) {
|
|
||||||
if (mapping.deviceId != deviceId || mapping.deviceAxisOrButton != keyCode)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (repeatCount == 0) {
|
|
||||||
AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.buttonMapping, pressed);
|
|
||||||
Log.d("EmulationSurfaceView", String.format("handleControllerKey %d -> %d %d", keyCode, mapping.buttonMapping, pressed ? 1 : 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result = true;
|
// device.getVibrator() always returns null, but might return a "null vibrator".
|
||||||
|
final Vibrator potentialVibrator = device.getVibrator();
|
||||||
|
if (potentialVibrator != null && potentialVibrator.hasVibrator())
|
||||||
|
vibrator = potentialVibrator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputDeviceData[] mInputDevices = null;
|
||||||
|
private boolean mHasAnyGamepads = false;
|
||||||
|
|
||||||
|
public boolean hasAnyGamePads() {
|
||||||
|
return mHasAnyGamepads;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void updateInputDevices() {
|
||||||
|
mInputDevices = null;
|
||||||
|
mHasAnyGamepads = false;
|
||||||
|
|
||||||
|
final ArrayList<InputDeviceData> inputDeviceIds = new ArrayList<>();
|
||||||
|
for (int deviceId : InputDevice.getDeviceIds()) {
|
||||||
|
final InputDevice device = InputDevice.getDevice(deviceId);
|
||||||
|
if (device == null || !isBindableDevice(device))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (isGamepadDevice(device))
|
||||||
|
mHasAnyGamepads = true;
|
||||||
|
|
||||||
|
final int controllerIndex = inputDeviceIds.size();
|
||||||
|
Log.d("EmulationSurfaceView", String.format("Tracking device %d/%s (%s)",
|
||||||
|
controllerIndex, device.getDescriptor(), device.getName()));
|
||||||
|
inputDeviceIds.add(new InputDeviceData(device, controllerIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
if (inputDeviceIds.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
mInputDevices = new InputDeviceData[inputDeviceIds.size()];
|
||||||
|
inputDeviceIds.toArray(mInputDevices);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String[] getInputDeviceNames() {
|
||||||
|
if (mInputDevices == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
final String[] deviceNames = new String[mInputDevices.length];
|
||||||
|
for (int i = 0; i < mInputDevices.length; i++) {
|
||||||
|
deviceNames[i] = mInputDevices[i].descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean hasInputDeviceVibration(int controllerIndex) {
|
||||||
|
if (mInputDevices == null || controllerIndex >= mInputDevices.length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return (mInputDevices[controllerIndex].vibrator != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setInputDeviceVibration(int controllerIndex, float smallMotor, float largeMotor) {
|
||||||
|
if (mInputDevices == null || controllerIndex >= mInputDevices.length)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// shouldn't get here
|
||||||
|
final InputDeviceData data = mInputDevices[controllerIndex];
|
||||||
|
if (data.vibrator == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
final float MINIMUM_INTENSITY = 0.1f;
|
||||||
|
if (smallMotor >= MINIMUM_INTENSITY || largeMotor >= MINIMUM_INTENSITY)
|
||||||
|
data.vibrator.vibrate(1000);
|
||||||
|
else
|
||||||
|
data.vibrator.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputDeviceData getDataForDeviceId(int deviceId) {
|
||||||
|
if (mInputDevices == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
for (InputDeviceData data : mInputDevices) {
|
||||||
|
if (data.deviceId == deviceId)
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getControllerIndexForDeviceId(int deviceId) {
|
||||||
|
final InputDeviceData data = getDataForDeviceId(deviceId);
|
||||||
|
return (data != null) ? data.controllerIndex : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean handleKeyEvent(int keyCode, KeyEvent event, boolean pressed) {
|
||||||
|
if (!isBindableDevice(event.getDevice()))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
/*Log.e("ESV", String.format("Code %d RC %d Pressed %d %s", keyCode,
|
||||||
|
event.getRepeatCount(), pressed? 1 : 0, event.toString()));*/
|
||||||
|
|
||||||
|
final AndroidHostInterface hi = AndroidHostInterface.getInstance();
|
||||||
|
final int controllerIndex = getControllerIndexForDeviceId(event.getDeviceId());
|
||||||
|
if (event.getRepeatCount() == 0 && controllerIndex >= 0)
|
||||||
|
hi.handleControllerButtonEvent(controllerIndex, keyCode, pressed);
|
||||||
|
|
||||||
|
// We don't want to eat external button events unless it's actually bound.
|
||||||
|
if (isExternalKeyCode(keyCode))
|
||||||
|
return (controllerIndex >= 0 && hi.hasControllerButtonBinding(controllerIndex, keyCode));
|
||||||
|
else
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||||
if (!isDPadOrButtonEvent(event))
|
return handleKeyEvent(keyCode, event, true);
|
||||||
return false;
|
|
||||||
|
|
||||||
if (handleControllerKey(event.getDeviceId(), keyCode, event.getRepeatCount(), true))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// eat non-external button events anyway
|
|
||||||
return !isExternalKeyCode(keyCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||||
if (!isDPadOrButtonEvent(event))
|
return handleKeyEvent(keyCode, event, false);
|
||||||
return false;
|
|
||||||
|
|
||||||
if (handleControllerKey(event.getDeviceId(), keyCode, 0, false))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// eat non-external button events anyway
|
|
||||||
return !isExternalKeyCode(keyCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private float clamp(float value, float min, float max) {
|
private float clamp(float value, float min, float max) {
|
||||||
|
@ -214,19 +233,19 @@ public class EmulationSurfaceView extends SurfaceView {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||||
final int source = event.getSource();
|
if (!isBindableDevice(event.getDevice()))
|
||||||
if ((source & (InputDevice.SOURCE_JOYSTICK | InputDevice.SOURCE_GAMEPAD | InputDevice.SOURCE_DPAD)) == 0)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
final int deviceId = event.getDeviceId();
|
final InputDeviceData data = getDataForDeviceId(event.getDeviceId());
|
||||||
for (AxisMapping mapping : mControllerAxisMapping) {
|
if (data == null || data.axes == null)
|
||||||
if (mapping.deviceId != deviceId)
|
return false;
|
||||||
continue;
|
|
||||||
|
|
||||||
final float axisValue = event.getAxisValue(mapping.deviceAxisOrButton);
|
for (int i = 0; i < data.axes.length; i++) {
|
||||||
|
final int axis = data.axes[i];
|
||||||
|
final float axisValue = event.getAxisValue(axis);
|
||||||
float emuValue;
|
float emuValue;
|
||||||
|
|
||||||
switch (mapping.deviceAxisOrButton) {
|
switch (axis) {
|
||||||
case MotionEvent.AXIS_BRAKE:
|
case MotionEvent.AXIS_BRAKE:
|
||||||
case MotionEvent.AXIS_GAS:
|
case MotionEvent.AXIS_GAS:
|
||||||
case MotionEvent.AXIS_LTRIGGER:
|
case MotionEvent.AXIS_LTRIGGER:
|
||||||
|
@ -241,109 +260,16 @@ public class EmulationSurfaceView extends SurfaceView {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("EmulationSurfaceView", String.format("axis %d value %f emuvalue %f", mapping.deviceAxisOrButton, axisValue, emuValue));
|
if (data.axisValues[i] == emuValue)
|
||||||
|
continue;
|
||||||
|
|
||||||
if (mapping.axisMapping >= 0) {
|
/*Log.d("EmulationSurfaceView",
|
||||||
AndroidHostInterface.getInstance().handleControllerAxisEvent(0, mapping.axisMapping, emuValue);
|
String.format("axis %d value %f emuvalue %f", axis, axisValue, emuValue));*/
|
||||||
}
|
|
||||||
|
|
||||||
final float DEAD_ZONE = 0.25f;
|
data.axisValues[i] = emuValue;
|
||||||
if (mapping.negativeButton >= 0) {
|
AndroidHostInterface.getInstance().handleControllerAxisEvent(data.controllerIndex, axis, emuValue);
|
||||||
AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.negativeButton, (emuValue <= -DEAD_ZONE));
|
|
||||||
}
|
|
||||||
if (mapping.positiveButton >= 0) {
|
|
||||||
AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.positiveButton, (emuValue >= DEAD_ZONE));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
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) {
|
|
||||||
range = curRange;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (range == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
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 (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) {
|
|
||||||
if (deviceId < 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
final InputDevice dev = InputDevice.getDevice(deviceId);
|
|
||||||
if (dev == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
final int sources = dev.getSources();
|
|
||||||
if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return (sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean initControllerMapping(String controllerType) {
|
|
||||||
mControllerKeyMapping = new ArrayList<>();
|
|
||||||
mControllerAxisMapping = new ArrayList<>();
|
|
||||||
|
|
||||||
final int[] deviceIds = InputDevice.getDeviceIds();
|
|
||||||
for (int deviceId : deviceIds) {
|
|
||||||
if (!isJoystickDevice(deviceId))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
InputDevice device = InputDevice.getDevice(deviceId);
|
|
||||||
List<InputDevice.MotionRange> motionRanges = device.getMotionRanges();
|
|
||||||
int controllerIndex = 0;
|
|
||||||
|
|
||||||
for (int keyCode : buttonKeyCodes) {
|
|
||||||
addControllerKeyMapping(deviceId, keyCode, controllerIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (motionRanges != null) {
|
|
||||||
for (int axisCode : axisCodes) {
|
|
||||||
addControllerAxisMapping(deviceId, motionRanges, axisCode, controllerIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return !mControllerKeyMapping.isEmpty() || !mControllerKeyMapping.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,11 @@
|
||||||
package com.github.stenzek.duckstation;
|
package com.github.stenzek.duckstation;
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Property;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.ListAdapter;
|
import android.widget.ListAdapter;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
@ -20,7 +15,6 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.ListFragment;
|
import androidx.fragment.app.ListFragment;
|
||||||
import androidx.preference.PreferenceFragmentCompat;
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
import androidx.preference.PreferenceScreen;
|
import androidx.preference.PreferenceScreen;
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||||
import androidx.viewpager2.widget.ViewPager2;
|
import androidx.viewpager2.widget.ViewPager2;
|
||||||
|
@ -28,8 +22,6 @@ import androidx.viewpager2.widget.ViewPager2;
|
||||||
import com.google.android.material.tabs.TabLayout;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
import com.google.android.material.tabs.TabLayoutMediator;
|
import com.google.android.material.tabs.TabLayoutMediator;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
public class GamePropertiesActivity extends AppCompatActivity {
|
public class GamePropertiesActivity extends AppCompatActivity {
|
||||||
PropertyListAdapter mPropertiesListAdapter;
|
PropertyListAdapter mPropertiesListAdapter;
|
||||||
GameListEntry mGameListEntry;
|
GameListEntry mGameListEntry;
|
||||||
|
@ -183,7 +175,7 @@ public class GamePropertiesActivity extends AppCompatActivity {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.fragment_controller_mapping, container, false);
|
return inflater.inflate(R.layout.fragment_controller_settings, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -26,8 +26,6 @@ import androidx.appcompat.app.AppCompatDelegate;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.core.app.ActivityCompat;
|
import androidx.core.app.ActivityCompat;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentFactory;
|
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
@ -237,8 +235,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
Intent intent = new Intent(this, SettingsActivity.class);
|
Intent intent = new Intent(this, SettingsActivity.class);
|
||||||
startActivityForResult(intent, REQUEST_SETTINGS);
|
startActivityForResult(intent, REQUEST_SETTINGS);
|
||||||
return true;
|
return true;
|
||||||
} else if (id == R.id.action_controller_mapping) {
|
} else if (id == R.id.action_controller_settings) {
|
||||||
Intent intent = new Intent(this, ControllerMappingActivity.class);
|
Intent intent = new Intent(this, ControllerSettingsActivity.class);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
return true;
|
||||||
} else if (id == R.id.action_switch_view) {
|
} else if (id == R.id.action_switch_view) {
|
||||||
|
|
|
@ -107,13 +107,10 @@ public class SettingsActivity extends AppCompatActivity {
|
||||||
case 3: // Enhancements
|
case 3: // Enhancements
|
||||||
return new SettingsFragment(R.xml.enhancements_preferences);
|
return new SettingsFragment(R.xml.enhancements_preferences);
|
||||||
|
|
||||||
case 4: // Controllers
|
case 4: // Achievements
|
||||||
return new SettingsFragment(R.xml.controllers_preferences);
|
|
||||||
|
|
||||||
case 5: // Achievements
|
|
||||||
return new AchievementSettingsFragment();
|
return new AchievementSettingsFragment();
|
||||||
|
|
||||||
case 6: // Advanced
|
case 5: // Advanced
|
||||||
return new SettingsFragment(R.xml.advanced_preferences);
|
return new SettingsFragment(R.xml.advanced_preferences);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -123,7 +120,7 @@ public class SettingsActivity extends AppCompatActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() {
|
||||||
return 7;
|
return 6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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="M0,15h2L2,9L0,9v6zM3,17h2L5,7L3,7v10zM22,9v6h2L24,9h-2zM19,17h2L21,7h-2v10zM16.5,3h-9C6.67,3 6,3.67 6,4.5v15c0,0.83 0.67,1.5 1.5,1.5h9c0.83,0 1.5,-0.67 1.5,-1.5v-15c0,-0.83 -0.67,-1.5 -1.5,-1.5zM16,19L8,19L8,5h8v14z"/>
|
||||||
|
</vector>
|
|
@ -10,7 +10,8 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:tabTextAppearance="@style/TabTextAppearance"
|
app:tabTextAppearance="@style/TabTextAppearance"
|
||||||
app:tabMode="fixed" />
|
app:tabMinWidth="150dp"
|
||||||
|
app:tabMode="scrollable" />
|
||||||
|
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
android:id="@+id/view_pager"
|
android:id="@+id/view_pager"
|
|
@ -21,12 +21,14 @@
|
||||||
android:id="@+id/action_switch_view"
|
android:id="@+id/action_switch_view"
|
||||||
android:icon="@drawable/ic_baseline_settings_24"
|
android:icon="@drawable/ic_baseline_settings_24"
|
||||||
android:orderInCategory="100"
|
android:orderInCategory="100"
|
||||||
android:title="@string/action_settings"
|
android:title="Switch View"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_controller_mapping"
|
android:id="@+id/action_controller_settings"
|
||||||
android:icon="@drawable/ic_baseline_gamepad_24"
|
android:icon="@drawable/ic_baseline_gamepad_24"
|
||||||
android:title="@string/action_controller_mapping" />
|
android:orderInCategory="102"
|
||||||
|
android:title="@string/action_controller_mapping"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_edit_game_directories"
|
android:id="@+id/action_edit_game_directories"
|
||||||
android:title="@string/menu_main_edit_game_directories" />
|
android:title="@string/menu_main_edit_game_directories" />
|
||||||
|
|
|
@ -69,8 +69,11 @@
|
||||||
<item>xBR (Sin unión de bordes)</item>
|
<item>xBR (Sin unión de bordes)</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_controller_type_entries">
|
<string-array name="settings_controller_type_entries">
|
||||||
|
<item>None</item>
|
||||||
<item>Control Digital (Mando)</item>
|
<item>Control Digital (Mando)</item>
|
||||||
<item>Control Analógico (DualShock)</item>
|
<item>Control Analógico (DualShock)</item>
|
||||||
|
<item>Analog Joystick</item>
|
||||||
|
<item>NeGcon</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_memory_card_mode_entries">
|
<string-array name="settings_memory_card_mode_entries">
|
||||||
<item>Sin tarjeta de memoria</item>
|
<item>Sin tarjeta de memoria</item>
|
||||||
|
@ -139,7 +142,6 @@
|
||||||
<item>Pantalla</item>
|
<item>Pantalla</item>
|
||||||
<item>Audio</item>
|
<item>Audio</item>
|
||||||
<item>Mejoras</item>
|
<item>Mejoras</item>
|
||||||
<item>Controles</item>
|
|
||||||
<item>Achievements</item>
|
<item>Achievements</item>
|
||||||
<item>Avanzado</item>
|
<item>Avanzado</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
|
@ -69,8 +69,11 @@
|
||||||
<item>xBR (No Blending Bordi)</item>
|
<item>xBR (No Blending Bordi)</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_controller_type_entries">
|
<string-array name="settings_controller_type_entries">
|
||||||
|
<item>None</item>
|
||||||
<item>Controller Digitale (Gamepad)</item>
|
<item>Controller Digitale (Gamepad)</item>
|
||||||
<item>Controller Analogico (DualShock)</item>
|
<item>Controller Analogico (DualShock)</item>
|
||||||
|
<item>Analog Joystick</item>
|
||||||
|
<item>NeGcon</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_memory_card_mode_entries">
|
<string-array name="settings_memory_card_mode_entries">
|
||||||
<item>No Memory Card</item>
|
<item>No Memory Card</item>
|
||||||
|
@ -139,7 +142,6 @@
|
||||||
<item>Display</item>
|
<item>Display</item>
|
||||||
<item>Audio</item>
|
<item>Audio</item>
|
||||||
<item>Miglioramenti</item>
|
<item>Miglioramenti</item>
|
||||||
<item>Controller</item>
|
|
||||||
<item>Achievements</item>
|
<item>Achievements</item>
|
||||||
<item>Avanzate</item>
|
<item>Avanzate</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
|
@ -69,8 +69,11 @@
|
||||||
<item>xBR (Geen Edge Blending)</item>
|
<item>xBR (Geen Edge Blending)</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_controller_type_entries">
|
<string-array name="settings_controller_type_entries">
|
||||||
|
<item>None</item>
|
||||||
<item>Digitale Controller (Gamepad)</item>
|
<item>Digitale Controller (Gamepad)</item>
|
||||||
<item>Analoge Controller (DualShock)</item>
|
<item>Analoge Controller (DualShock)</item>
|
||||||
|
<item>Analog Joystick</item>
|
||||||
|
<item>NeGcon</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_memory_card_mode_entries">
|
<string-array name="settings_memory_card_mode_entries">
|
||||||
<item>Geen Geheugenkaart</item>
|
<item>Geen Geheugenkaart</item>
|
||||||
|
@ -139,7 +142,6 @@
|
||||||
<item>Weergave</item>
|
<item>Weergave</item>
|
||||||
<item>Audio</item>
|
<item>Audio</item>
|
||||||
<item>Verbeteringen</item>
|
<item>Verbeteringen</item>
|
||||||
<item>Controllers</item>
|
|
||||||
<item>Achievements</item>
|
<item>Achievements</item>
|
||||||
<item>Geavanceerd</item>
|
<item>Geavanceerd</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
|
@ -69,8 +69,11 @@
|
||||||
<item>xBR (Sem ajustes laterais)</item>
|
<item>xBR (Sem ajustes laterais)</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_controller_type_entries">
|
<string-array name="settings_controller_type_entries">
|
||||||
|
<item>None</item>
|
||||||
<item>Controle Digital (Gamepad)</item>
|
<item>Controle Digital (Gamepad)</item>
|
||||||
<item>Controle Analógico (DualShock)</item>
|
<item>Controle Analógico (DualShock)</item>
|
||||||
|
<item>Analog Joystick</item>
|
||||||
|
<item>NeGcon</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_memory_card_mode_entries">
|
<string-array name="settings_memory_card_mode_entries">
|
||||||
<item>Sem Cartão de Memória</item>
|
<item>Sem Cartão de Memória</item>
|
||||||
|
@ -139,7 +142,6 @@
|
||||||
<item>Vídeo</item>
|
<item>Vídeo</item>
|
||||||
<item>Áudio</item>
|
<item>Áudio</item>
|
||||||
<item>Melhorias</item>
|
<item>Melhorias</item>
|
||||||
<item>Controles</item>
|
|
||||||
<item>Achievements</item>
|
<item>Achievements</item>
|
||||||
<item>Avançado</item>
|
<item>Avançado</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
|
@ -69,8 +69,11 @@
|
||||||
<item>xBR (без сглаживания краёв)</item>
|
<item>xBR (без сглаживания краёв)</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_controller_type_entries">
|
<string-array name="settings_controller_type_entries">
|
||||||
|
<item>None</item>
|
||||||
<item>Цифровой</item>
|
<item>Цифровой</item>
|
||||||
<item>Аналоговый (DualShock)</item>
|
<item>Аналоговый (DualShock)</item>
|
||||||
|
<item>Analog Joystick</item>
|
||||||
|
<item>NeGcon</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_memory_card_mode_entries">
|
<string-array name="settings_memory_card_mode_entries">
|
||||||
<item>Без карты памяти</item>
|
<item>Без карты памяти</item>
|
||||||
|
@ -145,7 +148,6 @@
|
||||||
<item>Экран</item>
|
<item>Экран</item>
|
||||||
<item>Звук</item>
|
<item>Звук</item>
|
||||||
<item>Улучшения</item>
|
<item>Улучшения</item>
|
||||||
<item>Контроллеры</item>
|
|
||||||
<item>Достижения</item>
|
<item>Достижения</item>
|
||||||
<item>Расширенные</item>
|
<item>Расширенные</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
|
@ -138,12 +138,18 @@
|
||||||
<item>xBRBinAlpha</item>
|
<item>xBRBinAlpha</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_controller_type_entries">
|
<string-array name="settings_controller_type_entries">
|
||||||
|
<item>None</item>
|
||||||
<item>Digital Controller (Gamepad)</item>
|
<item>Digital Controller (Gamepad)</item>
|
||||||
<item>Analog Controller (DualShock)</item>
|
<item>Analog Controller (DualShock)</item>
|
||||||
|
<item>Analog Joystick</item>
|
||||||
|
<item>NeGcon</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_controller_type_values">
|
<string-array name="settings_controller_type_values">
|
||||||
|
<item>None</item>
|
||||||
<item>DigitalController</item>
|
<item>DigitalController</item>
|
||||||
<item>AnalogController</item>
|
<item>AnalogController</item>
|
||||||
|
<item>AnalogJoystick</item>
|
||||||
|
<item>NeGcon</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_memory_card_mode_entries">
|
<string-array name="settings_memory_card_mode_entries">
|
||||||
<item>No Memory Card</item>
|
<item>No Memory Card</item>
|
||||||
|
@ -265,7 +271,6 @@
|
||||||
<item>Display</item>
|
<item>Display</item>
|
||||||
<item>Audio</item>
|
<item>Audio</item>
|
||||||
<item>Enhancements</item>
|
<item>Enhancements</item>
|
||||||
<item>Controllers</item>
|
|
||||||
<item>Achievements</item>
|
<item>Achievements</item>
|
||||||
<item>Advanced</item>
|
<item>Advanced</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
@ -467,4 +472,16 @@
|
||||||
<item>false</item>
|
<item>false</item>
|
||||||
<item>true</item>
|
<item>true</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="settings_multitap_mode_entries">
|
||||||
|
<item>Disabled</item>
|
||||||
|
<item>Enable on Port 1 Only</item>
|
||||||
|
<item>Enable on Port 2 Only</item>
|
||||||
|
<item>Enable on Ports 1 and 2</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="settings_multitap_mode_values">
|
||||||
|
<item>Disabled</item>
|
||||||
|
<item>Port1Only</item>
|
||||||
|
<item>Port2Only</item>
|
||||||
|
<item>BothPorts</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">DuckStation</string>
|
<string name="app_name">DuckStation</string>
|
||||||
<string name="action_settings">Settings</string>
|
<string name="action_settings">Settings</string>
|
||||||
<string name="action_controller_mapping">Controller Mapping</string>
|
<string name="action_controller_mapping">Controller Settings</string>
|
||||||
<string name="title_activity_settings">Settings</string>
|
<string name="title_activity_settings">Settings</string>
|
||||||
<string name="settings_console_region">Console Region</string>
|
<string name="settings_console_region">Console Region</string>
|
||||||
<string name="settings_console_tty_output">Enable TTY Output</string>
|
<string name="settings_console_tty_output">Enable TTY Output</string>
|
||||||
|
@ -167,7 +167,7 @@
|
||||||
<string name="controller_binding_dialog_no_binding"><![CDATA[<No Binding>]]></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_cancel">Cancel</string>
|
||||||
<string name="controller_binding_dialog_clear">Clear</string>
|
<string name="controller_binding_dialog_clear">Clear</string>
|
||||||
<string name="controller_mapping_activity_title">Controller Mapping</string>
|
<string name="controller_mapping_activity_title">Controller Settings</string>
|
||||||
<string name="controller_mapping_activity_no_profiles_found">No profiles found.</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_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_failed_to_load_profile">Failed to load profile \'%s\'</string>
|
||||||
|
@ -266,4 +266,14 @@
|
||||||
<string name="settings_achievements_disclaimer">DuckStation uses RetroAchievements (retroachievements.org) as an achievement database and for tracking progress.</string>
|
<string name="settings_achievements_disclaimer">DuckStation uses RetroAchievements (retroachievements.org) as an achievement database and for tracking progress.</string>
|
||||||
<string name="settings_achievements_confirm_logout_title">Confirm Logout</string>
|
<string name="settings_achievements_confirm_logout_title">Confirm Logout</string>
|
||||||
<string name="settings_achievements_confirm_logout_message">After logging out, no more achievements will be unlocked until you log back in again. Achievements already unlocked will not be lost.</string>
|
<string name="settings_achievements_confirm_logout_message">After logging out, no more achievements will be unlocked until you log back in again. Achievements already unlocked will not be lost.</string>
|
||||||
|
<string name="controller_binding_device_for_vibration">Device for Vibration</string>
|
||||||
|
<string name="controller_settings_tab_settings">Settings</string>
|
||||||
|
<string name="controller_settings_tab_hotkeys">Hotkeys</string>
|
||||||
|
<string name="controller_settings_category_button_bindings">Button Bindings</string>
|
||||||
|
<string name="controller_settings_category_axis_bindings">Axis Bindings</string>
|
||||||
|
<string name="controller_settings_category_settings">Settings</string>
|
||||||
|
<string name="controller_settings_category_touchscreen_controller">Touchscreen Controller</string>
|
||||||
|
<string name="controller_settings_category_ports">Ports</string>
|
||||||
|
<string name="controller_settings_main_port_format">Port %d</string>
|
||||||
|
<string name="controller_settings_sub_port_format">Port %1$d%2$c</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -14,88 +14,72 @@
|
||||||
~ limitations under the License.
|
~ 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
|
<PreferenceCategory
|
||||||
app:key="Controller1/Type"
|
app:iconSpaceReserved="false"
|
||||||
app:title="@string/settings_controller_type"
|
app:title="@string/controller_settings_category_touchscreen_controller">
|
||||||
app:entries="@array/settings_controller_type_entries"
|
<ListPreference
|
||||||
app:entryValues="@array/settings_controller_type_values"
|
app:defaultValue="digital"
|
||||||
app:defaultValue="DigitalController"
|
app:entries="@array/settings_touchscreen_controller_view_entries"
|
||||||
app:useSimpleSummaryProvider="true"
|
app:entryValues="@array/settings_touchscreen_controller_view_values"
|
||||||
app:iconSpaceReserved="false" />
|
app:iconSpaceReserved="false"
|
||||||
<ListPreference
|
app:key="Controller1/TouchscreenControllerView"
|
||||||
app:key="Controller1/TouchscreenControllerView"
|
app:title="@string/settings_touchscreen_controller_view"
|
||||||
app:title="@string/settings_touchscreen_controller_view"
|
app:useSimpleSummaryProvider="true" />
|
||||||
app:entries="@array/settings_touchscreen_controller_view_entries"
|
<SwitchPreferenceCompat
|
||||||
app:entryValues="@array/settings_touchscreen_controller_view_values"
|
app:defaultValue="false"
|
||||||
app:defaultValue="digital"
|
app:iconSpaceReserved="false"
|
||||||
app:useSimpleSummaryProvider="true"
|
app:key="Controller1/AutoHideTouchscreenController"
|
||||||
app:iconSpaceReserved="false" />
|
app:summary="@string/settings_summary_auto_hide_touchscreen_controller"
|
||||||
<Preference
|
app:title="@string/settings_auto_hide_touchscreen_controller" />
|
||||||
app:title="@string/settings_controller_mapping"
|
<SwitchPreferenceCompat
|
||||||
app:summary="@string/settings_controller_mapping_summary"
|
app:defaultValue="false"
|
||||||
app:iconSpaceReserved="false">
|
app:iconSpaceReserved="false"
|
||||||
<intent
|
app:key="Controller1/TouchGliding"
|
||||||
android:action="android.intent.action.VIEW"
|
app:summary="@string/settings_summary_touch_gliding"
|
||||||
android:targetClass="com.github.stenzek.duckstation.ControllerMappingActivity"
|
app:title="@string/settings_touch_gliding" />
|
||||||
android:targetPackage="com.github.stenzek.duckstation" />
|
<SwitchPreferenceCompat
|
||||||
</Preference>
|
app:defaultValue="false"
|
||||||
<SwitchPreferenceCompat
|
app:iconSpaceReserved="false"
|
||||||
app:key="Controller1/ForceAnalogOnReset"
|
app:key="Controller1/HapticFeedback"
|
||||||
app:title="@string/settings_enable_analog_mode_on_reset"
|
app:summary="@string/settings_summary_vibrate_on_press"
|
||||||
app:summary="@string/settings_summary_enable_analog_mode_on_reset"
|
app:title="@string/settings_vibrate_on_press" />
|
||||||
app:defaultValue="true"
|
<SwitchPreferenceCompat
|
||||||
app:iconSpaceReserved="false" />
|
app:defaultValue="false"
|
||||||
<SwitchPreferenceCompat
|
app:iconSpaceReserved="false"
|
||||||
app:key="Controller1/AnalogDPadInDigitalMode"
|
app:key="Controller1/Vibration"
|
||||||
app:title="@string/settings_use_analog_sticks_for_dpad"
|
app:summary="@string/settings_summary_enable_vibration"
|
||||||
app:summary="@string/settings_summary_use_analog_sticks_for_dpad"
|
app:title="@string/settings_enable_vibration" />
|
||||||
app:defaultValue="true"
|
</PreferenceCategory>
|
||||||
app:iconSpaceReserved="false" />
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
app:key="Controller1/AutoHideTouchscreenController"
|
|
||||||
app:title="@string/settings_auto_hide_touchscreen_controller"
|
|
||||||
app:defaultValue="false"
|
|
||||||
app:summary="@string/settings_summary_auto_hide_touchscreen_controller"
|
|
||||||
app:iconSpaceReserved="false" />
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
app:key="Controller1/HapticFeedback"
|
|
||||||
app:title="@string/settings_vibrate_on_press"
|
|
||||||
app:defaultValue="false"
|
|
||||||
app:summary="@string/settings_summary_vibrate_on_press"
|
|
||||||
app:iconSpaceReserved="false" />
|
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
<PreferenceCategory
|
||||||
app:key="Controller1/Vibration"
|
app:iconSpaceReserved="false"
|
||||||
app:title="@string/settings_enable_vibration"
|
app:title="@string/controller_settings_category_ports">
|
||||||
app:defaultValue="false"
|
<ListPreference
|
||||||
app:summary="@string/settings_summary_enable_vibration"
|
app:defaultValue="Disabled"
|
||||||
app:iconSpaceReserved="false" />
|
app:entries="@array/settings_multitap_mode_entries"
|
||||||
|
app:entryValues="@array/settings_multitap_mode_values"
|
||||||
<SwitchPreferenceCompat
|
app:iconSpaceReserved="false"
|
||||||
app:key="Controller1/TouchGliding"
|
app:key="ControllerPorts/MultitapMode"
|
||||||
app:title="@string/settings_touch_gliding"
|
app:title="Multitap Mode"
|
||||||
app:defaultValue="false"
|
app:useSimpleSummaryProvider="true" />
|
||||||
app:summary="@string/settings_summary_touch_gliding"
|
<ListPreference
|
||||||
app:iconSpaceReserved="false" />
|
app:defaultValue="PerGameTitle"
|
||||||
|
app:entries="@array/settings_memory_card_mode_entries"
|
||||||
<ListPreference
|
app:entryValues="@array/settings_memory_card_mode_values"
|
||||||
app:key="MemoryCards/Card1Type"
|
app:iconSpaceReserved="false"
|
||||||
app:title="@string/settings_memory_card_1_type"
|
app:key="MemoryCards/Card1Type"
|
||||||
app:entries="@array/settings_memory_card_mode_entries"
|
app:title="@string/settings_memory_card_1_type"
|
||||||
app:entryValues="@array/settings_memory_card_mode_values"
|
app:useSimpleSummaryProvider="true" />
|
||||||
app:defaultValue="PerGameTitle"
|
<ListPreference
|
||||||
app:useSimpleSummaryProvider="true"
|
app:defaultValue="None"
|
||||||
app:iconSpaceReserved="false" />
|
app:entries="@array/settings_memory_card_mode_entries"
|
||||||
<ListPreference
|
app:entryValues="@array/settings_memory_card_mode_values"
|
||||||
app:key="MemoryCards/Card2Type"
|
app:iconSpaceReserved="false"
|
||||||
app:title="@string/settings_memory_card_2_type"
|
app:key="MemoryCards/Card2Type"
|
||||||
app:entries="@array/settings_memory_card_mode_entries"
|
app:title="@string/settings_memory_card_2_type"
|
||||||
app:entryValues="@array/settings_memory_card_mode_values"
|
app:useSimpleSummaryProvider="true" />
|
||||||
app:defaultValue="None"
|
</PreferenceCategory>
|
||||||
app:useSimpleSummaryProvider="true"
|
|
||||||
app:iconSpaceReserved="false" />
|
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|
|
@ -1538,21 +1538,9 @@ bool CommonHostInterface::AddButtonToInputMap(const std::string& binding, const
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtil::StartsWith(device, "Controller"))
|
std::optional<int> controller_index;
|
||||||
|
if (m_controller_interface && (controller_index = m_controller_interface->GetControllerIndex(device)))
|
||||||
{
|
{
|
||||||
if (!m_controller_interface)
|
|
||||||
{
|
|
||||||
Log_ErrorPrintf("No controller interface set, cannot bind '%s'", binding.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::optional<int> controller_index = StringUtil::FromChars<int>(device.substr(10));
|
|
||||||
if (!controller_index || *controller_index < 0)
|
|
||||||
{
|
|
||||||
Log_WarningPrintf("Invalid controller index in button binding '%s'", binding.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtil::StartsWith(button, "Button"))
|
if (StringUtil::StartsWith(button, "Button"))
|
||||||
{
|
{
|
||||||
const std::optional<int> button_index = StringUtil::FromChars<int>(button.substr(6));
|
const std::optional<int> button_index = StringUtil::FromChars<int>(button.substr(6));
|
||||||
|
@ -1652,21 +1640,9 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtil::StartsWith(device, "Controller"))
|
std::optional<int> controller_index;
|
||||||
|
if (m_controller_interface && (controller_index = m_controller_interface->GetControllerIndex(device)))
|
||||||
{
|
{
|
||||||
if (!m_controller_interface)
|
|
||||||
{
|
|
||||||
Log_ErrorPrintf("No controller interface set, cannot bind '%s'", binding.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::optional<int> controller_index = StringUtil::FromChars<int>(device.substr(10));
|
|
||||||
if (!controller_index || *controller_index < 0)
|
|
||||||
{
|
|
||||||
Log_WarningPrintf("Invalid controller index in axis binding '%s'", binding.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtil::StartsWith(axis, "Axis") || StringUtil::StartsWith(axis, "+Axis") ||
|
if (StringUtil::StartsWith(axis, "Axis") || StringUtil::StartsWith(axis, "+Axis") ||
|
||||||
StringUtil::StartsWith(axis, "-Axis"))
|
StringUtil::StartsWith(axis, "-Axis"))
|
||||||
{
|
{
|
||||||
|
@ -1723,21 +1699,9 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st
|
||||||
|
|
||||||
bool CommonHostInterface::AddRumbleToInputMap(const std::string& binding, u32 controller_index, u32 num_motors)
|
bool CommonHostInterface::AddRumbleToInputMap(const std::string& binding, u32 controller_index, u32 num_motors)
|
||||||
{
|
{
|
||||||
if (StringUtil::StartsWith(binding, "Controller"))
|
std::optional<int> host_controller_index;
|
||||||
|
if (m_controller_interface && (host_controller_index = m_controller_interface->GetControllerIndex(binding)))
|
||||||
{
|
{
|
||||||
if (!m_controller_interface)
|
|
||||||
{
|
|
||||||
Log_ErrorPrintf("No controller interface set, cannot bind '%s'", binding.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::optional<int> host_controller_index = StringUtil::FromChars<int>(binding.substr(10));
|
|
||||||
if (!host_controller_index || *host_controller_index < 0)
|
|
||||||
{
|
|
||||||
Log_WarningPrintf("Invalid controller index in rumble binding '%s'", binding.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
AddControllerRumble(controller_index, num_motors,
|
AddControllerRumble(controller_index, num_motors,
|
||||||
std::bind(&ControllerInterface::SetControllerRumbleStrength, m_controller_interface.get(),
|
std::bind(&ControllerInterface::SetControllerRumbleStrength, m_controller_interface.get(),
|
||||||
host_controller_index.value(), std::placeholders::_1, std::placeholders::_2));
|
host_controller_index.value(), std::placeholders::_1, std::placeholders::_2));
|
||||||
|
|
|
@ -356,7 +356,7 @@ protected:
|
||||||
void RegisterHotkey(String category, String name, String display_name, InputButtonHandler handler);
|
void RegisterHotkey(String category, String name, String display_name, InputButtonHandler handler);
|
||||||
bool HandleHostKeyEvent(HostKeyCode code, HostKeyCode modifiers, bool pressed);
|
bool HandleHostKeyEvent(HostKeyCode code, HostKeyCode modifiers, bool pressed);
|
||||||
bool HandleHostMouseEvent(HostMouseButton button, bool pressed);
|
bool HandleHostMouseEvent(HostMouseButton button, bool pressed);
|
||||||
void UpdateInputMap(SettingsInterface& si);
|
virtual void UpdateInputMap(SettingsInterface& si);
|
||||||
void ClearInputMap();
|
void ClearInputMap();
|
||||||
|
|
||||||
void AddControllerRumble(u32 controller_index, u32 num_motors, ControllerRumbleCallback callback);
|
void AddControllerRumble(u32 controller_index, u32 num_motors, ControllerRumbleCallback callback);
|
||||||
|
|
|
@ -22,6 +22,22 @@ void ControllerInterface::Shutdown()
|
||||||
m_host_interface = nullptr;
|
m_host_interface = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<int> ControllerInterface::GetControllerIndex(const std::string_view& device)
|
||||||
|
{
|
||||||
|
if (!StringUtil::StartsWith(device, "Controller"))
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
const std::optional<int> controller_index = StringUtil::FromChars<int>(device.substr(10));
|
||||||
|
if (!controller_index || *controller_index < 0)
|
||||||
|
{
|
||||||
|
Log_WarningPrintf("Invalid controller index in button binding '%*s'", static_cast<int>(device.length()),
|
||||||
|
device.data());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return controller_index;
|
||||||
|
}
|
||||||
|
|
||||||
void ControllerInterface::SetHook(Hook::Callback callback)
|
void ControllerInterface::SetHook(Hook::Callback callback)
|
||||||
{
|
{
|
||||||
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string_view>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
|
|
||||||
class HostInterface;
|
class HostInterface;
|
||||||
|
@ -70,6 +71,7 @@ public:
|
||||||
virtual void ClearBindings() = 0;
|
virtual void ClearBindings() = 0;
|
||||||
|
|
||||||
// Binding to events. If a binding for this axis/button already exists, returns false.
|
// Binding to events. If a binding for this axis/button already exists, returns false.
|
||||||
|
virtual std::optional<int> GetControllerIndex(const std::string_view& device);
|
||||||
virtual bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) = 0;
|
virtual bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) = 0;
|
||||||
virtual bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) = 0;
|
virtual bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) = 0;
|
||||||
virtual bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
virtual bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||||
|
|
Loading…
Reference in a new issue