Path: blob/master/src/duckstation-qt/controllersettingswindow.cpp
4246 views
// SPDX-FileCopyrightText: 2019-2025 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/host.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/QMessageBox>21#include <QtWidgets/QTextEdit>22#include <array>2324#include "moc_controllersettingswindow.cpp"2526ControllerSettingsWindow::ControllerSettingsWindow(INISettingsInterface* game_sif /* = nullptr */,27bool edit_profiles /* = false */, QWidget* parent /* = nullptr */)28: QWidget(parent), m_editing_settings_interface(game_sif), m_editing_input_profiles(edit_profiles)29{30m_ui.setupUi(this);31m_ui.buttonBox->button(QDialogButtonBox::Close)->setDefault(true);3233setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);3435connect(m_ui.settingsCategory, &QListWidget::currentRowChanged, this,36&ControllerSettingsWindow::onCategoryCurrentRowChanged);37connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &ControllerSettingsWindow::close);3839if (!game_sif && !edit_profiles)40{41// editing global settings42m_ui.editProfileLayout->removeWidget(m_ui.editProfileLabel);43delete m_ui.editProfileLabel;44m_ui.editProfileLabel = nullptr;45m_ui.editProfileLayout->removeWidget(m_ui.currentProfile);46delete m_ui.currentProfile;47m_ui.currentProfile = nullptr;48m_ui.editProfileLayout->removeWidget(m_ui.newProfile);49delete m_ui.newProfile;50m_ui.newProfile = nullptr;51m_ui.editProfileLayout->removeWidget(m_ui.applyProfile);52delete m_ui.applyProfile;53m_ui.applyProfile = nullptr;54m_ui.editProfileLayout->removeWidget(m_ui.deleteProfile);55delete m_ui.deleteProfile;56m_ui.deleteProfile = nullptr;57m_ui.editProfileLayout->removeWidget(m_ui.copyGlobalSettings);58delete m_ui.copyGlobalSettings;59m_ui.copyGlobalSettings = nullptr;6061if (QPushButton* button = m_ui.buttonBox->button(QDialogButtonBox::RestoreDefaults))62connect(button, &QPushButton::clicked, this, &ControllerSettingsWindow::onRestoreDefaultsClicked);63}64else65{66if (QPushButton* button = m_ui.buttonBox->button(QDialogButtonBox::RestoreDefaults))67m_ui.buttonBox->removeButton(button);6869connect(m_ui.copyGlobalSettings, &QPushButton::clicked, this,70&ControllerSettingsWindow::onCopyGlobalSettingsClicked);7172if (edit_profiles)73{74setWindowTitle(tr("DuckStation Controller Presets"));75refreshProfileList();7677connect(m_ui.currentProfile, &QComboBox::currentIndexChanged, this,78&ControllerSettingsWindow::onCurrentProfileChanged);79connect(m_ui.newProfile, &QPushButton::clicked, this, &ControllerSettingsWindow::onNewProfileClicked);80connect(m_ui.applyProfile, &QPushButton::clicked, this, &ControllerSettingsWindow::onApplyProfileClicked);81connect(m_ui.deleteProfile, &QPushButton::clicked, this, &ControllerSettingsWindow::onDeleteProfileClicked);82}83else84{85// editing game settings86m_ui.editProfileLayout->removeWidget(m_ui.editProfileLabel);87delete m_ui.editProfileLabel;88m_ui.editProfileLabel = nullptr;89m_ui.editProfileLayout->removeWidget(m_ui.currentProfile);90delete m_ui.currentProfile;91m_ui.currentProfile = nullptr;92m_ui.editProfileLayout->removeWidget(m_ui.newProfile);93delete m_ui.newProfile;94m_ui.newProfile = nullptr;95m_ui.editProfileLayout->removeWidget(m_ui.applyProfile);96delete m_ui.applyProfile;97m_ui.applyProfile = nullptr;98m_ui.editProfileLayout->removeWidget(m_ui.deleteProfile);99delete m_ui.deleteProfile;100m_ui.deleteProfile = nullptr;101}102}103104createWidgets();105}106107ControllerSettingsWindow::~ControllerSettingsWindow() = default;108109void ControllerSettingsWindow::editControllerSettingsForGame(QWidget* parent, INISettingsInterface* sif)110{111ControllerSettingsWindow* dlg = new ControllerSettingsWindow(sif, false, parent);112dlg->setWindowFlag(Qt::Window);113dlg->setAttribute(Qt::WA_DeleteOnClose);114dlg->setWindowModality(Qt::WindowModality::WindowModal);115dlg->setWindowTitle(parent->windowTitle());116dlg->setWindowIcon(parent->windowIcon());117dlg->show();118}119120int ControllerSettingsWindow::getHotkeyCategoryIndex() const121{122const std::array<bool, 2> mtap_enabled = getEnabledMultitaps();123return 1 + (mtap_enabled[0] ? 4 : 1) + (mtap_enabled[1] ? 4 : 1);124}125126ControllerSettingsWindow::Category ControllerSettingsWindow::getCurrentCategory() const127{128const int index = m_ui.settingsCategory->currentRow();129if (index == 0)130return Category::GlobalSettings;131else if (index >= getHotkeyCategoryIndex())132return Category::HotkeySettings;133else134return Category::FirstControllerSettings;135}136137void ControllerSettingsWindow::setCategory(Category category)138{139switch (category)140{141case Category::GlobalSettings:142m_ui.settingsCategory->setCurrentRow(0);143break;144145case Category::FirstControllerSettings:146m_ui.settingsCategory->setCurrentRow(1);147break;148149case Category::HotkeySettings:150m_ui.settingsCategory->setCurrentRow(getHotkeyCategoryIndex());151break;152153default:154break;155}156}157158void ControllerSettingsWindow::onCategoryCurrentRowChanged(int row)159{160m_ui.settingsContainer->setCurrentIndex(row);161}162163void ControllerSettingsWindow::onCurrentProfileChanged(int index)164{165switchProfile(m_ui.currentProfile->itemText(index).toStdString());166}167168void ControllerSettingsWindow::onNewProfileClicked()169{170const std::string profile_name =171QInputDialog::getText(this, tr("Create Controller Preset"), tr("Enter the name for the new controller preset:"))172.toStdString();173if (profile_name.empty())174return;175176std::string profile_path = System::GetInputProfilePath(profile_name);177if (FileSystem::FileExists(profile_path.c_str()))178{179QMessageBox::critical(this, tr("Error"),180tr("A preset with the name '%1' already exists.").arg(QString::fromStdString(profile_name)));181return;182}183184const int res = QMessageBox::question(this, tr("Create Controller Preset"),185tr("Do you want to copy all bindings from the currently-selected preset to "186"the new preset? Selecting No will create a completely empty preset."),187QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);188if (res == QMessageBox::Cancel)189return;190191INISettingsInterface temp_si(std::move(profile_path));192if (res == QMessageBox::Yes)193{194// copy from global or the current profile195if (!m_editing_settings_interface)196{197const int hkres = QMessageBox::question(198this, tr("Create Controller Preset"),199tr("Do you want to copy the current hotkey bindings from global settings to the new controller preset?"),200QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);201if (hkres == QMessageBox::Cancel)202return;203204const bool copy_hotkey_bindings = (hkres == QMessageBox::Yes);205if (copy_hotkey_bindings)206temp_si.SetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", true);207208// from global209auto lock = Host::GetSettingsLock();210InputManager::CopyConfiguration(&temp_si, *Host::Internal::GetBaseSettingsLayer(), true, true, true,211copy_hotkey_bindings);212}213else214{215// from profile216const bool copy_hotkey_bindings =217m_editing_settings_interface->GetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", false);218temp_si.SetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", copy_hotkey_bindings);219InputManager::CopyConfiguration(&temp_si, *m_editing_settings_interface, true, true, true, copy_hotkey_bindings);220}221}222else223{224// still need to copy the source config225if (!m_editing_settings_interface)226InputManager::CopyConfiguration(&temp_si, *Host::Internal::GetBaseSettingsLayer(), false, true, false, false);227else228InputManager::CopyConfiguration(&temp_si, *m_editing_settings_interface, false, true, false, false);229}230231if (!temp_si.Save())232{233QMessageBox::critical(this, tr("Error"),234tr("Failed to save the new preset to '%1'.").arg(QString::fromStdString(temp_si.GetPath())));235return;236}237238refreshProfileList();239switchProfile(profile_name);240}241242void ControllerSettingsWindow::onApplyProfileClicked()243{244if (QMessageBox::question(this, tr("Load Controller Preset"),245tr("Are you sure you want to apply the controller preset named '%1'?\n\n"246"All current global bindings will be removed, and the preset bindings loaded.\n\n"247"You cannot undo this action.")248.arg(m_profile_name)) != QMessageBox::Yes)249{250return;251}252253{254const bool copy_hotkey_bindings =255m_editing_settings_interface->GetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", false);256auto lock = Host::GetSettingsLock();257InputManager::CopyConfiguration(Host::Internal::GetBaseSettingsLayer(), *m_editing_settings_interface, true, true,258true, copy_hotkey_bindings);259QtHost::QueueSettingsSave();260}261g_emu_thread->applySettings();262263// Recreate global widget on profile apply264g_main_window->getControllerSettingsWindow()->createWidgets();265}266267void ControllerSettingsWindow::onDeleteProfileClicked()268{269if (QMessageBox::question(this, tr("Delete Controller Preset"),270tr("Are you sure you want to delete the controller preset named '%1'?\n\n"271"You cannot undo this action.")272.arg(m_profile_name)) != QMessageBox::Yes)273{274return;275}276277std::string profile_path(System::GetInputProfilePath(m_profile_name.toStdString()));278if (!FileSystem::DeleteFile(profile_path.c_str()))279{280QMessageBox::critical(this, tr("Error"), tr("Failed to delete '%1'.").arg(QString::fromStdString(profile_path)));281return;282}283284// switch back to global285refreshProfileList();286switchProfile({});287}288289void ControllerSettingsWindow::onRestoreDefaultsClicked()290{291if (QMessageBox::question(this, tr("Restore Defaults"),292tr("Are you sure you want to restore the default controller configuration?\n\n"293"All bindings and configuration will be lost. You cannot undo this action.")) !=294QMessageBox::Yes)295{296return;297}298299// actually restore it300g_emu_thread->setDefaultSettings(false, true);301302// reload all settings303createWidgets();304}305306void ControllerSettingsWindow::onCopyGlobalSettingsClicked()307{308DebugAssert(!isEditingGlobalSettings());309310{311const auto lock = Host::GetSettingsLock();312InputManager::CopyConfiguration(m_editing_settings_interface, *Host::Internal::GetBaseSettingsLayer(), true, true,313true, false);314}315316m_editing_settings_interface->Save();317g_emu_thread->reloadGameSettings();318createWidgets();319320QMessageBox::information(QtUtils::GetRootWidget(this), tr("DuckStation Controller Settings"),321isEditingGameSettings() ? tr("Per-game controller configuration reset to global settings.") :322tr("Controller preset reset to global settings."));323}324325bool ControllerSettingsWindow::getBoolValue(const char* section, const char* key, bool default_value) const326{327if (m_editing_settings_interface)328return m_editing_settings_interface->GetBoolValue(section, key, default_value);329else330return Host::GetBaseBoolSettingValue(section, key, default_value);331}332333s32 ControllerSettingsWindow::getIntValue(const char* section, const char* key, s32 default_value) const334{335if (m_editing_settings_interface)336return m_editing_settings_interface->GetIntValue(section, key, default_value);337else338return Host::GetBaseIntSettingValue(section, key, default_value);339}340341std::string ControllerSettingsWindow::getStringValue(const char* section, const char* key,342const char* default_value) const343{344std::string value;345if (m_editing_settings_interface)346value = m_editing_settings_interface->GetStringValue(section, key, default_value);347else348value = Host::GetBaseStringSettingValue(section, key, default_value);349return value;350}351352void ControllerSettingsWindow::setBoolValue(const char* section, const char* key, bool value)353{354if (m_editing_settings_interface)355{356m_editing_settings_interface->SetBoolValue(section, key, value);357saveAndReloadGameSettings();358}359else360{361Host::SetBaseBoolSettingValue(section, key, value);362Host::CommitBaseSettingChanges();363g_emu_thread->applySettings();364}365}366367void ControllerSettingsWindow::setIntValue(const char* section, const char* key, s32 value)368{369if (m_editing_settings_interface)370{371m_editing_settings_interface->SetIntValue(section, key, value);372saveAndReloadGameSettings();373}374else375{376Host::SetBaseIntSettingValue(section, key, value);377Host::CommitBaseSettingChanges();378g_emu_thread->applySettings();379}380}381382void ControllerSettingsWindow::setStringValue(const char* section, const char* key, const char* value)383{384if (m_editing_settings_interface)385{386m_editing_settings_interface->SetStringValue(section, key, value);387saveAndReloadGameSettings();388}389else390{391Host::SetBaseStringSettingValue(section, key, value);392Host::CommitBaseSettingChanges();393g_emu_thread->applySettings();394}395}396397void ControllerSettingsWindow::saveAndReloadGameSettings()398{399DebugAssert(m_editing_settings_interface);400QtHost::SaveGameSettings(m_editing_settings_interface, false);401g_emu_thread->reloadGameSettings(false);402}403404void ControllerSettingsWindow::clearSettingValue(const char* section, const char* key)405{406if (m_editing_settings_interface)407{408m_editing_settings_interface->DeleteValue(section, key);409m_editing_settings_interface->Save();410g_emu_thread->reloadGameSettings();411}412else413{414Host::DeleteBaseSettingValue(section, key);415Host::CommitBaseSettingChanges();416g_emu_thread->applySettings();417}418}419420void ControllerSettingsWindow::createWidgets()421{422QSignalBlocker sb(m_ui.settingsContainer);423QSignalBlocker sb2(m_ui.settingsCategory);424425while (m_ui.settingsContainer->count() > 0)426{427QWidget* widget = m_ui.settingsContainer->widget(m_ui.settingsContainer->count() - 1);428m_ui.settingsContainer->removeWidget(widget);429widget->deleteLater();430}431432m_ui.settingsCategory->clear();433434m_global_settings = nullptr;435m_hotkey_settings = nullptr;436437{438// global settings439QListWidgetItem* item = new QListWidgetItem();440item->setText(tr("Global Settings"));441item->setIcon(QIcon::fromTheme(QStringLiteral("settings-3-line")));442m_ui.settingsCategory->addItem(item);443m_ui.settingsCategory->setCurrentRow(0);444m_global_settings = new ControllerGlobalSettingsWidget(m_ui.settingsContainer, this);445m_ui.settingsContainer->addWidget(m_global_settings);446connect(m_global_settings, &ControllerGlobalSettingsWidget::bindingSetupChanged, this,447&ControllerSettingsWindow::createWidgets);448}449450// load mtap settings451const std::array<bool, 2> mtap_enabled = getEnabledMultitaps();452for (u32 global_slot : Controller::PortDisplayOrder)453{454const bool is_mtap_port = Controller::PadIsMultitapSlot(global_slot);455const auto [port, slot] = Controller::ConvertPadToPortAndSlot(global_slot);456if (is_mtap_port && !mtap_enabled[port])457continue;458459m_port_bindings[global_slot] = new ControllerBindingWidget(m_ui.settingsContainer, this, global_slot);460m_ui.settingsContainer->addWidget(m_port_bindings[global_slot]);461462const QString display_name(463QtUtils::StringViewToQString(m_port_bindings[global_slot]->getControllerInfo()->GetDisplayName()));464465QListWidgetItem* item = new QListWidgetItem();466item->setText(tr("Controller Port %1\n%2")467.arg(Controller::GetPortDisplayName(port, slot, mtap_enabled[port]))468.arg(display_name));469item->setIcon(m_port_bindings[global_slot]->getIcon());470item->setData(Qt::UserRole, QVariant(global_slot));471m_ui.settingsCategory->addItem(item);472}473474// only add hotkeys if we're editing global settings475if (!m_editing_settings_interface ||476m_editing_settings_interface->GetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", false))477{478QListWidgetItem* item = new QListWidgetItem();479item->setText(tr("Hotkeys"));480item->setIcon(QIcon::fromTheme(QStringLiteral("keyboard-line")));481m_ui.settingsCategory->addItem(item);482m_hotkey_settings = new HotkeySettingsWidget(m_ui.settingsContainer, this);483m_ui.settingsContainer->addWidget(m_hotkey_settings);484}485486if (isEditingProfile())487{488const bool enable_buttons = static_cast<bool>(m_profile_settings_interface);489m_ui.applyProfile->setEnabled(enable_buttons);490m_ui.deleteProfile->setEnabled(enable_buttons);491m_ui.copyGlobalSettings->setEnabled(enable_buttons);492}493}494495void ControllerSettingsWindow::closeEvent(QCloseEvent* event)496{497QWidget::closeEvent(event);498emit windowClosed();499}500501void ControllerSettingsWindow::updateListDescription(u32 global_slot, ControllerBindingWidget* widget)502{503for (int i = 0; i < m_ui.settingsCategory->count(); i++)504{505QListWidgetItem* item = m_ui.settingsCategory->item(i);506const QVariant item_data(item->data(Qt::UserRole));507bool is_ok;508if (item_data.toUInt(&is_ok) == global_slot && is_ok)509{510const std::array<bool, 2> mtap_enabled = getEnabledMultitaps();511const auto [port, slot] = Controller::ConvertPadToPortAndSlot(global_slot);512513const QString display_name = QtUtils::StringViewToQString(widget->getControllerInfo()->GetDisplayName());514515item->setText(tr("Controller Port %1\n%2")516.arg(Controller::GetPortDisplayName(port, slot, mtap_enabled[port]))517.arg(display_name));518item->setIcon(widget->getIcon());519break;520}521}522}523524std::array<bool, 2> ControllerSettingsWindow::getEnabledMultitaps() const525{526const MultitapMode mtap_mode =527Settings::ParseMultitapModeName(528getStringValue("ControllerPorts", "MultitapMode", Settings::GetMultitapModeName(Settings::DEFAULT_MULTITAP_MODE))529.c_str())530.value_or(Settings::DEFAULT_MULTITAP_MODE);531return {{(mtap_mode == MultitapMode::Port1Only || mtap_mode == MultitapMode::BothPorts),532(mtap_mode == MultitapMode::Port2Only || mtap_mode == MultitapMode::BothPorts)}};533}534535void ControllerSettingsWindow::refreshProfileList()536{537const std::vector<std::string> names(InputManager::GetInputProfileNames());538539QSignalBlocker sb(m_ui.currentProfile);540m_ui.currentProfile->clear();541542bool current_profile_found = false;543for (const std::string& name : names)544{545const QString qname(QString::fromStdString(name));546m_ui.currentProfile->addItem(qname);547if (qname == m_profile_name)548{549m_ui.currentProfile->setCurrentIndex(m_ui.currentProfile->count() - 1);550current_profile_found = true;551}552}553554if (!current_profile_found)555switchProfile(names.empty() ? std::string_view() : std::string_view(names.front()));556}557558void ControllerSettingsWindow::switchProfile(const std::string_view name)559{560const QString name_qstr = QtUtils::StringViewToQString(name);561{562QSignalBlocker sb(m_ui.currentProfile);563m_ui.currentProfile->setCurrentIndex(m_ui.currentProfile->findText(name_qstr));564}565m_profile_name = name_qstr;566m_profile_settings_interface.reset();567m_editing_settings_interface = nullptr;568569// disable UI if there is no selection570const bool disable_ui = name.empty();571m_ui.settingsCategory->setDisabled(disable_ui);572m_ui.settingsContainer->setDisabled(disable_ui);573574if (name_qstr.isEmpty())575{576createWidgets();577return;578}579580std::string path = System::GetInputProfilePath(name);581if (!FileSystem::FileExists(path.c_str()))582{583QMessageBox::critical(this, tr("Error"), tr("The controller preset named '%1' cannot be found.").arg(name_qstr));584return;585}586587std::unique_ptr<INISettingsInterface> sif = std::make_unique<INISettingsInterface>(std::move(path));588sif->Load();589590m_profile_settings_interface = std::move(sif);591m_editing_settings_interface = m_profile_settings_interface.get();592593createWidgets();594}595596597