Files
emulator/src/citron/configuration/configure_input_player.cpp
Zephyron 16f815a7b9 Improve mouse wheel scrolling in configuration dialogs
Enable mouse wheel scrolling throughout scroll areas instead of
requiring hover over scrollbars. Add event filter to forward wheel
events to scrollbars. Fix Input tab scrolling by only consuming
wheel events in ConfigureInputPlayer when actively mapping inputs.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-11-12 16:41:57 +10:00

1750 lines
71 KiB
C++

// SPDX-FileCopyrightText: 2016 Citra Emulator Project
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <memory>
#include <utility>
#include <QApplication>
#include <QGridLayout>
#include <QInputDialog>
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QScrollArea>
#include <QScrollBar>
#include <QTimer>
#include <QWheelEvent>
#include "common/assert.h"
#include "common/param_package.h"
#include "configuration/qt_config.h"
#include "frontend_common/config.h"
#include "hid_core/frontend/emulated_controller.h"
#include "hid_core/hid_core.h"
#include "hid_core/hid_types.h"
#include "input_common/drivers/keyboard.h"
#include "input_common/drivers/mouse.h"
#include "input_common/main.h"
#include "ui_configure_input_player.h"
#include "citron/bootmanager.h"
#include "citron/configuration/configure_input_player.h"
#include "citron/configuration/configure_input_player_widget.h"
#include "citron/configuration/configure_mouse_panning.h"
#include "citron/configuration/input_profiles.h"
#include "citron/util/limitable_input_dialog.h"
#include "common/settings_input.h"
const std::array<std::string, ConfigureInputPlayer::ANALOG_SUB_BUTTONS_NUM>
ConfigureInputPlayer::analog_sub_buttons{{
"up",
"down",
"left",
"right",
}};
namespace {
QString GetKeyName(int key_code) {
switch (key_code) {
case Qt::Key_Shift:
return QObject::tr("Shift");
case Qt::Key_Control:
return QObject::tr("Ctrl");
case Qt::Key_Alt:
return QObject::tr("Alt");
case Qt::Key_Meta:
return {};
default:
return QKeySequence(key_code).toString();
}
}
QString GetButtonName(Common::Input::ButtonNames button_name) {
switch (button_name) {
case Common::Input::ButtonNames::ButtonLeft:
return QObject::tr("Left");
case Common::Input::ButtonNames::ButtonRight:
return QObject::tr("Right");
case Common::Input::ButtonNames::ButtonDown:
return QObject::tr("Down");
case Common::Input::ButtonNames::ButtonUp:
return QObject::tr("Up");
case Common::Input::ButtonNames::TriggerZ:
return QObject::tr("Z");
case Common::Input::ButtonNames::TriggerR:
return QObject::tr("R");
case Common::Input::ButtonNames::TriggerL:
return QObject::tr("L");
case Common::Input::ButtonNames::TriggerZR:
return QObject::tr("ZR");
case Common::Input::ButtonNames::TriggerZL:
return QObject::tr("ZL");
case Common::Input::ButtonNames::TriggerSR:
return QObject::tr("SR");
case Common::Input::ButtonNames::TriggerSL:
return QObject::tr("SL");
case Common::Input::ButtonNames::ButtonStickL:
return QObject::tr("Stick L");
case Common::Input::ButtonNames::ButtonStickR:
return QObject::tr("Stick R");
case Common::Input::ButtonNames::ButtonA:
return QObject::tr("A");
case Common::Input::ButtonNames::ButtonB:
return QObject::tr("B");
case Common::Input::ButtonNames::ButtonX:
return QObject::tr("X");
case Common::Input::ButtonNames::ButtonY:
return QObject::tr("Y");
case Common::Input::ButtonNames::ButtonStart:
return QObject::tr("Start");
case Common::Input::ButtonNames::ButtonPlus:
return QObject::tr("Plus");
case Common::Input::ButtonNames::ButtonMinus:
return QObject::tr("Minus");
case Common::Input::ButtonNames::ButtonHome:
return QObject::tr("Home");
case Common::Input::ButtonNames::ButtonCapture:
return QObject::tr("Capture");
case Common::Input::ButtonNames::L1:
return QObject::tr("L1");
case Common::Input::ButtonNames::L2:
return QObject::tr("L2");
case Common::Input::ButtonNames::L3:
return QObject::tr("L3");
case Common::Input::ButtonNames::R1:
return QObject::tr("R1");
case Common::Input::ButtonNames::R2:
return QObject::tr("R2");
case Common::Input::ButtonNames::R3:
return QObject::tr("R3");
case Common::Input::ButtonNames::Circle:
return QObject::tr("Circle");
case Common::Input::ButtonNames::Cross:
return QObject::tr("Cross");
case Common::Input::ButtonNames::Square:
return QObject::tr("Square");
case Common::Input::ButtonNames::Triangle:
return QObject::tr("Triangle");
case Common::Input::ButtonNames::Share:
return QObject::tr("Share");
case Common::Input::ButtonNames::Options:
return QObject::tr("Options");
case Common::Input::ButtonNames::Home:
return QObject::tr("Home");
case Common::Input::ButtonNames::Touch:
return QObject::tr("Touch");
case Common::Input::ButtonNames::ButtonMouseWheel:
return QObject::tr("Wheel", "Indicates the mouse wheel");
case Common::Input::ButtonNames::ButtonBackward:
return QObject::tr("Backward");
case Common::Input::ButtonNames::ButtonForward:
return QObject::tr("Forward");
case Common::Input::ButtonNames::ButtonTask:
return QObject::tr("Task");
case Common::Input::ButtonNames::ButtonExtra:
return QObject::tr("Extra");
default:
return QObject::tr("[undefined]");
}
}
QString GetDirectionName(const std::string& direction) {
if (direction == "left") {
return QObject::tr("Left");
}
if (direction == "right") {
return QObject::tr("Right");
}
if (direction == "up") {
return QObject::tr("Up");
}
if (direction == "down") {
return QObject::tr("Down");
}
UNIMPLEMENTED_MSG("Unimplemented direction name={}", direction);
return QString::fromStdString(direction);
}
void SetAnalogParam(const Common::ParamPackage& input_param, Common::ParamPackage& analog_param,
const std::string& button_name) {
// The poller returned a complete axis, so set all the buttons
if (input_param.Has("axis_x") && input_param.Has("axis_y")) {
analog_param = input_param;
return;
}
// Check if the current configuration has either no engine or an axis binding.
// Clears out the old binding and adds one with analog_from_button.
if (!analog_param.Has("engine") || analog_param.Has("axis_x") || analog_param.Has("axis_y")) {
analog_param = {
{"engine", "analog_from_button"},
};
}
analog_param.Set(button_name, input_param.Serialize());
}
} // namespace
QString ConfigureInputPlayer::ButtonToText(const Common::ParamPackage& param) {
if (!param.Has("engine")) {
return QObject::tr("[not set]");
}
const QString toggle = QString::fromStdString(param.Get("toggle", false) ? "~" : "");
const QString inverted = QString::fromStdString(param.Get("inverted", false) ? "!" : "");
const QString invert = QString::fromStdString(param.Get("invert", "+") == "-" ? "-" : "");
const QString turbo = QString::fromStdString(param.Get("turbo", false) ? "$" : "");
const auto common_button_name = input_subsystem->GetButtonName(param);
// Retrieve the names from Qt
if (param.Get("engine", "") == "keyboard") {
const QString button_str = GetKeyName(param.Get("code", 0));
return QObject::tr("%1%2%3%4").arg(turbo, toggle, inverted, button_str);
}
if (common_button_name == Common::Input::ButtonNames::Invalid) {
return QObject::tr("[invalid]");
}
if (common_button_name == Common::Input::ButtonNames::Engine) {
return QString::fromStdString(param.Get("engine", ""));
}
if (common_button_name == Common::Input::ButtonNames::Value) {
if (param.Has("hat")) {
const QString hat = GetDirectionName(param.Get("direction", ""));
return QObject::tr("%1%2%3Hat %4").arg(turbo, toggle, inverted, hat);
}
if (param.Has("axis")) {
const QString axis = QString::fromStdString(param.Get("axis", ""));
return QObject::tr("%1%2%3Axis %4").arg(toggle, inverted, invert, axis);
}
if (param.Has("axis_x") && param.Has("axis_y") && param.Has("axis_z")) {
const QString axis_x = QString::fromStdString(param.Get("axis_x", ""));
const QString axis_y = QString::fromStdString(param.Get("axis_y", ""));
const QString axis_z = QString::fromStdString(param.Get("axis_z", ""));
return QObject::tr("%1%2Axis %3,%4,%5").arg(toggle, inverted, axis_x, axis_y, axis_z);
}
if (param.Has("motion")) {
const QString motion = QString::fromStdString(param.Get("motion", ""));
return QObject::tr("%1%2Motion %3").arg(toggle, inverted, motion);
}
if (param.Has("button")) {
const QString button = QString::fromStdString(param.Get("button", ""));
return QObject::tr("%1%2%3Button %4").arg(turbo, toggle, inverted, button);
}
}
QString button_name = GetButtonName(common_button_name);
if (param.Has("hat")) {
return QObject::tr("%1%2%3Hat %4").arg(turbo, toggle, inverted, button_name);
}
if (param.Has("axis")) {
return QObject::tr("%1%2%3Axis %4").arg(toggle, inverted, invert, button_name);
}
if (param.Has("motion")) {
return QObject::tr("%1%2Axis %3").arg(toggle, inverted, button_name);
}
if (param.Has("button")) {
return QObject::tr("%1%2%3Button %4").arg(turbo, toggle, inverted, button_name);
}
return QObject::tr("[unknown]");
}
QString ConfigureInputPlayer::AnalogToText(const Common::ParamPackage& param,
const std::string& dir) {
if (!param.Has("engine")) {
return QObject::tr("[not set]");
}
if (param.Get("engine", "") == "analog_from_button") {
return ButtonToText(Common::ParamPackage{param.Get(dir, "")});
}
if (!param.Has("axis_x") || !param.Has("axis_y")) {
return QObject::tr("[unknown]");
}
const auto engine_str = param.Get("engine", "");
const QString axis_x_str = QString::fromStdString(param.Get("axis_x", ""));
const QString axis_y_str = QString::fromStdString(param.Get("axis_y", ""));
const bool invert_x = param.Get("invert_x", "+") == "-";
const bool invert_y = param.Get("invert_y", "+") == "-";
if (dir == "modifier") {
return QObject::tr("[unused]");
}
if (dir == "left") {
const QString invert_x_str = QString::fromStdString(invert_x ? "+" : "-");
return QObject::tr("Axis %1%2").arg(axis_x_str, invert_x_str);
}
if (dir == "right") {
const QString invert_x_str = QString::fromStdString(invert_x ? "-" : "+");
return QObject::tr("Axis %1%2").arg(axis_x_str, invert_x_str);
}
if (dir == "up") {
const QString invert_y_str = QString::fromStdString(invert_y ? "-" : "+");
return QObject::tr("Axis %1%2").arg(axis_y_str, invert_y_str);
}
if (dir == "down") {
const QString invert_y_str = QString::fromStdString(invert_y ? "+" : "-");
return QObject::tr("Axis %1%2").arg(axis_y_str, invert_y_str);
}
return QObject::tr("[unknown]");
}
ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_index_,
QWidget* bottom_row_,
InputCommon::InputSubsystem* input_subsystem_,
InputProfiles* profiles_, Core::HID::HIDCore& hid_core_,
bool is_powered_on_, bool debug_)
: QWidget(parent),
ui(std::make_unique<Ui::ConfigureInputPlayer>()), player_index{player_index_}, debug{debug_},
is_powered_on{is_powered_on_}, input_subsystem{input_subsystem_}, profiles(profiles_),
timeout_timer(std::make_unique<QTimer>()),
poll_timer(std::make_unique<QTimer>()), bottom_row{bottom_row_}, hid_core{hid_core_} {
if (player_index == 0) {
auto* emulated_controller_p1 =
hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
auto* emulated_controller_handheld =
hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
emulated_controller_p1->SaveCurrentConfig();
emulated_controller_p1->EnableConfiguration();
emulated_controller_handheld->SaveCurrentConfig();
emulated_controller_handheld->EnableConfiguration();
if (emulated_controller_handheld->IsConnected(true)) {
emulated_controller_p1->Disconnect();
emulated_controller = emulated_controller_handheld;
} else {
emulated_controller = emulated_controller_p1;
}
} else {
emulated_controller = hid_core.GetEmulatedControllerByIndex(player_index);
emulated_controller->SaveCurrentConfig();
emulated_controller->EnableConfiguration();
}
ui->setupUi(this);
setFocusPolicy(Qt::ClickFocus);
button_map = {
ui->buttonA, ui->buttonB, ui->buttonX, ui->buttonY,
ui->buttonLStick, ui->buttonRStick, ui->buttonL, ui->buttonR,
ui->buttonZL, ui->buttonZR, ui->buttonPlus, ui->buttonMinus,
ui->buttonDpadLeft, ui->buttonDpadUp, ui->buttonDpadRight, ui->buttonDpadDown,
ui->buttonSLLeft, ui->buttonSRLeft, ui->buttonHome, ui->buttonScreenshot,
ui->buttonSLRight, ui->buttonSRRight,
};
analog_map_buttons = {{
{
ui->buttonLStickUp,
ui->buttonLStickDown,
ui->buttonLStickLeft,
ui->buttonLStickRight,
},
{
ui->buttonRStickUp,
ui->buttonRStickDown,
ui->buttonRStickLeft,
ui->buttonRStickRight,
},
}};
motion_map = {
ui->buttonMotionLeft,
ui->buttonMotionRight,
};
analog_map_deadzone_label = {ui->labelLStickDeadzone, ui->labelRStickDeadzone};
analog_map_deadzone_slider = {ui->sliderLStickDeadzone, ui->sliderRStickDeadzone};
analog_map_modifier_groupbox = {ui->buttonLStickModGroup, ui->buttonRStickModGroup};
analog_map_modifier_button = {ui->buttonLStickMod, ui->buttonRStickMod};
analog_map_modifier_label = {ui->labelLStickModifierRange, ui->labelRStickModifierRange};
analog_map_modifier_slider = {ui->sliderLStickModifierRange, ui->sliderRStickModifierRange};
analog_map_range_groupbox = {ui->buttonLStickRangeGroup, ui->buttonRStickRangeGroup};
analog_map_range_spinbox = {ui->spinboxLStickRange, ui->spinboxRStickRange};
ui->controllerFrame->SetController(emulated_controller);
ui->controllerFrame->SetRawJoystickVisible(true);
connect(ui->buttonChooseColor, &QPushButton::clicked, this,
&ConfigureInputPlayer::OnButtonChooseColor);
connect(ui->buttonToggleGyro, &QPushButton::clicked, this,
&ConfigureInputPlayer::OnButtonToggleGyro);
for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) {
auto* const button = button_map[button_id];
if (button == nullptr) {
continue;
}
connect(button, &QPushButton::clicked, [=, this] {
HandleClick(
button, button_id,
[=, this](const Common::ParamPackage& params) {
emulated_controller->SetButtonParam(button_id, params);
},
InputCommon::Polling::InputType::Button);
});
button->setContextMenuPolicy(Qt::CustomContextMenu);
connect(button, &QPushButton::customContextMenuRequested,
[=, this](const QPoint& menu_location) {
QMenu context_menu;
Common::ParamPackage param = emulated_controller->GetButtonParam(button_id);
context_menu.addAction(tr("Clear"), [&] {
emulated_controller->SetButtonParam(button_id, {});
button_map[button_id]->setText(tr("[not set]"));
});
if (param.Has("code") || param.Has("button") || param.Has("hat")) {
context_menu.addAction(tr("Invert button"), [&] {
const bool invert_value = !param.Get("inverted", false);
param.Set("inverted", invert_value);
button_map[button_id]->setText(ButtonToText(param));
emulated_controller->SetButtonParam(button_id, param);
});
context_menu.addAction(tr("Toggle button"), [&] {
const bool toggle_value = !param.Get("toggle", false);
param.Set("toggle", toggle_value);
button_map[button_id]->setText(ButtonToText(param));
emulated_controller->SetButtonParam(button_id, param);
});
context_menu.addAction(tr("Turbo button"), [&] {
const bool turbo_value = !param.Get("turbo", false);
param.Set("turbo", turbo_value);
button_map[button_id]->setText(ButtonToText(param));
emulated_controller->SetButtonParam(button_id, param);
});
}
if (param.Has("axis")) {
context_menu.addAction(tr("Invert axis"), [&] {
const bool toggle_value = !(param.Get("invert", "+") == "-");
param.Set("invert", toggle_value ? "-" : "+");
button_map[button_id]->setText(ButtonToText(param));
emulated_controller->SetButtonParam(button_id, param);
});
context_menu.addAction(tr("Invert button"), [&] {
const bool invert_value = !param.Get("inverted", false);
param.Set("inverted", invert_value);
button_map[button_id]->setText(ButtonToText(param));
emulated_controller->SetButtonParam(button_id, param);
});
context_menu.addAction(tr("Set threshold"), [&] {
const int button_threshold =
static_cast<int>(param.Get("threshold", 0.5f) * 100.0f);
const int new_threshold = QInputDialog::getInt(
this, tr("Set threshold"), tr("Choose a value between 0% and 100%"),
button_threshold, 0, 100);
param.Set("threshold", new_threshold / 100.0f);
if (button_id == Settings::NativeButton::ZL) {
ui->sliderZLThreshold->setValue(new_threshold);
}
if (button_id == Settings::NativeButton::ZR) {
ui->sliderZRThreshold->setValue(new_threshold);
}
emulated_controller->SetButtonParam(button_id, param);
});
context_menu.addAction(tr("Toggle axis"), [&] {
const bool toggle_value = !param.Get("toggle", false);
param.Set("toggle", toggle_value);
button_map[button_id]->setText(ButtonToText(param));
emulated_controller->SetButtonParam(button_id, param);
});
}
context_menu.exec(button_map[button_id]->mapToGlobal(menu_location));
});
}
for (int motion_id = 0; motion_id < Settings::NativeMotion::NumMotions; ++motion_id) {
auto* const button = motion_map[motion_id];
if (button == nullptr) {
continue;
}
connect(button, &QPushButton::clicked, [=, this] {
HandleClick(
button, motion_id,
[=, this](const Common::ParamPackage& params) {
emulated_controller->SetMotionParam(motion_id, params);
},
InputCommon::Polling::InputType::Motion);
});
button->setContextMenuPolicy(Qt::CustomContextMenu);
connect(button, &QPushButton::customContextMenuRequested,
[=, this](const QPoint& menu_location) {
QMenu context_menu;
Common::ParamPackage param = emulated_controller->GetMotionParam(motion_id);
context_menu.addAction(tr("Clear"), [&] {
emulated_controller->SetMotionParam(motion_id, {});
motion_map[motion_id]->setText(tr("[not set]"));
});
if (param.Has("motion")) {
context_menu.addAction(tr("Set gyro threshold"), [&] {
const int gyro_threshold =
static_cast<int>(param.Get("threshold", 0.007f) * 1000.0f);
const int new_threshold = QInputDialog::getInt(
this, tr("Set threshold"), tr("Choose a value between 0% and 100%"),
gyro_threshold, 0, 100);
param.Set("threshold", new_threshold / 1000.0f);
emulated_controller->SetMotionParam(motion_id, param);
});
context_menu.addAction(tr("Calibrate sensor"), [&] {
emulated_controller->StartMotionCalibration();
});
}
context_menu.exec(motion_map[motion_id]->mapToGlobal(menu_location));
});
}
connect(ui->sliderZLThreshold, &QSlider::valueChanged, [=, this] {
Common::ParamPackage param =
emulated_controller->GetButtonParam(Settings::NativeButton::ZL);
if (param.Has("threshold")) {
const auto slider_value = ui->sliderZLThreshold->value();
param.Set("threshold", slider_value / 100.0f);
emulated_controller->SetButtonParam(Settings::NativeButton::ZL, param);
}
});
connect(ui->sliderZRThreshold, &QSlider::valueChanged, [=, this] {
Common::ParamPackage param =
emulated_controller->GetButtonParam(Settings::NativeButton::ZR);
if (param.Has("threshold")) {
const auto slider_value = ui->sliderZRThreshold->value();
param.Set("threshold", slider_value / 100.0f);
emulated_controller->SetButtonParam(Settings::NativeButton::ZR, param);
}
});
for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id) {
auto* const analog_button = analog_map_buttons[analog_id][sub_button_id];
if (analog_button == nullptr) {
continue;
}
connect(analog_button, &QPushButton::clicked, [=, this] {
if (!map_analog_stick_accepted) {
map_analog_stick_accepted =
QMessageBox::information(
this, tr("Map Analog Stick"),
tr("After pressing OK, first move your joystick horizontally, and then "
"vertically.\nTo invert the axes, first move your joystick "
"vertically, and then horizontally."),
QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok;
if (!map_analog_stick_accepted) {
return;
}
}
HandleClick(
analog_map_buttons[analog_id][sub_button_id], analog_id,
[=, this](const Common::ParamPackage& params) {
Common::ParamPackage param = emulated_controller->GetStickParam(analog_id);
SetAnalogParam(params, param, analog_sub_buttons[sub_button_id]);
// Correct axis direction for inverted sticks
if (input_subsystem->IsStickInverted(param)) {
switch (analog_id) {
case Settings::NativeAnalog::LStick: {
const bool invert_value = param.Get("invert_x", "+") == "-";
const std::string invert_str = invert_value ? "+" : "-";
param.Set("invert_x", invert_str);
break;
}
case Settings::NativeAnalog::RStick: {
const bool invert_value = param.Get("invert_y", "+") == "-";
const std::string invert_str = invert_value ? "+" : "-";
param.Set("invert_y", invert_str);
break;
}
default:
break;
}
}
emulated_controller->SetStickParam(analog_id, param);
},
InputCommon::Polling::InputType::Stick);
});
analog_button->setContextMenuPolicy(Qt::CustomContextMenu);
connect(analog_button, &QPushButton::customContextMenuRequested,
[=, this](const QPoint& menu_location) {
QMenu context_menu;
Common::ParamPackage param = emulated_controller->GetStickParam(analog_id);
context_menu.addAction(tr("Clear"), [&] {
if (param.Get("engine", "") != "analog_from_button") {
emulated_controller->SetStickParam(analog_id, {});
for (auto button : analog_map_buttons[analog_id]) {
button->setText(tr("[not set]"));
}
return;
}
switch (sub_button_id) {
case 0:
param.Erase("up");
break;
case 1:
param.Erase("down");
break;
case 2:
param.Erase("left");
break;
case 3:
param.Erase("right");
break;
}
emulated_controller->SetStickParam(analog_id, param);
analog_map_buttons[analog_id][sub_button_id]->setText(tr("[not set]"));
});
context_menu.addAction(tr("Center axis"), [&] {
const auto stick_value =
emulated_controller->GetSticksValues()[analog_id];
const float offset_x = stick_value.x.properties.offset;
const float offset_y = stick_value.y.properties.offset;
float raw_value_x = stick_value.x.raw_value;
float raw_value_y = stick_value.y.raw_value;
// See Core::HID::SanitizeStick() to obtain the original raw axis value
if (std::abs(offset_x) < 0.5f) {
if (raw_value_x > 0) {
raw_value_x *= 1 + offset_x;
} else {
raw_value_x *= 1 - offset_x;
}
}
if (std::abs(offset_x) < 0.5f) {
if (raw_value_y > 0) {
raw_value_y *= 1 + offset_y;
} else {
raw_value_y *= 1 - offset_y;
}
}
param.Set("offset_x", -raw_value_x + offset_x);
param.Set("offset_y", -raw_value_y + offset_y);
emulated_controller->SetStickParam(analog_id, param);
});
context_menu.addAction(tr("Invert axis"), [&] {
if (sub_button_id == 2 || sub_button_id == 3) {
const bool invert_value = param.Get("invert_x", "+") == "-";
const std::string invert_str = invert_value ? "+" : "-";
param.Set("invert_x", invert_str);
emulated_controller->SetStickParam(analog_id, param);
}
if (sub_button_id == 0 || sub_button_id == 1) {
const bool invert_value = param.Get("invert_y", "+") == "-";
const std::string invert_str = invert_value ? "+" : "-";
param.Set("invert_y", invert_str);
emulated_controller->SetStickParam(analog_id, param);
}
for (int analog_sub_button_id = 0;
analog_sub_button_id < ANALOG_SUB_BUTTONS_NUM;
++analog_sub_button_id) {
analog_map_buttons[analog_id][analog_sub_button_id]->setText(
AnalogToText(param, analog_sub_buttons[analog_sub_button_id]));
}
});
context_menu.exec(analog_map_buttons[analog_id][sub_button_id]->mapToGlobal(
menu_location));
});
}
// Handle clicks for the modifier buttons as well.
connect(analog_map_modifier_button[analog_id], &QPushButton::clicked, [=, this] {
HandleClick(
analog_map_modifier_button[analog_id], analog_id,
[=, this](const Common::ParamPackage& params) {
Common::ParamPackage param = emulated_controller->GetStickParam(analog_id);
param.Set("modifier", params.Serialize());
emulated_controller->SetStickParam(analog_id, param);
},
InputCommon::Polling::InputType::Button);
});
analog_map_modifier_button[analog_id]->setContextMenuPolicy(Qt::CustomContextMenu);
connect(
analog_map_modifier_button[analog_id], &QPushButton::customContextMenuRequested,
[=, this](const QPoint& menu_location) {
QMenu context_menu;
Common::ParamPackage param = emulated_controller->GetStickParam(analog_id);
context_menu.addAction(tr("Clear"), [&] {
param.Set("modifier", "");
analog_map_modifier_button[analog_id]->setText(tr("[not set]"));
emulated_controller->SetStickParam(analog_id, param);
});
context_menu.addAction(tr("Toggle button"), [&] {
Common::ParamPackage modifier_param =
Common::ParamPackage{param.Get("modifier", "")};
const bool toggle_value = !modifier_param.Get("toggle", false);
modifier_param.Set("toggle", toggle_value);
param.Set("modifier", modifier_param.Serialize());
analog_map_modifier_button[analog_id]->setText(ButtonToText(modifier_param));
emulated_controller->SetStickParam(analog_id, param);
});
context_menu.addAction(tr("Invert button"), [&] {
Common::ParamPackage modifier_param =
Common::ParamPackage{param.Get("modifier", "")};
const bool invert_value = !modifier_param.Get("inverted", false);
modifier_param.Set("inverted", invert_value);
param.Set("modifier", modifier_param.Serialize());
analog_map_modifier_button[analog_id]->setText(ButtonToText(modifier_param));
emulated_controller->SetStickParam(analog_id, param);
});
context_menu.exec(
analog_map_modifier_button[analog_id]->mapToGlobal(menu_location));
});
connect(analog_map_range_spinbox[analog_id], qOverload<int>(&QSpinBox::valueChanged),
[=, this] {
Common::ParamPackage param = emulated_controller->GetStickParam(analog_id);
const auto spinbox_value = analog_map_range_spinbox[analog_id]->value();
param.Set("range", spinbox_value / 100.0f);
emulated_controller->SetStickParam(analog_id, param);
});
connect(analog_map_deadzone_slider[analog_id], &QSlider::valueChanged, [=, this] {
Common::ParamPackage param = emulated_controller->GetStickParam(analog_id);
const auto slider_value = analog_map_deadzone_slider[analog_id]->value();
analog_map_deadzone_label[analog_id]->setText(tr("Deadzone: %1%").arg(slider_value));
param.Set("deadzone", slider_value / 100.0f);
emulated_controller->SetStickParam(analog_id, param);
});
connect(analog_map_modifier_slider[analog_id], &QSlider::valueChanged, [=, this] {
Common::ParamPackage param = emulated_controller->GetStickParam(analog_id);
const auto slider_value = analog_map_modifier_slider[analog_id]->value();
analog_map_modifier_label[analog_id]->setText(
tr("Modifier Range: %1%").arg(slider_value));
param.Set("modifier_scale", slider_value / 100.0f);
emulated_controller->SetStickParam(analog_id, param);
});
}
if (player_index_ == 0) {
connect(ui->mousePanningButton, &QPushButton::clicked, [this, input_subsystem_] {
const auto right_stick_param =
emulated_controller->GetStickParam(Settings::NativeAnalog::RStick);
ConfigureMousePanning dialog(this, input_subsystem_,
right_stick_param.Get("deadzone", 0.0f),
right_stick_param.Get("range", 1.0f));
if (dialog.exec() == QDialog::Accepted) {
dialog.ApplyConfiguration();
}
});
} else {
ui->mousePanningWidget->hide();
}
// Player Connected checkbox
connect(ui->groupConnectedController, &QGroupBox::toggled,
[this](bool checked) { emit Connected(checked); });
if (player_index == 0) {
connect(ui->comboControllerType, qOverload<int>(&QComboBox::currentIndexChanged),
[this](int index) {
emit HandheldStateChanged(GetControllerTypeFromIndex(index) ==
Core::HID::NpadStyleIndex::Handheld);
});
}
if (debug || player_index == 9) {
ui->groupConnectedController->setCheckable(false);
}
// The Debug Controller can only choose the Pro Controller.
if (debug) {
ui->buttonScreenshot->setEnabled(false);
ui->buttonHome->setEnabled(false);
ui->comboControllerType->addItem(tr("Pro Controller"));
} else {
SetConnectableControllers();
}
UpdateControllerAvailableButtons();
UpdateControllerEnabledButtons();
UpdateControllerButtonNames();
UpdateMotionButtons();
connect(ui->comboControllerType, qOverload<int>(&QComboBox::currentIndexChanged), [this](int) {
UpdateControllerAvailableButtons();
UpdateControllerEnabledButtons();
UpdateControllerButtonNames();
UpdateMotionButtons();
const Core::HID::NpadStyleIndex type =
GetControllerTypeFromIndex(ui->comboControllerType->currentIndex());
if (player_index == 0) {
auto* emulated_controller_p1 =
hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
auto* emulated_controller_handheld =
hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
bool is_connected = emulated_controller->IsConnected(true);
emulated_controller_p1->SetNpadStyleIndex(type);
emulated_controller_handheld->SetNpadStyleIndex(type);
if (is_connected) {
if (type == Core::HID::NpadStyleIndex::Handheld) {
emulated_controller_p1->Disconnect();
emulated_controller_handheld->Connect(true);
emulated_controller = emulated_controller_handheld;
} else {
emulated_controller_handheld->Disconnect();
emulated_controller_p1->Connect(true);
emulated_controller = emulated_controller_p1;
}
}
ui->controllerFrame->SetController(emulated_controller);
}
emulated_controller->SetNpadStyleIndex(type);
});
connect(ui->comboDevices, qOverload<int>(&QComboBox::activated), this,
&ConfigureInputPlayer::UpdateMappingWithDefaults);
ui->comboDevices->installEventFilter(this);
ui->comboDevices->setCurrentIndex(-1);
timeout_timer->setSingleShot(true);
connect(timeout_timer.get(), &QTimer::timeout, [this] { SetPollingResult({}, true); });
connect(poll_timer.get(), &QTimer::timeout, [this] {
const auto& params = input_subsystem->GetNextInput();
if (params.Has("engine") && IsInputAcceptable(params)) {
SetPollingResult(params, false);
return;
}
});
UpdateInputProfiles();
connect(ui->buttonProfilesNew, &QPushButton::clicked, this,
&ConfigureInputPlayer::CreateProfile);
connect(ui->buttonProfilesDelete, &QPushButton::clicked, this,
&ConfigureInputPlayer::DeleteProfile);
connect(ui->comboProfiles, qOverload<int>(&QComboBox::activated), this,
&ConfigureInputPlayer::LoadProfile);
connect(ui->buttonProfilesSave, &QPushButton::clicked, this,
&ConfigureInputPlayer::SaveProfile);
LoadConfiguration();
}
ConfigureInputPlayer::~ConfigureInputPlayer() {
if (player_index == 0) {
auto* emulated_controller_p1 =
hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
auto* emulated_controller_handheld =
hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
emulated_controller_p1->DisableConfiguration();
emulated_controller_handheld->DisableConfiguration();
} else {
emulated_controller->DisableConfiguration();
}
}
void ConfigureInputPlayer::ApplyConfiguration() {
if (player_index == 0) {
auto* emulated_controller_p1 =
hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
auto* emulated_controller_handheld =
hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
emulated_controller_p1->DisableConfiguration();
emulated_controller_p1->SaveCurrentConfig();
emulated_controller_p1->EnableConfiguration();
emulated_controller_handheld->DisableConfiguration();
emulated_controller_handheld->SaveCurrentConfig();
emulated_controller_handheld->EnableConfiguration();
return;
}
emulated_controller->DisableConfiguration();
emulated_controller->SaveCurrentConfig();
emulated_controller->EnableConfiguration();
}
void ConfigureInputPlayer::showEvent(QShowEvent* event) {
if (bottom_row == nullptr) {
return;
}
QWidget::showEvent(event);
ui->main->addWidget(bottom_row);
}
void ConfigureInputPlayer::changeEvent(QEvent* event) {
if (event->type() == QEvent::LanguageChange) {
RetranslateUI();
}
QWidget::changeEvent(event);
}
void ConfigureInputPlayer::RetranslateUI() {
ui->retranslateUi(this);
UpdateUI();
}
void ConfigureInputPlayer::LoadConfiguration() {
emulated_controller->ReloadFromSettings();
UpdateUI();
UpdateInputDeviceCombobox();
if (debug) {
return;
}
const int comboBoxIndex =
GetIndexFromControllerType(emulated_controller->GetNpadStyleIndex(true));
ui->comboControllerType->setCurrentIndex(comboBoxIndex);
ui->groupConnectedController->setChecked(emulated_controller->IsConnected(true));
}
void ConfigureInputPlayer::ConnectPlayer(bool connected) {
ui->groupConnectedController->setChecked(connected);
if (connected) {
emulated_controller->Connect(true);
} else {
emulated_controller->Disconnect();
}
}
void ConfigureInputPlayer::UpdateInputDeviceCombobox() {
// Skip input device persistence if "Input Devices" is set to "Any".
if (ui->comboDevices->currentIndex() == 0) {
UpdateInputDevices();
return;
}
const auto devices = emulated_controller->GetMappedDevices();
UpdateInputDevices();
if (devices.empty()) {
return;
}
if (devices.size() > 2) {
ui->comboDevices->setCurrentIndex(0);
return;
}
const auto first_engine = devices[0].Get("engine", "");
const auto first_guid = devices[0].Get("guid", "");
const auto first_port = devices[0].Get("port", 0);
const auto first_pad = devices[0].Get("pad", 0);
if (devices.size() == 1) {
const auto devices_it = std::find_if(
input_devices.begin(), input_devices.end(),
[first_engine, first_guid, first_port, first_pad](const Common::ParamPackage& param) {
return param.Get("engine", "") == first_engine &&
param.Get("guid", "") == first_guid && param.Get("port", 0) == first_port &&
param.Get("pad", 0) == first_pad;
});
const int device_index =
devices_it != input_devices.end()
? static_cast<int>(std::distance(input_devices.begin(), devices_it))
: 0;
ui->comboDevices->setCurrentIndex(device_index);
return;
}
const auto second_engine = devices[1].Get("engine", "");
const auto second_guid = devices[1].Get("guid", "");
const auto second_port = devices[1].Get("port", 0);
const bool is_keyboard_mouse = (first_engine == "keyboard" || first_engine == "mouse") &&
(second_engine == "keyboard" || second_engine == "mouse");
if (is_keyboard_mouse) {
ui->comboDevices->setCurrentIndex(2);
return;
}
const bool is_engine_equal = first_engine == second_engine;
const bool is_port_equal = first_port == second_port;
if (is_engine_equal && is_port_equal) {
const auto devices_it = std::find_if(
input_devices.begin(), input_devices.end(),
[first_engine, first_guid, second_guid, first_port](const Common::ParamPackage& param) {
const bool is_guid_valid =
(param.Get("guid", "") == first_guid &&
param.Get("guid2", "") == second_guid) ||
(param.Get("guid", "") == second_guid && param.Get("guid2", "") == first_guid);
return param.Get("engine", "") == first_engine && is_guid_valid &&
param.Get("port", 0) == first_port;
});
const int device_index =
devices_it != input_devices.end()
? static_cast<int>(std::distance(input_devices.begin(), devices_it))
: 0;
ui->comboDevices->setCurrentIndex(device_index);
} else {
ui->comboDevices->setCurrentIndex(0);
}
}
void ConfigureInputPlayer::RestoreDefaults() {
UpdateMappingWithDefaults();
}
void ConfigureInputPlayer::ClearAll() {
for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) {
const auto* const button = button_map[button_id];
if (button == nullptr) {
continue;
}
emulated_controller->SetButtonParam(button_id, {});
}
for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id) {
const auto* const analog_button = analog_map_buttons[analog_id][sub_button_id];
if (analog_button == nullptr) {
continue;
}
emulated_controller->SetStickParam(analog_id, {});
}
}
for (int motion_id = 0; motion_id < Settings::NativeMotion::NumMotions; ++motion_id) {
const auto* const motion_button = motion_map[motion_id];
if (motion_button == nullptr) {
continue;
}
emulated_controller->SetMotionParam(motion_id, {});
}
UpdateUI();
UpdateInputDevices();
}
void ConfigureInputPlayer::UpdateUI() {
for (int button = 0; button < Settings::NativeButton::NumButtons; ++button) {
const Common::ParamPackage param = emulated_controller->GetButtonParam(button);
button_map[button]->setText(ButtonToText(param));
}
const Common::ParamPackage ZL_param =
emulated_controller->GetButtonParam(Settings::NativeButton::ZL);
if (ZL_param.Has("threshold")) {
const int button_threshold = static_cast<int>(ZL_param.Get("threshold", 0.5f) * 100.0f);
ui->sliderZLThreshold->setValue(button_threshold);
}
const Common::ParamPackage ZR_param =
emulated_controller->GetButtonParam(Settings::NativeButton::ZR);
if (ZR_param.Has("threshold")) {
const int button_threshold = static_cast<int>(ZR_param.Get("threshold", 0.5f) * 100.0f);
ui->sliderZRThreshold->setValue(button_threshold);
}
for (int motion_id = 0; motion_id < Settings::NativeMotion::NumMotions; ++motion_id) {
const Common::ParamPackage param = emulated_controller->GetMotionParam(motion_id);
motion_map[motion_id]->setText(ButtonToText(param));
}
for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
const Common::ParamPackage param = emulated_controller->GetStickParam(analog_id);
for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id) {
auto* const analog_button = analog_map_buttons[analog_id][sub_button_id];
if (analog_button == nullptr) {
continue;
}
analog_button->setText(AnalogToText(param, analog_sub_buttons[sub_button_id]));
}
analog_map_modifier_button[analog_id]->setText(
ButtonToText(Common::ParamPackage{param.Get("modifier", "")}));
const auto deadzone_label = analog_map_deadzone_label[analog_id];
const auto deadzone_slider = analog_map_deadzone_slider[analog_id];
const auto modifier_groupbox = analog_map_modifier_groupbox[analog_id];
const auto modifier_label = analog_map_modifier_label[analog_id];
const auto modifier_slider = analog_map_modifier_slider[analog_id];
const auto range_groupbox = analog_map_range_groupbox[analog_id];
const auto range_spinbox = analog_map_range_spinbox[analog_id];
int slider_value;
const bool is_controller = input_subsystem->IsController(param);
if (is_controller) {
slider_value = static_cast<int>(param.Get("deadzone", 0.15f) * 100);
deadzone_label->setText(tr("Deadzone: %1%").arg(slider_value));
deadzone_slider->setValue(slider_value);
range_spinbox->setValue(static_cast<int>(param.Get("range", 0.95f) * 100));
} else {
slider_value = static_cast<int>(param.Get("modifier_scale", 0.5f) * 100);
modifier_label->setText(tr("Modifier Range: %1%").arg(slider_value));
modifier_slider->setValue(slider_value);
}
deadzone_label->setVisible(is_controller);
deadzone_slider->setVisible(is_controller);
modifier_groupbox->setVisible(!is_controller);
modifier_label->setVisible(!is_controller);
modifier_slider->setVisible(!is_controller);
range_groupbox->setVisible(is_controller);
}
}
void ConfigureInputPlayer::SetConnectableControllers() {
const auto npad_style_set = hid_core.GetSupportedStyleTag();
index_controller_type_pairs.clear();
ui->comboControllerType->clear();
const auto add_item = [&](Core::HID::NpadStyleIndex controller_type,
const QString& controller_name) {
index_controller_type_pairs.emplace_back(ui->comboControllerType->count(), controller_type);
ui->comboControllerType->addItem(controller_name);
};
if (npad_style_set.fullkey == 1) {
add_item(Core::HID::NpadStyleIndex::Fullkey, tr("Pro Controller"));
}
if (npad_style_set.joycon_dual == 1) {
add_item(Core::HID::NpadStyleIndex::JoyconDual, tr("Dual Joycons"));
}
if (npad_style_set.joycon_left == 1) {
add_item(Core::HID::NpadStyleIndex::JoyconLeft, tr("Left Joycon"));
}
if (npad_style_set.joycon_right == 1) {
add_item(Core::HID::NpadStyleIndex::JoyconRight, tr("Right Joycon"));
}
if (player_index == 0 && npad_style_set.handheld == 1) {
add_item(Core::HID::NpadStyleIndex::Handheld, tr("Handheld"));
}
if (npad_style_set.gamecube == 1) {
add_item(Core::HID::NpadStyleIndex::GameCube, tr("GameCube Controller"));
}
// Disable all unsupported controllers
if (!Settings::values.enable_all_controllers) {
return;
}
if (npad_style_set.palma == 1) {
add_item(Core::HID::NpadStyleIndex::Pokeball, tr("Poke Ball Plus"));
}
if (npad_style_set.lark == 1) {
add_item(Core::HID::NpadStyleIndex::NES, tr("NES Controller"));
}
if (npad_style_set.lucia == 1) {
add_item(Core::HID::NpadStyleIndex::SNES, tr("SNES Controller"));
}
if (npad_style_set.lagoon == 1) {
add_item(Core::HID::NpadStyleIndex::N64, tr("N64 Controller"));
}
if (npad_style_set.lager == 1) {
add_item(Core::HID::NpadStyleIndex::SegaGenesis, tr("Sega Genesis"));
}
}
Core::HID::NpadStyleIndex ConfigureInputPlayer::GetControllerTypeFromIndex(int index) const {
const auto it =
std::find_if(index_controller_type_pairs.begin(), index_controller_type_pairs.end(),
[index](const auto& pair) { return pair.first == index; });
if (it == index_controller_type_pairs.end()) {
return Core::HID::NpadStyleIndex::Fullkey;
}
return it->second;
}
int ConfigureInputPlayer::GetIndexFromControllerType(Core::HID::NpadStyleIndex type) const {
const auto it =
std::find_if(index_controller_type_pairs.begin(), index_controller_type_pairs.end(),
[type](const auto& pair) { return pair.second == type; });
if (it == index_controller_type_pairs.end()) {
return -1;
}
return it->first;
}
void ConfigureInputPlayer::UpdateInputDevices() {
input_devices = input_subsystem->GetInputDevices();
ui->comboDevices->clear();
for (const auto& device : input_devices) {
ui->comboDevices->addItem(QString::fromStdString(device.Get("display", "Unknown")), {});
}
}
void ConfigureInputPlayer::UpdateControllerAvailableButtons() {
auto layout = GetControllerTypeFromIndex(ui->comboControllerType->currentIndex());
if (debug) {
layout = Core::HID::NpadStyleIndex::Fullkey;
}
// List of all the widgets that will be hidden by any of the following layouts that need
// "unhidden" after the controller type changes
const std::array<QWidget*, 14> layout_show = {
ui->buttonShoulderButtonsSLSRLeft,
ui->buttonShoulderButtonsSLSRRight,
ui->horizontalSpacerShoulderButtonsWidget,
ui->horizontalSpacerShoulderButtonsWidget2,
ui->horizontalSpacerShoulderButtonsWidget3,
ui->horizontalSpacerShoulderButtonsWidget4,
ui->buttonShoulderButtonsLeft,
ui->buttonMiscButtonsMinusScreenshot,
ui->bottomLeft,
ui->buttonShoulderButtonsRight,
ui->buttonMiscButtonsPlusHome,
ui->bottomRight,
ui->buttonMiscButtonsMinusGroup,
ui->buttonMiscButtonsScreenshotGroup,
};
for (auto* widget : layout_show) {
widget->show();
}
std::vector<QWidget*> layout_hidden;
switch (layout) {
case Core::HID::NpadStyleIndex::Fullkey:
case Core::HID::NpadStyleIndex::Handheld:
layout_hidden = {
ui->buttonShoulderButtonsSLSRLeft,
ui->buttonShoulderButtonsSLSRRight,
ui->horizontalSpacerShoulderButtonsWidget2,
ui->horizontalSpacerShoulderButtonsWidget4,
};
break;
case Core::HID::NpadStyleIndex::JoyconLeft:
layout_hidden = {
ui->buttonShoulderButtonsSLSRRight,
ui->horizontalSpacerShoulderButtonsWidget2,
ui->horizontalSpacerShoulderButtonsWidget3,
ui->buttonShoulderButtonsRight,
ui->buttonMiscButtonsPlusHome,
ui->bottomRight,
};
break;
case Core::HID::NpadStyleIndex::JoyconRight:
layout_hidden = {
ui->buttonShoulderButtonsSLSRLeft, ui->horizontalSpacerShoulderButtonsWidget,
ui->horizontalSpacerShoulderButtonsWidget4, ui->buttonShoulderButtonsLeft,
ui->buttonMiscButtonsMinusScreenshot, ui->bottomLeft,
};
break;
case Core::HID::NpadStyleIndex::GameCube:
layout_hidden = {
ui->buttonShoulderButtonsSLSRLeft,
ui->buttonShoulderButtonsSLSRRight,
ui->horizontalSpacerShoulderButtonsWidget2,
ui->horizontalSpacerShoulderButtonsWidget4,
ui->buttonMiscButtonsMinusGroup,
ui->buttonMiscButtonsScreenshotGroup,
};
break;
default:
break;
}
for (auto* widget : layout_hidden) {
widget->hide();
}
}
void ConfigureInputPlayer::UpdateControllerEnabledButtons() {
auto layout = GetControllerTypeFromIndex(ui->comboControllerType->currentIndex());
if (debug) {
layout = Core::HID::NpadStyleIndex::Fullkey;
}
// List of all the widgets that will be disabled by any of the following layouts that need
// "enabled" after the controller type changes
const std::array<QWidget*, 3> layout_enable = {
ui->buttonLStickPressedGroup,
ui->groupRStickPressed,
ui->buttonShoulderButtonsButtonLGroup,
};
for (auto* widget : layout_enable) {
widget->setEnabled(true);
}
std::vector<QWidget*> layout_disable;
switch (layout) {
case Core::HID::NpadStyleIndex::Fullkey:
case Core::HID::NpadStyleIndex::JoyconDual:
case Core::HID::NpadStyleIndex::Handheld:
case Core::HID::NpadStyleIndex::JoyconLeft:
case Core::HID::NpadStyleIndex::JoyconRight:
break;
case Core::HID::NpadStyleIndex::GameCube:
layout_disable = {
ui->buttonHome,
ui->buttonLStickPressedGroup,
ui->groupRStickPressed,
ui->buttonShoulderButtonsButtonLGroup,
};
break;
default:
break;
}
for (auto* widget : layout_disable) {
widget->setEnabled(false);
}
}
void ConfigureInputPlayer::UpdateMotionButtons() {
if (debug) {
// Motion isn't used with the debug controller, hide both groupboxes.
ui->buttonMotionLeftGroup->hide();
ui->buttonMotionRightGroup->hide();
return;
}
// Show/hide the "Motion 1/2" groupboxes depending on the currently selected controller.
switch (GetControllerTypeFromIndex(ui->comboControllerType->currentIndex())) {
case Core::HID::NpadStyleIndex::Fullkey:
case Core::HID::NpadStyleIndex::JoyconLeft:
case Core::HID::NpadStyleIndex::Handheld:
// Show "Motion 1" and hide "Motion 2".
ui->buttonMotionLeftGroup->show();
ui->buttonMotionRightGroup->hide();
break;
case Core::HID::NpadStyleIndex::JoyconRight:
// Show "Motion 2" and hide "Motion 1".
ui->buttonMotionLeftGroup->hide();
ui->buttonMotionRightGroup->show();
break;
case Core::HID::NpadStyleIndex::GameCube:
// Hide both "Motion 1/2".
ui->buttonMotionLeftGroup->hide();
ui->buttonMotionRightGroup->hide();
break;
case Core::HID::NpadStyleIndex::JoyconDual:
default:
// Show both "Motion 1/2".
ui->buttonMotionLeftGroup->show();
ui->buttonMotionRightGroup->show();
break;
}
}
void ConfigureInputPlayer::UpdateControllerButtonNames() {
auto layout = GetControllerTypeFromIndex(ui->comboControllerType->currentIndex());
if (debug) {
layout = Core::HID::NpadStyleIndex::Fullkey;
}
switch (layout) {
case Core::HID::NpadStyleIndex::Fullkey:
case Core::HID::NpadStyleIndex::JoyconDual:
case Core::HID::NpadStyleIndex::Handheld:
case Core::HID::NpadStyleIndex::JoyconLeft:
case Core::HID::NpadStyleIndex::JoyconRight:
ui->buttonMiscButtonsPlusGroup->setTitle(tr("Plus"));
ui->buttonShoulderButtonsButtonZLGroup->setTitle(tr("ZL"));
ui->buttonShoulderButtonsZRGroup->setTitle(tr("ZR"));
ui->buttonShoulderButtonsRGroup->setTitle(tr("R"));
ui->LStick->setTitle(tr("Left Stick"));
ui->RStick->setTitle(tr("Right Stick"));
break;
case Core::HID::NpadStyleIndex::GameCube:
ui->buttonMiscButtonsPlusGroup->setTitle(tr("Start / Pause"));
ui->buttonShoulderButtonsButtonZLGroup->setTitle(tr("L"));
ui->buttonShoulderButtonsZRGroup->setTitle(tr("R"));
ui->buttonShoulderButtonsRGroup->setTitle(tr("Z"));
ui->LStick->setTitle(tr("Control Stick"));
ui->RStick->setTitle(tr("C-Stick"));
break;
default:
break;
}
}
void ConfigureInputPlayer::UpdateMappingWithDefaults() {
if (ui->comboDevices->currentIndex() == 0) {
return;
}
for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) {
const auto* const button = button_map[button_id];
if (button == nullptr) {
continue;
}
emulated_controller->SetButtonParam(button_id, {});
}
for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id) {
const auto* const analog_button = analog_map_buttons[analog_id][sub_button_id];
if (analog_button == nullptr) {
continue;
}
emulated_controller->SetStickParam(analog_id, {});
}
}
for (int motion_id = 0; motion_id < Settings::NativeMotion::NumMotions; ++motion_id) {
const auto* const motion_button = motion_map[motion_id];
if (motion_button == nullptr) {
continue;
}
emulated_controller->SetMotionParam(motion_id, {});
}
// Reset keyboard or mouse bindings
if (ui->comboDevices->currentIndex() == 1 || ui->comboDevices->currentIndex() == 2) {
for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) {
emulated_controller->SetButtonParam(
button_id, Common::ParamPackage{InputCommon::GenerateKeyboardParam(
QtConfig::default_buttons[button_id])});
}
for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
Common::ParamPackage analog_param{};
for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id) {
Common::ParamPackage params{InputCommon::GenerateKeyboardParam(
QtConfig::default_analogs[analog_id][sub_button_id])};
SetAnalogParam(params, analog_param, analog_sub_buttons[sub_button_id]);
}
analog_param.Set("modifier", InputCommon::GenerateKeyboardParam(
QtConfig::default_stick_mod[analog_id]));
emulated_controller->SetStickParam(analog_id, analog_param);
}
for (int motion_id = 0; motion_id < Settings::NativeMotion::NumMotions; ++motion_id) {
emulated_controller->SetMotionParam(
motion_id, Common::ParamPackage{InputCommon::GenerateKeyboardParam(
QtConfig::default_motions[motion_id])});
}
// If mouse is selected we want to override with mappings from the driver
if (ui->comboDevices->currentIndex() == 1) {
UpdateUI();
return;
}
}
// Reset controller bindings
const auto& device = input_devices[ui->comboDevices->currentIndex()];
auto button_mappings = input_subsystem->GetButtonMappingForDevice(device);
auto analog_mappings = input_subsystem->GetAnalogMappingForDevice(device);
auto motion_mappings = input_subsystem->GetMotionMappingForDevice(device);
for (const auto& button_mapping : button_mappings) {
const std::size_t index = button_mapping.first;
emulated_controller->SetButtonParam(index, button_mapping.second);
}
for (const auto& analog_mapping : analog_mappings) {
const std::size_t index = analog_mapping.first;
emulated_controller->SetStickParam(index, analog_mapping.second);
}
for (const auto& motion_mapping : motion_mappings) {
const std::size_t index = motion_mapping.first;
emulated_controller->SetMotionParam(index, motion_mapping.second);
}
UpdateUI();
}
void ConfigureInputPlayer::HandleClick(
QPushButton* button, std::size_t button_id,
std::function<void(const Common::ParamPackage&)> new_input_setter,
InputCommon::Polling::InputType type) {
if (timeout_timer->isActive()) {
return;
}
if (button == ui->buttonMotionLeft || button == ui->buttonMotionRight) {
button->setText(tr("Shake!"));
} else {
button->setText(tr("[waiting]"));
}
button->setFocus();
input_setter = std::move(new_input_setter);
input_subsystem->BeginMapping(type);
QWidget::grabMouse();
QWidget::grabKeyboard();
if (type == InputCommon::Polling::InputType::Button) {
ui->controllerFrame->BeginMappingButton(button_id);
} else if (type == InputCommon::Polling::InputType::Stick) {
ui->controllerFrame->BeginMappingAnalog(button_id);
}
timeout_timer->start(4000); // Cancel after 4 seconds
poll_timer->start(25); // Check for new inputs every 25ms
}
void ConfigureInputPlayer::SetPollingResult(const Common::ParamPackage& params, bool abort) {
timeout_timer->stop();
poll_timer->stop();
input_subsystem->StopMapping();
QWidget::releaseMouse();
QWidget::releaseKeyboard();
if (!abort) {
(*input_setter)(params);
}
UpdateUI();
UpdateInputDeviceCombobox();
ui->controllerFrame->EndMapping();
input_setter = std::nullopt;
}
bool ConfigureInputPlayer::IsInputAcceptable(const Common::ParamPackage& params) const {
if (ui->comboDevices->currentIndex() == 0) {
return true;
}
if (params.Has("motion")) {
return true;
}
// Keyboard/Mouse
if (ui->comboDevices->currentIndex() == 1 || ui->comboDevices->currentIndex() == 2) {
return params.Get("engine", "") == "keyboard" || params.Get("engine", "") == "mouse";
}
const auto& current_input_device = input_devices[ui->comboDevices->currentIndex()];
return params.Get("engine", "") == current_input_device.Get("engine", "") &&
(params.Get("guid", "") == current_input_device.Get("guid", "") ||
params.Get("guid", "") == current_input_device.Get("guid2", "")) &&
params.Get("port", 0) == current_input_device.Get("port", 0);
}
void ConfigureInputPlayer::mousePressEvent(QMouseEvent* event) {
if (!input_setter || !event) {
return;
}
const auto button = GRenderWindow::QtButtonToMouseButton(event->button());
input_subsystem->GetMouse()->PressButton(0, 0, button);
}
void ConfigureInputPlayer::wheelEvent(QWheelEvent* event) {
// Only handle wheel events for input mapping when in input mapping mode
// Otherwise, forward to the scroll area for scrolling
if (input_setter) {
const int x = event->angleDelta().x();
const int y = event->angleDelta().y();
input_subsystem->GetMouse()->MouseWheelChange(x, y);
event->accept();
} else {
// Find the parent scroll area and forward the event to its scrollbar
// The widget hierarchy is: ConfigureInputPlayer -> QWidget (tab) -> QTabWidget ->
// ConfigureInput -> QWidget (viewport) -> QScrollArea
QWidget* current = this;
QScrollArea* scroll_area = nullptr;
// Traverse up the parent chain to find the scroll area
while (current) {
// Check if current widget is a scroll area
if (auto* sa = qobject_cast<QScrollArea*>(current)) {
scroll_area = sa;
break;
}
// Check if current widget's parent is a scroll area (for viewport case)
QWidget* parent = current->parentWidget();
if (parent) {
if (auto* sa = qobject_cast<QScrollArea*>(parent)) {
scroll_area = sa;
break;
}
}
current = parent;
}
if (scroll_area) {
// Forward to vertical scrollbar if available
if (scroll_area->verticalScrollBar()->maximum() > 0) {
QApplication::sendEvent(scroll_area->verticalScrollBar(), event);
event->accept();
return;
}
// Otherwise try horizontal scrollbar
if (scroll_area->horizontalScrollBar()->maximum() > 0) {
QApplication::sendEvent(scroll_area->horizontalScrollBar(), event);
event->accept();
return;
}
}
// If no scroll area found, let the event propagate normally
event->ignore();
}
}
void ConfigureInputPlayer::keyPressEvent(QKeyEvent* event) {
if (!input_setter || !event) {
return;
}
event->ignore();
if (event->key() != Qt::Key_Escape) {
input_subsystem->GetKeyboard()->PressKey(event->key());
}
}
bool ConfigureInputPlayer::eventFilter(QObject* object, QEvent* event) {
if (object == ui->comboDevices && event->type() == QEvent::MouseButtonPress) {
RefreshInputDevices();
}
return object->eventFilter(object, event);
}
void ConfigureInputPlayer::CreateProfile() {
const auto profile_name =
LimitableInputDialog::GetText(this, tr("New Profile"), tr("Enter a profile name:"), 1, 30,
LimitableInputDialog::InputLimiter::Filesystem);
if (profile_name.isEmpty()) {
return;
}
if (!InputProfiles::IsProfileNameValid(profile_name.toStdString())) {
QMessageBox::critical(this, tr("Create Input Profile"),
tr("The given profile name is not valid!"));
return;
}
ApplyConfiguration();
if (!profiles->CreateProfile(profile_name.toStdString(), player_index)) {
QMessageBox::critical(this, tr("Create Input Profile"),
tr("Failed to create the input profile \"%1\"").arg(profile_name));
UpdateInputProfiles();
emit RefreshInputProfiles(player_index);
return;
}
emit RefreshInputProfiles(player_index);
ui->comboProfiles->addItem(profile_name);
ui->comboProfiles->setCurrentIndex(ui->comboProfiles->count() - 1);
}
void ConfigureInputPlayer::DeleteProfile() {
const QString profile_name = ui->comboProfiles->itemText(ui->comboProfiles->currentIndex());
if (profile_name.isEmpty()) {
return;
}
if (!profiles->DeleteProfile(profile_name.toStdString())) {
QMessageBox::critical(this, tr("Delete Input Profile"),
tr("Failed to delete the input profile \"%1\"").arg(profile_name));
UpdateInputProfiles();
emit RefreshInputProfiles(player_index);
return;
}
emit RefreshInputProfiles(player_index);
ui->comboProfiles->removeItem(ui->comboProfiles->currentIndex());
ui->comboProfiles->setCurrentIndex(-1);
}
void ConfigureInputPlayer::LoadProfile() {
const QString profile_name = ui->comboProfiles->itemText(ui->comboProfiles->currentIndex());
if (profile_name.isEmpty()) {
return;
}
ApplyConfiguration();
if (!profiles->LoadProfile(profile_name.toStdString(), player_index)) {
QMessageBox::critical(this, tr("Load Input Profile"),
tr("Failed to load the input profile \"%1\"").arg(profile_name));
UpdateInputProfiles();
emit RefreshInputProfiles(player_index);
return;
}
LoadConfiguration();
}
void ConfigureInputPlayer::SaveProfile() {
static constexpr size_t HANDHELD_INDEX = 8;
const QString profile_name = ui->comboProfiles->itemText(ui->comboProfiles->currentIndex());
if (profile_name.isEmpty()) {
return;
}
ApplyConfiguration();
// When we're in handheld mode, only the handheld emulated controller bindings are updated
const bool is_handheld = player_index == 0 && emulated_controller->GetNpadIdType() ==
Core::HID::NpadIdType::Handheld;
const auto profile_player_index = is_handheld ? HANDHELD_INDEX : player_index;
if (!profiles->SaveProfile(profile_name.toStdString(), profile_player_index)) {
QMessageBox::critical(this, tr("Save Input Profile"),
tr("Failed to save the input profile \"%1\"").arg(profile_name));
UpdateInputProfiles();
emit RefreshInputProfiles(player_index);
return;
}
}
void ConfigureInputPlayer::UpdateInputProfiles() {
ui->comboProfiles->clear();
// Set current profile as empty by default
int profile_index = -1;
// Add every available profile and search the player profile to set it as current one
auto& current_profile = Settings::values.players.GetValue()[player_index].profile_name;
std::vector<std::string> profile_names = profiles->GetInputProfileNames();
std::string profile_name;
for (size_t i = 0; i < profile_names.size(); i++) {
profile_name = profile_names[i];
ui->comboProfiles->addItem(QString::fromStdString(profile_name));
if (current_profile == profile_name) {
profile_index = (int)i;
}
}
LOG_DEBUG(Frontend, "Setting the current input profile to index {}", profile_index);
ui->comboProfiles->setCurrentIndex(profile_index);
}
void ConfigureInputPlayer::OnButtonChooseColor() {
const u32 current_color_val = emulated_controller->GetBodyColor();
const QColor initial_color =
QColor(current_color_val != 0 ? current_color_val : Settings::DEFAULT_CONTROLLER_COLOR);
const QColor color = QColorDialog::getColor(initial_color, this, tr("Choose Controller Color"));
if (color.isValid()) {
emulated_controller->SetBodyColor(color.rgb());
// The update call must be on the UI widget, not the controller object.
ui->controllerFrame->ControllerUpdate(Core::HID::ControllerTriggerType::Color);
}
}
void ConfigureInputPlayer::OnButtonToggleGyro() {
bool current_visibility = emulated_controller->IsGyroOverlayVisible();
emulated_controller->SetGyroOverlayVisible(!current_visibility);
// We can just re-draw everything to apply the change
ui->controllerFrame->ControllerUpdate(Core::HID::ControllerTriggerType::All);
}