Path: blob/master/src/duckstation-qt/controllersettingswindow.cpp
7418 views
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <[email protected]>1// SPDX-License-Identifier: CC-BY-NC-ND-4.023#include "controllersettingswindow.h"4#include "controllerbindingwidgets.h"5#include "controllerglobalsettingswidget.h"6#include "hotkeysettingswidget.h"7#include "mainwindow.h"8#include "qthost.h"910#include "core/controller.h"11#include "core/core.h"1213#include "util/ini_settings_interface.h"14#include "util/input_manager.h"1516#include "common/assert.h"17#include "common/file_system.h"1819#include <QtWidgets/QInputDialog>20#include <QtWidgets/QTextEdit>21#include <array>2223#include "moc_controllersettingswindow.cpp"2425using namespace Qt::StringLiterals;2627ControllerSettingsWindow::ControllerSettingsWindow(INISettingsInterface* game_sif /* = nullptr */,28bool edit_profiles /* = false */, QWidget* parent /* = nullptr */)29: QWidget(parent), m_editing_settings_interface(game_sif), m_editing_input_profiles(edit_profiles)30{31m_ui.setupUi(this);32m_ui.buttonBox->button(QDialogButtonBox::Close)->setDefault(true);3334setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);3536connect(m_ui.settingsCategory, &QListWidget::currentRowChanged, this,37&ControllerSettingsWindow::onCategoryCurrentRowChanged);38connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &ControllerSettingsWindow::close);3940if (!game_sif && !edit_profiles)41{42// editing global settings43m_ui.editProfileLayout->removeWidget(m_ui.editProfileLabel);44delete m_ui.editProfileLabel;45m_ui.editProfileLabel = nullptr;46m_ui.editProfileLayout->removeWidget(m_ui.currentProfile);47delete m_ui.currentProfile;48m_ui.currentProfile = nullptr;49m_ui.editProfileLayout->removeWidget(m_ui.newProfile);50delete m_ui.newProfile;51m_ui.newProfile = nullptr;52m_ui.editProfileLayout->removeWidget(m_ui.applyProfile);53delete m_ui.applyProfile;54m_ui.applyProfile = nullptr;55m_ui.editProfileLayout->removeWidget(m_ui.deleteProfile);56delete m_ui.deleteProfile;57m_ui.deleteProfile = nullptr;58m_ui.editProfileLayout->removeWidget(m_ui.copyGlobalSettings);59delete m_ui.copyGlobalSettings;60m_ui.copyGlobalSettings = nullptr;6162if (QPushButton* button = m_ui.buttonBox->button(QDialogButtonBox::RestoreDefaults))63connect(button, &QPushButton::clicked, this, &ControllerSettingsWindow::onRestoreDefaultsClicked);64}65else66{67if (QPushButton* button = m_ui.buttonBox->button(QDialogButtonBox::RestoreDefaults))68m_ui.buttonBox->removeButton(button);6970connect(m_ui.copyGlobalSettings, &QPushButton::clicked, this,71&ControllerSettingsWindow::onCopyGlobalSettingsClicked);7273if (edit_profiles)74{75setWindowTitle(tr("DuckStation Controller Presets"));76refreshProfileList();7778connect(m_ui.currentProfile, &QComboBox::currentIndexChanged, this,79&ControllerSettingsWindow::onCurrentProfileChanged);80connect(m_ui.newProfile, &QPushButton::clicked, this, &ControllerSettingsWindow::onNewProfileClicked);81connect(m_ui.applyProfile, &QPushButton::clicked, this, &ControllerSettingsWindow::onApplyProfileClicked);82connect(m_ui.deleteProfile, &QPushButton::clicked, this, &ControllerSettingsWindow::onDeleteProfileClicked);83}84else85{86// editing game settings87m_ui.editProfileLayout->removeWidget(m_ui.editProfileLabel);88delete m_ui.editProfileLabel;89m_ui.editProfileLabel = nullptr;90m_ui.editProfileLayout->removeWidget(m_ui.currentProfile);91delete m_ui.currentProfile;92m_ui.currentProfile = nullptr;93m_ui.editProfileLayout->removeWidget(m_ui.newProfile);94delete m_ui.newProfile;95m_ui.newProfile = nullptr;96m_ui.editProfileLayout->removeWidget(m_ui.applyProfile);97delete m_ui.applyProfile;98m_ui.applyProfile = nullptr;99m_ui.editProfileLayout->removeWidget(m_ui.deleteProfile);100delete m_ui.deleteProfile;101m_ui.deleteProfile = nullptr;102}103}104105if (m_ui.settingsContainer->count() == 0)106createWidgets();107}108109ControllerSettingsWindow::~ControllerSettingsWindow() = default;110111void ControllerSettingsWindow::editControllerSettingsForGame(QWidget* parent, INISettingsInterface* sif)112{113ControllerSettingsWindow* dlg = new ControllerSettingsWindow(sif, false, parent);114dlg->setWindowFlag(Qt::Window);115dlg->setAttribute(Qt::WA_DeleteOnClose);116dlg->setWindowModality(Qt::WindowModal);117dlg->setWindowTitle(parent->windowTitle());118dlg->setWindowIcon(parent->windowIcon());119dlg->show();120}121122int ControllerSettingsWindow::getHotkeyCategoryIndex() const123{124const std::array<bool, 2> mtap_enabled = getEnabledMultitaps();125return 1 + (mtap_enabled[0] ? 4 : 1) + (mtap_enabled[1] ? 4 : 1);126}127128int ControllerSettingsWindow::getCategoryRow() const129{130return m_ui.settingsCategory->currentRow();131}132133void ControllerSettingsWindow::setCategoryRow(int row)134{135m_ui.settingsCategory->setCurrentRow(row);136}137138void ControllerSettingsWindow::setCategory(u32 category)139{140switch (category)141{142case CATEGORY_GLOBAL_SETTINGS:143m_ui.settingsCategory->setCurrentRow(0);144break;145146case CATEGORY_FIRST_CONTROLLER_SETTINGS:147m_ui.settingsCategory->setCurrentRow(1);148break;149150case CATEGORY_HOTKEY_SETTINGS:151m_ui.settingsCategory->setCurrentRow(getHotkeyCategoryIndex());152break;153154default:155break;156}157}158159void ControllerSettingsWindow::onCategoryCurrentRowChanged(int row)160{161m_ui.settingsContainer->setCurrentIndex(row);162}163164void ControllerSettingsWindow::onCurrentProfileChanged(int index)165{166switchProfile(m_ui.currentProfile->itemText(index).toStdString());167}168169void ControllerSettingsWindow::onNewProfileClicked()170{171const std::string profile_name =172QInputDialog::getText(this, tr("Create Controller Preset"), tr("Enter the name for the new controller preset:"))173.toStdString();174if (profile_name.empty())175return;176177std::string profile_path = System::GetInputProfilePath(profile_name);178if (FileSystem::FileExists(profile_path.c_str()))179{180QtUtils::AsyncMessageBox(181this, QMessageBox::Critical, tr("Error"),182tr("A preset with the name '%1' already exists.").arg(QString::fromStdString(profile_name)));183return;184}185186const int res =187QtUtils::MessageBoxQuestion(this, tr("Create Controller Preset"),188tr("Do you want to copy all bindings from the currently-selected preset to "189"the new preset? Selecting No will create a completely empty preset."),190QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);191if (res == QMessageBox::Cancel)192return;193194INISettingsInterface temp_si(std::move(profile_path));195if (res == QMessageBox::Yes)196{197// copy from global or the current profile198if (!m_editing_settings_interface)199{200const int hkres = QtUtils::MessageBoxQuestion(201this, tr("Create Controller Preset"),202tr("Do you want to copy the current hotkey bindings from global settings to the new controller preset?"),203QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);204if (hkres == QMessageBox::Cancel)205return;206207const bool copy_hotkey_bindings = (hkres == QMessageBox::Yes);208if (copy_hotkey_bindings)209temp_si.SetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", true);210211// from global212const auto lock = Core::GetSettingsLock();213InputManager::CopyConfiguration(&temp_si, *Core::GetBaseSettingsLayer(), true, false, true, copy_hotkey_bindings);214}215else216{217// from profile218const bool copy_hotkey_bindings =219m_editing_settings_interface->GetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", false);220const bool copy_sources =221m_editing_settings_interface->GetBoolValue("ControllerPorts", "UseProfileInputSources", false);222temp_si.SetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", copy_hotkey_bindings);223temp_si.SetBoolValue("ControllerPorts", "UseProfileInputSources", copy_sources);224InputManager::CopyConfiguration(&temp_si, *m_editing_settings_interface, true, copy_sources, true,225copy_hotkey_bindings);226}227}228229if (!temp_si.Save())230{231QtUtils::AsyncMessageBox(232this, QMessageBox::Critical, tr("Error"),233tr("Failed to save the new preset to '%1'.").arg(QString::fromStdString(temp_si.GetPath())));234return;235}236237refreshProfileList();238switchProfile(profile_name);239}240241void ControllerSettingsWindow::onApplyProfileClicked()242{243if (QtUtils::MessageBoxQuestion(this, tr("Load Controller Preset"),244tr("Are you sure you want to apply the controller preset named '%1'?\n\n"245"All current global bindings will be removed, and the preset bindings loaded.\n\n"246"You cannot undo this action.")247.arg(m_profile_name)) != QMessageBox::Yes)248{249return;250}251252{253const bool copy_hotkey_bindings =254m_editing_settings_interface->GetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", false);255const bool copy_sources =256m_editing_settings_interface->GetBoolValue("ControllerPorts", "UseProfileInputSources", false);257const auto lock = Core::GetSettingsLock();258InputManager::CopyConfiguration(Core::GetBaseSettingsLayer(), *m_editing_settings_interface, true, copy_sources,259true, copy_hotkey_bindings);260QtHost::QueueSettingsSave();261}262g_core_thread->applySettings();263264// Recreate global widget on profile apply265g_main_window->getControllerSettingsWindow()->createWidgets();266}267268void ControllerSettingsWindow::onDeleteProfileClicked()269{270if (QtUtils::MessageBoxQuestion(this, tr("Delete Controller Preset"),271tr("Are you sure you want to delete the controller preset named '%1'?\n\n"272"You cannot undo this action.")273.arg(m_profile_name)) != QMessageBox::Yes)274{275return;276}277278std::string profile_path(System::GetInputProfilePath(m_profile_name.toStdString()));279if (!FileSystem::DeleteFile(profile_path.c_str()))280{281QtUtils::AsyncMessageBox(this, QMessageBox::Critical, tr("Error"),282tr("Failed to delete '%1'.").arg(QString::fromStdString(profile_path)));283return;284}285286// switch back to global287refreshProfileList();288switchProfile({});289}290291void ControllerSettingsWindow::onRestoreDefaultsClicked()292{293if (QtUtils::MessageBoxQuestion(this, tr("Restore Defaults"),294tr("Are you sure you want to restore the default controller configuration?\n\n"295"All bindings and configuration will be lost. You cannot undo this action.")) !=296QMessageBox::Yes)297{298return;299}300301// actually restore it302g_core_thread->setDefaultSettings(false, false, true);303304// reload all settings305createWidgets();306}307308void ControllerSettingsWindow::onCopyGlobalSettingsClicked()309{310DebugAssert(!isEditingGlobalSettings());311312{313const auto lock = Core::GetSettingsLock();314InputManager::CopyConfiguration(m_editing_settings_interface, *Core::GetBaseSettingsLayer(), true, false, true,315false);316}317318m_editing_settings_interface->Save();319g_core_thread->reloadGameSettings();320createWidgets();321322QtUtils::AsyncMessageBox(this, QMessageBox::Information, tr("DuckStation Controller Settings"),323isEditingGameSettings() ? tr("Per-game controller configuration reset to global settings.") :324tr("Controller preset reset to global settings."));325}326327bool ControllerSettingsWindow::getBoolValue(const char* section, const char* key, bool default_value) const328{329if (m_editing_settings_interface)330return m_editing_settings_interface->GetBoolValue(section, key, default_value);331else332return Core::GetBaseBoolSettingValue(section, key, default_value);333}334335s32 ControllerSettingsWindow::getIntValue(const char* section, const char* key, s32 default_value) const336{337if (m_editing_settings_interface)338return m_editing_settings_interface->GetIntValue(section, key, default_value);339else340return Core::GetBaseIntSettingValue(section, key, default_value);341}342343std::string ControllerSettingsWindow::getStringValue(const char* section, const char* key,344const char* default_value) const345{346std::string value;347if (m_editing_settings_interface)348value = m_editing_settings_interface->GetStringValue(section, key, default_value);349else350value = Core::GetBaseStringSettingValue(section, key, default_value);351return value;352}353354void ControllerSettingsWindow::setBoolValue(const char* section, const char* key, bool value)355{356if (m_editing_settings_interface)357{358m_editing_settings_interface->SetBoolValue(section, key, value);359saveAndReloadGameSettings();360}361else362{363Core::SetBaseBoolSettingValue(section, key, value);364Host::CommitBaseSettingChanges();365g_core_thread->applySettings();366}367}368369void ControllerSettingsWindow::setIntValue(const char* section, const char* key, s32 value)370{371if (m_editing_settings_interface)372{373m_editing_settings_interface->SetIntValue(section, key, value);374saveAndReloadGameSettings();375}376else377{378Core::SetBaseIntSettingValue(section, key, value);379Host::CommitBaseSettingChanges();380g_core_thread->applySettings();381}382}383384void ControllerSettingsWindow::setStringValue(const char* section, const char* key, const char* value)385{386if (m_editing_settings_interface)387{388m_editing_settings_interface->SetStringValue(section, key, value);389saveAndReloadGameSettings();390}391else392{393Core::SetBaseStringSettingValue(section, key, value);394Host::CommitBaseSettingChanges();395g_core_thread->applySettings();396}397}398399void ControllerSettingsWindow::saveAndReloadGameSettings()400{401DebugAssert(m_editing_settings_interface);402QtHost::SaveGameSettings(m_editing_settings_interface, false);403g_core_thread->reloadGameSettings(false);404}405406void ControllerSettingsWindow::clearSettingValue(const char* section, const char* key)407{408if (m_editing_settings_interface)409{410m_editing_settings_interface->DeleteValue(section, key);411m_editing_settings_interface->Save();412g_core_thread->reloadGameSettings();413}414else415{416Core::DeleteBaseSettingValue(section, key);417Host::CommitBaseSettingChanges();418g_core_thread->applySettings();419}420}421422void ControllerSettingsWindow::createWidgets()423{424QSignalBlocker sb(m_ui.settingsContainer);425QSignalBlocker sb2(m_ui.settingsCategory);426427while (m_ui.settingsContainer->count() > 0)428{429QWidget* widget = m_ui.settingsContainer->widget(m_ui.settingsContainer->count() - 1);430m_ui.settingsContainer->removeWidget(widget);431widget->deleteLater();432}433434m_ui.settingsCategory->clear();435436m_global_settings = nullptr;437m_hotkey_settings = nullptr;438439{440// global settings441QListWidgetItem* item = new QListWidgetItem();442item->setText(tr("Global Settings"));443item->setIcon(QIcon::fromTheme("settings-3-line"_L1));444m_ui.settingsCategory->addItem(item);445m_ui.settingsCategory->setCurrentRow(0);446m_global_settings = new ControllerGlobalSettingsWidget(m_ui.settingsContainer, this);447m_ui.settingsContainer->addWidget(m_global_settings);448connect(m_global_settings, &ControllerGlobalSettingsWidget::bindingSetupChanged, this,449&ControllerSettingsWindow::createWidgets);450}451452// load mtap settings453const std::array<bool, 2> mtap_enabled = getEnabledMultitaps();454for (u32 global_slot : Controller::PortDisplayOrder)455{456const bool is_mtap_port = Controller::PadIsMultitapSlot(global_slot);457const auto [port, slot] = Controller::ConvertPadToPortAndSlot(global_slot);458if (is_mtap_port && !mtap_enabled[port])459continue;460461m_port_bindings[global_slot] = new ControllerBindingWidget(m_ui.settingsContainer, this, global_slot);462m_ui.settingsContainer->addWidget(m_port_bindings[global_slot]);463464const QString display_name(465QtUtils::StringViewToQString(m_port_bindings[global_slot]->getControllerInfo()->GetDisplayName()));466467QListWidgetItem* item = new QListWidgetItem();468item->setText(tr("Controller Port %1\n%2")469.arg(Controller::GetPortDisplayName(port, slot, mtap_enabled[port]))470.arg(display_name));471item->setIcon(m_port_bindings[global_slot]->getIcon());472item->setData(Qt::UserRole, QVariant(global_slot));473m_ui.settingsCategory->addItem(item);474}475476// only add hotkeys if we're editing global settings477if (!m_editing_settings_interface ||478m_editing_settings_interface->GetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", false))479{480QListWidgetItem* item = new QListWidgetItem();481item->setText(tr("Hotkeys"));482item->setIcon(QIcon::fromTheme("keyboard-line"_L1));483m_ui.settingsCategory->addItem(item);484m_hotkey_settings = new HotkeySettingsWidget(m_ui.settingsContainer, this);485m_ui.settingsContainer->addWidget(m_hotkey_settings);486}487488if (isEditingProfile())489{490const bool enable_buttons = static_cast<bool>(m_profile_settings_interface);491m_ui.applyProfile->setEnabled(enable_buttons);492m_ui.deleteProfile->setEnabled(enable_buttons);493m_ui.copyGlobalSettings->setEnabled(enable_buttons);494}495}496497void ControllerSettingsWindow::closeEvent(QCloseEvent* event)498{499if (isEditingGlobalSettings())500QtUtils::SaveWindowGeometry(this);501502QWidget::closeEvent(event);503}504505void ControllerSettingsWindow::updateListDescription(u32 global_slot, ControllerBindingWidget* widget)506{507for (int i = 0; i < m_ui.settingsCategory->count(); i++)508{509QListWidgetItem* item = m_ui.settingsCategory->item(i);510const QVariant item_data(item->data(Qt::UserRole));511bool is_ok;512if (item_data.toUInt(&is_ok) == global_slot && is_ok)513{514const std::array<bool, 2> mtap_enabled = getEnabledMultitaps();515const auto [port, slot] = Controller::ConvertPadToPortAndSlot(global_slot);516517const QString display_name = QtUtils::StringViewToQString(widget->getControllerInfo()->GetDisplayName());518519item->setText(tr("Controller Port %1\n%2")520.arg(Controller::GetPortDisplayName(port, slot, mtap_enabled[port]))521.arg(display_name));522item->setIcon(widget->getIcon());523break;524}525}526}527528std::array<bool, 2> ControllerSettingsWindow::getEnabledMultitaps() const529{530const MultitapMode mtap_mode =531Settings::ParseMultitapModeName(532getStringValue("ControllerPorts", "MultitapMode", Settings::GetMultitapModeName(Settings::DEFAULT_MULTITAP_MODE))533.c_str())534.value_or(Settings::DEFAULT_MULTITAP_MODE);535return {{(mtap_mode == MultitapMode::Port1Only || mtap_mode == MultitapMode::BothPorts),536(mtap_mode == MultitapMode::Port2Only || mtap_mode == MultitapMode::BothPorts)}};537}538539void ControllerSettingsWindow::refreshProfileList()540{541const std::vector<std::string> names(InputManager::GetInputProfileNames());542543QSignalBlocker sb(m_ui.currentProfile);544m_ui.currentProfile->clear();545546bool current_profile_found = false;547for (const std::string& name : names)548{549const QString qname(QString::fromStdString(name));550m_ui.currentProfile->addItem(qname);551if (qname == m_profile_name)552{553m_ui.currentProfile->setCurrentIndex(m_ui.currentProfile->count() - 1);554current_profile_found = true;555}556}557558if (!current_profile_found)559switchProfile(names.empty() ? std::string_view() : std::string_view(names.front()));560}561562void ControllerSettingsWindow::switchProfile(const std::string_view name)563{564const QString name_qstr = QtUtils::StringViewToQString(name);565{566QSignalBlocker sb(m_ui.currentProfile);567m_ui.currentProfile->setCurrentIndex(m_ui.currentProfile->findText(name_qstr));568}569m_profile_name = name_qstr;570m_profile_settings_interface.reset();571m_editing_settings_interface = nullptr;572573// disable UI if there is no selection574const bool disable_ui = name.empty();575m_ui.settingsCategory->setDisabled(disable_ui);576m_ui.settingsContainer->setDisabled(disable_ui);577578if (name_qstr.isEmpty())579{580createWidgets();581return;582}583584std::string path = System::GetInputProfilePath(name);585if (!FileSystem::FileExists(path.c_str()))586{587QtUtils::AsyncMessageBox(this, QMessageBox::Critical, tr("Error"),588tr("The controller preset named '%1' cannot be found.").arg(name_qstr));589return;590}591592std::unique_ptr<INISettingsInterface> sif = std::make_unique<INISettingsInterface>(std::move(path));593sif->Load();594595m_profile_settings_interface = std::move(sif);596m_editing_settings_interface = m_profile_settings_interface.get();597598createWidgets();599}600601602