mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-19 18:36:53 +02:00
Input mapping tabs UI, emulated touchpad, and rumble for non-PS5 controllers (#1541)
* Revamp input mapping dialog with tabs, gamepad remapping and gyro emulation toggle Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Add emulated touchpad (mouse/gamepad) with particle skins; widen input dialog Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Disable mouse touchpad mode when the cursor leaves the window Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Add touchpad hand-cursor toggle; share touchpad options across both tabs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Hide touchpad trail and particles when the visual toggle is off Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Rename input mapping tabs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Rumble non-DualSense gamepads from haptic audio Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix input mapping, touchpad transitions, and rumble fallback --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
@@ -113,7 +113,7 @@ jobs:
|
||||
shell: cmd
|
||||
run: |
|
||||
cmake --build _Build/windows --target launcher ^
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests host_input_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
|
||||
shader_recompiler_compute_tests ^
|
||||
resource_tracking_tests scalar_provenance_tests ^
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests --parallel
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
- name: Test
|
||||
shell: cmd
|
||||
run: |
|
||||
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$"
|
||||
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|controller_settings|host_input|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$"
|
||||
|
||||
- name: Install
|
||||
shell: cmd
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --build _Build/macos \
|
||||
--target launcher audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
--target launcher audio_out2_port_tests pad_haptics_tests controller_settings_tests host_input_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
memory_tracker_tests page_manager_tests shader_recompiler_compute_tests \
|
||||
resource_tracking_tests scalar_provenance_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
|
||||
@@ -212,7 +212,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/macos --output-on-failure \
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|host_input|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
@@ -388,7 +388,7 @@ jobs:
|
||||
run: |
|
||||
cmake --build _Build/linux \
|
||||
--target launcher page_manager_tests memory_tracker_tests \
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests host_input_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
resource_tracking_tests scalar_provenance_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
|
||||
--parallel
|
||||
@@ -397,7 +397,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/linux --output-on-failure \
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|host_input|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
|
||||
@@ -526,6 +526,10 @@ add_executable(controller_settings_tests EXCLUDE_FROM_ALL
|
||||
target_link_libraries(controller_settings_tests common fmt::fmt SDL3::SDL3-static)
|
||||
target_include_directories(controller_settings_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(host_input_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/HostInputTests.cpp")
|
||||
target_link_libraries(host_input_tests common fmt::fmt SDL3::SDL3-static)
|
||||
target_include_directories(host_input_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(save_data_memory_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/SaveDataMemoryTests.cpp"
|
||||
)
|
||||
@@ -644,6 +648,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME ngs2_sampler COMMAND $<TARGET_FILE:ngs2_sampler_tests>)
|
||||
add_test(NAME pad_haptics COMMAND $<TARGET_FILE:pad_haptics_tests>)
|
||||
add_test(NAME controller_settings COMMAND $<TARGET_FILE:controller_settings_tests>)
|
||||
add_test(NAME host_input COMMAND $<TARGET_FILE:host_input_tests>)
|
||||
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
|
||||
add_test(NAME png_enc COMMAND $<TARGET_FILE:png_enc_tests>)
|
||||
add_test(NAME ces COMMAND $<TARGET_FILE:ces_tests>)
|
||||
@@ -723,6 +728,7 @@ if(BUILD_TESTING)
|
||||
ngs2_sampler_tests
|
||||
pad_haptics_tests
|
||||
controller_settings_tests
|
||||
host_input_tests
|
||||
save_data_memory_tests
|
||||
png_enc_tests
|
||||
ces_tests
|
||||
@@ -760,6 +766,14 @@ add_custom_target(kyty_trophy_sound
|
||||
add_dependencies(kyty_emulator kyty_trophy_sound)
|
||||
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION assets/sounds)
|
||||
|
||||
add_custom_target(kyty_touch_cursors
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/assets/cursors"
|
||||
"$<TARGET_FILE_DIR:kyty_emulator>/assets/cursors"
|
||||
VERBATIM)
|
||||
add_dependencies(kyty_emulator kyty_touch_cursors)
|
||||
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/assets/cursors" DESTINATION assets)
|
||||
|
||||
target_link_libraries(kyty_emulator ${kyty_emulator_link_libraries})
|
||||
if (WIN32)
|
||||
target_link_libraries(kyty_emulator opengl32)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -10,6 +10,7 @@
|
||||
#include "gpu_blit_shaders/gpu_imgui_hdr_spv.h"
|
||||
#include "graphics/host_gpu/graphicContext.h"
|
||||
#include "graphics/presentation/ddsImage.h"
|
||||
#include "graphics/presentation/window/hostInput.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "imgui_impl_vulkan.h"
|
||||
@@ -149,6 +150,47 @@ bool GetOverlaySnapshot(OverlaySnapshot* snapshot) {
|
||||
}
|
||||
|
||||
constexpr size_t INPUT_QUEUE_CAPACITY = 128;
|
||||
constexpr int MAX_TOUCH_CURSOR_IMAGE_DIMENSION = 2048;
|
||||
constexpr float TOUCH_CURSOR_IMAGE_SIZE = 48.0f;
|
||||
|
||||
enum class TouchParticleSkin : uint8_t { Classic, BlueNeon, Monochrome, Crimson, RedBlue };
|
||||
|
||||
TouchParticleSkin GetTouchParticleSkin() {
|
||||
static const auto skin = [] {
|
||||
for (const auto& value: Config::GetKeymap()) {
|
||||
constexpr std::string_view key = "TouchParticleSkin=";
|
||||
if (std::string_view(value).starts_with(key)) {
|
||||
const auto name = std::string_view(value).substr(key.size());
|
||||
if (name == "BlueNeon") {
|
||||
return TouchParticleSkin::BlueNeon;
|
||||
}
|
||||
if (name == "Monochrome") {
|
||||
return TouchParticleSkin::Monochrome;
|
||||
}
|
||||
if (name == "Crimson") {
|
||||
return TouchParticleSkin::Crimson;
|
||||
}
|
||||
if (name == "RedBlue") {
|
||||
return TouchParticleSkin::RedBlue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return TouchParticleSkin::Classic;
|
||||
}();
|
||||
return skin;
|
||||
}
|
||||
|
||||
bool TouchCursorImageEnabled() {
|
||||
static const bool enabled = [] {
|
||||
for (const auto& value: Config::GetKeymap()) {
|
||||
if (std::string_view(value) == "TouchCursorImage=Off") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
enum class InputKind : uint8_t {
|
||||
Button,
|
||||
@@ -552,9 +594,11 @@ SystemOverlayVisualState GetSystemOverlayVisualState() noexcept {
|
||||
const auto system = SystemDialog::GetVisualState();
|
||||
const bool trophy_active = g_trophy_notification_active.load(std::memory_order_acquire);
|
||||
const bool splash = g_splash_active.load(std::memory_order_acquire);
|
||||
return {core.active || dialog.active || system.active || trophy_active || splash,
|
||||
const auto cursor = GetHostInputTouchPadVisualState();
|
||||
return {core.active || dialog.active || system.active || trophy_active || splash ||
|
||||
(cursor.enabled && cursor.cursor_visible),
|
||||
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u) +
|
||||
(splash ? 2u : 0u)};
|
||||
(splash ? 2u : 0u) + static_cast<decltype(core.revision)>(cursor.revision)};
|
||||
}
|
||||
|
||||
bool ProcessSystemOverlayInput(const SDL_Event& event) {
|
||||
@@ -694,6 +738,20 @@ void NotifyTrophyUnlocked(std::string_view name, int32_t grade,
|
||||
}
|
||||
|
||||
struct SystemOverlay::Impl {
|
||||
enum class TouchParticleIcon : uint8_t { Square, Triangle, Cross, Circle };
|
||||
|
||||
struct TouchParticle {
|
||||
ImVec2 position;
|
||||
Clock::time_point born;
|
||||
TouchParticleIcon icon;
|
||||
};
|
||||
|
||||
struct TouchTrailSegment {
|
||||
ImVec2 from;
|
||||
ImVec2 to;
|
||||
Clock::time_point born;
|
||||
};
|
||||
|
||||
explicit Impl(GraphicContext& context): graphics(context) {}
|
||||
|
||||
~Impl() {
|
||||
@@ -705,6 +763,12 @@ struct SystemOverlay::Impl {
|
||||
ReleaseVulkan();
|
||||
if (imgui_context != nullptr) {
|
||||
ImGui::SetCurrentContext(imgui_context);
|
||||
if (hand_cursor_image) {
|
||||
ImGui::UnregisterUserTexture(hand_cursor_image.get());
|
||||
}
|
||||
if (drawing_cursor_image) {
|
||||
ImGui::UnregisterUserTexture(drawing_cursor_image.get());
|
||||
}
|
||||
if (trophy_notification_image) {
|
||||
ImGui::UnregisterUserTexture(trophy_notification_image.get());
|
||||
}
|
||||
@@ -735,6 +799,8 @@ struct SystemOverlay::Impl {
|
||||
style.WindowRounding = 10.0f;
|
||||
style.FrameRounding = 6.0f;
|
||||
style.ItemSpacing = {8.0f, 8.0f};
|
||||
hand_cursor_image = LoadTouchCursorImage("hand.png");
|
||||
drawing_cursor_image = LoadTouchCursorImage("drawing.png");
|
||||
}
|
||||
|
||||
void PlayTrophySound() {
|
||||
@@ -775,6 +841,55 @@ struct SystemOverlay::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<ImTextureData> LoadTouchCursorImage(std::string_view filename) {
|
||||
const char* base_path = SDL_GetBasePath();
|
||||
if (base_path == nullptr) {
|
||||
LOGF("Could not locate the emulator folder for touchpad cursor images: %s\n",
|
||||
SDL_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
const std::filesystem::path path =
|
||||
std::filesystem::path(base_path) / "assets" / "cursors" / std::string(filename);
|
||||
|
||||
Common::File file;
|
||||
if (!file.Open(path, Common::File::Mode::Read)) {
|
||||
LOGF("Touchpad cursor image not found: %s\n", Common::PathToString(path).c_str());
|
||||
return nullptr;
|
||||
}
|
||||
const auto encoded = file.ReadWholeBuffer();
|
||||
if (encoded.empty() ||
|
||||
encoded.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||
LOGF("Touchpad cursor image is empty or too large: %s\n",
|
||||
Common::PathToString(path).c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
auto* pixels = stbi_load_from_memory(
|
||||
reinterpret_cast<const stbi_uc*>(encoded.data()), static_cast<int>(encoded.size()),
|
||||
&width, &height, nullptr, 4);
|
||||
if (pixels == nullptr || width <= 0 || height <= 0 ||
|
||||
width > MAX_TOUCH_CURSOR_IMAGE_DIMENSION ||
|
||||
height > MAX_TOUCH_CURSOR_IMAGE_DIMENSION) {
|
||||
LOGF("Could not load touchpad cursor image %s: %s\n",
|
||||
Common::PathToString(path).c_str(),
|
||||
pixels == nullptr ? stbi_failure_reason() : "image dimensions exceed 2048x2048");
|
||||
stbi_image_free(pixels);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto texture = std::make_unique<ImTextureData>();
|
||||
texture->Create(ImTextureFormat_RGBA32, width, height);
|
||||
std::memcpy(texture->Pixels, pixels,
|
||||
static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4);
|
||||
texture->UseColors = true;
|
||||
stbi_image_free(pixels);
|
||||
ImGui::RegisterUserTexture(texture.get());
|
||||
LOGF("Loaded touchpad cursor image: %s\n", Common::PathToString(path).c_str());
|
||||
return texture;
|
||||
}
|
||||
|
||||
std::unique_ptr<ImTextureData> LoadTrophyIcon(std::span<const std::byte> encoded) {
|
||||
if (encoded.empty() ||
|
||||
encoded.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||
@@ -1191,6 +1306,248 @@ struct SystemOverlay::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
void EmitTouchParticles(ImVec2 from, ImVec2 to, Clock::time_point now) {
|
||||
const float dx = to.x - from.x;
|
||||
const float dy = to.y - from.y;
|
||||
const float distance = std::hypot(dx, dy);
|
||||
if (distance < 0.01f) {
|
||||
return;
|
||||
}
|
||||
if (distance >= 1.0f) {
|
||||
trail_segments.push_back({from, to, now});
|
||||
if (trail_segments.size() > MAX_TOUCH_TRAIL_SEGMENTS) {
|
||||
trail_segments.erase(trail_segments.begin());
|
||||
}
|
||||
}
|
||||
|
||||
constexpr float PARTICLE_SPACING = 24.0f;
|
||||
float traveled = 0.0f;
|
||||
while (particle_distance + distance - traveled >= PARTICLE_SPACING) {
|
||||
traveled += PARTICLE_SPACING - particle_distance;
|
||||
const float fraction = std::clamp(traveled / distance, 0.0f, 1.0f);
|
||||
particles.push_back(
|
||||
{{from.x + dx * fraction, from.y + dy * fraction}, now, next_particle_icon});
|
||||
next_particle_icon = static_cast<TouchParticleIcon>(
|
||||
(static_cast<uint8_t>(next_particle_icon) + 1) % 4);
|
||||
if (particles.size() > MAX_TOUCH_PARTICLES) {
|
||||
particles.erase(particles.begin());
|
||||
}
|
||||
particle_distance = 0.0f;
|
||||
}
|
||||
particle_distance += distance - traveled;
|
||||
}
|
||||
|
||||
void DrawTouchParticles(ImDrawList* draw, Clock::time_point now) {
|
||||
const auto skin = GetTouchParticleSkin();
|
||||
constexpr float PARTICLE_LIFETIME = 1.0f;
|
||||
constexpr float TRAIL_LIFETIME = 0.35f;
|
||||
std::erase_if(trail_segments, [now](const auto& segment) {
|
||||
return std::chrono::duration<float>(now - segment.born).count() >= TRAIL_LIFETIME;
|
||||
});
|
||||
std::erase_if(particles, [now](const auto& particle) {
|
||||
return std::chrono::duration<float>(now - particle.born).count() >=
|
||||
PARTICLE_LIFETIME;
|
||||
});
|
||||
for (const auto& segment: trail_segments) {
|
||||
const float age =
|
||||
std::chrono::duration<float>(now - segment.born).count() / TRAIL_LIFETIME;
|
||||
const float fade = 1.0f - age;
|
||||
if (skin == TouchParticleSkin::RedBlue) {
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(20, 75, 220, static_cast<int>(105.0f * fade)),
|
||||
9.0f * (1.0f - age * 0.5f));
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(255, 45, 65, static_cast<int>(230.0f * fade)),
|
||||
4.0f * (1.0f - age * 0.5f));
|
||||
} else if (skin == TouchParticleSkin::Crimson) {
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(110, 0, 8, static_cast<int>(130.0f * fade)),
|
||||
9.0f * (1.0f - age * 0.5f));
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(255, 35, 20, static_cast<int>(235.0f * fade)),
|
||||
4.0f * (1.0f - age * 0.5f));
|
||||
} else {
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(35, 145, 255, static_cast<int>(180.0f * fade)),
|
||||
5.0f * (1.0f - age * 0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& particle: particles) {
|
||||
const float age =
|
||||
std::chrono::duration<float>(now - particle.born).count() / PARTICLE_LIFETIME;
|
||||
const float alpha = 1.0f - age;
|
||||
const float size = 9.0f * (1.0f - age * 0.35f);
|
||||
const float drift = (static_cast<uint8_t>(particle.icon) % 2 == 0 ? -1.0f : 1.0f) *
|
||||
8.0f * age;
|
||||
const ImVec2 center {particle.position.x + drift, particle.position.y - 18.0f * age};
|
||||
const auto faded = [alpha, skin, icon = particle.icon](int r, int g, int b) {
|
||||
if (skin == TouchParticleSkin::BlueNeon) {
|
||||
r = 75;
|
||||
g = 220;
|
||||
b = 255;
|
||||
} else if (skin == TouchParticleSkin::RedBlue) {
|
||||
const bool red = icon == TouchParticleIcon::Square ||
|
||||
icon == TouchParticleIcon::Cross;
|
||||
r = red ? 255 : 75;
|
||||
g = red ? 65 : 175;
|
||||
b = red ? 85 : 255;
|
||||
} else if (skin == TouchParticleSkin::Monochrome) {
|
||||
r = 235;
|
||||
g = 245;
|
||||
b = 255;
|
||||
} else if (skin == TouchParticleSkin::Crimson) {
|
||||
const bool ember = icon == TouchParticleIcon::Triangle ||
|
||||
icon == TouchParticleIcon::Circle;
|
||||
r = 255;
|
||||
g = ember ? 205 : 115;
|
||||
b = ember ? 70 : 15;
|
||||
}
|
||||
return IM_COL32(r, g, b, static_cast<int>(255.0f * alpha));
|
||||
};
|
||||
|
||||
if (skin != TouchParticleSkin::Classic) {
|
||||
const ImU32 glow =
|
||||
skin == TouchParticleSkin::BlueNeon
|
||||
? IM_COL32(0, 115, 255, static_cast<int>(38.0f * alpha))
|
||||
: (skin == TouchParticleSkin::RedBlue
|
||||
? IM_COL32(75, 100, 255, static_cast<int>(55.0f * alpha))
|
||||
: (skin == TouchParticleSkin::Crimson
|
||||
? IM_COL32(255, 70, 0, static_cast<int>(70.0f * alpha))
|
||||
: IM_COL32(115, 165, 255, static_cast<int>(30.0f * alpha))));
|
||||
draw->AddCircleFilled(center, size + 6.0f, glow);
|
||||
}
|
||||
const ImU32 particle_glow =
|
||||
skin == TouchParticleSkin::Crimson
|
||||
? (particle.icon == TouchParticleIcon::Triangle ||
|
||||
particle.icon == TouchParticleIcon::Circle
|
||||
? IM_COL32(255, 195, 30, static_cast<int>(135.0f * alpha))
|
||||
: IM_COL32(255, 95, 0, static_cast<int>(135.0f * alpha)))
|
||||
: (skin == TouchParticleSkin::RedBlue
|
||||
? (particle.icon == TouchParticleIcon::Square ||
|
||||
particle.icon == TouchParticleIcon::Cross
|
||||
? IM_COL32(255, 40, 65, static_cast<int>(105.0f * alpha))
|
||||
: IM_COL32(35, 125, 255, static_cast<int>(105.0f * alpha)))
|
||||
: IM_COL32(35, 145, 255,
|
||||
static_cast<int>((skin == TouchParticleSkin::BlueNeon
|
||||
? 100.0f
|
||||
: 65.0f) *
|
||||
alpha)));
|
||||
draw->AddCircleFilled(center, size + 2.0f, particle_glow);
|
||||
switch (particle.icon) {
|
||||
case TouchParticleIcon::Square:
|
||||
draw->AddRect({center.x - size * 0.65f, center.y - size * 0.65f},
|
||||
{center.x + size * 0.65f, center.y + size * 0.65f},
|
||||
faded(255, 105, 180), 2.0f, 0, 2.2f);
|
||||
break;
|
||||
case TouchParticleIcon::Triangle:
|
||||
draw->AddLine({center.x, center.y - size}, {center.x - size, center.y + size},
|
||||
faded(80, 230, 130), 2.2f);
|
||||
draw->AddLine({center.x - size, center.y + size},
|
||||
{center.x + size, center.y + size}, faded(80, 230, 130), 2.2f);
|
||||
draw->AddLine({center.x + size, center.y + size},
|
||||
{center.x, center.y - size}, faded(80, 230, 130), 2.2f);
|
||||
break;
|
||||
case TouchParticleIcon::Cross:
|
||||
draw->AddLine({center.x - size * 0.75f, center.y - size * 0.75f},
|
||||
{center.x + size * 0.75f, center.y + size * 0.75f},
|
||||
faded(90, 170, 255), 2.5f);
|
||||
draw->AddLine({center.x + size * 0.75f, center.y - size * 0.75f},
|
||||
{center.x - size * 0.75f, center.y + size * 0.75f},
|
||||
faded(90, 170, 255), 2.5f);
|
||||
break;
|
||||
case TouchParticleIcon::Circle:
|
||||
draw->AddCircle(center, size * 0.8f, faded(255, 105, 105), 0, 2.2f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawTouchPadCursor(const HostInputTouchPadVisualState& state) {
|
||||
if (!TouchCursorImageEnabled()) {
|
||||
particles.clear();
|
||||
trail_segments.clear();
|
||||
return;
|
||||
}
|
||||
const auto now = Clock::now();
|
||||
if (state.enabled && state.position_valid) {
|
||||
const ImVec2 target {state.x * extent.width, state.y * extent.height};
|
||||
if (!cursor_initialized || state.gesture != cursor_gesture) {
|
||||
cursor_position = target;
|
||||
cursor_initialized = true;
|
||||
cursor_gesture = state.gesture;
|
||||
particle_distance = 0.0f;
|
||||
} else {
|
||||
const float dt = last_cursor_update == Clock::time_point {}
|
||||
? 1.0f / 60.0f
|
||||
: std::clamp(std::chrono::duration<float>(now -
|
||||
last_cursor_update)
|
||||
.count(),
|
||||
1.0f / 1000.0f, 0.05f);
|
||||
const ImVec2 previous = cursor_position;
|
||||
const float follow = 1.0f - std::exp(-18.0f * dt);
|
||||
cursor_position.x += (target.x - cursor_position.x) * follow;
|
||||
cursor_position.y += (target.y - cursor_position.y) * follow;
|
||||
if (state.drawing) {
|
||||
EmitTouchParticles(previous, cursor_position, now);
|
||||
}
|
||||
}
|
||||
last_cursor_update = now;
|
||||
}
|
||||
auto* draw = ImGui::GetForegroundDrawList();
|
||||
DrawTouchParticles(draw, now);
|
||||
if (!state.enabled || !state.cursor_visible ||
|
||||
!state.position_valid || !cursor_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImVec2 position = cursor_position;
|
||||
const auto& cursor_image =
|
||||
state.drawing ? drawing_cursor_image : hand_cursor_image;
|
||||
if (cursor_image != nullptr) {
|
||||
const float scale = TOUCH_CURSOR_IMAGE_SIZE /
|
||||
static_cast<float>(std::max(cursor_image->Height, 1));
|
||||
const ImVec2 size {static_cast<float>(cursor_image->Width) * scale,
|
||||
static_cast<float>(cursor_image->Height) * scale};
|
||||
draw->AddImage(cursor_image->GetTexRef(), position,
|
||||
{position.x + size.x, position.y + size.y});
|
||||
return;
|
||||
}
|
||||
|
||||
const ImU32 blue = IM_COL32(35, 145, 255, 220);
|
||||
const ImVec2 hand[] = {
|
||||
{position.x, position.y}, {position.x, position.y + 20.0f},
|
||||
{position.x + 4.0f, position.y + 16.0f},
|
||||
{position.x + 9.0f, position.y + 27.0f},
|
||||
{position.x + 13.0f, position.y + 25.0f},
|
||||
{position.x + 10.0f, position.y + 17.0f},
|
||||
{position.x + 18.0f, position.y + 18.0f},
|
||||
{position.x + 20.0f, position.y + 15.0f},
|
||||
{position.x + 19.0f, position.y + 12.0f},
|
||||
{position.x + 12.0f, position.y + 10.0f},
|
||||
{position.x + 12.0f, position.y + 4.0f},
|
||||
{position.x + 10.0f, position.y + 2.0f},
|
||||
{position.x + 8.0f, position.y + 3.0f},
|
||||
{position.x + 8.0f, position.y + 10.0f},
|
||||
{position.x + 6.0f, position.y + 2.0f},
|
||||
{position.x + 4.0f, position.y + 2.0f},
|
||||
{position.x + 3.0f, position.y + 10.0f},
|
||||
{position.x + 2.0f, position.y + 1.0f},
|
||||
};
|
||||
draw->AddConvexPolyFilled(hand, IM_ARRAYSIZE(hand), IM_COL32(250, 250, 250, 255));
|
||||
for (std::size_t i = 0; i < IM_ARRAYSIZE(hand); i++) {
|
||||
draw->AddLine(hand[i], hand[(i + 1) % IM_ARRAYSIZE(hand)],
|
||||
IM_COL32(20, 25, 35, 255), 1.5f);
|
||||
}
|
||||
if (state.drawing) {
|
||||
const ImVec2 pen_start {position.x + 12.0f, position.y + 8.0f};
|
||||
const ImVec2 pen_end {position.x + 23.0f, position.y - 3.0f};
|
||||
draw->AddLine(pen_start, pen_end, IM_COL32(10, 45, 95, 255), 5.0f);
|
||||
draw->AddLine(pen_start, pen_end, blue, 2.5f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void DrawTrophyNotification(vk::Extent2D frame_extent, Clock::time_point now) {
|
||||
std::erase_if(retired_trophy_images, [](const auto& texture) {
|
||||
if (texture->Status != ImTextureStatus_Destroyed) {
|
||||
@@ -1316,7 +1673,8 @@ struct SystemOverlay::Impl {
|
||||
const bool has_overlay = GetOverlaySnapshot(&snapshot);
|
||||
const auto now = Clock::now();
|
||||
if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire) &&
|
||||
!g_splash_active.load(std::memory_order_acquire)) {
|
||||
!g_splash_active.load(std::memory_order_acquire) &&
|
||||
!GetHostInputTouchPadVisualState().enabled) {
|
||||
return false;
|
||||
}
|
||||
const auto prepared_session = snapshot.session;
|
||||
@@ -1356,6 +1714,7 @@ struct SystemOverlay::Impl {
|
||||
}
|
||||
}
|
||||
DrawTrophyNotification(frame_extent, now);
|
||||
DrawTouchPadCursor(GetHostInputTouchPadVisualState());
|
||||
ImGui::Render();
|
||||
extent = frame_extent;
|
||||
return true;
|
||||
@@ -1410,6 +1769,18 @@ struct SystemOverlay::Impl {
|
||||
ImVec2 right_stick {};
|
||||
OverlaySession session;
|
||||
std::unique_ptr<ImTextureData> trophy_notification_image;
|
||||
std::unique_ptr<ImTextureData> hand_cursor_image;
|
||||
std::unique_ptr<ImTextureData> drawing_cursor_image;
|
||||
bool cursor_initialized = false;
|
||||
ImVec2 cursor_position {};
|
||||
float particle_distance = 0.0f;
|
||||
TouchParticleIcon next_particle_icon = TouchParticleIcon::Square;
|
||||
Clock::time_point last_cursor_update;
|
||||
uint64_t cursor_gesture = 0;
|
||||
std::vector<TouchParticle> particles;
|
||||
std::vector<TouchTrailSegment> trail_segments;
|
||||
static constexpr std::size_t MAX_TOUCH_PARTICLES = 64;
|
||||
static constexpr std::size_t MAX_TOUCH_TRAIL_SEGMENTS = 128;
|
||||
std::optional<TrophyNotification> trophy_notification;
|
||||
Clock::time_point trophy_notification_born;
|
||||
std::vector<std::unique_ptr<ImTextureData>> retired_trophy_images;
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
@@ -27,6 +29,8 @@ struct ControlInfo {
|
||||
float touch_x = 0.0f;
|
||||
|
||||
Controller::Setting setting {};
|
||||
bool gyro = false;
|
||||
bool touchpad_mode = false;
|
||||
};
|
||||
|
||||
static constexpr std::array CONTROL_INFO = {
|
||||
@@ -61,10 +65,66 @@ static constexpr std::array CONTROL_INFO = {
|
||||
ControlInfo {.name = "VibrationIntensity", .setting = Controller::Setting::VibrationIntensity},
|
||||
ControlInfo {.name = "TriggerEffectIntensity",
|
||||
.setting = Controller::Setting::TriggerEffectIntensity},
|
||||
ControlInfo {.name = "GyroToggle", .gyro = true},
|
||||
ControlInfo {.name = "TouchPadMode", .touchpad_mode = true},
|
||||
};
|
||||
|
||||
constexpr std::size_t INVALID_CONTROL = CONTROL_INFO.size();
|
||||
|
||||
struct PadSource {
|
||||
std::string_view name;
|
||||
int button; // SDL_GamepadButton, or -1 for an analog trigger
|
||||
};
|
||||
|
||||
constexpr std::array PAD_SOURCES = {
|
||||
PadSource {"South", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
PadSource {"East", SDL_GAMEPAD_BUTTON_EAST},
|
||||
PadSource {"West", SDL_GAMEPAD_BUTTON_WEST},
|
||||
PadSource {"North", SDL_GAMEPAD_BUTTON_NORTH},
|
||||
PadSource {"Back", SDL_GAMEPAD_BUTTON_BACK},
|
||||
PadSource {"Guide", SDL_GAMEPAD_BUTTON_GUIDE},
|
||||
PadSource {"Start", SDL_GAMEPAD_BUTTON_START},
|
||||
PadSource {"LeftStick", SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||
PadSource {"RightStick", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
||||
PadSource {"LeftShoulder", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||
PadSource {"RightShoulder", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||
PadSource {"DpadUp", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
||||
PadSource {"DpadDown", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||
PadSource {"DpadLeft", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
||||
PadSource {"DpadRight", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||
PadSource {"Misc1", SDL_GAMEPAD_BUTTON_MISC1},
|
||||
PadSource {"RightPaddle1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
|
||||
PadSource {"LeftPaddle1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
|
||||
PadSource {"RightPaddle2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
|
||||
PadSource {"LeftPaddle2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
|
||||
PadSource {"Touchpad", SDL_GAMEPAD_BUTTON_TOUCHPAD},
|
||||
PadSource {"LeftTrigger", -1},
|
||||
PadSource {"RightTrigger", -1},
|
||||
};
|
||||
|
||||
constexpr std::size_t LEFT_TRIGGER_SOURCE = PAD_SOURCES.size() - 2;
|
||||
constexpr std::size_t RIGHT_TRIGGER_SOURCE = PAD_SOURCES.size() - 1;
|
||||
constexpr std::size_t INVALID_SOURCE = PAD_SOURCES.size();
|
||||
|
||||
struct PadDefault {
|
||||
std::string_view control;
|
||||
std::string_view source;
|
||||
};
|
||||
|
||||
constexpr std::array PAD_DEFAULTS = {
|
||||
PadDefault {"Cross", "South"}, PadDefault {"Circle", "East"},
|
||||
PadDefault {"Square", "West"}, PadDefault {"Triangle", "North"},
|
||||
PadDefault {"Options", "Start"}, PadDefault {"L3", "LeftStick"},
|
||||
PadDefault {"R3", "RightStick"}, PadDefault {"L1", "LeftShoulder"},
|
||||
PadDefault {"R1", "RightShoulder"}, PadDefault {"L2", "LeftTrigger"},
|
||||
PadDefault {"R2", "RightTrigger"}, PadDefault {"Up", "DpadUp"},
|
||||
PadDefault {"Down", "DpadDown"}, PadDefault {"Left", "DpadLeft"},
|
||||
PadDefault {"Right", "DpadRight"}, PadDefault {"TouchPad", "Touchpad"},
|
||||
PadDefault {"GyroToggle", "Back"},
|
||||
};
|
||||
|
||||
constexpr std::string_view PAD_PREFIX = "Pad:";
|
||||
|
||||
struct Binding {
|
||||
SDL_Keycode key = SDLK_UNKNOWN;
|
||||
uint8_t mouse_button = 0;
|
||||
@@ -74,6 +134,7 @@ struct Binding {
|
||||
constexpr int MOUSE_POLL_INTERVAL_MS = 33;
|
||||
constexpr uint64_t CURSOR_IDLE_HIDE_MS = 2000;
|
||||
constexpr std::string_view MOUSE_SENSITIVITY = "MouseSensitivity=";
|
||||
constexpr std::string_view TOUCH_PARTICLE_SKIN = "TouchParticleSkin=";
|
||||
|
||||
struct MouseJoystickState {
|
||||
bool enabled = false;
|
||||
@@ -85,6 +146,38 @@ MouseJoystickState g_mouse;
|
||||
SDL_Window* g_mouse_window = nullptr;
|
||||
uint64_t g_cursor_hide_at = 0;
|
||||
|
||||
struct MouseTouchPadState {
|
||||
bool enabled = false;
|
||||
bool cursor_visible = true;
|
||||
bool drawing = false;
|
||||
bool position_valid = false;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
uint64_t revision = 0;
|
||||
uint64_t gesture = 0;
|
||||
};
|
||||
|
||||
struct GamepadTouchPadState {
|
||||
bool enabled = false;
|
||||
bool drawing = false;
|
||||
int id = -1;
|
||||
int x_axis = 128;
|
||||
int y_axis = 128;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
uint64_t next_poll = 0;
|
||||
uint64_t last_poll = 0;
|
||||
};
|
||||
|
||||
constexpr int GAMEPAD_TOUCHPAD_POLL_MS = 8;
|
||||
constexpr int GAMEPAD_TOUCHPAD_DEADZONE = 18;
|
||||
constexpr float GAMEPAD_TOUCHPAD_SPEED = 0.8f;
|
||||
|
||||
MouseTouchPadState g_mouse_touchpad;
|
||||
GamepadTouchPadState g_gamepad_touchpad;
|
||||
std::mutex g_touchpad_mutex;
|
||||
std::unordered_map<int, std::array<bool, 2>> g_pad_trigger_pressed;
|
||||
|
||||
std::size_t ControlFromName(std::string_view name) {
|
||||
const auto info = std::find_if(CONTROL_INFO.begin(), CONTROL_INFO.end(),
|
||||
[name](const auto& item) { return item.name == name; });
|
||||
@@ -126,6 +219,14 @@ public:
|
||||
InputMap() {
|
||||
for (const auto& value: Config::GetKeymap()) {
|
||||
const std::string_view entry = value;
|
||||
if (entry == "Keyboard=Disabled") {
|
||||
m_custom = true;
|
||||
continue;
|
||||
}
|
||||
if (entry == "Pad=Disabled") {
|
||||
m_custom_pad = true;
|
||||
continue;
|
||||
}
|
||||
if (entry.starts_with(MOUSE_SENSITIVITY)) {
|
||||
const float sensitivity =
|
||||
std::strtof(value.c_str() + MOUSE_SENSITIVITY.size(), nullptr);
|
||||
@@ -133,6 +234,13 @@ public:
|
||||
std::clamp(std::isfinite(sensitivity) ? sensitivity : 1.0f, 0.1f, 5.0f);
|
||||
continue;
|
||||
}
|
||||
if (entry.starts_with(TOUCH_PARTICLE_SKIN) || entry.starts_with("TouchCursorImage=")) {
|
||||
continue;
|
||||
}
|
||||
if (entry.starts_with(PAD_PREFIX)) {
|
||||
AddPadEntry(entry.substr(PAD_PREFIX.size()), value);
|
||||
continue;
|
||||
}
|
||||
const auto split = entry.find('=');
|
||||
|
||||
Binding binding;
|
||||
@@ -153,10 +261,25 @@ public:
|
||||
}
|
||||
Add(binding);
|
||||
}
|
||||
if (!m_custom_pad) {
|
||||
for (const auto& item: PAD_DEFAULTS) {
|
||||
AssignPad(ControlFromName(item.control), SourceFromName(item.source));
|
||||
}
|
||||
[[nodiscard]] bool Custom() const { return m_size != 0; }
|
||||
}
|
||||
}
|
||||
[[nodiscard]] bool Custom() const { return m_custom; }
|
||||
[[nodiscard]] float MouseSensitivity() const { return m_mouse_sensitivity; }
|
||||
|
||||
[[nodiscard]] std::size_t PadControl(std::size_t source) const {
|
||||
return source < m_pad_controls.size() ? m_pad_controls[source] : INVALID_CONTROL;
|
||||
}
|
||||
|
||||
static std::size_t SourceFromButton(int button) {
|
||||
const auto item = std::find_if(PAD_SOURCES.begin(), PAD_SOURCES.end(),
|
||||
[button](const auto& s) { return s.button == button; });
|
||||
return static_cast<std::size_t>(std::distance(PAD_SOURCES.begin(), item));
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t FindKey(int key_code) const {
|
||||
key_code = NormalizeKey(static_cast<SDL_Keycode>(key_code));
|
||||
const auto binding =
|
||||
@@ -173,7 +296,39 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static std::size_t SourceFromName(std::string_view name) {
|
||||
const auto item = std::find_if(PAD_SOURCES.begin(), PAD_SOURCES.end(),
|
||||
[name](const auto& s) { return s.name == name; });
|
||||
return static_cast<std::size_t>(std::distance(PAD_SOURCES.begin(), item));
|
||||
}
|
||||
|
||||
void AssignPad(std::size_t control, std::size_t source) {
|
||||
for (auto& existing: m_pad_controls) {
|
||||
if (existing == control) {
|
||||
existing = INVALID_CONTROL;
|
||||
}
|
||||
}
|
||||
m_pad_controls[source] = control;
|
||||
}
|
||||
|
||||
void AddPadEntry(std::string_view entry, const std::string& original) {
|
||||
const auto split = entry.find('=');
|
||||
const auto control = split != std::string_view::npos ? ControlFromName(entry.substr(0, split))
|
||||
: INVALID_CONTROL;
|
||||
const auto source = split != std::string_view::npos ? SourceFromName(entry.substr(split + 1))
|
||||
: INVALID_SOURCE;
|
||||
if (control == INVALID_CONTROL || source == INVALID_SOURCE) {
|
||||
EXIT("Invalid input mapping: %s\n", original.c_str());
|
||||
}
|
||||
const auto& info = CONTROL_INFO[control];
|
||||
if (info.button == 0 && !info.gyro && info.axis != Controller::Axis::AxisMax) {
|
||||
EXIT("Invalid input mapping: %s\n", original.c_str());
|
||||
}
|
||||
m_custom_pad = true;
|
||||
AssignPad(control, source);
|
||||
}
|
||||
void Add(const Binding& binding) {
|
||||
m_custom = true;
|
||||
for (std::size_t index = 0; index < m_size;) {
|
||||
if (Conflicts(m_bindings[index], binding)) {
|
||||
m_bindings[index] = m_bindings[--m_size];
|
||||
@@ -186,6 +341,13 @@ private:
|
||||
}
|
||||
|
||||
std::array<Binding, CONTROL_INFO.size()> m_bindings {};
|
||||
std::array<std::size_t, PAD_SOURCES.size()> m_pad_controls = [] {
|
||||
std::array<std::size_t, PAD_SOURCES.size()> a {};
|
||||
a.fill(INVALID_CONTROL);
|
||||
return a;
|
||||
}();
|
||||
bool m_custom = false;
|
||||
bool m_custom_pad = false;
|
||||
std::size_t m_size = 0;
|
||||
float m_mouse_sensitivity = 1.0f;
|
||||
};
|
||||
@@ -211,6 +373,39 @@ void SetTouchPad(float x, bool down) {
|
||||
Controller::SetTouchPad(Controller::HOST_INPUT_CONTROLLER_ID, 0, down, x, 0.5f);
|
||||
}
|
||||
|
||||
void SetSwipeTouchPad(bool down, float x, float y) {
|
||||
Controller::SetTouchPad(Controller::HOST_INPUT_CONTROLLER_ID, 1, down, x, y);
|
||||
}
|
||||
|
||||
void RecordMouseTouchPadPosition(float x, float y) {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
if (g_mouse_window != nullptr) {
|
||||
SDL_GetWindowSize(g_mouse_window, &width, &height);
|
||||
}
|
||||
if (width > 0 && height > 0) {
|
||||
x = std::clamp(x / static_cast<float>(width), 0.0f, 1.0f);
|
||||
y = std::clamp(y / static_cast<float>(height), 0.0f, 1.0f);
|
||||
} else {
|
||||
x = 0.5f;
|
||||
y = 0.5f;
|
||||
}
|
||||
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_mouse_touchpad.enabled) {
|
||||
return;
|
||||
}
|
||||
g_mouse_touchpad.x = x;
|
||||
g_mouse_touchpad.y = y;
|
||||
g_mouse_touchpad.position_valid = true;
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
|
||||
bool TouchPadModeActive() {
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
return g_mouse_touchpad.enabled || g_gamepad_touchpad.enabled;
|
||||
}
|
||||
|
||||
uint32_t DefaultKeyboardButton(int key_code) {
|
||||
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
|
||||
case SDLK_UP: return Controller::PAD_BUTTON_UP;
|
||||
@@ -270,6 +465,18 @@ void SetControl(std::size_t control, bool down) {
|
||||
SetButton(info.button, down);
|
||||
return;
|
||||
}
|
||||
if (info.gyro) {
|
||||
if (down) {
|
||||
Controller::ToggleGyroEmulation(Controller::HOST_INPUT_CONTROLLER_ID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (info.touchpad_mode) {
|
||||
if (down) {
|
||||
HostInputToggleTouchPadMode();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (info.axis == Controller::Axis::AxisMax) {
|
||||
CycleSettingOnPress(info.setting, down);
|
||||
return;
|
||||
@@ -297,6 +504,11 @@ void DefaultKeyboardInput(int key_code, bool down) {
|
||||
|
||||
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
|
||||
case SDLK_BACKSPACE: SetTouchPad(0.25f, down); return;
|
||||
case SDLK_F6:
|
||||
if (down) {
|
||||
HostInputToggleTouchPadMode();
|
||||
}
|
||||
return;
|
||||
case SDLK_TAB: SetTouchPad(0.75f, down); return;
|
||||
case SDLK_1: CycleSettingOnPress(Controller::Setting::SpeakerVolume, down); return;
|
||||
case SDLK_2: CycleSettingOnPress(Controller::Setting::VibrationIntensity, down); return;
|
||||
@@ -399,8 +611,134 @@ bool IsCursorActivity(const SDL_Event& event) {
|
||||
return which != SDL_TOUCH_MOUSEID && which != SDL_PEN_MOUSEID;
|
||||
}
|
||||
|
||||
std::array<int, 2> GamepadLeftStick(int id) {
|
||||
auto* pad = SDL_GetGamepadFromID(id);
|
||||
if (pad == nullptr) {
|
||||
return {128, 128};
|
||||
}
|
||||
const auto value = [pad](SDL_GamepadAxis axis) {
|
||||
return Controller::controller_get_axis(SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX,
|
||||
SDL_GetGamepadAxis(pad, axis));
|
||||
};
|
||||
return {value(SDL_GAMEPAD_AXIS_LEFTX), value(SDL_GAMEPAD_AXIS_LEFTY)};
|
||||
}
|
||||
|
||||
void ApplyPadControl(int id, std::size_t control, std::size_t source, bool down) {
|
||||
if (control == INVALID_CONTROL) {
|
||||
return;
|
||||
}
|
||||
const auto& info = CONTROL_INFO[control];
|
||||
if (info.gyro) {
|
||||
if (down) {
|
||||
Controller::ToggleGyroEmulation(id);
|
||||
}
|
||||
} else if (info.touchpad_mode) {
|
||||
if (down) {
|
||||
HostInputToggleGamepadTouchPad(id);
|
||||
}
|
||||
} else if (info.button == Controller::PAD_BUTTON_CROSS && HostInputGamepadDrawButton(id, down)) {
|
||||
return;
|
||||
} else if (info.button == Controller::PAD_BUTTON_TOUCH_PAD) {
|
||||
// Real touchpads report their own finger position; other inputs emulate a click.
|
||||
if (source != PAD_SOURCES.size() && PAD_SOURCES[source].button != SDL_GAMEPAD_BUTTON_TOUCHPAD) {
|
||||
Controller::SetTouchPad(id, 0, down, info.touch_x != 0.0f ? info.touch_x : 0.5f, 0.5f);
|
||||
}
|
||||
Controller::SetButton(id, Controller::PAD_BUTTON_TOUCH_PAD, down);
|
||||
} else if (info.button == Controller::PAD_BUTTON_L2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerLeft, down ? 255 : 0);
|
||||
} else if (info.button == Controller::PAD_BUTTON_R2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerRight, down ? 255 : 0);
|
||||
} else if (info.button != 0) {
|
||||
Controller::SetButton(id, info.button, down);
|
||||
} else if (info.axis == Controller::Axis::AxisMax) {
|
||||
CycleSettingOnPress(info.setting, down);
|
||||
}
|
||||
}
|
||||
|
||||
int PollGamepadTouchPad(uint64_t now_ms) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
int id = -1;
|
||||
bool drawing = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled) {
|
||||
return -1;
|
||||
}
|
||||
if (now_ms < g_gamepad_touchpad.next_poll) {
|
||||
return static_cast<int>(g_gamepad_touchpad.next_poll - now_ms);
|
||||
}
|
||||
|
||||
const float dt = g_gamepad_touchpad.last_poll == 0 || now_ms <= g_gamepad_touchpad.last_poll
|
||||
? 0.008f
|
||||
: std::min(static_cast<float>(now_ms - g_gamepad_touchpad.last_poll) *
|
||||
0.001f,
|
||||
0.05f);
|
||||
g_gamepad_touchpad.last_poll = now_ms;
|
||||
g_gamepad_touchpad.next_poll = now_ms + GAMEPAD_TOUCHPAD_POLL_MS;
|
||||
const auto velocity = [](int axis) {
|
||||
const int offset = axis - 128;
|
||||
if (std::abs(offset) <= GAMEPAD_TOUCHPAD_DEADZONE) {
|
||||
return 0.0f;
|
||||
}
|
||||
const int adjusted =
|
||||
offset > 0 ? offset - GAMEPAD_TOUCHPAD_DEADZONE : offset + GAMEPAD_TOUCHPAD_DEADZONE;
|
||||
return static_cast<float>(adjusted) / static_cast<float>(127 - GAMEPAD_TOUCHPAD_DEADZONE);
|
||||
};
|
||||
const float old_x = g_gamepad_touchpad.x;
|
||||
const float old_y = g_gamepad_touchpad.y;
|
||||
g_gamepad_touchpad.x = std::clamp(
|
||||
old_x + velocity(g_gamepad_touchpad.x_axis) * GAMEPAD_TOUCHPAD_SPEED * dt, 0.0f, 1.0f);
|
||||
g_gamepad_touchpad.y = std::clamp(
|
||||
old_y + velocity(g_gamepad_touchpad.y_axis) * GAMEPAD_TOUCHPAD_SPEED * dt, 0.0f, 1.0f);
|
||||
if (old_x != g_gamepad_touchpad.x || old_y != g_gamepad_touchpad.y) {
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
id = g_gamepad_touchpad.id;
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
drawing = g_gamepad_touchpad.drawing;
|
||||
}
|
||||
if (drawing) {
|
||||
Controller::SetTouchPad(id, 1, true, x, y);
|
||||
}
|
||||
return GAMEPAD_TOUCHPAD_POLL_MS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void HostInputPadButton(int id, int button, bool down) {
|
||||
const auto source = InputMap::SourceFromButton(button);
|
||||
const auto& map = GetInputMap();
|
||||
ApplyPadControl(id, map.PadControl(source), source, down);
|
||||
}
|
||||
|
||||
void HostInputPadTrigger(int id, bool left, int value) {
|
||||
const auto source = left ? LEFT_TRIGGER_SOURCE : RIGHT_TRIGGER_SOURCE;
|
||||
const auto control = GetInputMap().PadControl(source);
|
||||
if (control == INVALID_CONTROL) {
|
||||
return;
|
||||
}
|
||||
const auto& info = CONTROL_INFO[control];
|
||||
if (info.button == Controller::PAD_BUTTON_L2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerLeft, value);
|
||||
} else if (info.button == Controller::PAD_BUTTON_R2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerRight, value);
|
||||
} else {
|
||||
auto& state = g_pad_trigger_pressed[id][left ? 0 : 1];
|
||||
const bool down = state ? value > 32 : value > 96;
|
||||
if (down != state) {
|
||||
state = down;
|
||||
ApplyPadControl(id, control, source, down);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HostInputDisconnectGamepad(int id) {
|
||||
g_pad_trigger_pressed.erase(id);
|
||||
HostInputDisconnectGamepadTouchPad(id);
|
||||
}
|
||||
|
||||
void HostInputInit(SDL_Window* window) {
|
||||
GetInputMap();
|
||||
g_mouse_window = window;
|
||||
@@ -410,6 +748,16 @@ void HostInputInit(SDL_Window* window) {
|
||||
}
|
||||
|
||||
void HostInputShutdown() {
|
||||
g_pad_trigger_pressed.clear();
|
||||
HostInputSetTouchPadMode(false);
|
||||
int gamepad_touch_id = -1;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
gamepad_touch_id = g_gamepad_touchpad.enabled ? g_gamepad_touchpad.id : -1;
|
||||
}
|
||||
if (gamepad_touch_id >= 0) {
|
||||
HostInputDisconnectGamepadTouchPad(gamepad_touch_id);
|
||||
}
|
||||
if (g_mouse.enabled) {
|
||||
SetRelativeMouseMode(false);
|
||||
CenterMouseStick();
|
||||
@@ -438,6 +786,219 @@ void HostInputMouseButton(uint8_t mouse_button, bool down) {
|
||||
}
|
||||
}
|
||||
|
||||
void HostInputMouseTouchPad(bool down, float x, float y) {
|
||||
RecordMouseTouchPadPosition(x, y);
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_mouse_touchpad.enabled) {
|
||||
return;
|
||||
}
|
||||
if (down && !g_mouse_touchpad.drawing) {
|
||||
g_mouse_touchpad.gesture++;
|
||||
}
|
||||
g_mouse_touchpad.drawing = down;
|
||||
g_mouse_touchpad.revision++;
|
||||
x = g_mouse_touchpad.x;
|
||||
y = g_mouse_touchpad.y;
|
||||
}
|
||||
SetSwipeTouchPad(down, x, y);
|
||||
}
|
||||
|
||||
void HostInputMouseMotion(float x, float y) {
|
||||
RecordMouseTouchPadPosition(x, y);
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_mouse_touchpad.enabled && g_mouse_touchpad.drawing) {
|
||||
SetSwipeTouchPad(true, g_mouse_touchpad.x, g_mouse_touchpad.y);
|
||||
}
|
||||
}
|
||||
|
||||
void HostInputSetTouchPadMode(bool enabled) {
|
||||
if (enabled) {
|
||||
int gamepad_id = -1;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_gamepad_touchpad.enabled) {
|
||||
gamepad_id = g_gamepad_touchpad.id;
|
||||
}
|
||||
}
|
||||
if (gamepad_id >= 0) {
|
||||
HostInputDisconnectGamepadTouchPad(gamepad_id);
|
||||
}
|
||||
}
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_mouse_touchpad.enabled == enabled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!enabled) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
(void) SDL_GetMouseState(&x, &y);
|
||||
HostInputMouseTouchPad(false, x, y);
|
||||
}
|
||||
bool show_system_cursor = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
g_mouse_touchpad.enabled = enabled;
|
||||
g_mouse_touchpad.cursor_visible = true;
|
||||
g_mouse_touchpad.drawing = false;
|
||||
g_mouse_touchpad.position_valid = false;
|
||||
g_mouse_touchpad.gesture++;
|
||||
g_mouse_touchpad.revision++;
|
||||
show_system_cursor = !enabled && !g_gamepad_touchpad.enabled;
|
||||
}
|
||||
if (enabled) {
|
||||
if (!SDL_HideCursor()) {
|
||||
LOGF("Failed to hide mouse cursor for touchpad mode: %s\n", SDL_GetError());
|
||||
}
|
||||
} else if (show_system_cursor && !Config::HideCursorEnabled() && !SDL_ShowCursor()) {
|
||||
LOGF("Failed to restore mouse cursor after touchpad mode: %s\n", SDL_GetError());
|
||||
}
|
||||
LOGF("Mouse touchpad mode: %s\n", enabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
void HostInputToggleTouchPadMode() {
|
||||
bool enabled = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
enabled = !g_mouse_touchpad.enabled;
|
||||
}
|
||||
HostInputSetTouchPadMode(enabled);
|
||||
}
|
||||
|
||||
void HostInputToggleGamepadTouchPad(int id) {
|
||||
bool mouse_mode = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
mouse_mode = g_mouse_touchpad.enabled;
|
||||
}
|
||||
if (mouse_mode) {
|
||||
HostInputSetTouchPadMode(false);
|
||||
}
|
||||
|
||||
const auto stick = GamepadLeftStick(id);
|
||||
bool enabled = false;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_gamepad_touchpad.enabled && g_gamepad_touchpad.id != id) {
|
||||
return;
|
||||
}
|
||||
enabled = !g_gamepad_touchpad.enabled;
|
||||
if (enabled) {
|
||||
g_gamepad_touchpad = {};
|
||||
g_gamepad_touchpad.enabled = true;
|
||||
g_gamepad_touchpad.id = id;
|
||||
g_gamepad_touchpad.x_axis = stick[0];
|
||||
g_gamepad_touchpad.y_axis = stick[1];
|
||||
g_gamepad_touchpad.last_poll = SDL_GetTicks();
|
||||
g_gamepad_touchpad.next_poll = g_gamepad_touchpad.last_poll + GAMEPAD_TOUCHPAD_POLL_MS;
|
||||
g_mouse_touchpad.gesture++;
|
||||
} else {
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
g_gamepad_touchpad = {};
|
||||
}
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
Controller::SetAxis(id, Controller::Axis::LeftX, enabled ? 128 : stick[0]);
|
||||
Controller::SetAxis(id, Controller::Axis::LeftY, enabled ? 128 : stick[1]);
|
||||
if (enabled) {
|
||||
if (!SDL_HideCursor()) {
|
||||
LOGF("Failed to hide mouse cursor for gamepad touchpad mode: %s\n", SDL_GetError());
|
||||
}
|
||||
} else {
|
||||
Controller::SetTouchPad(id, 1, false, x, y);
|
||||
if (!Config::HideCursorEnabled() && !SDL_ShowCursor()) {
|
||||
LOGF("Failed to restore mouse cursor after gamepad touchpad mode: %s\n",
|
||||
SDL_GetError());
|
||||
}
|
||||
}
|
||||
LOGF("Gamepad touchpad mode: %s\n", enabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
bool HostInputGamepadDrawButton(int id, bool down) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled || g_gamepad_touchpad.id != id) {
|
||||
return false;
|
||||
}
|
||||
if (!down && !g_gamepad_touchpad.drawing) {
|
||||
return false;
|
||||
}
|
||||
if (down && !g_gamepad_touchpad.drawing) {
|
||||
g_mouse_touchpad.gesture++;
|
||||
}
|
||||
g_gamepad_touchpad.drawing = down;
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
Controller::SetTouchPad(id, 1, down, x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HostInputGamepadAxis(int id, bool horizontal, int value) {
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled || g_gamepad_touchpad.id != id) {
|
||||
return false;
|
||||
}
|
||||
(horizontal ? g_gamepad_touchpad.x_axis : g_gamepad_touchpad.y_axis) = std::clamp(value, 0, 255);
|
||||
g_gamepad_touchpad.next_poll = SDL_GetTicks();
|
||||
return true;
|
||||
}
|
||||
|
||||
void HostInputDisconnectGamepadTouchPad(int id) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled || g_gamepad_touchpad.id != id) {
|
||||
return;
|
||||
}
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
g_gamepad_touchpad = {};
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
Controller::SetTouchPad(id, 1, false, x, y);
|
||||
const auto stick = GamepadLeftStick(id);
|
||||
Controller::SetAxis(id, Controller::Axis::LeftX, stick[0]);
|
||||
Controller::SetAxis(id, Controller::Axis::LeftY, stick[1]);
|
||||
if (!TouchPadModeActive() && !Config::HideCursorEnabled() && !SDL_ShowCursor()) {
|
||||
LOGF("Failed to restore mouse cursor after gamepad disconnect: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
HostInputTouchPadVisualState GetHostInputTouchPadVisualState() {
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
HostInputTouchPadVisualState state {};
|
||||
if (g_gamepad_touchpad.enabled) {
|
||||
state.enabled = true;
|
||||
state.cursor_visible = true;
|
||||
state.drawing = g_gamepad_touchpad.drawing;
|
||||
state.x = g_gamepad_touchpad.x;
|
||||
state.y = g_gamepad_touchpad.y;
|
||||
state.revision = g_mouse_touchpad.revision;
|
||||
state.gesture = g_mouse_touchpad.gesture;
|
||||
state.position_valid = true;
|
||||
return state;
|
||||
}
|
||||
state.enabled = g_mouse_touchpad.enabled;
|
||||
state.cursor_visible = g_mouse_touchpad.cursor_visible;
|
||||
state.drawing = g_mouse_touchpad.drawing;
|
||||
state.x = g_mouse_touchpad.x;
|
||||
state.y = g_mouse_touchpad.y;
|
||||
state.revision = g_mouse_touchpad.revision;
|
||||
state.gesture = g_mouse_touchpad.gesture;
|
||||
state.position_valid = g_mouse_touchpad.position_valid;
|
||||
return state;
|
||||
}
|
||||
|
||||
void HostInputToggleMouseToJoystick() {
|
||||
if (g_mouse.enabled) {
|
||||
SetRelativeMouseMode(false);
|
||||
@@ -467,16 +1028,19 @@ bool HostInputWaitEvent(SDL_Event* event, int max_wait_ms) {
|
||||
timeout = PollMouse(SDL_GetTicks());
|
||||
}
|
||||
|
||||
if (g_cursor_hide_at != 0) {
|
||||
if (g_cursor_hide_at != 0 && !TouchPadModeActive()) {
|
||||
const auto now_ms = SDL_GetTicks();
|
||||
const int cursor_timeout =
|
||||
now_ms < g_cursor_hide_at ? static_cast<int>(g_cursor_hide_at - now_ms) : 0;
|
||||
timeout = timeout < 0 ? cursor_timeout : std::min(timeout, cursor_timeout);
|
||||
}
|
||||
if (const int touch_timeout = PollGamepadTouchPad(SDL_GetTicks()); touch_timeout >= 0) {
|
||||
timeout = timeout < 0 ? touch_timeout : std::min(timeout, touch_timeout);
|
||||
}
|
||||
timeout = timeout < 0 ? max_wait_ms : std::min(timeout, max_wait_ms);
|
||||
const bool has_event = SDL_WaitEventTimeout(event, timeout);
|
||||
|
||||
if (Config::HideCursorEnabled()) {
|
||||
if (Config::HideCursorEnabled() && !TouchPadModeActive()) {
|
||||
const auto now_ms = SDL_GetTicks();
|
||||
if (has_event && !g_mouse.enabled && IsCursorActivity(*event) &&
|
||||
SDL_GetWindowFromEvent(event) == g_mouse_window) {
|
||||
|
||||
@@ -8,10 +8,33 @@ union SDL_Event;
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
struct HostInputTouchPadVisualState {
|
||||
bool enabled = false;
|
||||
bool cursor_visible = true;
|
||||
bool drawing = false;
|
||||
bool position_valid = false;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
uint64_t revision = 0;
|
||||
uint64_t gesture = 0;
|
||||
};
|
||||
|
||||
void HostInputInit(SDL_Window* window);
|
||||
void HostInputShutdown();
|
||||
void HostInputKey(int key_code, bool down);
|
||||
void HostInputMouseButton(uint8_t mouse_button, bool down);
|
||||
void HostInputMouseTouchPad(bool down, float x, float y);
|
||||
void HostInputMouseMotion(float x, float y);
|
||||
void HostInputPadButton(int id, int sdl_button, bool down);
|
||||
void HostInputPadTrigger(int id, bool left, int value);
|
||||
void HostInputDisconnectGamepad(int id);
|
||||
void HostInputSetTouchPadMode(bool enabled);
|
||||
void HostInputToggleTouchPadMode();
|
||||
void HostInputToggleGamepadTouchPad(int id);
|
||||
[[nodiscard]] bool HostInputGamepadDrawButton(int id, bool down);
|
||||
[[nodiscard]] bool HostInputGamepadAxis(int id, bool horizontal, int value);
|
||||
void HostInputDisconnectGamepadTouchPad(int id);
|
||||
[[nodiscard]] HostInputTouchPadVisualState GetHostInputTouchPadVisualState();
|
||||
void HostInputToggleMouseToJoystick();
|
||||
[[nodiscard]] bool HostInputWaitEvent(SDL_Event* event, int max_wait_ms);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "graphics/presentation/window.h"
|
||||
#include "graphics/presentation/window.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
@@ -54,25 +54,6 @@ struct EventKeyboard {
|
||||
double timestamp_seconds;
|
||||
};
|
||||
|
||||
static uint32_t ControllerButtonToPadButton(int button) {
|
||||
switch (button) {
|
||||
case SDL_GAMEPAD_BUTTON_SOUTH: return Controller::PAD_BUTTON_CROSS;
|
||||
case SDL_GAMEPAD_BUTTON_EAST: return Controller::PAD_BUTTON_CIRCLE;
|
||||
case SDL_GAMEPAD_BUTTON_WEST: return Controller::PAD_BUTTON_SQUARE;
|
||||
case SDL_GAMEPAD_BUTTON_NORTH: return Controller::PAD_BUTTON_TRIANGLE;
|
||||
case SDL_GAMEPAD_BUTTON_START: return Controller::PAD_BUTTON_OPTIONS;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_STICK: return Controller::PAD_BUTTON_L3;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_STICK: return Controller::PAD_BUTTON_R3;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: return Controller::PAD_BUTTON_L1;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: return Controller::PAD_BUTTON_R1;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_UP: return Controller::PAD_BUTTON_UP;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_DOWN: return Controller::PAD_BUTTON_DOWN;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_LEFT: return Controller::PAD_BUTTON_LEFT;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: return Controller::PAD_BUTTON_RIGHT;
|
||||
case SDL_GAMEPAD_BUTTON_TOUCHPAD: return Controller::PAD_BUTTON_TOUCH_PAD;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static Controller::Axis ControllerAxisFromSdl(int axis_id) {
|
||||
switch (axis_id) {
|
||||
@@ -268,6 +249,15 @@ static void GameEventMouse([[maybe_unused]] const EventMouse& mb) {
|
||||
}
|
||||
|
||||
HostInputMouseButton(mouse_button, mb.down);
|
||||
if (!mb.touch && mb.left) {
|
||||
HostInputMouseTouchPad(mb.down, static_cast<float>(mb.x), static_cast<float>(mb.y));
|
||||
}
|
||||
} else if (mb.motion && !mb.touch) {
|
||||
if (mb.left) {
|
||||
HostInputMouseTouchPad(true, static_cast<float>(mb.x), static_cast<float>(mb.y));
|
||||
} else {
|
||||
HostInputMouseMotion(static_cast<float>(mb.x), static_cast<float>(mb.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +306,7 @@ static void GameEventController([[maybe_unused]] const EventController& f) {
|
||||
}
|
||||
|
||||
if (f.removed) {
|
||||
HostInputDisconnectGamepad(f.id);
|
||||
if (auto* pad = SDL_GetGamepadFromID(f.id); pad != nullptr) {
|
||||
Controller::Disconnect(f.id);
|
||||
SDL_CloseGamepad(pad);
|
||||
@@ -323,30 +314,22 @@ static void GameEventController([[maybe_unused]] const EventController& f) {
|
||||
}
|
||||
|
||||
if (f.down || f.up) {
|
||||
const auto button = ControllerButtonToPadButton(f.button);
|
||||
if (button != 0) {
|
||||
Controller::SetButton(f.id, button, f.down);
|
||||
} else if (f.button == SDL_GAMEPAD_BUTTON_BACK) {
|
||||
// Pads without a touchpad (Switch, Xbox, ...) have no way to click it, which
|
||||
// blocks games that require it. Use their Back/Select button as a centered click.
|
||||
auto* pad = SDL_GetGamepadFromID(f.id);
|
||||
if (pad != nullptr && SDL_GetNumGamepadTouchpads(pad) == 0) {
|
||||
if (f.down) {
|
||||
Controller::SetTouchPad(f.id, 0, true, 0.5f, 0.5f);
|
||||
}
|
||||
Controller::SetButton(f.id, Controller::PAD_BUTTON_TOUCH_PAD, f.down);
|
||||
if (f.up) {
|
||||
Controller::SetTouchPad(f.id, 0, false, 0.5f, 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
HostInputPadButton(f.id, f.button, f.down);
|
||||
}
|
||||
|
||||
if (f.axis) {
|
||||
const auto axis = ControllerAxisFromSdl(f.axis_id);
|
||||
if (axis != Controller::Axis::AxisMax) {
|
||||
Controller::SetAxis(f.id, axis,
|
||||
if (f.axis_id == SDL_GAMEPAD_AXIS_LEFT_TRIGGER ||
|
||||
f.axis_id == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
|
||||
HostInputPadTrigger(f.id, f.axis_id == SDL_GAMEPAD_AXIS_LEFT_TRIGGER,
|
||||
ControllerAxisValueFromSdl(f.axis_id, f.axis_value));
|
||||
} else if (const auto axis = ControllerAxisFromSdl(f.axis_id);
|
||||
axis != Controller::Axis::AxisMax) {
|
||||
const int value = ControllerAxisValueFromSdl(f.axis_id, f.axis_value);
|
||||
if ((f.axis_id == SDL_GAMEPAD_AXIS_LEFTX || f.axis_id == SDL_GAMEPAD_AXIS_LEFTY) &&
|
||||
HostInputGamepadAxis(f.id, f.axis_id == SDL_GAMEPAD_AXIS_LEFTX, value)) {
|
||||
return;
|
||||
}
|
||||
Controller::SetAxis(f.id, axis, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,6 +414,7 @@ void WindowContext::ProcessWindowEvent(const SDL_WindowEvent& event) {
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
||||
LOGF("Mouse left window %" PRIu32 "\n", window_event.windowID);
|
||||
HostInputSetTouchPadMode(false);
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
LOGF("Window %" PRIu32 " gained keyboard focus\n", window_event.windowID);
|
||||
|
||||
@@ -105,6 +105,7 @@ if(APPLE)
|
||||
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
|
||||
install(TARGETS kyty_updater DESTINATION "KytyPS5.app/Contents/MacOS")
|
||||
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION "KytyPS5.app/Contents/Resources/assets/sounds")
|
||||
install(DIRECTORY "${CMAKE_SOURCE_DIR}/assets/cursors" DESTINATION "KytyPS5.app/Contents/Resources/assets")
|
||||
# Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle.
|
||||
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
|
||||
if(NOT KYTY_MACDEPLOYQT)
|
||||
|
||||
@@ -24,6 +24,7 @@ public:
|
||||
void SetSuspended(bool suspended);
|
||||
|
||||
[[nodiscard]] const QSet<QString>& Pressed() const { return m_pressed; }
|
||||
[[nodiscard]] const QSet<QString>& RawPressed() const { return m_raw_pressed; }
|
||||
[[nodiscard]] QPointF LeftStick() const { return m_left_stick; }
|
||||
[[nodiscard]] QPointF RightStick() const { return m_right_stick; }
|
||||
[[nodiscard]] const QString& DeviceName() const { return m_device_name; }
|
||||
@@ -44,6 +45,7 @@ private:
|
||||
bool m_preview_enabled = false;
|
||||
bool m_suspended = false;
|
||||
QSet<QString> m_pressed;
|
||||
QSet<QString> m_raw_pressed;
|
||||
QPointF m_left_stick;
|
||||
QPointF m_right_stick;
|
||||
QString m_device_name;
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
#ifndef LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
|
||||
#ifndef LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
|
||||
#define LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
|
||||
|
||||
#include <QDialog>
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
|
||||
class ControllerPreview;
|
||||
class DualSenseWidget;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QDoubleSpinBox;
|
||||
class QTabWidget;
|
||||
class QWidget;
|
||||
|
||||
class InputMappingDialog final: public QDialog {
|
||||
public:
|
||||
@@ -20,7 +25,10 @@ public:
|
||||
[[nodiscard]] QStringList Mapping() const;
|
||||
|
||||
private:
|
||||
void ChangeBinding(const QString& id);
|
||||
QWidget* BuildTab(bool pad);
|
||||
void ChangeKeyboardBinding(const QString& id);
|
||||
void ChangePadBinding(const QString& id);
|
||||
void ClearBinding(bool pad, const QString& id);
|
||||
void RestoreDefaults();
|
||||
void UpdateButtons();
|
||||
void UpdatePreview();
|
||||
@@ -28,12 +36,19 @@ private:
|
||||
|
||||
ControllerPreview* m_preview;
|
||||
DualSenseWidget* m_controller = nullptr;
|
||||
QTabWidget* m_tabs = nullptr;
|
||||
QHash<QString, QString> m_bindings;
|
||||
QHash<QString, QString> m_pad_bindings;
|
||||
QHash<QString, QPushButton*> m_buttons;
|
||||
QHash<QString, QPushButton*> m_pad_buttons;
|
||||
QSet<QString> m_pad_pressed;
|
||||
QString m_selected;
|
||||
QLabel* m_status = nullptr;
|
||||
QDoubleSpinBox* m_sensitivity = nullptr;
|
||||
QComboBox* m_particle_skin = nullptr;
|
||||
QCheckBox* m_touch_cursor = nullptr;
|
||||
bool m_custom_bindings = false;
|
||||
bool m_custom_pad = false;
|
||||
};
|
||||
|
||||
#endif /* LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_ */
|
||||
|
||||
@@ -20,6 +20,34 @@ constexpr struct {
|
||||
{SDL_GAMEPAD_BUTTON_START, "Options"},
|
||||
};
|
||||
|
||||
// Host-side names; they must match PAD_SOURCES in hostInput.cpp.
|
||||
constexpr struct {
|
||||
SDL_GamepadButton button;
|
||||
const char* name;
|
||||
} RAW_BUTTONS[] = {
|
||||
{SDL_GAMEPAD_BUTTON_SOUTH, "South"},
|
||||
{SDL_GAMEPAD_BUTTON_EAST, "East"},
|
||||
{SDL_GAMEPAD_BUTTON_WEST, "West"},
|
||||
{SDL_GAMEPAD_BUTTON_NORTH, "North"},
|
||||
{SDL_GAMEPAD_BUTTON_BACK, "Back"},
|
||||
{SDL_GAMEPAD_BUTTON_GUIDE, "Guide"},
|
||||
{SDL_GAMEPAD_BUTTON_START, "Start"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_STICK, "LeftStick"},
|
||||
{SDL_GAMEPAD_BUTTON_RIGHT_STICK, "RightStick"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, "LeftShoulder"},
|
||||
{SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, "RightShoulder"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_UP, "DpadUp"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_DOWN, "DpadDown"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, "DpadLeft"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "DpadRight"},
|
||||
{SDL_GAMEPAD_BUTTON_MISC1, "Misc1"},
|
||||
{SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1, "RightPaddle1"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_PADDLE1, "LeftPaddle1"},
|
||||
{SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2, "RightPaddle2"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_PADDLE2, "LeftPaddle2"},
|
||||
{SDL_GAMEPAD_BUTTON_TOUCHPAD, "Touchpad"},
|
||||
};
|
||||
|
||||
double Axis(SDL_Gamepad* gamepad, SDL_GamepadAxis axis) {
|
||||
const int value = SDL_GetGamepadAxis(gamepad, axis);
|
||||
return value / (value < 0 ? 32768.0 : 32767.0);
|
||||
@@ -100,6 +128,7 @@ void ControllerPreview::UpdateRunning() {
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
|
||||
}
|
||||
m_pressed.clear();
|
||||
m_raw_pressed.clear();
|
||||
m_left_stick = {};
|
||||
m_right_stick = {};
|
||||
m_device_name.clear();
|
||||
@@ -131,12 +160,25 @@ void ControllerPreview::Refresh() {
|
||||
}
|
||||
|
||||
QSet<QString> pressed;
|
||||
QSet<QString> raw;
|
||||
QPointF left;
|
||||
QPointF right;
|
||||
QString name;
|
||||
if (!m_gamepads.isEmpty()) {
|
||||
auto* gamepad = m_gamepads.first();
|
||||
name = QString::fromUtf8(SDL_GetGamepadName(gamepad));
|
||||
for (const auto& binding: RAW_BUTTONS) {
|
||||
if (SDL_GamepadHasButton(gamepad, binding.button) &&
|
||||
SDL_GetGamepadButton(gamepad, binding.button)) {
|
||||
raw.insert(QString::fromLatin1(binding.name));
|
||||
}
|
||||
}
|
||||
if (Axis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0.5) {
|
||||
raw.insert(QStringLiteral("LeftTrigger"));
|
||||
}
|
||||
if (Axis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0.5) {
|
||||
raw.insert(QStringLiteral("RightTrigger"));
|
||||
}
|
||||
for (const auto& binding: BUTTONS) {
|
||||
if (SDL_GetGamepadButton(gamepad, binding.button)) {
|
||||
pressed.insert(QString::fromLatin1(binding.control));
|
||||
@@ -180,9 +222,10 @@ void ControllerPreview::Refresh() {
|
||||
pressed.insert(QStringLiteral("TouchPadRight"));
|
||||
}
|
||||
}
|
||||
if (pressed != m_pressed || left != m_left_stick || right != m_right_stick ||
|
||||
name != m_device_name) {
|
||||
if (pressed != m_pressed || raw != m_raw_pressed || left != m_left_stick ||
|
||||
right != m_right_stick || name != m_device_name) {
|
||||
m_pressed = pressed;
|
||||
m_raw_pressed = raw;
|
||||
m_left_stick = left;
|
||||
m_right_stick = right;
|
||||
m_device_name = name;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "inputMappingDialog.h"
|
||||
#include "inputMappingDialog.h"
|
||||
|
||||
#include "controllerPreview.h"
|
||||
#include "dualsenseWidget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QGridLayout>
|
||||
@@ -16,56 +18,113 @@
|
||||
#include <QPushButton>
|
||||
#include <QScreen>
|
||||
#include <QScrollArea>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto DEFAULT_MOUSE_SENSITIVITY = 1.0;
|
||||
constexpr char MOUSE_SENSITIVITY[] = "MouseSensitivity=";
|
||||
constexpr char TOUCH_PARTICLE_SKIN[] = "TouchParticleSkin=";
|
||||
constexpr char TOUCH_CURSOR_OFF[] = "TouchCursorImage=Off";
|
||||
constexpr char KEYBOARD_DISABLED[] = "Keyboard=Disabled";
|
||||
constexpr char PAD_DISABLED[] = "Pad=Disabled";
|
||||
|
||||
enum Group { Dpad, LeftStick, RightStick, Face, Shoulders, Center, Shortcuts, GroupCount };
|
||||
constexpr char PAD_PREFIX[] = "Pad:";
|
||||
|
||||
enum Section { Face, Dpad, Shoulders, Sticks, System, LeftStick, RightStick, Shortcuts, SectionCount };
|
||||
|
||||
const char* const SECTION_TITLES[SectionCount] = {
|
||||
QT_TR_NOOP("Face buttons"), QT_TR_NOOP("D-pad"),
|
||||
QT_TR_NOOP("Shoulders && triggers"), QT_TR_NOOP("Stick clicks"),
|
||||
QT_TR_NOOP("System"), QT_TR_NOOP("Left stick"),
|
||||
QT_TR_NOOP("Right stick"), QT_TR_NOOP("In-game shortcuts (cycle)"),
|
||||
};
|
||||
|
||||
// pad_default is null for controls that have no gamepad equivalent.
|
||||
struct PadControl {
|
||||
const char* id;
|
||||
const char* label;
|
||||
const char* default_binding;
|
||||
Group group;
|
||||
int row;
|
||||
int column;
|
||||
const char* keyboard_default;
|
||||
const char* pad_default;
|
||||
Section section;
|
||||
};
|
||||
|
||||
constexpr PadControl PAD_CONTROLS[] = {
|
||||
{"Up", "Up", "Up", Dpad, 0, 1},
|
||||
{"Down", "Down", "Down", Dpad, 2, 1},
|
||||
{"Left", "Left", "Left", Dpad, 1, 0},
|
||||
{"Right", "Right", "Right", Dpad, 1, 2},
|
||||
{"LeftStickUp", "Up", "W", LeftStick, 0, 1},
|
||||
{"LeftStickDown", "Down", "S", LeftStick, 2, 1},
|
||||
{"LeftStickLeft", "Left", "A", LeftStick, 1, 0},
|
||||
{"LeftStickRight", "Right", "D", LeftStick, 1, 2},
|
||||
{"RightStickUp", "Up", "T", RightStick, 0, 1},
|
||||
{"RightStickDown", "Down", "G", RightStick, 2, 1},
|
||||
{"RightStickLeft", "Left", "F", RightStick, 1, 0},
|
||||
{"RightStickRight", "Right", "H", RightStick, 1, 2},
|
||||
{"Triangle", "Triangle", "I", Face, 0, 1},
|
||||
{"Circle", "Circle", "L", Face, 1, 2},
|
||||
{"Cross", "Cross", "J", Face, 2, 1},
|
||||
{"Square", "Square", "K", Face, 1, 0},
|
||||
{"L1", "L1", "Q", Shoulders, 0, 1},
|
||||
{"R1", "R1", "E", Shoulders, 0, 2},
|
||||
{"L2", "L2", "Z", Shoulders, 0, 0},
|
||||
{"R2", "R2", "C", Shoulders, 0, 3},
|
||||
{"L3", "Press / L3", "Left Shift", LeftStick, 1, 1},
|
||||
{"R3", "Press / R3", "Left Ctrl", RightStick, 1, 1},
|
||||
{"Options", "Options", "Return", Center, 0, 1},
|
||||
{"TouchPad", "Touch pad left", "Backspace", Center, 0, 0},
|
||||
{"TouchPadRight", "Touch pad right", "Tab", Center, 0, 2},
|
||||
{"SpeakerVolume", "Speaker volume", "1", Shortcuts, 0, 0},
|
||||
{"VibrationIntensity", "Vibration intensity", "2", Shortcuts, 0, 1},
|
||||
{"TriggerEffectIntensity", "Trigger intensity", "3", Shortcuts, 0, 2},
|
||||
{"Cross", "Cross", "J", "South", Face},
|
||||
{"Circle", "Circle", "L", "East", Face},
|
||||
{"Square", "Square", "K", "West", Face},
|
||||
{"Triangle", "Triangle", "I", "North", Face},
|
||||
{"Up", "Up", "Up", "DpadUp", Dpad},
|
||||
{"Down", "Down", "Down", "DpadDown", Dpad},
|
||||
{"Left", "Left", "Left", "DpadLeft", Dpad},
|
||||
{"Right", "Right", "Right", "DpadRight", Dpad},
|
||||
{"L1", "L1", "Q", "LeftShoulder", Shoulders},
|
||||
{"R1", "R1", "E", "RightShoulder", Shoulders},
|
||||
{"L2", "L2", "Z", "LeftTrigger", Shoulders},
|
||||
{"R2", "R2", "C", "RightTrigger", Shoulders},
|
||||
{"L3", "L3 (left stick click)", "Left Shift", "LeftStick", Sticks},
|
||||
{"R3", "R3 (right stick click)", "Left Ctrl", "RightStick", Sticks},
|
||||
{"Options", "Options", "Return", "Start", System},
|
||||
{"TouchPad", "Touch pad click", "Backspace", "Touchpad", System},
|
||||
{"TouchPadRight", "Touch pad right", "Tab", nullptr, System},
|
||||
{"GyroToggle", "Gyro emulation toggle", "", "Back", System},
|
||||
{"TouchPadMode", "Emulated touch pad mode", "F6", "", System},
|
||||
{"LeftStickUp", "Up", "W", nullptr, LeftStick},
|
||||
{"LeftStickDown", "Down", "S", nullptr, LeftStick},
|
||||
{"LeftStickLeft", "Left", "A", nullptr, LeftStick},
|
||||
{"LeftStickRight", "Right", "D", nullptr, LeftStick},
|
||||
{"RightStickUp", "Up", "T", nullptr, RightStick},
|
||||
{"RightStickDown", "Down", "G", nullptr, RightStick},
|
||||
{"RightStickLeft", "Left", "F", nullptr, RightStick},
|
||||
{"RightStickRight", "Right", "H", nullptr, RightStick},
|
||||
{"SpeakerVolume", "Speaker volume", "1", nullptr, Shortcuts},
|
||||
{"VibrationIntensity", "Vibration intensity", "2", nullptr, Shortcuts},
|
||||
{"TriggerEffectIntensity", "Trigger intensity", "3", nullptr, Shortcuts},
|
||||
};
|
||||
|
||||
struct PadSourceName {
|
||||
const char* id;
|
||||
const char* label;
|
||||
};
|
||||
|
||||
constexpr PadSourceName PAD_SOURCES[] = {
|
||||
{"South", "A / Cross"},
|
||||
{"East", "B / Circle"},
|
||||
{"West", "X / Square"},
|
||||
{"North", "Y / Triangle"},
|
||||
{"Back", "View / Back / Share"},
|
||||
{"Guide", "Guide / Home"},
|
||||
{"Start", "Menu / Start"},
|
||||
{"LeftStick", "Left stick click"},
|
||||
{"RightStick", "Right stick click"},
|
||||
{"LeftShoulder", "LB / L1"},
|
||||
{"RightShoulder", "RB / R1"},
|
||||
{"DpadUp", "D-pad Up"},
|
||||
{"DpadDown", "D-pad Down"},
|
||||
{"DpadLeft", "D-pad Left"},
|
||||
{"DpadRight", "D-pad Right"},
|
||||
{"Misc1", "Share / Misc"},
|
||||
{"RightPaddle1", "Right paddle 1"},
|
||||
{"LeftPaddle1", "Left paddle 1"},
|
||||
{"RightPaddle2", "Right paddle 2"},
|
||||
{"LeftPaddle2", "Left paddle 2"},
|
||||
{"Touchpad", "Touch pad click"},
|
||||
{"LeftTrigger", "LT / L2"},
|
||||
{"RightTrigger", "RT / R2"},
|
||||
};
|
||||
|
||||
QString PadSourceLabel(const QString& id) {
|
||||
for (const auto& source: PAD_SOURCES) {
|
||||
if (id == QLatin1String(source.id)) {
|
||||
return QObject::tr(source.label);
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
QString KeypadName(int key) {
|
||||
if (key >= Qt::Key_0 && key <= Qt::Key_9) {
|
||||
return QStringLiteral("Keypad %1").arg(key - Qt::Key_0);
|
||||
@@ -191,6 +250,54 @@ private:
|
||||
QString m_binding;
|
||||
};
|
||||
|
||||
class PadCaptureDialog final: public QDialog {
|
||||
public:
|
||||
PadCaptureDialog(ControllerPreview* preview, QWidget* parent): QDialog(parent) {
|
||||
setWindowTitle(tr("Set Binding"));
|
||||
setModal(true);
|
||||
setMinimumWidth(360);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
m_label = new QLabel(this);
|
||||
m_label->setAlignment(Qt::AlignCenter);
|
||||
m_label->setWordWrap(true);
|
||||
layout->addWidget(m_label);
|
||||
|
||||
if (preview == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Inputs already held when the dialog opens must be released before they count.
|
||||
m_held = preview->RawPressed();
|
||||
connect(preview, &ControllerPreview::StateChanged, this, [this, preview] {
|
||||
const auto& pressed = preview->RawPressed();
|
||||
m_held.intersect(pressed);
|
||||
for (const auto& name: pressed) {
|
||||
if (!m_held.contains(name)) {
|
||||
m_binding = name;
|
||||
accept();
|
||||
return;
|
||||
}
|
||||
}
|
||||
UpdateText(preview);
|
||||
});
|
||||
UpdateText(preview);
|
||||
}
|
||||
|
||||
[[nodiscard]] const QString& Binding() const { return m_binding; }
|
||||
|
||||
private:
|
||||
void UpdateText(const ControllerPreview* preview) {
|
||||
m_label->setText(preview->DeviceName().isEmpty()
|
||||
? tr("No controller connected.\nConnect one, or press Esc to cancel.")
|
||||
: tr("Press a button or trigger on %1.\nEsc cancels.")
|
||||
.arg(preview->DeviceName()));
|
||||
}
|
||||
|
||||
QLabel* m_label = nullptr;
|
||||
QString m_binding;
|
||||
QSet<QString> m_held;
|
||||
};
|
||||
|
||||
void AssignBinding(QHash<QString, QString>& bindings, const QString& id, const QString& binding) {
|
||||
for (auto item = bindings.begin(); item != bindings.end();) {
|
||||
if (item.value().compare(binding, Qt::CaseInsensitive) == 0) {
|
||||
@@ -205,7 +312,11 @@ void AssignBinding(QHash<QString, QString>& bindings, const QString& id, const Q
|
||||
QHash<QString, QString> ParseMapping(const QStringList& mapping) {
|
||||
QHash<QString, QString> result;
|
||||
for (const auto& entry: mapping) {
|
||||
if (entry.startsWith(QLatin1String(MOUSE_SENSITIVITY))) {
|
||||
if (entry == QLatin1String(KEYBOARD_DISABLED) || entry == QLatin1String(PAD_DISABLED) ||
|
||||
entry.startsWith(QLatin1String(MOUSE_SENSITIVITY)) ||
|
||||
entry.startsWith(QLatin1String(TOUCH_PARTICLE_SKIN)) ||
|
||||
entry.startsWith(QLatin1String("TouchCursorImage=")) ||
|
||||
entry.startsWith(QLatin1String(PAD_PREFIX))) {
|
||||
continue;
|
||||
}
|
||||
const auto separator = entry.indexOf(QLatin1Char('='));
|
||||
@@ -216,6 +327,15 @@ QHash<QString, QString> ParseMapping(const QStringList& mapping) {
|
||||
return result;
|
||||
}
|
||||
|
||||
QString ParseTouchParticleSkin(const QStringList& mapping) {
|
||||
for (const auto& entry: mapping) {
|
||||
if (entry.startsWith(QLatin1String(TOUCH_PARTICLE_SKIN))) {
|
||||
return entry.mid(sizeof(TOUCH_PARTICLE_SKIN) - 1);
|
||||
}
|
||||
}
|
||||
return QStringLiteral("Classic");
|
||||
}
|
||||
|
||||
double ParseMouseSensitivity(const QStringList& mapping) {
|
||||
for (const auto& entry: mapping) {
|
||||
if (entry.startsWith(QLatin1String(MOUSE_SENSITIVITY))) {
|
||||
@@ -231,106 +351,67 @@ InputMappingDialog::InputMappingDialog(const QStringList& mapping, ControllerPre
|
||||
QWidget* parent)
|
||||
: QDialog(parent), m_preview(preview) {
|
||||
setWindowTitle(tr("Controls"));
|
||||
resize(QSize(1100, 820).boundedTo(screen()->availableGeometry().size()));
|
||||
resize(QSize(1360, 780).boundedTo(screen()->availableGeometry().size()));
|
||||
|
||||
m_bindings = ParseMapping(mapping);
|
||||
m_custom_bindings = !m_bindings.isEmpty() || mapping.contains(QLatin1String(KEYBOARD_DISABLED));
|
||||
m_custom_pad = mapping.contains(QLatin1String(PAD_DISABLED));
|
||||
for (const auto& entry: mapping) {
|
||||
if (!entry.startsWith(QLatin1String(PAD_PREFIX))) {
|
||||
continue;
|
||||
}
|
||||
const auto body = entry.mid(static_cast<int>(sizeof(PAD_PREFIX)) - 1);
|
||||
const auto separator = body.indexOf(QLatin1Char('='));
|
||||
if (separator > 0 && separator + 1 < body.size()) {
|
||||
AssignBinding(m_pad_bindings, body.left(separator), body.mid(separator + 1));
|
||||
m_custom_pad = true;
|
||||
}
|
||||
}
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
if (!m_custom_bindings && control.keyboard_default[0] != '\0') {
|
||||
m_bindings.insert(id, QString::fromLatin1(control.keyboard_default));
|
||||
}
|
||||
if (!m_custom_pad && control.pad_default != nullptr) {
|
||||
m_pad_bindings.insert(id, QString::fromLatin1(control.pad_default));
|
||||
}
|
||||
}
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->setSpacing(10);
|
||||
auto* title = new QLabel(tr("Controller setup"), this);
|
||||
auto font = title->font();
|
||||
font.setPointSize(font.pointSize() + 7);
|
||||
font.setBold(true);
|
||||
title->setFont(font);
|
||||
layout->addWidget(title);
|
||||
auto* hint = new QLabel(
|
||||
tr("Select a button to remap. Press controller buttons to test."), this);
|
||||
hint->setWordWrap(true);
|
||||
layout->addWidget(hint);
|
||||
m_status = new QLabel(this);
|
||||
m_status->setObjectName(QStringLiteral("controllerStatus"));
|
||||
m_status->setWordWrap(true);
|
||||
layout->addWidget(m_status);
|
||||
|
||||
auto* scroll = new QScrollArea(this);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
auto* content = new QWidget(scroll);
|
||||
auto* grid = new QGridLayout(content);
|
||||
grid->setContentsMargins(0, 0, 0, 0);
|
||||
grid->setSpacing(12);
|
||||
QGroupBox* groups[GroupCount];
|
||||
QGridLayout* cells[GroupCount];
|
||||
const char* titles[] = {"D-Pad",
|
||||
"Left stick",
|
||||
"Right stick",
|
||||
"Face buttons",
|
||||
"Shoulders and triggers",
|
||||
"Touch pad and options",
|
||||
"In-game shortcuts (cycle)"};
|
||||
for (int index = 0; index < GroupCount; ++index) {
|
||||
groups[index] = new QGroupBox(tr(titles[index]), content);
|
||||
cells[index] = new QGridLayout(groups[index]);
|
||||
cells[index]->setSpacing(5);
|
||||
}
|
||||
|
||||
m_bindings = ParseMapping(mapping);
|
||||
m_custom_bindings = !m_bindings.isEmpty();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
auto* cell = new QGroupBox(tr(control.label), groups[control.group]);
|
||||
auto* cell_layout = new QVBoxLayout(cell);
|
||||
cell_layout->setContentsMargins(3, 5, 3, 3);
|
||||
auto* button = new QPushButton(cell);
|
||||
button->setObjectName(QStringLiteral("binding_") + id);
|
||||
button->setAutoDefault(false);
|
||||
button->setMinimumWidth(64);
|
||||
button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
button->setAccessibleName(tr(titles[control.group]) + QStringLiteral(" / ") +
|
||||
tr(control.label));
|
||||
cell_layout->addWidget(button);
|
||||
cells[control.group]->addWidget(cell, control.row, control.column);
|
||||
m_buttons.insert(id, button);
|
||||
if (!m_custom_bindings) {
|
||||
m_bindings.insert(id, QString::fromLatin1(control.default_binding));
|
||||
}
|
||||
connect(button, &QPushButton::clicked, this, [this, id] { ChangeBinding(id); });
|
||||
}
|
||||
|
||||
auto* left = new QVBoxLayout;
|
||||
left->addWidget(groups[Dpad]);
|
||||
left->addWidget(groups[LeftStick]);
|
||||
left->addStretch();
|
||||
grid->addLayout(left, 0, 0);
|
||||
auto* center = new QVBoxLayout;
|
||||
center->addWidget(groups[Shoulders]);
|
||||
m_controller = new DualSenseWidget(content);
|
||||
m_controller->setObjectName(QStringLiteral("controllerDiagram"));
|
||||
center->addWidget(m_controller, 1);
|
||||
center->addWidget(groups[Center]);
|
||||
grid->addLayout(center, 0, 1);
|
||||
auto* right = new QVBoxLayout;
|
||||
right->addWidget(groups[Face]);
|
||||
right->addWidget(groups[RightStick]);
|
||||
right->addStretch();
|
||||
grid->addLayout(right, 0, 2);
|
||||
grid->setColumnStretch(1, 1);
|
||||
grid->addWidget(groups[Shortcuts], 1, 0, 1, 3);
|
||||
scroll->setWidget(content);
|
||||
layout->addWidget(scroll, 1);
|
||||
|
||||
auto* sensitivity_layout = new QHBoxLayout;
|
||||
sensitivity_layout->addWidget(new QLabel(tr("Mouse sensitivity"), this));
|
||||
m_sensitivity = new QDoubleSpinBox(this);
|
||||
m_sensitivity->setObjectName(QStringLiteral("mouseSensitivity"));
|
||||
m_sensitivity->setRange(0.1, 5.0);
|
||||
m_sensitivity->setSingleStep(0.1);
|
||||
m_sensitivity->setDecimals(1);
|
||||
m_sensitivity->setSuffix(QStringLiteral("×"));
|
||||
m_tabs = new QTabWidget(this);
|
||||
m_tabs->setObjectName(QStringLiteral("inputTabs"));
|
||||
m_tabs->setDocumentMode(true);
|
||||
m_tabs->addTab(BuildTab(true), tr("Gamepad"));
|
||||
m_tabs->addTab(BuildTab(false), tr("Keyboard && Mouse"));
|
||||
layout->addWidget(m_tabs, 1);
|
||||
m_sensitivity->setValue(ParseMouseSensitivity(mapping));
|
||||
sensitivity_layout->addWidget(m_sensitivity);
|
||||
sensitivity_layout->addWidget(
|
||||
new QLabel(tr("F7 in-game toggles mouse movement on the right stick."), this));
|
||||
sensitivity_layout->addStretch();
|
||||
layout->addLayout(sensitivity_layout);
|
||||
|
||||
auto* touch_group = new QGroupBox(tr("Emulated touch pad (shared by both tabs)"), this);
|
||||
auto* touch_layout = new QHBoxLayout(touch_group);
|
||||
touch_layout->addWidget(new QLabel(tr("Particle skin"), touch_group));
|
||||
m_particle_skin = new QComboBox(touch_group);
|
||||
m_particle_skin->setObjectName(QStringLiteral("touchParticleSkin"));
|
||||
m_particle_skin->addItem(tr("Classic"), QStringLiteral("Classic"));
|
||||
m_particle_skin->addItem(tr("Blue neon"), QStringLiteral("BlueNeon"));
|
||||
m_particle_skin->addItem(tr("Monochrome"), QStringLiteral("Monochrome"));
|
||||
m_particle_skin->addItem(tr("Crimson"), QStringLiteral("Crimson"));
|
||||
m_particle_skin->addItem(tr("Red/Blue"), QStringLiteral("RedBlue"));
|
||||
touch_layout->addWidget(m_particle_skin);
|
||||
m_touch_cursor = new QCheckBox(tr("Show hand cursor and trail"), touch_group);
|
||||
m_touch_cursor->setObjectName(QStringLiteral("touchCursorImage"));
|
||||
m_touch_cursor->setToolTip(tr("When off, the emulated touch pad still works but the hand "
|
||||
"image and drawing trail are not shown."));
|
||||
m_touch_cursor->setChecked(!mapping.contains(QLatin1String(TOUCH_CURSOR_OFF)));
|
||||
touch_layout->addWidget(m_touch_cursor);
|
||||
touch_layout->addStretch();
|
||||
touch_layout->addWidget(new QLabel(
|
||||
tr("Toggle with the touch pad mode key (F6 by default); draw with left mouse / Cross."),
|
||||
touch_group));
|
||||
layout->addWidget(touch_group);
|
||||
const auto skin_index = m_particle_skin->findData(ParseTouchParticleSkin(mapping));
|
||||
m_particle_skin->setCurrentIndex(skin_index >= 0 ? skin_index : 0);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults |
|
||||
QDialogButtonBox::Save | QDialogButtonBox::Cancel,
|
||||
@@ -338,9 +419,14 @@ InputMappingDialog::InputMappingDialog(const QStringList& mapping, ControllerPre
|
||||
auto* defaults = buttons->button(QDialogButtonBox::RestoreDefaults);
|
||||
defaults->setObjectName(QStringLiteral("defaults"));
|
||||
defaults->setAutoDefault(false);
|
||||
layout->addWidget(buttons);
|
||||
connect(m_controller, &DualSenseWidget::ControlClicked, this,
|
||||
&InputMappingDialog::ChangeBinding);
|
||||
defaults->setToolTip(tr("Restore the defaults of the current tab."));
|
||||
auto* tip = new QLabel(tr("Tip: right-click a binding to unmap it."), this);
|
||||
auto* footer = new QHBoxLayout;
|
||||
footer->addWidget(tip);
|
||||
footer->addStretch();
|
||||
footer->addWidget(buttons);
|
||||
layout->addLayout(footer);
|
||||
|
||||
connect(defaults, &QPushButton::clicked, this, &InputMappingDialog::RestoreDefaults);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
@@ -359,6 +445,128 @@ InputMappingDialog::~InputMappingDialog() {
|
||||
}
|
||||
}
|
||||
|
||||
QWidget* InputMappingDialog::BuildTab(bool pad) {
|
||||
auto* page = new QWidget(this);
|
||||
auto* layout = new QVBoxLayout(page);
|
||||
layout->setSpacing(10);
|
||||
|
||||
auto* hint = new QLabel(
|
||||
pad ? tr("Choose which button on your gamepad triggers each PS5 control. Click a "
|
||||
"binding, then press the button or trigger you want to use.")
|
||||
: tr("Choose which keyboard key or mouse button triggers each PS5 control. Click a "
|
||||
"binding, then press the key or mouse button."),
|
||||
page);
|
||||
hint->setWordWrap(true);
|
||||
layout->addWidget(hint);
|
||||
if (pad) {
|
||||
m_status = new QLabel(page);
|
||||
m_status->setObjectName(QStringLiteral("controllerStatus"));
|
||||
m_status->setWordWrap(true);
|
||||
layout->addWidget(m_status);
|
||||
}
|
||||
|
||||
auto* body = new QHBoxLayout;
|
||||
body->setSpacing(14);
|
||||
layout->addLayout(body, 1);
|
||||
|
||||
auto* scroll = new QScrollArea(page);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
auto* content = new QWidget(scroll);
|
||||
auto* columns = new QGridLayout(content);
|
||||
columns->setContentsMargins(0, 0, 8, 0);
|
||||
columns->setSpacing(12);
|
||||
columns->setAlignment(Qt::AlignTop);
|
||||
|
||||
// Section order and the column each one lives in.
|
||||
static constexpr Section ORDER[] = {Face, Dpad, Shoulders, Sticks, System, LeftStick,
|
||||
RightStick, Shortcuts};
|
||||
int column_rows[2] = {0, 0};
|
||||
for (const auto section: ORDER) {
|
||||
QGroupBox* group = nullptr;
|
||||
QGridLayout* rows = nullptr;
|
||||
int row = 0;
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
if (control.section != section || (pad && control.pad_default == nullptr)) {
|
||||
continue;
|
||||
}
|
||||
if (group == nullptr) {
|
||||
group = new QGroupBox(tr(SECTION_TITLES[section]), content);
|
||||
rows = new QGridLayout(group);
|
||||
rows->setHorizontalSpacing(12);
|
||||
rows->setVerticalSpacing(6);
|
||||
rows->setColumnStretch(0, 1);
|
||||
rows->setColumnMinimumWidth(1, 190);
|
||||
}
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
const auto label = !pad && id == QLatin1String("TouchPad") ? tr("Touch pad left")
|
||||
: tr(control.label);
|
||||
auto* name = new QLabel(label, group);
|
||||
auto* button = new QPushButton(group);
|
||||
button->setObjectName((pad ? QStringLiteral("padBinding_") : QStringLiteral("binding_")) +
|
||||
id);
|
||||
button->setAutoDefault(false);
|
||||
button->setMinimumWidth(190);
|
||||
button->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
button->setAccessibleName(tr(SECTION_TITLES[section]) + QStringLiteral(" / ") + label);
|
||||
rows->addWidget(name, row, 0);
|
||||
rows->addWidget(button, row, 1);
|
||||
++row;
|
||||
(pad ? m_pad_buttons : m_buttons).insert(id, button);
|
||||
connect(button, &QPushButton::clicked, this, [this, pad, id] {
|
||||
pad ? ChangePadBinding(id) : ChangeKeyboardBinding(id);
|
||||
});
|
||||
connect(button, &QPushButton::customContextMenuRequested, this,
|
||||
[this, pad, id] { ClearBinding(pad, id); });
|
||||
}
|
||||
if (group != nullptr) {
|
||||
const int target = column_rows[0] <= column_rows[1] ? 0 : 1;
|
||||
columns->addWidget(group, column_rows[target]++, target);
|
||||
}
|
||||
}
|
||||
columns->setColumnStretch(0, 1);
|
||||
columns->setColumnStretch(1, 1);
|
||||
scroll->setWidget(content);
|
||||
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
scroll->setMinimumWidth(pad ? 640 : 700);
|
||||
body->addWidget(scroll, 3);
|
||||
|
||||
if (pad) {
|
||||
m_controller = new DualSenseWidget(page);
|
||||
m_controller->setObjectName(QStringLiteral("controllerDiagram"));
|
||||
m_controller->setMinimumWidth(320);
|
||||
body->addWidget(m_controller, 2);
|
||||
connect(m_controller, &DualSenseWidget::ControlClicked, this,
|
||||
[this](const QString& id) {
|
||||
ChangePadBinding(id == QLatin1String("TouchPadRight") ? QStringLiteral("TouchPad")
|
||||
: id);
|
||||
});
|
||||
auto* note = new QLabel(tr("The gyro emulation toggle activates tilt mode on gamepads "
|
||||
"without a gyroscope: the right stick then controls pitch and "
|
||||
"roll. It defaults to the View/Back button."),
|
||||
page);
|
||||
note->setWordWrap(true);
|
||||
layout->addWidget(note);
|
||||
} else {
|
||||
auto* sensitivity_layout = new QHBoxLayout;
|
||||
sensitivity_layout->addWidget(new QLabel(tr("Mouse sensitivity"), page));
|
||||
m_sensitivity = new QDoubleSpinBox(page);
|
||||
m_sensitivity->setObjectName(QStringLiteral("mouseSensitivity"));
|
||||
m_sensitivity->setRange(0.1, 5.0);
|
||||
m_sensitivity->setSingleStep(0.1);
|
||||
m_sensitivity->setDecimals(1);
|
||||
m_sensitivity->setSuffix(QStringLiteral("×"));
|
||||
m_sensitivity->setValue(DEFAULT_MOUSE_SENSITIVITY);
|
||||
sensitivity_layout->addWidget(m_sensitivity);
|
||||
sensitivity_layout->addWidget(
|
||||
new QLabel(tr("F7 in-game toggles mouse movement on the right stick."), page));
|
||||
sensitivity_layout->addStretch();
|
||||
layout->addLayout(sensitivity_layout);
|
||||
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
QStringList InputMappingDialog::Mapping() const {
|
||||
QStringList result;
|
||||
if (m_custom_bindings) {
|
||||
@@ -369,15 +577,38 @@ QStringList InputMappingDialog::Mapping() const {
|
||||
result.append(id + QLatin1Char('=') + binding);
|
||||
}
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
result.append(QLatin1String(KEYBOARD_DISABLED));
|
||||
}
|
||||
}
|
||||
if (m_custom_pad) {
|
||||
const auto start = result.size();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
const auto source = m_pad_bindings.value(id);
|
||||
if (!source.isEmpty()) {
|
||||
result.append(QLatin1String(PAD_PREFIX) + id + QLatin1Char('=') + source);
|
||||
}
|
||||
}
|
||||
if (result.size() == start) {
|
||||
result.append(QLatin1String(PAD_DISABLED));
|
||||
}
|
||||
}
|
||||
if (m_sensitivity->value() != DEFAULT_MOUSE_SENSITIVITY) {
|
||||
result.append(QLatin1String(MOUSE_SENSITIVITY) +
|
||||
QString::number(m_sensitivity->value(), 'f', 1));
|
||||
}
|
||||
const auto skin = m_particle_skin->currentData().toString();
|
||||
if (skin != QLatin1String("Classic")) {
|
||||
result.append(QLatin1String(TOUCH_PARTICLE_SKIN) + skin);
|
||||
}
|
||||
if (!m_touch_cursor->isChecked()) {
|
||||
result.append(QLatin1String(TOUCH_CURSOR_OFF));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void InputMappingDialog::ChangeBinding(const QString& id) {
|
||||
void InputMappingDialog::ChangeKeyboardBinding(const QString& id) {
|
||||
if (!m_buttons.contains(id)) {
|
||||
return;
|
||||
}
|
||||
@@ -393,14 +624,54 @@ void InputMappingDialog::ChangeBinding(const QString& id) {
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
void InputMappingDialog::ChangePadBinding(const QString& id) {
|
||||
if (!m_pad_buttons.contains(id)) {
|
||||
return;
|
||||
}
|
||||
m_selected = id;
|
||||
UpdateHighlights();
|
||||
PadCaptureDialog dialog(m_preview, this);
|
||||
dialog.setWindowTitle(tr("Set binding — %1").arg(m_pad_buttons.value(id)->accessibleName()));
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
AssignBinding(m_pad_bindings, id, dialog.Binding());
|
||||
m_custom_pad = true;
|
||||
}
|
||||
m_selected.clear();
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
void InputMappingDialog::ClearBinding(bool pad, const QString& id) {
|
||||
if (pad) {
|
||||
m_pad_bindings.remove(id);
|
||||
m_custom_pad = true;
|
||||
} else {
|
||||
m_bindings.remove(id);
|
||||
m_custom_bindings = true;
|
||||
}
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
void InputMappingDialog::RestoreDefaults() {
|
||||
if (m_tabs->currentIndex() == 0) {
|
||||
m_pad_bindings.clear();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
if (control.pad_default != nullptr) {
|
||||
m_pad_bindings.insert(QString::fromLatin1(control.id),
|
||||
QString::fromLatin1(control.pad_default));
|
||||
}
|
||||
}
|
||||
m_custom_pad = false;
|
||||
} else {
|
||||
m_bindings.clear();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
if (control.keyboard_default[0] != '\0') {
|
||||
m_bindings.insert(QString::fromLatin1(control.id),
|
||||
QString::fromLatin1(control.default_binding));
|
||||
QString::fromLatin1(control.keyboard_default));
|
||||
}
|
||||
}
|
||||
m_sensitivity->setValue(DEFAULT_MOUSE_SENSITIVITY);
|
||||
m_custom_bindings = false;
|
||||
}
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
@@ -410,14 +681,42 @@ void InputMappingDialog::UpdateButtons() {
|
||||
const auto binding = m_bindings.value(item.key());
|
||||
const auto label = binding.isEmpty() ? tr("None") : binding;
|
||||
button->setText(QString(label).replace(QLatin1Char('&'), QLatin1String("&&")));
|
||||
button->setToolTip(tr("%1: %2. Click to change.").arg(button->accessibleName(), label));
|
||||
button->setToolTip(
|
||||
tr("%1: %2. Click to change, right-click to clear.").arg(button->accessibleName(), label));
|
||||
}
|
||||
for (auto item = m_pad_buttons.begin(); item != m_pad_buttons.end(); ++item) {
|
||||
auto* button = item.value();
|
||||
const auto source = m_pad_bindings.value(item.key());
|
||||
const auto label = source.isEmpty() ? tr("None") : PadSourceLabel(source);
|
||||
button->setText(QString(label).replace(QLatin1Char('&'), QLatin1String("&&")));
|
||||
button->setToolTip(
|
||||
tr("%1: %2. Click to change, right-click to clear.").arg(button->accessibleName(), label));
|
||||
}
|
||||
UpdateHighlights();
|
||||
}
|
||||
|
||||
void InputMappingDialog::UpdatePreview() {
|
||||
const auto pressed = m_preview != nullptr ? m_preview->Pressed() : QSet<QString>();
|
||||
m_controller->SetPressed(pressed);
|
||||
m_pad_pressed.clear();
|
||||
if (m_preview != nullptr) {
|
||||
const auto& raw = m_preview->RawPressed();
|
||||
for (auto item = m_pad_bindings.begin(); item != m_pad_bindings.end(); ++item) {
|
||||
if (raw.contains(item.value())) {
|
||||
m_pad_pressed.insert(item.key());
|
||||
}
|
||||
}
|
||||
if (m_pad_pressed.contains(QStringLiteral("TouchPad"))) {
|
||||
m_pad_pressed.insert(QStringLiteral("TouchPadRight"));
|
||||
}
|
||||
for (const auto& name: m_preview->Pressed()) {
|
||||
// Stick directions and touch pad fingers are shown as the pad reports them.
|
||||
if (name.startsWith(QLatin1String("LeftStick")) ||
|
||||
name.startsWith(QLatin1String("RightStick")) ||
|
||||
name.startsWith(QLatin1String("TouchPad"))) {
|
||||
m_pad_pressed.insert(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
m_controller->SetPressed(m_pad_pressed);
|
||||
m_controller->SetSticks(m_preview != nullptr ? m_preview->LeftStick() : QPointF(),
|
||||
m_preview != nullptr ? m_preview->RightStick() : QPointF());
|
||||
UpdateHighlights();
|
||||
@@ -432,8 +731,8 @@ void InputMappingDialog::UpdatePreview() {
|
||||
|
||||
void InputMappingDialog::UpdateHighlights() {
|
||||
m_controller->SetSelected(m_selected);
|
||||
for (auto item = m_buttons.begin(); item != m_buttons.end(); ++item) {
|
||||
const bool down = m_preview != nullptr && m_preview->Pressed().contains(item.key());
|
||||
for (auto item = m_pad_buttons.begin(); item != m_pad_buttons.end(); ++item) {
|
||||
const bool down = m_pad_pressed.contains(item.key());
|
||||
auto colors = palette();
|
||||
if (down) {
|
||||
colors.setColor(QPalette::Button, colors.color(QPalette::Highlight));
|
||||
@@ -446,4 +745,11 @@ void InputMappingDialog::UpdateHighlights() {
|
||||
item.value()->setPalette(colors);
|
||||
item.value()->setFont(label_font);
|
||||
}
|
||||
for (auto item = m_buttons.begin(); item != m_buttons.end(); ++item) {
|
||||
auto colors = palette();
|
||||
if (item.key() == m_selected) {
|
||||
colors.setColor(QPalette::ButtonText, colors.color(QPalette::Highlight));
|
||||
}
|
||||
item.value()->setPalette(colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ public:
|
||||
void Motion(int id, Sensor sensor, const float* data, uint64_t time_us);
|
||||
void SetMotionSensorState(bool enable);
|
||||
void ResetOrientation();
|
||||
void ToggleGyroEmulation(int id);
|
||||
void ResetInputState();
|
||||
void ReleaseHostPads();
|
||||
void GetConnectionInfo(bool* flag, int* count);
|
||||
@@ -147,6 +148,8 @@ private:
|
||||
|
||||
void CheckActive();
|
||||
void AddState();
|
||||
// Emulated tilt: the right stick drives pitch and roll instead of the stick axes.
|
||||
void UpdateEmulatedTilt();
|
||||
// Apply cached output without changing its game-requested lifetime; caller holds m_mutex.
|
||||
void ApplyVibration();
|
||||
bool SendTriggerEffect(const PadTriggerEffectParam& param);
|
||||
@@ -160,6 +163,9 @@ private:
|
||||
int m_connected_count = 0;
|
||||
bool m_motion_enabled = true;
|
||||
uint64_t m_gyro_time = 0;
|
||||
bool m_gyro_emulation = false;
|
||||
uint64_t m_emulated_time = 0;
|
||||
std::array<int, 2> m_emulated_stick {128, 128};
|
||||
// Accelerometer gravity direction in the reset frame.
|
||||
std::array<float, 3> m_up_reference {0.0f, 1.0f, 0.0f};
|
||||
bool m_up_reference_valid = false;
|
||||
@@ -479,6 +485,9 @@ void GameController::CheckActive() {
|
||||
m_connected = new_connected;
|
||||
m_state = {};
|
||||
m_gyro_time = 0;
|
||||
m_gyro_emulation = false;
|
||||
m_emulated_time = 0;
|
||||
m_emulated_stick = {128, 128};
|
||||
m_up_reference_valid = false;
|
||||
m_states_num = 0;
|
||||
m_first_state = 0;
|
||||
@@ -524,6 +533,12 @@ void GameController::Axis(int id, Controller::Axis axis, int value) {
|
||||
|
||||
EXIT_IF(axis_id < 0 || axis_id >= static_cast<int>(Controller::Axis::AxisMax));
|
||||
|
||||
if (m_gyro_emulation && (axis == Controller::Axis::RightX || axis == Controller::Axis::RightY)) {
|
||||
m_emulated_stick[axis == Controller::Axis::RightY ? 1 : 0] = value;
|
||||
UpdateEmulatedTilt();
|
||||
return;
|
||||
}
|
||||
|
||||
m_state.axes[axis_id] = value;
|
||||
|
||||
uint32_t trigger = 0;
|
||||
@@ -546,6 +561,11 @@ void GameController::RightStick(int id, int x, int y) {
|
||||
|
||||
if (m_active_id == id || id == HOST_INPUT_CONTROLLER_ID) {
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
if (m_gyro_emulation) {
|
||||
m_emulated_stick = {x, y};
|
||||
UpdateEmulatedTilt();
|
||||
return;
|
||||
}
|
||||
m_state.axes[static_cast<int>(Axis::RightX)] = x;
|
||||
m_state.axes[static_cast<int>(Axis::RightY)] = y;
|
||||
AddState();
|
||||
@@ -677,6 +697,82 @@ void GameController::SetMotionSensorState(bool enable) {
|
||||
}
|
||||
}
|
||||
|
||||
static Quat QuatMultiply(const Quat& a, const Quat& b) {
|
||||
return {a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
|
||||
a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
|
||||
a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
|
||||
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]};
|
||||
}
|
||||
|
||||
void GameController::UpdateEmulatedTilt() {
|
||||
constexpr float max_angle = 0.7853982f; // 45 degrees at full stick deflection
|
||||
const auto angle = [](int value) {
|
||||
return std::clamp((value - 128) / 127.0f, -1.0f, 1.0f) * max_angle;
|
||||
};
|
||||
const float roll = angle(m_emulated_stick[0]);
|
||||
const float pitch = angle(m_emulated_stick[1]);
|
||||
|
||||
const Quat q_pitch {std::sin(pitch * 0.5f), 0.0f, 0.0f, std::cos(pitch * 0.5f)};
|
||||
const Quat q_roll {0.0f, 0.0f, std::sin(-roll * 0.5f), std::cos(-roll * 0.5f)};
|
||||
Quat q = QuatMultiply(q_roll, q_pitch);
|
||||
|
||||
const uint64_t now = LibKernel::KernelGetProcessTime();
|
||||
Vec3 rate {};
|
||||
if (m_emulated_time != 0 && now > m_emulated_time) {
|
||||
const float dt = std::clamp(static_cast<float>(now - m_emulated_time) * 0.000001f,
|
||||
0.001f, 0.1f);
|
||||
auto delta = QuatMultiply(QuatConjugate(m_state.orientation), q);
|
||||
if (delta[3] < 0.0f) {
|
||||
for (auto& v: delta) {
|
||||
v = -v;
|
||||
}
|
||||
}
|
||||
rate = {2.0f * delta[0] / dt, 2.0f * delta[1] / dt, 2.0f * delta[2] / dt};
|
||||
}
|
||||
m_emulated_time = now;
|
||||
|
||||
m_state.time = now;
|
||||
m_state.orientation = q;
|
||||
m_state.accel = QuatRotate(QuatConjugate(q), {0.0f, 1.0f, 0.0f});
|
||||
m_state.gyro = rate;
|
||||
AddState();
|
||||
// Angular velocity is only nonzero while the tilt changes.
|
||||
m_state.gyro = {};
|
||||
}
|
||||
|
||||
void GameController::ToggleGyroEmulation(int id) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
if (id != HOST_INPUT_CONTROLLER_ID && id != m_active_id) {
|
||||
return;
|
||||
}
|
||||
// Pads with a real gyroscope report it directly.
|
||||
if (m_active_id != HOST_INPUT_CONTROLLER_ID) {
|
||||
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(m_active_id));
|
||||
if (pad != nullptr && SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_gyro_emulation = !m_gyro_emulation;
|
||||
m_emulated_time = 0;
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
if (m_gyro_emulation) {
|
||||
m_emulated_stick = {m_state.axes[static_cast<int>(Axis::RightX)],
|
||||
m_state.axes[static_cast<int>(Axis::RightY)]};
|
||||
m_state.axes[static_cast<int>(Axis::RightX)] = 128;
|
||||
m_state.axes[static_cast<int>(Axis::RightY)] = 128;
|
||||
UpdateEmulatedTilt();
|
||||
} else {
|
||||
m_state.orientation = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
m_state.accel = {0.0f, 1.0f, 0.0f};
|
||||
m_state.gyro = {};
|
||||
m_state.axes[static_cast<int>(Axis::RightX)] = m_emulated_stick[0];
|
||||
m_state.axes[static_cast<int>(Axis::RightY)] = m_emulated_stick[1];
|
||||
AddState();
|
||||
}
|
||||
LOGF("Emulated gyro tilt: %s\n", m_gyro_emulation ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
void GameController::ResetOrientation() {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
m_state.orientation = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
@@ -931,6 +1027,10 @@ void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us) {
|
||||
g_controller->Motion(id, sensor, data, time_us);
|
||||
}
|
||||
|
||||
void ToggleGyroEmulation(int id) {
|
||||
g_controller->ToggleGyroEmulation(id);
|
||||
}
|
||||
|
||||
void ResetInputState() {
|
||||
g_controller->ResetInputState();
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ void SetAxis(int id, Axis axis, int value);
|
||||
void SetRightStick(int id, int x, int y);
|
||||
void SetTouchPad(int id, int finger, bool down, float x, float y);
|
||||
void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us);
|
||||
void ToggleGyroEmulation(int id);
|
||||
void ResetInputState();
|
||||
int GetActiveControllerId();
|
||||
|
||||
|
||||
@@ -326,6 +326,33 @@ uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames
|
||||
if (frames == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (!stream->speaker &&
|
||||
SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) != SDL_GAMEPAD_TYPE_PS5) {
|
||||
CloseDevice(stream);
|
||||
stream->next_check = 0;
|
||||
// Non-DualSense pads have no haptic audio path: approximate it with the rumble motors.
|
||||
float peak[2] = {0.0f, 0.0f};
|
||||
for (uint32_t frame = 0; frame < frames; frame++) {
|
||||
for (uint32_t ch = 0; ch < 2; ch++) {
|
||||
const auto src_ch = channels == 1 ? 0 : ch;
|
||||
const auto index = (static_cast<size_t>(first_frame) + frame) * channels + src_ch;
|
||||
float value = is_float ? static_cast<const float*>(data)[index]
|
||||
: static_cast<const int16_t*>(data)[index] / 32768.0f;
|
||||
value *= volume[src_ch] / 32768.0f * gain * (gains != nullptr ? gains[src_ch] : 1.0f);
|
||||
peak[ch] = std::max(peak[ch], std::fabs(value));
|
||||
}
|
||||
}
|
||||
const auto to_motor = [](float v) {
|
||||
return static_cast<uint16_t>(std::clamp(v * 2.0f, 0.0f, 1.0f) * 65535.0f);
|
||||
};
|
||||
SDL_LockJoysticks();
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller));
|
||||
pad != nullptr) {
|
||||
(void) SDL_RumbleGamepad(pad, to_motor(peak[0]), to_motor(peak[1]), 100);
|
||||
}
|
||||
SDL_UnlockJoysticks();
|
||||
return static_cast<uint64_t>(frames) * 1000000 / stream->freq;
|
||||
}
|
||||
const bool wireless = IsWireless(controller);
|
||||
if (!wireless && !CanUseDevice(controller)) {
|
||||
CloseDevice(stream);
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
// Exercise production host input with deterministic gamepads and guest output.
|
||||
#include <SDL3/SDL.h>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace {
|
||||
void Check(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "HostInputTests: %s\n", message);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
std::array<Sint16, 2> physical_stick {};
|
||||
std::array<int, 2> guest_stick {128, 128};
|
||||
uint32_t guest_buttons = 0;
|
||||
bool guest_touch = false;
|
||||
} // namespace
|
||||
|
||||
namespace Fake {
|
||||
SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
|
||||
return id == 1 || id == 2 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
: nullptr;
|
||||
}
|
||||
Sint16 GetGamepadAxis(SDL_Gamepad*, SDL_GamepadAxis axis) {
|
||||
return physical_stick[axis == SDL_GAMEPAD_AXIS_LEFTY ? 1 : 0];
|
||||
}
|
||||
Uint64 GetTicks() {
|
||||
return 1000;
|
||||
}
|
||||
bool HideCursor() {
|
||||
return true;
|
||||
}
|
||||
bool ShowCursor() {
|
||||
return true;
|
||||
}
|
||||
SDL_MouseButtonFlags GetMouseState(float* x, float* y) {
|
||||
*x = *y = 0.0f;
|
||||
return 0;
|
||||
}
|
||||
} // namespace Fake
|
||||
|
||||
#define SDL_GetGamepadFromID Fake::GetGamepadFromID
|
||||
#define SDL_GetGamepadAxis Fake::GetGamepadAxis
|
||||
#define SDL_GetTicks Fake::GetTicks
|
||||
#define SDL_HideCursor Fake::HideCursor
|
||||
#define SDL_ShowCursor Fake::ShowCursor
|
||||
#define SDL_GetMouseState Fake::GetMouseState
|
||||
#include "graphics/presentation/window/hostInput.cpp"
|
||||
#undef SDL_GetGamepadFromID
|
||||
#undef SDL_GetGamepadAxis
|
||||
#undef SDL_GetTicks
|
||||
#undef SDL_HideCursor
|
||||
#undef SDL_ShowCursor
|
||||
#undef SDL_GetMouseState
|
||||
|
||||
namespace Libs::Controller {
|
||||
void SetButton(int id, uint32_t button, bool down) {
|
||||
if (id == 1) {
|
||||
guest_buttons = down ? guest_buttons | button : guest_buttons & ~button;
|
||||
}
|
||||
}
|
||||
void SetAxis(int id, Axis axis, int value) {
|
||||
if (id == 1 && (axis == Axis::LeftX || axis == Axis::LeftY)) {
|
||||
guest_stick[axis == Axis::LeftY ? 1 : 0] = value;
|
||||
}
|
||||
}
|
||||
void SetTouchPad(int id, int finger, bool down, float, float) {
|
||||
if (id == 1 && finger == 1) {
|
||||
guest_touch = down;
|
||||
}
|
||||
}
|
||||
void SetRightStick(int, int, int) {}
|
||||
void ToggleGyroEmulation(int) {}
|
||||
void CycleSetting(Setting) {}
|
||||
} // namespace Libs::Controller
|
||||
|
||||
namespace {
|
||||
using namespace Libs::Graphics;
|
||||
using namespace Libs::Controller;
|
||||
|
||||
struct Inputs {
|
||||
Inputs() {
|
||||
physical_stick = {};
|
||||
guest_stick = {128, 128};
|
||||
guest_buttons = 0;
|
||||
guest_touch = false;
|
||||
HostInputInit(nullptr);
|
||||
}
|
||||
~Inputs() { HostInputShutdown(); }
|
||||
};
|
||||
|
||||
void TestTouchPadStickHandoff() {
|
||||
Inputs inputs;
|
||||
physical_stick = {SDL_JOYSTICK_AXIS_MAX, SDL_JOYSTICK_AXIS_MIN};
|
||||
guest_stick = {255, 0};
|
||||
HostInputToggleGamepadTouchPad(1);
|
||||
Check(guest_stick == std::array {128, 128}, "entering touchpad mode retained guest movement");
|
||||
PollGamepadTouchPad(1008);
|
||||
const auto cursor = GetHostInputTouchPadVisualState();
|
||||
Check(cursor.x > 0.5f && cursor.y < 0.5f,
|
||||
"entering touchpad mode did not sample an already-held stick");
|
||||
Check(HostInputGamepadAxis(1, true, 127) && HostInputGamepadAxis(1, false, 127),
|
||||
"touchpad mode did not capture stick releases");
|
||||
Check(guest_stick == std::array {128, 128}, "touchpad movement leaked to guest axes");
|
||||
// Sample current physical axes on exit even if an event was consumed elsewhere.
|
||||
physical_stick = {SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX};
|
||||
HostInputToggleGamepadTouchPad(1);
|
||||
Check(guest_stick == std::array {0, 255}, "leaving touchpad mode did not restore current axes");
|
||||
Check(!HostInputGamepadAxis(1, true, 127), "leaving touchpad mode retained stick capture");
|
||||
}
|
||||
|
||||
void TestKeyboardSwitchesFromGamepadTouchPad() {
|
||||
Inputs inputs;
|
||||
physical_stick = {SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX};
|
||||
HostInputToggleGamepadTouchPad(1);
|
||||
Check(HostInputGamepadDrawButton(1, true) && guest_touch, "gamepad drawing did not start");
|
||||
HostInputKey(SDLK_F6, true);
|
||||
Check(GetHostInputTouchPadVisualState().enabled && !HostInputGamepadAxis(1, true, 127),
|
||||
"F6 failed to switch from gamepad to mouse touchpad mode");
|
||||
Check(!guest_touch && guest_stick == std::array {0, 255},
|
||||
"switching to mouse touchpad mode retained gamepad input capture");
|
||||
HostInputKey(SDLK_F6, false);
|
||||
HostInputKey(SDLK_F6, true);
|
||||
Check(!GetHostInputTouchPadVisualState().enabled, "F6 failed to disable mouse touchpad mode");
|
||||
}
|
||||
|
||||
void TestIndependentTriggerEdges() {
|
||||
Inputs inputs;
|
||||
HostInputPadTrigger(2, true, 255);
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
Check(guest_buttons == PAD_BUTTON_CROSS, "an inactive pad suppressed the active trigger press");
|
||||
HostInputPadTrigger(2, true, 0);
|
||||
HostInputPadTrigger(1, true, 0);
|
||||
Check(guest_buttons == 0, "an inactive pad suppressed the active trigger release");
|
||||
HostInputPadTrigger(1, false, 255);
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
HostInputPadTrigger(1, true, 0);
|
||||
Check(guest_buttons == PAD_BUTTON_CIRCLE, "left and right trigger edges were mixed");
|
||||
HostInputPadTrigger(1, false, 0);
|
||||
Check(guest_buttons == 0, "right trigger release was lost");
|
||||
}
|
||||
|
||||
void TestTriggerLifecycleReset() {
|
||||
Inputs inputs;
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
HostInputDisconnectGamepad(1);
|
||||
guest_buttons = 0;
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
Check(guest_buttons == PAD_BUTTON_CROSS, "disconnect retained a previous trigger press");
|
||||
HostInputShutdown();
|
||||
guest_buttons = 0;
|
||||
HostInputInit(nullptr);
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
Check(guest_buttons == PAD_BUTTON_CROSS, "shutdown retained a previous trigger press");
|
||||
}
|
||||
|
||||
void TestDisabledMappingTabs() {
|
||||
Config::ConfigOptions options;
|
||||
const auto south = InputMap::SourceFromButton(SDL_GAMEPAD_BUTTON_SOUTH);
|
||||
Config::Load(options);
|
||||
const InputMap defaults;
|
||||
Check(!defaults.Custom() && defaults.PadControl(south) == ControlFromName("Cross"),
|
||||
"empty configuration lost its default mappings");
|
||||
options.keymap = {"Keyboard=Disabled"};
|
||||
Config::Load(options);
|
||||
const InputMap keyboard_disabled;
|
||||
Check(keyboard_disabled.Custom() && keyboard_disabled.FindKey(SDLK_J) == INVALID_CONTROL &&
|
||||
keyboard_disabled.PadControl(south) == ControlFromName("Cross"),
|
||||
"disabling keyboard mappings changed gamepad defaults or restored keyboard input");
|
||||
for (const auto& keymap: {Config::Keymap {"Pad=Disabled"},
|
||||
Config::Keymap {"Keyboard=Disabled", "Pad=Disabled"}}) {
|
||||
options.keymap = keymap;
|
||||
Config::Load(options);
|
||||
const InputMap disabled;
|
||||
Check(disabled.Custom() == (keymap.size() == 2),
|
||||
"disabling gamepad mappings changed keyboard defaults");
|
||||
for (std::size_t source = 0; source < PAD_SOURCES.size(); source++) {
|
||||
Check(disabled.PadControl(source) == INVALID_CONTROL,
|
||||
"a disabled gamepad mapping reverted to its default");
|
||||
}
|
||||
}
|
||||
options.keymap = {"Cross=J", "Pad:Circle=South"};
|
||||
Config::Load(options);
|
||||
const InputMap mapped;
|
||||
Check(mapped.Custom() && mapped.FindKey(SDLK_J) == ControlFromName("Cross") &&
|
||||
mapped.PadControl(south) == ControlFromName("Circle"),
|
||||
"explicit mappings no longer override defaults");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
Config::Initialize();
|
||||
Config::ConfigOptions options;
|
||||
options.keymap = {"Pad:Cross=LeftTrigger", "Pad:Circle=RightTrigger"};
|
||||
Config::Load(options);
|
||||
TestTouchPadStickHandoff();
|
||||
TestKeyboardSwitchesFromGamepadTouchPad();
|
||||
TestIndependentTriggerEdges();
|
||||
TestTriggerLifecycleReset();
|
||||
TestDisabledMappingTabs();
|
||||
Config::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
@@ -143,12 +143,12 @@ void DestroyAudioStream(SDL_AudioStream* stream) {
|
||||
SDL_AudioDeviceID GetAudioStreamDevice(SDL_AudioStream*) {
|
||||
return 999;
|
||||
}
|
||||
// Pads 1 and 3 are DualSenses.
|
||||
// Pads 1 and 3 are DualSenses; pad 2 only supports rumble.
|
||||
SDL_GamepadType GetGamepadTypeForID(SDL_JoystickID id) {
|
||||
return id == 1 || id == 3 ? SDL_GAMEPAD_TYPE_PS5 : SDL_GAMEPAD_TYPE_UNKNOWN;
|
||||
}
|
||||
SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
|
||||
return id == 1 || id == 3 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
return id >= 1 && id <= 3 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
: nullptr;
|
||||
}
|
||||
SDL_GamepadType GetGamepadType(SDL_Gamepad* pad) {
|
||||
@@ -458,6 +458,47 @@ void TestDiscoveryAndHotplug() {
|
||||
Check(streams.empty(), "disconnected endpoint retained its stream");
|
||||
}
|
||||
|
||||
void TestRumbleFallbackGains() {
|
||||
Fixture f;
|
||||
auto port = Open();
|
||||
const auto queue = [&](std::array<float, 2> gains) {
|
||||
Check(Haptics::Queue(port.get(), 2, pcm.data(), 2, 2, true, unity.data(), 1.0f,
|
||||
gains.data()) == 2 * 1000000 / 48000,
|
||||
"rumble fallback playback time is wrong");
|
||||
};
|
||||
queue({0.0f, 0.25f});
|
||||
Check(pad_rumble[2].large == 0 && pad_rumble[2].small == 16383,
|
||||
"rumble fallback ignored per-channel gains");
|
||||
queue({0.5f, 0.0f});
|
||||
Check(pad_rumble[2].large == 32767 && pad_rumble[2].small == 0,
|
||||
"rumble fallback swapped per-channel gains");
|
||||
queue({0.0f, 0.0f});
|
||||
Check(pad_rumble[2].large == 0 && pad_rumble[2].small == 0,
|
||||
"muted fallback channels still rumbled");
|
||||
Check(streams.empty() && hid_opens == 0, "rumble fallback opened an audio device");
|
||||
}
|
||||
|
||||
void TestRumbleFallbackSwitch() {
|
||||
for (bool bluetooth: {false, true}) {
|
||||
Fixture f;
|
||||
wireless = hid_available = bluetooth;
|
||||
auto port = Open();
|
||||
Queue(port, pcm.data());
|
||||
Check(bluetooth ? Haptics::UsesBluetooth(port.get()) : streams.size() == 1,
|
||||
"DualSense transport did not open before fallback");
|
||||
Haptics::Queue(port.get(), 2, pcm.data(), 2, 2, true, unity.data());
|
||||
Check(streams.empty() && !Haptics::UsesBluetooth(port.get()) &&
|
||||
(!bluetooth || hid_closes == 1),
|
||||
"switching to rumble retained the DualSense transport");
|
||||
Check(pad_rumble[2].large == 65535 && pad_rumble[2].small == 65535,
|
||||
"switching from DualSense did not start rumble fallback");
|
||||
Queue(port, pcm.data());
|
||||
Check(bluetooth ? Haptics::UsesBluetooth(port.get()) && hid_opens == 2
|
||||
: streams.size() == 1 && opens == 2,
|
||||
"switching back delayed reopening the DualSense transport");
|
||||
}
|
||||
}
|
||||
|
||||
void TestSpeaker() {
|
||||
Fixture f;
|
||||
devices = {{10, "DualSense Wireless Controller", 2}};
|
||||
@@ -1279,6 +1320,8 @@ int main() {
|
||||
TestAudioDefaultResumeFailure();
|
||||
TestFormatsAndVolume();
|
||||
TestDiscoveryAndHotplug();
|
||||
TestRumbleFallbackGains();
|
||||
TestRumbleFallbackSwitch();
|
||||
TestSpeaker();
|
||||
TestSpeakerUnplug();
|
||||
TestSpeakerSwitchAndFailures();
|
||||
|
||||
Reference in New Issue
Block a user