feat(input): add host input remapping

Add compact keyboard and mouse mapping with analog-stick defaults and integrate a global remapping editor into the Qt launcher.

Based on the host input remapping work from PR #109.

Co-authored-by: DistantMyth <thetarun3@gmail.com>
This commit is contained in:
nmzik
2026-08-04 03:24:24 +02:00
parent a3a00464bc
commit 6396dc9a85
21 changed files with 756 additions and 41 deletions
+4
View File
@@ -101,4 +101,8 @@ bool ReadbackLinearImagesEnabled() {
return g_config->readback_linear_images;
}
const Keymap& GetKeymap() {
return g_config->keymap;
}
} // namespace Config
+7
View File
@@ -5,6 +5,8 @@
#include "common/subsystems.h"
#include <filesystem>
#include <string>
#include <vector>
namespace Config {
@@ -18,6 +20,8 @@ enum class ProfilerDirection { None, Network };
enum class OutputDirection { Silent, Console, File };
using Keymap = std::vector<std::string>;
constexpr uint32_t DEFAULT_CONSOLE_LANGUAGE = 1;
constexpr uint32_t MAX_CONSOLE_LANGUAGE = 29;
@@ -41,6 +45,7 @@ struct ConfigOptions {
bool spirv_debug_printf_enabled = false;
bool renderdoc_enabled = false;
bool readback_linear_images = false;
Keymap keymap;
};
void Load(const ConfigOptions& cfg);
@@ -72,6 +77,8 @@ bool SpirvDebugPrintfEnabled();
bool RenderDocEnabled();
bool ReadbackLinearImagesEnabled();
const Keymap& GetKeymap();
} // namespace Config
#endif /* KYTY_COMMON_EMULATOR_CONFIG_H_ */
@@ -0,0 +1,256 @@
#include "graphics/presentation/window/hostInput.h"
#include "SDL_keyboard.h"
#include "SDL_keycode.h"
#include "SDL_mouse.h"
#include "common/assert.h"
#include "common/emulatorConfig.h"
#include "libs/controller.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <string>
#include <string_view>
#include <utility>
namespace Libs::Graphics {
namespace {
struct Binding {
SDL_Keycode key = SDLK_UNKNOWN;
uint8_t mouse_button = 0;
uint32_t pad_button = 0;
};
static constexpr std::array PAD_BUTTONS = {
std::pair {std::string_view("L3"), Controller::PAD_BUTTON_L3},
std::pair {std::string_view("R3"), Controller::PAD_BUTTON_R3},
std::pair {std::string_view("Options"), Controller::PAD_BUTTON_OPTIONS},
std::pair {std::string_view("Up"), Controller::PAD_BUTTON_UP},
std::pair {std::string_view("Right"), Controller::PAD_BUTTON_RIGHT},
std::pair {std::string_view("Down"), Controller::PAD_BUTTON_DOWN},
std::pair {std::string_view("Left"), Controller::PAD_BUTTON_LEFT},
std::pair {std::string_view("L2"), Controller::PAD_BUTTON_L2},
std::pair {std::string_view("R2"), Controller::PAD_BUTTON_R2},
std::pair {std::string_view("L1"), Controller::PAD_BUTTON_L1},
std::pair {std::string_view("R1"), Controller::PAD_BUTTON_R1},
std::pair {std::string_view("Triangle"), Controller::PAD_BUTTON_TRIANGLE},
std::pair {std::string_view("Circle"), Controller::PAD_BUTTON_CIRCLE},
std::pair {std::string_view("Cross"), Controller::PAD_BUTTON_CROSS},
std::pair {std::string_view("Square"), Controller::PAD_BUTTON_SQUARE},
std::pair {std::string_view("TouchPad"), Controller::PAD_BUTTON_TOUCH_PAD},
};
uint32_t PadButtonFromName(std::string_view name) {
const auto button = std::find_if(PAD_BUTTONS.begin(), PAD_BUTTONS.end(),
[name](const auto& item) { return item.first == name; });
return button != PAD_BUTTONS.end() ? button->second : 0;
}
SDL_Keycode NormalizeKey(SDL_Keycode key) {
switch (key) {
case SDLK_RSHIFT: return SDLK_LSHIFT;
case SDLK_RCTRL: return SDLK_LCTRL;
case SDLK_RALT: return SDLK_LALT;
case SDLK_RGUI: return SDLK_LGUI;
default: return key;
}
}
uint8_t MouseButtonFromName(std::string_view name) {
static constexpr std::array buttons = {
std::pair {std::string_view("Mouse:Left"), uint8_t {SDL_BUTTON_LEFT}},
std::pair {std::string_view("Mouse:Middle"), uint8_t {SDL_BUTTON_MIDDLE}},
std::pair {std::string_view("Mouse:Right"), uint8_t {SDL_BUTTON_RIGHT}},
std::pair {std::string_view("Mouse:X1"), uint8_t {SDL_BUTTON_X1}},
std::pair {std::string_view("Mouse:X2"), uint8_t {SDL_BUTTON_X2}},
};
const auto button = std::find_if(buttons.begin(), buttons.end(),
[name](const auto& item) { return item.first == name; });
return button != buttons.end() ? button->second : 0;
}
bool Conflicts(const Binding& first, const Binding& second) {
return first.pad_button == second.pad_button ||
(first.key != SDLK_UNKNOWN && first.key == second.key) ||
(first.mouse_button != 0 && first.mouse_button == second.mouse_button);
}
class InputMap {
public:
InputMap(): m_custom(!Config::GetKeymap().empty()) {
for (const auto& value: Config::GetKeymap()) {
const std::string_view entry = value;
const auto split = entry.find('=');
Binding binding;
if (split != std::string_view::npos) {
binding.pad_button = PadButtonFromName(entry.substr(0, split));
const auto host_input = entry.substr(split + 1);
binding.mouse_button = MouseButtonFromName(host_input);
if (binding.mouse_button == 0) {
binding.key = NormalizeKey(SDL_GetKeyFromName(std::string(host_input).c_str()));
}
}
const bool reserved =
binding.key == SDLK_ESCAPE || binding.key == SDLK_SPACE || binding.key == SDLK_F1;
if (binding.pad_button == 0 || reserved ||
(binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) {
EXIT("Invalid input mapping: %s\n", value.c_str());
}
Add(binding);
}
}
[[nodiscard]] bool Custom() const { return m_custom; }
[[nodiscard]] uint32_t FindKey(int key_code) const {
key_code = NormalizeKey(static_cast<SDL_Keycode>(key_code));
const auto binding =
std::find_if(m_bindings.begin(), m_bindings.begin() + m_size,
[key_code](const auto& item) { return item.key == key_code; });
return binding != m_bindings.begin() + m_size ? binding->pad_button : 0;
}
[[nodiscard]] uint32_t FindMouseButton(uint8_t mouse_button) const {
const auto binding = std::find_if(
m_bindings.begin(), m_bindings.begin() + m_size,
[mouse_button](const auto& item) { return item.mouse_button == mouse_button; });
return binding != m_bindings.begin() + m_size ? binding->pad_button : 0;
}
private:
void Add(const Binding& binding) {
for (std::size_t index = 0; index < m_size;) {
if (Conflicts(m_bindings[index], binding)) {
m_bindings[index] = m_bindings[--m_size];
} else {
index++;
}
}
EXIT_IF(m_size >= m_bindings.size());
m_bindings[m_size++] = binding;
}
std::array<Binding, PAD_BUTTONS.size()> m_bindings {};
std::size_t m_size = 0;
bool m_custom;
};
const InputMap& GetInputMap() {
static const InputMap map;
return map;
}
void SetButton(uint32_t button, bool down) {
if (button == Controller::PAD_BUTTON_L2) {
Controller::ControllerAxis(Controller::HOST_INPUT_CONTROLLER_ID,
Controller::Axis::TriggerLeft, down ? 255 : 0);
} else if (button == Controller::PAD_BUTTON_R2) {
Controller::ControllerAxis(Controller::HOST_INPUT_CONTROLLER_ID,
Controller::Axis::TriggerRight, down ? 255 : 0);
} else if (button != 0) {
Controller::ControllerButton(Controller::HOST_INPUT_CONTROLLER_ID, button, down);
}
}
uint32_t DefaultKeyboardButton(int key_code) {
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
case SDLK_UP: return Controller::PAD_BUTTON_UP;
case SDLK_LEFT: return Controller::PAD_BUTTON_LEFT;
case SDLK_DOWN: return Controller::PAD_BUTTON_DOWN;
case SDLK_RIGHT: return Controller::PAD_BUTTON_RIGHT;
case SDLK_j: return Controller::PAD_BUTTON_CROSS;
case SDLK_i: return Controller::PAD_BUTTON_TRIANGLE;
case SDLK_k: return Controller::PAD_BUTTON_SQUARE;
case SDLK_l: return Controller::PAD_BUTTON_CIRCLE;
case SDLK_q: return Controller::PAD_BUTTON_L1;
case SDLK_e: return Controller::PAD_BUTTON_R1;
case SDLK_LSHIFT: return Controller::PAD_BUTTON_L3;
case SDLK_LCTRL: return Controller::PAD_BUTTON_R3;
case SDLK_RETURN:
case SDLK_RETURN2: return Controller::PAD_BUTTON_OPTIONS;
case SDLK_BACKSPACE:
case SDLK_TAB: return Controller::PAD_BUTTON_TOUCH_PAD;
default: return 0;
}
}
struct StickKeys {
bool left = false;
bool right = false;
bool up = false;
bool down = false;
};
void SetStickAxis(Controller::Axis axis, bool negative, bool positive) {
const int value = negative == positive ? 128 : negative ? 0 : 255;
Controller::ControllerAxis(Controller::HOST_INPUT_CONTROLLER_ID, axis, value);
}
void DefaultKeyboardInput(int key_code, bool down) {
static StickKeys left;
static StickKeys right;
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
case SDLK_a:
left.left = down;
SetStickAxis(Controller::Axis::LeftX, left.left, left.right);
return;
case SDLK_d:
left.right = down;
SetStickAxis(Controller::Axis::LeftX, left.left, left.right);
return;
case SDLK_w:
left.up = down;
SetStickAxis(Controller::Axis::LeftY, left.up, left.down);
return;
case SDLK_s:
left.down = down;
SetStickAxis(Controller::Axis::LeftY, left.up, left.down);
return;
case SDLK_f:
right.left = down;
SetStickAxis(Controller::Axis::RightX, right.left, right.right);
return;
case SDLK_h:
right.right = down;
SetStickAxis(Controller::Axis::RightX, right.left, right.right);
return;
case SDLK_t:
right.up = down;
SetStickAxis(Controller::Axis::RightY, right.up, right.down);
return;
case SDLK_g:
right.down = down;
SetStickAxis(Controller::Axis::RightY, right.up, right.down);
return;
default: SetButton(DefaultKeyboardButton(key_code), down); return;
}
}
} // namespace
void HostInputInit() {
GetInputMap();
}
void HostInputKey(int key_code, bool down) {
const auto& map = GetInputMap();
if (map.Custom()) {
SetButton(map.FindKey(key_code), down);
} else {
DefaultKeyboardInput(key_code, down);
}
}
void HostInputMouseButton(uint8_t mouse_button, bool down) {
const auto& map = GetInputMap();
if (map.Custom() && mouse_button != 0) {
SetButton(map.FindMouseButton(mouse_button), down);
}
}
} // namespace Libs::Graphics
@@ -0,0 +1,14 @@
#ifndef KYTY_GRAPHICS_PRESENTATION_WINDOW_HOST_INPUT_H_
#define KYTY_GRAPHICS_PRESENTATION_WINDOW_HOST_INPUT_H_
#include <cstdint>
namespace Libs::Graphics {
void HostInputInit();
void HostInputKey(int key_code, bool down);
void HostInputMouseButton(uint8_t mouse_button, bool down);
} // namespace Libs::Graphics
#endif /* KYTY_GRAPHICS_PRESENTATION_WINDOW_HOST_INPUT_H_ */
+23 -31
View File
@@ -32,6 +32,7 @@
#include "graphics/host_gpu/vma.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/presentation/renderDoc.h"
#include "graphics/presentation/window/hostInput.h"
#include "graphics/presentation/window/windowInternal.h"
#include "kytyGitVersion.h"
#include "libs/controller.h"
@@ -59,8 +60,6 @@
namespace Libs::Graphics {
constexpr int KEYBOARD_CONTROLLER_ID = -1000;
struct EventKeyboard {
bool down;
bool up;
@@ -73,26 +72,6 @@ struct EventKeyboard {
double timestamp_seconds;
};
static uint32_t KeyboardKeyToPadButton(int key_code) {
switch (key_code) {
case SDLK_w: return Controller::PAD_BUTTON_UP;
case SDLK_a: return Controller::PAD_BUTTON_LEFT;
case SDLK_s: return Controller::PAD_BUTTON_DOWN;
case SDLK_d: return Controller::PAD_BUTTON_RIGHT;
case SDLK_j: return Controller::PAD_BUTTON_CROSS;
case SDLK_i: return Controller::PAD_BUTTON_TRIANGLE;
case SDLK_k: return Controller::PAD_BUTTON_SQUARE;
case SDLK_l: return Controller::PAD_BUTTON_CIRCLE;
case SDLK_q: return Controller::PAD_BUTTON_L1;
case SDLK_e: return Controller::PAD_BUTTON_R1;
case SDLK_RETURN:
case SDLK_RETURN2: return Controller::PAD_BUTTON_OPTIONS;
case SDLK_BACKSPACE:
case SDLK_TAB: return Controller::PAD_BUTTON_TOUCH_PAD;
default: return 0;
}
}
static uint32_t ControllerButtonToPadButton(int button) {
switch (button) {
case SDL_CONTROLLER_BUTTON_A: return Controller::PAD_BUTTON_CROSS;
@@ -261,16 +240,11 @@ static void GameEventKeyboard(WindowLoopState& game, const EventKeyboard& key) {
}
}
const auto button = KeyboardKeyToPadButton(key.key_code);
if (button != 0 && (key.down || key.up) && !key.repeat) {
static bool keyboard_connected = false;
if (!keyboard_connected) {
Controller::ControllerConnect(KEYBOARD_CONTROLLER_ID);
keyboard_connected = true;
}
Controller::ControllerButton(KEYBOARD_CONTROLLER_ID, button, key.down);
}
#endif
if ((key.down || key.up) && !key.repeat) {
HostInputKey(key.key_code, key.down);
}
}
static void GameEventMouse([[maybe_unused]] const EventMouse& mb) {
@@ -292,6 +266,23 @@ static void GameEventMouse([[maybe_unused]] const EventMouse& mb) {
mb.x, mb.y);
}
#endif
if (mb.down || mb.up) {
uint8_t mouse_button = 0;
if (mb.left) {
mouse_button = SDL_BUTTON_LEFT;
} else if (mb.middle) {
mouse_button = SDL_BUTTON_MIDDLE;
} else if (mb.right) {
mouse_button = SDL_BUTTON_RIGHT;
} else if (mb.x1) {
mouse_button = SDL_BUTTON_X1;
} else if (mb.x2) {
mouse_button = SDL_BUTTON_X2;
}
HostInputMouseButton(mouse_button, mb.down);
}
}
static void GameEventFinger([[maybe_unused]] const EventFinger& f) {
@@ -795,6 +786,7 @@ static void WindowCreate(WindowContext& context) {
if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) {
EXIT("%s\n", SDL_GetError());
}
HostInputInit();
LOGF("WindowCreate(): width = %d, height = %d\n", width, height);
+1
View File
@@ -16,6 +16,7 @@ file(GLOB launcher_forms
add_executable(launcher WIN32
${launcher_src}
${launcher_forms}
resources.qrc
)
@@ -69,6 +69,31 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="input_mapping_button">
<property name="minimumSize">
<size>
<width>36</width>
<height>36</height>
</size>
</property>
<property name="toolTip">
<string>Edit input mapping</string>
</property>
<property name="text">
<string/>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="edit_button">
<property name="minimumSize">
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#e8f1ff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M8 4h11l5 5v7M24 9h-5V4M16 27H8a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"/>
<path d="m15 24 1-4 8-8 4 4-8 8-4 1Z"/>
<path d="m22 14 4 4" opacity=".8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 336 B

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#e8f1ff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M5 8h3M14 8h13M5 16h13M24 16h3M5 24h6M17 24h10"/>
<circle cx="11" cy="8" r="3"/>
<circle cx="21" cy="16" r="3"/>
<circle cx="14" cy="24" r="3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 344 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#e8f1ff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M10 10h12c3.6 0 5.8 2.3 6.6 6.8l.9 5.1c.4 2.4-.8 4.1-2.8 4.1-1.4 0-2.5-.7-3.6-2.3L21.3 21H10.7l-1.8 2.7C7.8 25.3 6.7 26 5.3 26c-2 0-3.2-1.7-2.8-4.1l.9-5.1C4.2 12.3 6.4 10 10 10Z"/>
<path d="M8 16h6M11 13v6"/>
<circle cx="22" cy="15" r="1" fill="#e8f1ff" stroke="none"/>
<circle cx="25" cy="18" r="1" fill="#e8f1ff" stroke="none"/>
<path d="M13 7h6M16 4v6" opacity=".8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 574 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#e8f1ff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M6 9h20M12 9V5h8v4M9 9l1 18h12l1-18"/>
<path d="M14 14v8M18 14v8" opacity=".8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 272 B

+5 -1
View File
@@ -99,6 +99,7 @@ public:
QString printf_output_file = "_kyty.txt";
ProfilerDirection profiler_direction = ProfilerDirection::None;
bool renderdoc_enabled = false;
QStringList host_input_mapping;
QString elf = QStringLiteral("eboot.bin");
@@ -118,6 +119,7 @@ public:
printf_output_file = other.printf_output_file;
profiler_direction = other.profiler_direction;
renderdoc_enabled = other.renderdoc_enabled;
host_input_mapping = other.host_input_mapping;
}
void CopyFrom(const Configuration& other) {
@@ -154,6 +156,7 @@ public:
KYTY_CFG_SET(printf_output_file);
KYTY_CFG_SET(profiler_direction);
KYTY_CFG_SET(renderdoc_enabled);
s->setValue("host_input_mapping", host_input_mapping);
KYTY_CFG_SET(elf);
}
@@ -180,7 +183,8 @@ public:
KYTY_CFG_GET(printf_output_file);
KYTY_CFG_GET(profiler_direction);
KYTY_CFG_GET(renderdoc_enabled);
elf = s->value("elf", elf).toString();
host_input_mapping = s->value("host_input_mapping", host_input_mapping).toStringList();
elf = s->value("elf", elf).toString();
}
};
@@ -38,7 +38,10 @@ public:
[[nodiscard]] const QString& GetSettingsFile() const { return m_settings_file; }
[[nodiscard]] const QStringList& GetGameDirectories() const { return m_game_dirs; }
[[nodiscard]] bool CanViewSelectedTrophies() const;
[[nodiscard]] const QStringList& GetHostInputMapping() const {
return m_global_info.host_input_mapping;
}
[[nodiscard]] bool CanViewSelectedTrophies() const;
bool EnsureGameDirectory();
void ScanGameDirectory();
@@ -58,6 +61,7 @@ protected slots:
void edit_configuration();
void delete_configuartion();
void edit_global_settings();
void edit_input_mapping();
void run_configuration();
void list_currentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous);
void list_itemDoubleClicked(QTreeWidgetItem* witem, int column);
+31
View File
@@ -0,0 +1,31 @@
#ifndef LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
#define LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
#include <QDialog>
#include <QStringList>
class QPushButton;
class QTreeWidget;
class QTreeWidgetItem;
class QWidget;
class InputMappingDialog final: public QDialog {
public:
explicit InputMappingDialog(const QStringList& mapping, QWidget* parent = nullptr);
[[nodiscard]] QStringList Mapping() const;
private:
void ChangeBinding();
void ClearBinding();
void RestoreDefaults();
void SetBinding(QTreeWidgetItem* item, const QString& binding);
void UpdateButtons();
QTreeWidget* m_bindings = nullptr;
QPushButton* m_change_button = nullptr;
QPushButton* m_clear_button = nullptr;
bool m_customized = false;
};
#endif /* LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_ */
+8
View File
@@ -0,0 +1,8 @@
<RCC>
<qresource prefix="/icons">
<file alias="edit-configuration.svg">icons/edit-configuration.svg</file>
<file alias="global-settings.svg">icons/global-settings.svg</file>
<file alias="input-mapping.svg">icons/input-mapping.svg</file>
<file alias="remove-configuration.svg">icons/remove-configuration.svg</file>
</qresource>
</RCC>
+17 -3
View File
@@ -6,6 +6,7 @@
#include "configurationEditDialog.h"
#include "configurationItem.h"
#include "gameListTreeWidget.h"
#include "inputMappingDialog.h"
#include "mainDialog.h"
#include "patchesDialog.h"
#include "trophyViewerDialog.h"
@@ -169,6 +170,7 @@ static void ApplyGameListStyle(Ui::ConfigurationListWidget* ui) {
"QToolButton:disabled { background: transparent; }"));
ui->edit_button->setStyleSheet(ui->global_settings_button->styleSheet());
ui->delete_button->setStyleSheet(ui->global_settings_button->styleSheet());
ui->input_mapping_button->setStyleSheet(ui->global_settings_button->styleSheet());
}
static void AddSaveDataDir(QStringList* dirs, QSet<QString>* seen, const QString& root,
@@ -226,10 +228,12 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
m_ui->setupUi(this);
ApplyGameListStyle(m_ui);
m_ui->global_settings_button->setIcon(style()->standardIcon(QStyle::SP_FileDialogDetailedView));
m_ui->global_settings_button->setIcon(QIcon(QStringLiteral(":/icons/global-settings.svg")));
m_ui->global_settings_button->setToolTip(tr("Edit global settings and game folders"));
m_ui->edit_button->setIcon(style()->standardIcon(QStyle::SP_FileIcon));
m_ui->delete_button->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton));
m_ui->input_mapping_button->setIcon(QIcon(QStringLiteral(":/icons/input-mapping.svg")));
m_ui->input_mapping_button->setToolTip(tr("Edit global input mapping"));
m_ui->edit_button->setIcon(QIcon(QStringLiteral(":/icons/edit-configuration.svg")));
m_ui->delete_button->setIcon(QIcon(QStringLiteral(":/icons/remove-configuration.svg")));
m_ui->delete_button->setEnabled(false);
m_ui->edit_button->setEnabled(false);
@@ -249,6 +253,8 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
connect(m_ui->global_settings_button, &QToolButton::clicked, this,
&ConfigurationListWidget::edit_global_settings);
connect(m_ui->input_mapping_button, &QToolButton::clicked, this,
&ConfigurationListWidget::edit_input_mapping);
connect(m_ui->edit_button, &QToolButton::clicked, this,
&ConfigurationListWidget::edit_configuration);
connect(m_ui->delete_button, &QToolButton::clicked, this,
@@ -717,6 +723,14 @@ void ConfigurationListWidget::edit_global_settings() {
}
}
void ConfigurationListWidget::edit_input_mapping() {
InputMappingDialog dialog(m_global_info.host_input_mapping, this);
if (dialog.exec() == QDialog::Accepted) {
m_global_info.host_input_mapping = dialog.Mapping();
WriteSettings();
}
}
void ConfigurationListWidget::ClearCustomSettings(ConfigurationItem* item) {
delete m_custom_infos.take(item->GetInfo().game_path);
}
+311
View File
@@ -0,0 +1,311 @@
#include "inputMappingDialog.h"
#include <QAbstractItemView>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QHash>
#include <QHeaderView>
#include <QKeyEvent>
#include <QKeySequence>
#include <QLabel>
#include <QMouseEvent>
#include <QPushButton>
#include <QStringList>
#include <QTreeWidget>
#include <QVBoxLayout>
namespace {
constexpr int BINDING_COLUMN = 1;
struct PadButton {
const char* id;
const char* label;
const char* default_binding;
};
constexpr PadButton PAD_BUTTONS[] = {
{"Up", "D-pad Up", "Up"},
{"Down", "D-pad Down", "Down"},
{"Left", "D-pad Left", "Left"},
{"Right", "D-pad Right", "Right"},
{"Triangle", "Triangle", "I"},
{"Circle", "Circle", "L"},
{"Cross", "Cross", "J"},
{"Square", "Square", "K"},
{"L1", "L1", "Q"},
{"R1", "R1", "E"},
{"L2", "L2", ""},
{"R2", "R2", ""},
{"L3", "L3", "Left Shift"},
{"R3", "R3", "Left Ctrl"},
{"Options", "Options", "Return"},
{"TouchPad", "Touch pad", "Backspace"},
};
QString KeypadName(int key) {
if (key >= Qt::Key_0 && key <= Qt::Key_9) {
return QStringLiteral("Keypad %1").arg(key - Qt::Key_0);
}
switch (key) {
case Qt::Key_Return:
case Qt::Key_Enter: return QStringLiteral("Keypad Enter");
case Qt::Key_Slash: return QStringLiteral("Keypad /");
case Qt::Key_Asterisk: return QStringLiteral("Keypad *");
case Qt::Key_Minus: return QStringLiteral("Keypad -");
case Qt::Key_Plus: return QStringLiteral("Keypad +");
case Qt::Key_Period: return QStringLiteral("Keypad .");
case Qt::Key_Equal: return QStringLiteral("Keypad =");
case Qt::Key_Comma: return QStringLiteral("Keypad ,");
default: return {};
}
}
QString KeyName(const QKeyEvent& event) {
const int key = event.key();
if (event.modifiers().testFlag(Qt::KeypadModifier)) {
return KeypadName(key);
}
if (key >= Qt::Key_A && key <= Qt::Key_Z) {
return QChar(key);
}
if (key >= Qt::Key_0 && key <= Qt::Key_9) {
return QChar(key);
}
if (key >= Qt::Key_F1 && key <= Qt::Key_F24) {
return QStringLiteral("F%1").arg(key - Qt::Key_F1 + 1);
}
switch (key) {
case Qt::Key_Return:
case Qt::Key_Enter: return QStringLiteral("Return");
case Qt::Key_Backspace: return QStringLiteral("Backspace");
case Qt::Key_Tab: return QStringLiteral("Tab");
case Qt::Key_Shift: return QStringLiteral("Left Shift");
case Qt::Key_Control: return QStringLiteral("Left Ctrl");
case Qt::Key_Alt: return QStringLiteral("Left Alt");
case Qt::Key_Meta: return QStringLiteral("Left GUI");
case Qt::Key_Insert: return QStringLiteral("Insert");
case Qt::Key_Delete: return QStringLiteral("Delete");
case Qt::Key_Home: return QStringLiteral("Home");
case Qt::Key_End: return QStringLiteral("End");
case Qt::Key_PageUp: return QStringLiteral("PageUp");
case Qt::Key_PageDown: return QStringLiteral("PageDown");
case Qt::Key_Left: return QStringLiteral("Left");
case Qt::Key_Right: return QStringLiteral("Right");
case Qt::Key_Up: return QStringLiteral("Up");
case Qt::Key_Down: return QStringLiteral("Down");
case Qt::Key_CapsLock: return QStringLiteral("CapsLock");
case Qt::Key_NumLock: return QStringLiteral("Numlock");
case Qt::Key_ScrollLock: return QStringLiteral("ScrollLock");
case Qt::Key_Pause: return QStringLiteral("Pause");
case Qt::Key_Print: return QStringLiteral("PrintScreen");
default: break;
}
if (event.modifiers() != Qt::NoModifier) {
return {};
}
const auto name = QKeySequence(key).toString(QKeySequence::PortableText);
return name.size() == 1 ? name : QString();
}
class InputCaptureDialog final: public QDialog {
public:
explicit InputCaptureDialog(QWidget* parent): QDialog(parent) {
setWindowTitle(tr("Set Binding"));
setModal(true);
setMinimumWidth(360);
auto* layout = new QVBoxLayout(this);
m_label = new QLabel(
tr("Press a key or mouse button.\nSpace and F1 are reserved; Esc cancels."), this);
m_label->setAlignment(Qt::AlignCenter);
layout->addWidget(m_label);
}
[[nodiscard]] const QString& Binding() const { return m_binding; }
protected:
void keyPressEvent(QKeyEvent* event) override {
if (event->isAutoRepeat()) {
return;
}
if (event->key() == Qt::Key_Escape) {
reject();
return;
}
if (event->key() == Qt::Key_Space || event->key() == Qt::Key_F1) {
m_label->setText(tr("That key is reserved by the emulator."));
return;
}
m_binding = KeyName(*event);
if (!m_binding.isEmpty()) {
accept();
} else {
m_label->setText(tr("That key is not supported."));
}
}
void mousePressEvent(QMouseEvent* event) override {
switch (event->button()) {
case Qt::LeftButton: m_binding = QStringLiteral("Mouse:Left"); break;
case Qt::RightButton: m_binding = QStringLiteral("Mouse:Right"); break;
case Qt::MiddleButton: m_binding = QStringLiteral("Mouse:Middle"); break;
case Qt::BackButton: m_binding = QStringLiteral("Mouse:X1"); break;
case Qt::ForwardButton: m_binding = QStringLiteral("Mouse:X2"); break;
default: return;
}
accept();
}
private:
QLabel* m_label = nullptr;
QString m_binding;
};
QHash<QString, QString> ParseMapping(const QStringList& mapping) {
QHash<QString, QString> result;
for (const auto& entry: mapping) {
const auto separator = entry.indexOf(QLatin1Char('='));
if (separator > 0 && separator + 1 < entry.size()) {
const auto binding = entry.mid(separator + 1);
for (auto item = result.begin(); item != result.end();) {
if (item.value().compare(binding, Qt::CaseInsensitive) == 0) {
item = result.erase(item);
} else {
++item;
}
}
result.insert(entry.left(separator), binding);
}
}
return result;
}
} // namespace
InputMappingDialog::InputMappingDialog(const QStringList& mapping, QWidget* parent)
: QDialog(parent), m_customized(!mapping.isEmpty()) {
setWindowTitle(tr("Input Mapping"));
resize(440, 520);
auto* layout = new QVBoxLayout(this);
layout->addWidget(new QLabel(tr("Map keyboard or mouse input to DualSense buttons.\n"
"Defaults: left stick W/A/S/D, right stick T/F/G/H."),
this));
m_bindings = new QTreeWidget(this);
m_bindings->setColumnCount(2);
m_bindings->setHeaderLabels({tr("DualSense button"), tr("Host input")});
m_bindings->setRootIsDecorated(false);
m_bindings->setSelectionMode(QAbstractItemView::SingleSelection);
m_bindings->header()->setSectionResizeMode(0, QHeaderView::Stretch);
m_bindings->header()->setSectionResizeMode(1, QHeaderView::Stretch);
layout->addWidget(m_bindings);
const auto parsed = ParseMapping(mapping);
for (const auto& button: PAD_BUTTONS) {
auto* item = new QTreeWidgetItem(m_bindings);
item->setText(0, tr(button.label));
item->setData(0, Qt::UserRole, QString::fromLatin1(button.id));
SetBinding(item, m_customized ? parsed.value(QString::fromLatin1(button.id))
: QString::fromLatin1(button.default_binding));
}
m_bindings->setCurrentItem(m_bindings->topLevelItem(0));
auto* controls = new QHBoxLayout;
m_change_button = new QPushButton(tr("Change..."), this);
m_clear_button = new QPushButton(tr("Clear"), this);
auto* defaults = new QPushButton(tr("Defaults"), this);
controls->addWidget(m_change_button);
controls->addWidget(m_clear_button);
controls->addWidget(defaults);
controls->addStretch();
layout->addLayout(controls);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
layout->addWidget(buttons);
connect(m_bindings, &QTreeWidget::itemDoubleClicked, this,
[this](QTreeWidgetItem*, int) { ChangeBinding(); });
connect(m_bindings, &QTreeWidget::itemSelectionChanged, this, [this]() { UpdateButtons(); });
connect(m_change_button, &QPushButton::clicked, this, [this]() { ChangeBinding(); });
connect(m_clear_button, &QPushButton::clicked, this, [this]() { ClearBinding(); });
connect(defaults, &QPushButton::clicked, this, [this]() { RestoreDefaults(); });
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
UpdateButtons();
}
QStringList InputMappingDialog::Mapping() const {
QStringList result;
if (!m_customized) {
return result;
}
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
const auto* item = m_bindings->topLevelItem(index);
const auto binding = item->data(BINDING_COLUMN, Qt::UserRole).toString();
if (!binding.isEmpty()) {
result.append(item->data(0, Qt::UserRole).toString() + QLatin1Char('=') + binding);
}
}
return result;
}
void InputMappingDialog::ChangeBinding() {
auto* item = m_bindings->currentItem();
if (item == nullptr) {
return;
}
InputCaptureDialog dialog(this);
if (dialog.exec() != QDialog::Accepted) {
return;
}
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
auto* other = m_bindings->topLevelItem(index);
if (other != item && other->data(BINDING_COLUMN, Qt::UserRole)
.toString()
.compare(dialog.Binding(), Qt::CaseInsensitive) == 0) {
SetBinding(other, {});
}
}
SetBinding(item, dialog.Binding());
m_customized = true;
}
void InputMappingDialog::ClearBinding() {
SetBinding(m_bindings->currentItem(), {});
m_customized = true;
}
void InputMappingDialog::RestoreDefaults() {
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
SetBinding(m_bindings->topLevelItem(index),
QString::fromLatin1(PAD_BUTTONS[index].default_binding));
}
m_customized = false;
}
void InputMappingDialog::SetBinding(QTreeWidgetItem* item, const QString& binding) {
if (item == nullptr) {
return;
}
item->setData(BINDING_COLUMN, Qt::UserRole, binding);
item->setText(BINDING_COLUMN, binding.isEmpty() ? tr("None") : binding);
UpdateButtons();
}
void InputMappingDialog::UpdateButtons() {
if (m_change_button == nullptr) {
return;
}
const auto* item = m_bindings->currentItem();
m_change_button->setEnabled(item != nullptr);
m_clear_button->setEnabled(item != nullptr &&
!item->data(BINDING_COLUMN, Qt::UserRole).toString().isEmpty());
}
+7 -1
View File
@@ -218,6 +218,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--printf-output-file" << info.printf_output_file;
args << "--profiler-direction" << EnumToText(info.profiler_direction);
args << "--spirv-debug-printf" << "false";
for (const auto& binding: info.host_input_mapping) {
args << "--keymap" << binding;
}
if (info.renderdoc_enabled) {
args << "--rd";
}
@@ -428,7 +431,10 @@ void MainDialogPrivate::Run() {
m_running_item->SetRunning(true);
m_main_dialog->RunInterpreter(&m_process, m_running_item->GetInfo());
Configuration info;
info.CopyFrom(m_running_item->GetInfo());
info.host_input_mapping = m_ui->widget->GetHostInputMapping();
m_main_dialog->RunInterpreter(&m_process, info);
Update();
}
+2 -4
View File
@@ -85,8 +85,6 @@ private:
static GameController* g_controller = nullptr;
static constexpr int KEYBOARD_CONTROLLER_ID = -1000;
static uint8_t pad_connected_count_to_u8(int connected_count) {
return static_cast<uint8_t>(connected_count > 255 ? 255 : connected_count);
}
@@ -117,7 +115,7 @@ KYTY_SUBSYSTEM_INIT(Controller) {
EXIT_IF(g_controller != nullptr);
g_controller = new GameController;
g_controller->Connect(KEYBOARD_CONTROLLER_ID);
g_controller->Connect(HOST_INPUT_CONTROLLER_ID);
}
KYTY_SUBSYSTEM_UNEXPECTED_SHUTDOWN(Controller) {}
@@ -155,7 +153,7 @@ void GameController::CheckActive() {
if (!m_connected_ids.empty()) {
new_active_id = m_connected_ids[0];
for (const auto id: m_connected_ids) {
if (id != KEYBOARD_CONTROLLER_ID) {
if (id != HOST_INPUT_CONTROLLER_ID) {
new_active_id = id;
break;
}
+2
View File
@@ -9,6 +9,8 @@ namespace Libs::Controller {
KYTY_SUBSYSTEM_DEFINE(Controller);
constexpr int HOST_INPUT_CONTROLLER_ID = -1000;
constexpr uint32_t PAD_BUTTON_L3 = 0x00000002;
constexpr uint32_t PAD_BUTTON_R3 = 0x00000004;
constexpr uint32_t PAD_BUTTON_OPTIONS = 0x00000008;
+8
View File
@@ -64,6 +64,7 @@ static void PrintUsage() {
::printf(" --spirv-debug-printf <true|false> Enable SPIR-V debug printf.\n");
::printf(
" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
::printf(" --keymap <Pad=Key> DualSense mapping; may be repeated.\n");
::printf(" --rd Enable RenderDoc capture.\n");
}
@@ -247,6 +248,13 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--keymap") {
const auto split = value.find('=');
if (split == std::string::npos || split == 0 || split + 1 == value.size()) {
::printf("invalid keymap: %s\n", value.c_str());
return false;
}
options.config.keymap.push_back(value);
} else {
::printf("unknown option: %s\n", arg.c_str());
return false;