diff options
Diffstat (limited to 'src/citron/configuration/configure_ringcon.cpp')
-rw-r--r-- | src/citron/configuration/configure_ringcon.cpp | 497 |
1 files changed, 497 insertions, 0 deletions
diff --git a/src/citron/configuration/configure_ringcon.cpp b/src/citron/configuration/configure_ringcon.cpp new file mode 100644 index 000000000..9fd094ab6 --- /dev/null +++ b/src/citron/configuration/configure_ringcon.cpp @@ -0,0 +1,497 @@ +// 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 "hid_core/frontend/emulated_controller.h" +#include "hid_core/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]"); +} |