// Copyright 2020 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include #include "common/assert.h" #include "common/string_util.h" #include "core/core.h" #include "core/hle/lock.h" #include "core/hle/service/hid/controllers/npad.h" #include "core/hle/service/hid/hid.h" #include "core/hle/service/sm/sm.h" #include "ui_controller.h" #include "yuzu/applets/controller.h" #include "yuzu/configuration/configure_input_dialog.h" #include "yuzu/configuration/configure_vibration.h" #include "yuzu/main.h" namespace { constexpr std::size_t HANDHELD_INDEX = 8; constexpr std::array, 8> led_patterns{{ {true, false, false, false}, {true, true, false, false}, {true, true, true, false}, {true, true, true, true}, {true, false, false, true}, {true, false, true, false}, {true, false, true, true}, {false, true, true, false}, }}; void UpdateController(Settings::ControllerType controller_type, std::size_t npad_index, bool connected) { Core::System& system{Core::System::GetInstance()}; if (!system.IsPoweredOn()) { return; } Service::SM::ServiceManager& sm = system.ServiceManager(); auto& npad = sm.GetService("hid") ->GetAppletResource() ->GetController(Service::HID::HidController::NPad); npad.UpdateControllerAt(npad.MapSettingsTypeToNPad(controller_type), npad_index, connected); } // Returns true if the given controller type is compatible with the given parameters. bool IsControllerCompatible(Settings::ControllerType controller_type, Core::Frontend::ControllerParameters parameters) { switch (controller_type) { case Settings::ControllerType::ProController: return parameters.allow_pro_controller; case Settings::ControllerType::DualJoyconDetached: return parameters.allow_dual_joycons; case Settings::ControllerType::LeftJoycon: return parameters.allow_left_joycon; case Settings::ControllerType::RightJoycon: return parameters.allow_right_joycon; case Settings::ControllerType::Handheld: return parameters.enable_single_mode && parameters.allow_handheld; default: return false; } } /// Maps the controller type combobox index to Controller Type enum constexpr Settings::ControllerType GetControllerTypeFromIndex(int index) { switch (index) { case 0: default: return Settings::ControllerType::ProController; case 1: return Settings::ControllerType::DualJoyconDetached; case 2: return Settings::ControllerType::LeftJoycon; case 3: return Settings::ControllerType::RightJoycon; case 4: return Settings::ControllerType::Handheld; } } /// Maps the Controller Type enum to controller type combobox index constexpr int GetIndexFromControllerType(Settings::ControllerType type) { switch (type) { case Settings::ControllerType::ProController: default: return 0; case Settings::ControllerType::DualJoyconDetached: return 1; case Settings::ControllerType::LeftJoycon: return 2; case Settings::ControllerType::RightJoycon: return 3; case Settings::ControllerType::Handheld: return 4; } } } // namespace QtControllerSelectorDialog::QtControllerSelectorDialog( QWidget* parent, Core::Frontend::ControllerParameters parameters_, InputCommon::InputSubsystem* input_subsystem_) : QDialog(parent), ui(std::make_unique()), parameters(std::move(parameters_)), input_subsystem(input_subsystem_) { ui->setupUi(this); player_widgets = { ui->widgetPlayer1, ui->widgetPlayer2, ui->widgetPlayer3, ui->widgetPlayer4, ui->widgetPlayer5, ui->widgetPlayer6, ui->widgetPlayer7, ui->widgetPlayer8, }; player_groupboxes = { ui->groupPlayer1Connected, ui->groupPlayer2Connected, ui->groupPlayer3Connected, ui->groupPlayer4Connected, ui->groupPlayer5Connected, ui->groupPlayer6Connected, ui->groupPlayer7Connected, ui->groupPlayer8Connected, }; connected_controller_icons = { ui->controllerPlayer1, ui->controllerPlayer2, ui->controllerPlayer3, ui->controllerPlayer4, ui->controllerPlayer5, ui->controllerPlayer6, ui->controllerPlayer7, ui->controllerPlayer8, }; led_patterns_boxes = {{ {ui->checkboxPlayer1LED1, ui->checkboxPlayer1LED2, ui->checkboxPlayer1LED3, ui->checkboxPlayer1LED4}, {ui->checkboxPlayer2LED1, ui->checkboxPlayer2LED2, ui->checkboxPlayer2LED3, ui->checkboxPlayer2LED4}, {ui->checkboxPlayer3LED1, ui->checkboxPlayer3LED2, ui->checkboxPlayer3LED3, ui->checkboxPlayer3LED4}, {ui->checkboxPlayer4LED1, ui->checkboxPlayer4LED2, ui->checkboxPlayer4LED3, ui->checkboxPlayer4LED4}, {ui->checkboxPlayer5LED1, ui->checkboxPlayer5LED2, ui->checkboxPlayer5LED3, ui->checkboxPlayer5LED4}, {ui->checkboxPlayer6LED1, ui->checkboxPlayer6LED2, ui->checkboxPlayer6LED3, ui->checkboxPlayer6LED4}, {ui->checkboxPlayer7LED1, ui->checkboxPlayer7LED2, ui->checkboxPlayer7LED3, ui->checkboxPlayer7LED4}, {ui->checkboxPlayer8LED1, ui->checkboxPlayer8LED2, ui->checkboxPlayer8LED3, ui->checkboxPlayer8LED4}, }}; explain_text_labels = { ui->labelPlayer1Explain, ui->labelPlayer2Explain, ui->labelPlayer3Explain, ui->labelPlayer4Explain, ui->labelPlayer5Explain, ui->labelPlayer6Explain, ui->labelPlayer7Explain, ui->labelPlayer8Explain, }; emulated_controllers = { ui->comboPlayer1Emulated, ui->comboPlayer2Emulated, ui->comboPlayer3Emulated, ui->comboPlayer4Emulated, ui->comboPlayer5Emulated, ui->comboPlayer6Emulated, ui->comboPlayer7Emulated, ui->comboPlayer8Emulated, }; player_labels = { ui->labelPlayer1, ui->labelPlayer2, ui->labelPlayer3, ui->labelPlayer4, ui->labelPlayer5, ui->labelPlayer6, ui->labelPlayer7, ui->labelPlayer8, }; connected_controller_labels = { ui->labelConnectedPlayer1, ui->labelConnectedPlayer2, ui->labelConnectedPlayer3, ui->labelConnectedPlayer4, ui->labelConnectedPlayer5, ui->labelConnectedPlayer6, ui->labelConnectedPlayer7, ui->labelConnectedPlayer8, }; connected_controller_checkboxes = { ui->checkboxPlayer1Connected, ui->checkboxPlayer2Connected, ui->checkboxPlayer3Connected, ui->checkboxPlayer4Connected, ui->checkboxPlayer5Connected, ui->checkboxPlayer6Connected, ui->checkboxPlayer7Connected, ui->checkboxPlayer8Connected, }; // Setup/load everything prior to setting up connections. // This avoids unintentionally changing the states of elements while loading them in. SetSupportedControllers(); DisableUnsupportedPlayers(); LoadConfiguration(); for (std::size_t i = 0; i < NUM_PLAYERS; ++i) { SetExplainText(i); UpdateControllerIcon(i); UpdateLEDPattern(i); UpdateBorderColor(i); connect(player_groupboxes[i], &QGroupBox::toggled, [this, i](bool checked) { if (checked) { for (std::size_t index = 0; index <= i; ++index) { connected_controller_checkboxes[index]->setChecked(checked); } } else { for (std::size_t index = i; index < NUM_PLAYERS; ++index) { connected_controller_checkboxes[index]->setChecked(checked); } } }); connect(emulated_controllers[i], qOverload(&QComboBox::currentIndexChanged), [this, i](int) { UpdateControllerIcon(i); UpdateControllerState(i); UpdateLEDPattern(i); CheckIfParametersMet(); }); connect(connected_controller_checkboxes[i], &QCheckBox::stateChanged, [this, i](int state) { player_groupboxes[i]->setChecked(state == Qt::Checked); UpdateControllerIcon(i); UpdateControllerState(i); UpdateLEDPattern(i); UpdateBorderColor(i); CheckIfParametersMet(); }); if (i == 0) { connect(emulated_controllers[i], qOverload(&QComboBox::currentIndexChanged), [this](int index) { UpdateDockedState(GetControllerTypeFromIndex(index) == Settings::ControllerType::Handheld); }); } } connect(ui->vibrationButton, &QPushButton::clicked, this, &QtControllerSelectorDialog::CallConfigureVibrationDialog); connect(ui->inputConfigButton, &QPushButton::clicked, this, &QtControllerSelectorDialog::CallConfigureInputDialog); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QtControllerSelectorDialog::ApplyConfiguration); // Enhancement: Check if the parameters have already been met before disconnecting controllers. // If all the parameters are met AND only allows a single player, // stop the constructor here as we do not need to continue. if (CheckIfParametersMet() && parameters.enable_single_mode) { return; } // If keep_controllers_connected is false, forcefully disconnect all controllers if (!parameters.keep_controllers_connected) { for (auto player : player_groupboxes) { player->setChecked(false); } } resize(0, 0); } QtControllerSelectorDialog::~QtControllerSelectorDialog() = default; int QtControllerSelectorDialog::exec() { if (parameters_met && parameters.enable_single_mode) { return QDialog::Accepted; } return QDialog::exec(); } void QtControllerSelectorDialog::ApplyConfiguration() { const bool pre_docked_mode = Settings::values.use_docked_mode.GetValue(); Settings::values.use_docked_mode.SetValue(ui->radioDocked->isChecked()); OnDockedModeChanged(pre_docked_mode, Settings::values.use_docked_mode.GetValue()); Settings::values.vibration_enabled.SetValue(ui->vibrationGroup->isChecked()); Settings::values.motion_enabled.SetValue(ui->motionGroup->isChecked()); } void QtControllerSelectorDialog::LoadConfiguration() { for (std::size_t index = 0; index < NUM_PLAYERS; ++index) { const auto connected = Settings::values.players.GetValue()[index].connected || (index == 0 && Settings::values.players.GetValue()[HANDHELD_INDEX].connected); player_groupboxes[index]->setChecked(connected); connected_controller_checkboxes[index]->setChecked(connected); emulated_controllers[index]->setCurrentIndex( GetIndexFromControllerType(Settings::values.players.GetValue()[index].controller_type)); } UpdateDockedState(Settings::values.players.GetValue()[HANDHELD_INDEX].connected); ui->vibrationGroup->setChecked(Settings::values.vibration_enabled.GetValue()); ui->motionGroup->setChecked(Settings::values.motion_enabled.GetValue()); } void QtControllerSelectorDialog::CallConfigureVibrationDialog() { ConfigureVibration dialog(this); dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowSystemMenuHint); dialog.setWindowModality(Qt::WindowModal); if (dialog.exec() == QDialog::Accepted) { dialog.ApplyConfiguration(); } } void QtControllerSelectorDialog::CallConfigureInputDialog() { const auto max_supported_players = parameters.enable_single_mode ? 1 : parameters.max_players; ConfigureInputDialog dialog(this, max_supported_players, input_subsystem); dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowSystemMenuHint); dialog.setWindowModality(Qt::WindowModal); dialog.exec(); dialog.ApplyConfiguration(); LoadConfiguration(); CheckIfParametersMet(); } bool QtControllerSelectorDialog::CheckIfParametersMet() { // Here, we check and validate the current configuration against all applicable parameters. const auto num_connected_players = static_cast( std::count_if(player_groupboxes.begin(), player_groupboxes.end(), [this](const QGroupBox* player) { return player->isChecked(); })); const auto min_supported_players = parameters.enable_single_mode ? 1 : parameters.min_players; const auto max_supported_players = parameters.enable_single_mode ? 1 : parameters.max_players; // First, check against the number of connected players. if (num_connected_players < min_supported_players || num_connected_players > max_supported_players) { parameters_met = false; ui->buttonBox->setEnabled(parameters_met); return parameters_met; } // Next, check against all connected controllers. const auto all_controllers_compatible = [this] { for (std::size_t index = 0; index < NUM_PLAYERS; ++index) { // Skip controllers that are not used, we only care about the currently connected ones. if (!player_groupboxes[index]->isChecked() || !player_groupboxes[index]->isEnabled()) { continue; } const auto compatible = IsControllerCompatible( GetControllerTypeFromIndex(emulated_controllers[index]->currentIndex()), parameters); // If any controller is found to be incompatible, return false early. if (!compatible) { return false; } } // Reaching here means all currently connected controllers are compatible. return true; }(); parameters_met = all_controllers_compatible; ui->buttonBox->setEnabled(parameters_met); return parameters_met; } void QtControllerSelectorDialog::SetSupportedControllers() { const QString theme = [this] { if (QIcon::themeName().contains(QStringLiteral("dark"))) { return QStringLiteral("_dark"); } else if (QIcon::themeName().contains(QStringLiteral("midnight"))) { return QStringLiteral("_midnight"); } else { return QString{}; } }(); if (parameters.enable_single_mode && parameters.allow_handheld) { ui->controllerSupported1->setStyleSheet( QStringLiteral("image: url(:/controller/applet_handheld%0); ").arg(theme)); } else { ui->controllerSupported1->setStyleSheet( QStringLiteral("image: url(:/controller/applet_handheld%0_disabled); ").arg(theme)); } if (parameters.allow_dual_joycons) { ui->controllerSupported2->setStyleSheet( QStringLiteral("image: url(:/controller/applet_dual_joycon%0); ").arg(theme)); } else { ui->controllerSupported2->setStyleSheet( QStringLiteral("image: url(:/controller/applet_dual_joycon%0_disabled); ").arg(theme)); } if (parameters.allow_left_joycon) { ui->controllerSupported3->setStyleSheet( QStringLiteral("image: url(:/controller/applet_joycon_left%0); ").arg(theme)); } else { ui->controllerSupported3->setStyleSheet( QStringLiteral("image: url(:/controller/applet_joycon_left%0_disabled); ").arg(theme)); } if (parameters.allow_right_joycon) { ui->controllerSupported4->setStyleSheet( QStringLiteral("image: url(:/controller/applet_joycon_right%0); ").arg(theme)); } else { ui->controllerSupported4->setStyleSheet( QStringLiteral("image: url(:/controller/applet_joycon_right%0_disabled); ").arg(theme)); } if (parameters.allow_pro_controller) { ui->controllerSupported5->setStyleSheet( QStringLiteral("image: url(:/controller/applet_pro_controller%0); ").arg(theme)); } else { ui->controllerSupported5->setStyleSheet( QStringLiteral("image: url(:/controller/applet_pro_controller%0_disabled); ") .arg(theme)); } // enable_single_mode overrides min_players and max_players. if (parameters.enable_single_mode) { ui->numberSupportedLabel->setText(QStringLiteral("1")); return; } if (parameters.min_players == parameters.max_players) { ui->numberSupportedLabel->setText(QStringLiteral("%1").arg(parameters.max_players)); } else { ui->numberSupportedLabel->setText( QStringLiteral("%1 - %2").arg(parameters.min_players).arg(parameters.max_players)); } } void QtControllerSelectorDialog::UpdateControllerIcon(std::size_t player_index) { if (!player_groupboxes[player_index]->isChecked()) { connected_controller_icons[player_index]->setStyleSheet(QString{}); player_labels[player_index]->show(); return; } const QString stylesheet = [this, player_index] { switch (GetControllerTypeFromIndex(emulated_controllers[player_index]->currentIndex())) { case Settings::ControllerType::ProController: return QStringLiteral("image: url(:/controller/applet_pro_controller%0); "); case Settings::ControllerType::DualJoyconDetached: return QStringLiteral("image: url(:/controller/applet_dual_joycon%0); "); case Settings::ControllerType::LeftJoycon: return QStringLiteral("image: url(:/controller/applet_joycon_left%0); "); case Settings::ControllerType::RightJoycon: return QStringLiteral("image: url(:/controller/applet_joycon_right%0); "); case Settings::ControllerType::Handheld: return QStringLiteral("image: url(:/controller/applet_handheld%0); "); default: return QString{}; } }(); const QString theme = [this] { if (QIcon::themeName().contains(QStringLiteral("dark"))) { return QStringLiteral("_dark"); } else if (QIcon::themeName().contains(QStringLiteral("midnight"))) { return QStringLiteral("_midnight"); } else { return QString{}; } }(); connected_controller_icons[player_index]->setStyleSheet(stylesheet.arg(theme)); player_labels[player_index]->hide(); } void QtControllerSelectorDialog::UpdateControllerState(std::size_t player_index) { auto& player = Settings::values.players.GetValue()[player_index]; const auto controller_type = GetControllerTypeFromIndex(emulated_controllers[player_index]->currentIndex()); const auto player_connected = player_groupboxes[player_index]->isChecked() && controller_type != Settings::ControllerType::Handheld; if (player.controller_type == controller_type && player.connected == player_connected) { // Set vibration devices in the event that the input device has changed. ConfigureVibration::SetVibrationDevices(player_index); return; } // Disconnect the controller first. UpdateController(controller_type, player_index, false); player.controller_type = controller_type; player.connected = player_connected; ConfigureVibration::SetVibrationDevices(player_index); // Handheld if (player_index == 0) { auto& handheld = Settings::values.players.GetValue()[HANDHELD_INDEX]; if (controller_type == Settings::ControllerType::Handheld) { handheld = player; } handheld.connected = player_groupboxes[player_index]->isChecked() && controller_type == Settings::ControllerType::Handheld; UpdateController(Settings::ControllerType::Handheld, 8, handheld.connected); } if (!player.connected) { return; } // This emulates a delay between disconnecting and reconnecting controllers as some games // do not respond to a change in controller type if it was instantaneous. using namespace std::chrono_literals; std::this_thread::sleep_for(20ms); UpdateController(controller_type, player_index, player_connected); } void QtControllerSelectorDialog::UpdateLEDPattern(std::size_t player_index) { if (!player_groupboxes[player_index]->isChecked() || GetControllerTypeFromIndex(emulated_controllers[player_index]->currentIndex()) == Settings::ControllerType::Handheld) { led_patterns_boxes[player_index][0]->setChecked(false); led_patterns_boxes[player_index][1]->setChecked(false); led_patterns_boxes[player_index][2]->setChecked(false); led_patterns_boxes[player_index][3]->setChecked(false); return; } led_patterns_boxes[player_index][0]->setChecked(led_patterns[player_index][0]); led_patterns_boxes[player_index][1]->setChecked(led_patterns[player_index][1]); led_patterns_boxes[player_index][2]->setChecked(led_patterns[player_index][2]); led_patterns_boxes[player_index][3]->setChecked(led_patterns[player_index][3]); } void QtControllerSelectorDialog::UpdateBorderColor(std::size_t player_index) { if (!parameters.enable_border_color || player_index >= static_cast(parameters.max_players) || player_groupboxes[player_index]->styleSheet().contains(QStringLiteral("QGroupBox"))) { return; } player_groupboxes[player_index]->setStyleSheet( player_groupboxes[player_index]->styleSheet().append( QStringLiteral("QGroupBox#groupPlayer%1Connected:checked " "{ border: 1px solid rgba(%2, %3, %4, %5); }") .arg(player_index + 1) .arg(parameters.border_colors[player_index][0]) .arg(parameters.border_colors[player_index][1]) .arg(parameters.border_colors[player_index][2]) .arg(parameters.border_colors[player_index][3]))); } void QtControllerSelectorDialog::SetExplainText(std::size_t player_index) { if (!parameters.enable_explain_text || player_index >= static_cast(parameters.max_players)) { return; } explain_text_labels[player_index]->setText(QString::fromStdString( Common::StringFromFixedZeroTerminatedBuffer(parameters.explain_text[player_index].data(), parameters.explain_text[player_index].size()))); } void QtControllerSelectorDialog::UpdateDockedState(bool is_handheld) { // Disallow changing the console mode if the controller type is handheld. ui->radioDocked->setEnabled(!is_handheld); ui->radioUndocked->setEnabled(!is_handheld); ui->radioDocked->setChecked(Settings::values.use_docked_mode.GetValue()); ui->radioUndocked->setChecked(!Settings::values.use_docked_mode.GetValue()); // Also force into undocked mode if the controller type is handheld. if (is_handheld) { ui->radioUndocked->setChecked(true); } } void QtControllerSelectorDialog::DisableUnsupportedPlayers() { const auto max_supported_players = parameters.enable_single_mode ? 1 : parameters.max_players; switch (max_supported_players) { case 0: default: UNREACHABLE(); return; case 1: ui->widgetSpacer->hide(); ui->widgetSpacer2->hide(); ui->widgetSpacer3->hide(); ui->widgetSpacer4->hide(); break; case 2: ui->widgetSpacer->hide(); ui->widgetSpacer2->hide(); ui->widgetSpacer3->hide(); break; case 3: ui->widgetSpacer->hide(); ui->widgetSpacer2->hide(); break; case 4: ui->widgetSpacer->hide(); break; case 5: case 6: case 7: case 8: break; } for (std::size_t index = max_supported_players; index < NUM_PLAYERS; ++index) { // Disconnect any unsupported players here and disable or hide them if applicable. Settings::values.players.GetValue()[index].connected = false; UpdateController(Settings::values.players.GetValue()[index].controller_type, index, false); // Hide the player widgets when max_supported_controllers is less than or equal to 4. if (max_supported_players <= 4) { player_widgets[index]->hide(); } // Disable and hide the following to prevent these from interaction. player_widgets[index]->setDisabled(true); connected_controller_checkboxes[index]->setDisabled(true); connected_controller_labels[index]->hide(); connected_controller_checkboxes[index]->hide(); } } QtControllerSelector::QtControllerSelector(GMainWindow& parent) { connect(this, &QtControllerSelector::MainWindowReconfigureControllers, &parent, &GMainWindow::ControllerSelectorReconfigureControllers, Qt::QueuedConnection); connect(&parent, &GMainWindow::ControllerSelectorReconfigureFinished, this, &QtControllerSelector::MainWindowReconfigureFinished, Qt::QueuedConnection); } QtControllerSelector::~QtControllerSelector() = default; void QtControllerSelector::ReconfigureControllers( std::function callback, const Core::Frontend::ControllerParameters& parameters) const { this->callback = std::move(callback); emit MainWindowReconfigureControllers(parameters); } void QtControllerSelector::MainWindowReconfigureFinished() { // Acquire the HLE mutex std::lock_guard lock(HLE::g_hle_lock); callback(); }