suyu/src/yuzu/applets/controller.cpp

637 lines
25 KiB
C++
Raw Normal View History

// Copyright 2020 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
2020-08-28 16:45:15 +00:00
#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.h"
#include "yuzu/configuration/configure_input_profile_dialog.h"
#include "yuzu/configuration/configure_vibration.h"
#include "yuzu/configuration/input_profiles.h"
#include "yuzu/main.h"
2020-08-28 16:45:15 +00:00
namespace {
constexpr std::size_t HANDHELD_INDEX = 8;
constexpr std::array<std::array<bool, 4>, 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<Service::HID::Hid>("hid")
->GetAppletResource()
->GetController<Service::HID::Controller_NPad>(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(
2020-08-28 15:44:36 +00:00
QWidget* parent, Core::Frontend::ControllerParameters parameters_,
InputCommon::InputSubsystem* input_subsystem_)
: QDialog(parent), ui(std::make_unique<Ui::QtControllerSelectorDialog>()),
parameters(std::move(parameters_)), input_subsystem{input_subsystem_},
input_profiles(std::make_unique<InputProfiles>()) {
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<int>(&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<int>(&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::CallConfigureInputProfileDialog);
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::CallConfigureInputProfileDialog() {
ConfigureInputProfileDialog dialog(this, input_subsystem, input_profiles.get());
dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint |
Qt::WindowSystemMenuHint);
dialog.setWindowModality(Qt::WindowModal);
dialog.exec();
}
bool QtControllerSelectorDialog::CheckIfParametersMet() {
// Here, we check and validate the current configuration against all applicable parameters.
const auto num_connected_players = static_cast<int>(
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<std::size_t>(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<std::size_t>(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<void()> 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();
}