yuzu/src/yuzu/configuration/configure_ringcon.cpp
t895 da14c7b8e4 config: Unify config handling under frontend_common
Replaces every way of handling config for each frontend with SimpleIni. frontend_common's Config class is at the center where it saves and loads all of the cross-platform settings and provides a set of pure virtual functions for platform specific settings.

As a result of making config handling platform specific, several parts had to be moved to each platform's own config class or to other parts. Default keys were put in platform specific config classes and translatable strings for Qt were moved to shared_translation. Default hotkeys, default_theme, window geometry, and qt metatypes were moved to uisettings. Additionally, to reduce dependence on Qt, QStrings were converted to std::strings where applicable.
2023-11-21 01:58:13 -05:00

497 lines
18 KiB
C++

// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <memory>
#include <QKeyEvent>
#include <QMenu>
#include <QMessageBox>
#include <QTimer>
#include <fmt/format.h>
#include "configuration/qt_config.h"
#include "core/hid/emulated_controller.h"
#include "core/hid/hid_core.h"
#include "input_common/drivers/keyboard.h"
#include "input_common/drivers/mouse.h"
#include "input_common/main.h"
#include "ui_configure_ringcon.h"
#include "yuzu/bootmanager.h"
#include "yuzu/configuration/configure_ringcon.h"
const std::array<std::string, ConfigureRingController::ANALOG_SUB_BUTTONS_NUM>
ConfigureRingController::analog_sub_buttons{{
"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::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::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");
default:
return QObject::tr("[undefined]");
}
}
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
ConfigureRingController::ConfigureRingController(QWidget* parent,
InputCommon::InputSubsystem* input_subsystem_,
Core::HID::HIDCore& hid_core_)
: QDialog(parent), timeout_timer(std::make_unique<QTimer>()),
poll_timer(std::make_unique<QTimer>()), input_subsystem{input_subsystem_},
ui(std::make_unique<Ui::ConfigureRingController>()) {
ui->setupUi(this);
analog_map_buttons = {
ui->buttonRingAnalogPull,
ui->buttonRingAnalogPush,
};
emulated_controller = hid_core_.GetEmulatedController(Core::HID::NpadIdType::Player1);
emulated_controller->SaveCurrentConfig();
emulated_controller->EnableConfiguration();
Core::HID::ControllerUpdateCallback engine_callback{
.on_change = [this](Core::HID::ControllerTriggerType type) { ControllerUpdate(type); },
.is_npad_service = false,
};
callback_key = emulated_controller->SetCallback(engine_callback);
is_controller_set = true;
LoadConfiguration();
for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id) {
auto* const analog_button = analog_map_buttons[sub_button_id];
if (analog_button == nullptr) {
continue;
}
connect(analog_button, &QPushButton::clicked, [=, this] {
HandleClick(
analog_map_buttons[sub_button_id],
[=, this](const Common::ParamPackage& params) {
Common::ParamPackage param = emulated_controller->GetRingParam();
SetAnalogParam(params, param, analog_sub_buttons[sub_button_id]);
emulated_controller->SetRingParam(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->GetRingParam();
context_menu.addAction(tr("Clear"), [&] {
emulated_controller->SetRingParam(param);
analog_map_buttons[sub_button_id]->setText(tr("[not set]"));
});
context_menu.addAction(tr("Invert axis"), [&] {
const bool invert_value = param.Get("invert_x", "+") == "-";
const std::string invert_str = invert_value ? "+" : "-";
param.Set("invert_x", invert_str);
emulated_controller->SetRingParam(param);
for (int sub_button_id2 = 0; sub_button_id2 < ANALOG_SUB_BUTTONS_NUM;
++sub_button_id2) {
analog_map_buttons[sub_button_id2]->setText(
AnalogToText(param, analog_sub_buttons[sub_button_id2]));
}
});
context_menu.exec(
analog_map_buttons[sub_button_id]->mapToGlobal(menu_location));
});
}
connect(ui->sliderRingAnalogDeadzone, &QSlider::valueChanged, [=, this] {
Common::ParamPackage param = emulated_controller->GetRingParam();
const auto slider_value = ui->sliderRingAnalogDeadzone->value();
ui->labelRingAnalogDeadzone->setText(tr("Deadzone: %1%").arg(slider_value));
param.Set("deadzone", slider_value / 100.0f);
emulated_controller->SetRingParam(param);
});
connect(ui->restore_defaults_button, &QPushButton::clicked, this,
&ConfigureRingController::RestoreDefaults);
connect(ui->enable_ring_controller_button, &QPushButton::clicked, this,
&ConfigureRingController::EnableRingController);
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;
}
});
resize(0, 0);
}
ConfigureRingController::~ConfigureRingController() {
emulated_controller->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::Active);
emulated_controller->DisableConfiguration();
if (is_controller_set) {
emulated_controller->DeleteCallback(callback_key);
is_controller_set = false;
}
};
void ConfigureRingController::changeEvent(QEvent* event) {
if (event->type() == QEvent::LanguageChange) {
RetranslateUI();
}
QDialog::changeEvent(event);
}
void ConfigureRingController::RetranslateUI() {
ui->retranslateUi(this);
}
void ConfigureRingController::UpdateUI() {
RetranslateUI();
const Common::ParamPackage param = emulated_controller->GetRingParam();
for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id) {
auto* const analog_button = analog_map_buttons[sub_button_id];
if (analog_button == nullptr) {
continue;
}
analog_button->setText(AnalogToText(param, analog_sub_buttons[sub_button_id]));
}
const auto deadzone_label = ui->labelRingAnalogDeadzone;
const auto deadzone_slider = ui->sliderRingAnalogDeadzone;
int 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);
}
void ConfigureRingController::ApplyConfiguration() {
emulated_controller->DisableConfiguration();
emulated_controller->SaveCurrentConfig();
emulated_controller->EnableConfiguration();
}
void ConfigureRingController::LoadConfiguration() {
UpdateUI();
}
void ConfigureRingController::RestoreDefaults() {
const std::string default_ring_string = InputCommon::GenerateAnalogParamFromKeys(
0, 0, QtConfig::default_ringcon_analogs[0], QtConfig::default_ringcon_analogs[1], 0, 0.05f);
emulated_controller->SetRingParam(Common::ParamPackage(default_ring_string));
UpdateUI();
}
void ConfigureRingController::EnableRingController() {
const auto dialog_title = tr("Error enabling ring input");
is_ring_enabled = false;
ui->ring_controller_sensor_value->setText(tr("Not connected"));
if (!Settings::values.enable_joycon_driver) {
QMessageBox::warning(this, dialog_title, tr("Direct Joycon driver is not enabled"));
return;
}
ui->enable_ring_controller_button->setEnabled(false);
ui->enable_ring_controller_button->setText(tr("Configuring"));
// SetPollingMode is blocking. Allow to update the button status before calling the command
repaint();
const auto result = emulated_controller->SetPollingMode(
Core::HID::EmulatedDeviceIndex::RightIndex, Common::Input::PollingMode::Ring);
switch (result) {
case Common::Input::DriverResult::Success:
is_ring_enabled = true;
break;
case Common::Input::DriverResult::NotSupported:
QMessageBox::warning(this, dialog_title,
tr("The current mapped device doesn't support the ring controller"));
break;
case Common::Input::DriverResult::NoDeviceDetected:
QMessageBox::warning(this, dialog_title,
tr("The current mapped device doesn't have a ring attached"));
break;
case Common::Input::DriverResult::InvalidHandle:
QMessageBox::warning(this, dialog_title, tr("The current mapped device is not connected"));
break;
default:
QMessageBox::warning(this, dialog_title,
tr("Unexpected driver result %1").arg(static_cast<int>(result)));
break;
}
ui->enable_ring_controller_button->setEnabled(true);
ui->enable_ring_controller_button->setText(tr("Enable"));
}
void ConfigureRingController::ControllerUpdate(Core::HID::ControllerTriggerType type) {
if (!is_ring_enabled) {
return;
}
if (type != Core::HID::ControllerTriggerType::RingController) {
return;
}
const auto value = emulated_controller->GetRingSensorValues();
const auto tex_value = QString::fromStdString(fmt::format("{:.3f}", value.raw_value));
ui->ring_controller_sensor_value->setText(tex_value);
}
void ConfigureRingController::HandleClick(
QPushButton* button, std::function<void(const Common::ParamPackage&)> new_input_setter,
InputCommon::Polling::InputType type) {
button->setText(tr("[waiting]"));
button->setFocus();
input_setter = new_input_setter;
input_subsystem->BeginMapping(type);
QWidget::grabMouse();
QWidget::grabKeyboard();
timeout_timer->start(2500); // Cancel after 2.5 seconds
poll_timer->start(25); // Check for new inputs every 25ms
}
void ConfigureRingController::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();
input_setter = std::nullopt;
}
bool ConfigureRingController::IsInputAcceptable(const Common::ParamPackage& params) const {
return true;
}
void ConfigureRingController::mousePressEvent(QMouseEvent* event) {
if (!input_setter || !event) {
return;
}
const auto button = GRenderWindow::QtButtonToMouseButton(event->button());
input_subsystem->GetMouse()->PressButton(0, 0, button);
}
void ConfigureRingController::keyPressEvent(QKeyEvent* event) {
if (!input_setter || !event) {
return;
}
event->ignore();
if (event->key() != Qt::Key_Escape) {
input_subsystem->GetKeyboard()->PressKey(event->key());
}
}
QString ConfigureRingController::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 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").arg(toggle, 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 = QString::fromStdString(param.Get("direction", ""));
return QObject::tr("%1%2Hat %3").arg(toggle, inverted, hat);
}
if (param.Has("axis")) {
const QString axis = QString::fromStdString(param.Get("axis", ""));
return QObject::tr("%1%2Axis %3").arg(toggle, inverted, 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%2Button %3").arg(toggle, inverted, button);
}
}
QString button_name = GetButtonName(common_button_name);
if (param.Has("hat")) {
return QObject::tr("%1%2Hat %3").arg(toggle, inverted, button_name);
}
if (param.Has("axis")) {
return QObject::tr("%1%2Axis %3").arg(toggle, inverted, 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%2Button %3").arg(toggle, inverted, button_name);
}
return QObject::tr("[unknown]");
}
QString ConfigureRingController::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]");
}