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:
Felipe
2026-10-15 23:04:37 -03:00
committed by GitHub
parent 6a0a666a41
commit 3ff6606fcb
18 changed files with 1909 additions and 210 deletions
+1
View File
@@ -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)
+2
View File
@@ -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;
+21 -6
View File
@@ -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,20 +25,30 @@ public:
[[nodiscard]] QStringList Mapping() const;
private:
void ChangeBinding(const QString& id);
void RestoreDefaults();
void UpdateButtons();
void UpdatePreview();
void UpdateHighlights();
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();
void UpdateHighlights();
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_ */
+45 -2
View File
@@ -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;
+452 -146
View File
@@ -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::RestoreDefaults() {
m_bindings.clear();
for (const auto& control: PAD_CONTROLS) {
m_bindings.insert(QString::fromLatin1(control.id),
QString::fromLatin1(control.default_binding));
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.keyboard_default));
}
}
m_sensitivity->setValue(DEFAULT_MOUSE_SENSITIVITY);
m_custom_bindings = false;
}
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);
}
}