First Commit

This commit is contained in:
2025-11-18 14:18:26 -07:00
parent 33eb6e3707
commit 27277ec342
6106 changed files with 3571167 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "AchievementLoginDialog.h"
#include "QtHost.h"
#include "QtUtils.h"
#include "pcsx2/Achievements.h"
#include "common/Error.h"
#include <QtWidgets/QMessageBox>
AchievementLoginDialog::AchievementLoginDialog(QWidget* parent, Achievements::LoginRequestReason reason)
: QDialog(parent)
, m_reason(reason)
{
m_ui.setupUi(this);
QtUtils::SetScalableIcon(m_ui.loginIcon, QIcon::fromTheme(QStringLiteral("login-box-line")), QSize(32, 32));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
// Adjust text if needed based on reason.
if (reason == Achievements::LoginRequestReason::TokenInvalid)
{
m_ui.instructionText->setText(
tr("<strong>Your RetroAchievements login token is no longer valid.</strong> You must re-enter your "
"credentials for achievements to be tracked. Your password will not be saved in PCSX2, an access token "
"will be generated and used instead."));
}
m_login = m_ui.buttonBox->addButton(tr("&Login"), QDialogButtonBox::AcceptRole);
m_login->setEnabled(false);
connectUi();
}
AchievementLoginDialog::~AchievementLoginDialog() = default;
void AchievementLoginDialog::loginClicked()
{
std::string username(m_ui.userName->text().toStdString());
std::string password(m_ui.password->text().toStdString());
// TODO: Make cancellable.
m_ui.status->setText(tr("Logging in..."));
enableUI(false);
Host::RunOnCPUThread([this, username = std::move(username), password = std::move(password)]() {
Error error;
const bool result = Achievements::Login(username.c_str(), password.c_str(), &error);
const QString message = QString::fromStdString(error.GetDescription());
QMetaObject::invokeMethod(this, "processLoginResult", Qt::QueuedConnection, Q_ARG(bool, result), Q_ARG(const QString&, message));
});
}
void AchievementLoginDialog::cancelClicked()
{
// Disable hardcore mode if we cancelled reauthentication.
if (m_reason == Achievements::LoginRequestReason::TokenInvalid && QtHost::IsVMValid())
{
Host::RunOnCPUThread([]() {
if (VMManager::HasValidVM() && !Achievements::HasActiveGame())
Achievements::DisableHardcoreMode();
});
}
done(1);
}
void AchievementLoginDialog::processLoginResult(bool result, const QString& message)
{
if (!result)
{
QMessageBox::critical(
this, tr("Login Error"),
tr("Login failed.\nError: %1\n\nPlease check your username and password, and try again.").arg(message));
m_ui.status->setText(tr("Login failed."));
enableUI(true);
return;
}
if (m_reason == Achievements::LoginRequestReason::UserInitiated)
{
if (!Host::GetBaseBoolSettingValue("Achievements", "Enabled", false) &&
QMessageBox::question(this, tr("Enable Achievements"),
tr("Achievement tracking is not currently enabled. Your login will have no effect until "
"after tracking is enabled.\n\nDo you want to enable tracking now?"),
QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes)
{
Host::SetBaseBoolSettingValue("Achievements", "Enabled", true);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
}
if (!Host::GetBaseBoolSettingValue("Achievements", "ChallengeMode", false) &&
QMessageBox::question(
this, tr("Enable Hardcore Mode"),
tr("Hardcore mode is not currently enabled. Enabling hardcore mode allows you to set times, scores, and "
"participate in game-specific leaderboards.\n\nHowever, hardcore mode also prevents the usage of save "
"states, cheats and slowdown functionality.\n\nDo you want to enable hardcore mode?"),
QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes)
{
Host::SetBaseBoolSettingValue("Achievements", "ChallengeMode", true);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
bool has_active_game;
{
auto lock = Achievements::GetLock();
has_active_game = Achievements::HasActiveGame();
}
if (has_active_game &&
QMessageBox::question(this, tr("Reset System"),
tr("Hardcore mode will not be enabled until the system is reset. Do you want to reset the system now?")) ==
QMessageBox::Yes)
{
g_emu_thread->resetVM();
}
}
}
done(0);
}
void AchievementLoginDialog::connectUi()
{
connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &AchievementLoginDialog::loginClicked);
connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &AchievementLoginDialog::cancelClicked);
auto enableLoginButton = [this](const QString&) { m_login->setEnabled(canEnableLoginButton()); };
connect(m_ui.userName, &QLineEdit::textChanged, enableLoginButton);
connect(m_ui.password, &QLineEdit::textChanged, enableLoginButton);
}
void AchievementLoginDialog::enableUI(bool enabled)
{
m_ui.userName->setEnabled(enabled);
m_ui.password->setEnabled(enabled);
m_ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(enabled);
m_login->setEnabled(enabled && canEnableLoginButton());
}
bool AchievementLoginDialog::canEnableLoginButton() const
{
return !m_ui.userName->text().isEmpty() && !m_ui.password->text().isEmpty();
}

View File

@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "ui_AchievementLoginDialog.h"
#include <QtWidgets/QDialog>
#include <QtWidgets/QPushButton>
namespace Achievements
{
enum class LoginRequestReason;
}
class AchievementLoginDialog : public QDialog
{
Q_OBJECT
public:
AchievementLoginDialog(QWidget* parent, Achievements::LoginRequestReason reason);
~AchievementLoginDialog();
private Q_SLOTS:
void loginClicked();
void cancelClicked();
void processLoginResult(bool result, const QString& message);
private:
void connectUi();
void enableUI(bool enabled);
bool canEnableLoginButton() const;
Ui::AchievementLoginDialog m_ui;
QPushButton* m_login;
Achievements::LoginRequestReason m_reason;
};

View File

@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AchievementLoginDialog</class>
<widget class="QDialog" name="AchievementLoginDialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>447</width>
<height>196</height>
</rect>
</property>
<property name="windowTitle">
<string comment="Window title">RetroAchievements Login</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
<item>
<widget class="QLabel" name="loginIcon">
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>:/icons/black/48/login-box-line.png</pixmap>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<pointsize>14</pointsize>
<bold>false</bold>
</font>
</property>
<property name="text">
<string comment="Header text">RetroAchievements Login</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="instructionText">
<property name="text">
<string>Please enter user name and password for retroachievements.org below. Your password will not be saved in PCSX2, an access token will be generated and used instead.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>User Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="userName"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0">
<item>
<widget class="QLabel" name="status">
<property name="text">
<string>Ready...</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,256 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "AchievementSettingsWidget.h"
#include "AchievementLoginDialog.h"
#include "MainWindow.h"
#include "SettingsWindow.h"
#include "SettingWidgetBinder.h"
#include "QtUtils.h"
#include "pcsx2/Achievements.h"
#include "pcsx2/Host.h"
#include "common/StringUtil.h"
#include <QtCore/QDateTime>
#include <QtWidgets/QMessageBox>
const char* AUDIO_FILE_FILTER = QT_TRANSLATE_NOOP("MainWindow", "Audio Files (*.wav)");
AchievementSettingsWidget::AchievementSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: QWidget(parent)
, m_dialog(dialog)
{
SettingsInterface* sif = dialog->getSettingsInterface();
m_ui.setupUi(this);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enable, "Achievements", "Enabled", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.hardcoreMode, "Achievements", "ChallengeMode", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.achievementNotifications, "Achievements", "Notifications", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.leaderboardNotifications, "Achievements", "LeaderboardNotifications", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.soundEffects, "Achievements", "SoundEffects", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.notificationSound, "Achievements", "InfoSound", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.unlockSound, "Achievements", "UnlockSound", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.lbSound, "Achievements", "LBSubmitSound", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.overlays, "Achievements", "Overlays", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.encoreMode, "Achievements", "EncoreMode", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.spectatorMode, "Achievements", "SpectatorMode", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.unofficialAchievements, "Achievements", "UnofficialTestMode",false);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.achievementNotificationsDuration, "Achievements", "NotificationsDuration", Pcsx2Config::AchievementsOptions::DEFAULT_NOTIFICATION_DURATION);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.leaderboardNotificationsDuration, "Achievements", "LeaderboardsDuration", Pcsx2Config::AchievementsOptions::DEFAULT_LEADERBOARD_DURATION);
SettingWidgetBinder::BindWidgetToFileSetting(sif, m_ui.notificationSoundPath, m_ui.notificationSoundBrowse, m_ui.notificationSoundOpen, m_ui.notificationSoundReset, "Achievements", "InfoSoundName", Path::Combine(EmuFolders::Resources, EmuConfig.Achievements.DEFAULT_INFO_SOUND_NAME), AUDIO_FILE_FILTER, true, false);
SettingWidgetBinder::BindWidgetToFileSetting(sif, m_ui.unlockSoundPath, m_ui.unlockSoundBrowse, m_ui.unlockSoundOpen, m_ui.unlockSoundReset, "Achievements", "UnlockSoundName", Path::Combine(EmuFolders::Resources, EmuConfig.Achievements.DEFAULT_UNLOCK_SOUND_NAME), AUDIO_FILE_FILTER, true, false);
SettingWidgetBinder::BindWidgetToFileSetting(sif, m_ui.lbSoundPath, m_ui.lbSoundBrowse, m_ui.lbSoundOpen, m_ui.lbSoundReset, "Achievements", "LBSubmitSoundName", Path::Combine(EmuFolders::Resources, EmuConfig.Achievements.DEFAULT_LBSUBMIT_SOUND_NAME), AUDIO_FILE_FILTER, true, false);
dialog->registerWidgetHelp(m_ui.enable, tr("Enable Achievements"), tr("Unchecked"), tr("When enabled and logged in, PCSX2 will scan for achievements on startup."));
dialog->registerWidgetHelp(m_ui.hardcoreMode, tr("Enable Hardcore Mode"), tr("Unchecked"), tr("\"Challenge\" mode for achievements, including leaderboard tracking. Disables save state, cheats, and slowdown functions."));
dialog->registerWidgetHelp(m_ui.achievementNotifications, tr("Show Achievement Notifications"), tr("Checked"), tr("Displays popup messages on events such as achievement unlocks and game completion."));
dialog->registerWidgetHelp(m_ui.leaderboardNotifications, tr("Show Leaderboard Notifications"), tr("Checked"), tr("Displays popup messages when starting, submitting, or failing a leaderboard challenge."));
dialog->registerWidgetHelp(m_ui.soundEffects, tr("Enable Sound Effects"), tr("Checked"), tr("Plays sound effects for events such as achievement unlocks and leaderboard submissions."));
dialog->registerWidgetHelp(m_ui.soundEffectsBox, tr("Custom Sound Effect"), tr("Any"), tr("Customize the sound effect that are played whenever you received a notification, earned an achievement or submitted an entry to the leaderboard."));
dialog->registerWidgetHelp(m_ui.overlays, tr("Enable In-Game Overlays"), tr("Checked"), tr("Shows icons in the lower-right corner of the screen when a challenge/primed achievement is active."));
dialog->registerWidgetHelp(m_ui.encoreMode, tr("Enable Encore Mode"), tr("Unchecked"),tr("When enabled, each session will behave as if no achievements have been unlocked."));
dialog->registerWidgetHelp(m_ui.spectatorMode, tr("Enable Spectator Mode"), tr("Unchecked"), tr("When enabled, PCSX2 will assume all achievements are locked and not send any unlock notifications to the server."));
dialog->registerWidgetHelp(m_ui.unofficialAchievements, tr("Test Unofficial Achievements"), tr("Unchecked"), tr("When enabled, PCSX2 will list achievements from unofficial sets. Please note that these achievements are not tracked by RetroAchievements, so they unlock every time."));
connectCheckStateChanged(m_ui.enable, this, &AchievementSettingsWidget::updateEnableState);
connectCheckStateChanged(m_ui.hardcoreMode, this, &AchievementSettingsWidget::updateEnableState);
connectCheckStateChanged(m_ui.hardcoreMode, this, &AchievementSettingsWidget::onHardcoreModeStateChanged);
connectCheckStateChanged(m_ui.achievementNotifications, this, &AchievementSettingsWidget::updateEnableState);
connectCheckStateChanged(m_ui.leaderboardNotifications, this, &AchievementSettingsWidget::updateEnableState);
connectCheckStateChanged(m_ui.soundEffects, this, &AchievementSettingsWidget::updateEnableState);
connectCheckStateChanged(m_ui.notificationSound, this, &AchievementSettingsWidget::updateEnableState);
connectCheckStateChanged(m_ui.unlockSound, this, &AchievementSettingsWidget::updateEnableState);
connectCheckStateChanged(m_ui.lbSound, this, &AchievementSettingsWidget::updateEnableState);
connect(m_ui.achievementNotificationsDuration, &QSlider::valueChanged, this, &AchievementSettingsWidget::onAchievementsNotificationDurationSliderChanged);
connect(m_ui.leaderboardNotificationsDuration, &QSlider::valueChanged, this, &AchievementSettingsWidget::onLeaderboardsNotificationDurationSliderChanged);
if (!m_dialog->isPerGameSettings())
{
connect(m_ui.loginButton, &QPushButton::clicked, this, &AchievementSettingsWidget::onLoginLogoutPressed);
connect(m_ui.viewProfile, &QPushButton::clicked, this, &AchievementSettingsWidget::onViewProfilePressed);
connect(g_emu_thread, &EmuThread::onAchievementsRefreshed, this, &AchievementSettingsWidget::onAchievementsRefreshed);
updateLoginState();
// force a refresh of game info
Host::RunOnCPUThread(Host::OnAchievementsRefreshed);
}
else
{
// remove login and game info, not relevant for per-game
m_ui.verticalLayout->removeWidget(m_ui.gameInfoBox);
m_ui.gameInfoBox->deleteLater();
m_ui.gameInfoBox = nullptr;
m_ui.verticalLayout->removeWidget(m_ui.loginBox);
m_ui.loginBox->deleteLater();
m_ui.loginBox = nullptr;
// sound effects
m_ui.verticalLayout->removeWidget(m_ui.soundEffectsBox);
m_ui.soundEffectsBox->deleteLater();
m_ui.soundEffectsBox = nullptr;
}
updateEnableState();
onAchievementsNotificationDurationSliderChanged();
onLeaderboardsNotificationDurationSliderChanged();
}
AchievementSettingsWidget::~AchievementSettingsWidget() = default;
void AchievementSettingsWidget::updateEnableState()
{
const bool enabled = m_dialog->getEffectiveBoolValue("Achievements", "Enabled", false);
const bool notifications = enabled && m_dialog->getEffectiveBoolValue("Achievements", "Notifications", true);
const bool lb_notifications = enabled && m_dialog->getEffectiveBoolValue("Achievements", "LeaderboardNotifications", true);
const bool sound = m_dialog->getEffectiveBoolValue("Achievements", "SoundEffects", true);
const bool info = enabled && sound && m_dialog->getEffectiveBoolValue("Achievements", "InfoSound", true);
const bool unlock = enabled && sound && m_dialog->getEffectiveBoolValue("Achievements", "UnlockSound", true);
const bool lbsound = enabled && sound && m_dialog->getEffectiveBoolValue("Achievements", "LBSubmitSound", true);
m_ui.hardcoreMode->setEnabled(enabled);
m_ui.achievementNotifications->setEnabled(enabled);
m_ui.leaderboardNotifications->setEnabled(enabled);
m_ui.achievementNotificationsDuration->setEnabled(notifications);
m_ui.achievementNotificationsDurationLabel->setEnabled(notifications);
m_ui.leaderboardNotificationsDuration->setEnabled(lb_notifications);
m_ui.leaderboardNotificationsDurationLabel->setEnabled(lb_notifications);
if (!m_dialog->isPerGameSettings())
{
m_ui.notificationSoundPath->setEnabled(info);
m_ui.notificationSoundBrowse->setEnabled(info);
m_ui.notificationSoundOpen->setEnabled(info);
m_ui.notificationSoundReset->setEnabled(info);
m_ui.notificationSound->setEnabled(enabled);
m_ui.unlockSoundPath->setEnabled(unlock);
m_ui.unlockSoundBrowse->setEnabled(unlock);
m_ui.unlockSoundOpen->setEnabled(unlock);
m_ui.unlockSoundReset->setEnabled(unlock);
m_ui.unlockSound->setEnabled(enabled);
m_ui.lbSoundPath->setEnabled(lbsound);
m_ui.lbSoundOpen->setEnabled(lbsound);
m_ui.lbSoundBrowse->setEnabled(lbsound);
m_ui.lbSoundReset->setEnabled(lbsound);
m_ui.lbSound->setEnabled(enabled);
}
m_ui.soundEffects->setEnabled(enabled);
m_ui.overlays->setEnabled(enabled);
m_ui.encoreMode->setEnabled(enabled);
m_ui.spectatorMode->setEnabled(enabled);
m_ui.unofficialAchievements->setEnabled(enabled);
}
void AchievementSettingsWidget::onHardcoreModeStateChanged()
{
if (!QtHost::IsVMValid())
return;
const bool enabled = m_dialog->getEffectiveBoolValue("Achievements", "Enabled", false);
const bool challenge = m_dialog->getEffectiveBoolValue("Achievements", "ChallengeMode", false);
if (!enabled || !challenge)
return;
// don't bother prompting if the game doesn't have achievements
auto lock = Achievements::GetLock();
if (!Achievements::HasActiveGame() || !Achievements::HasAchievementsOrLeaderboards())
return;
if (QMessageBox::question(
QtUtils::GetRootWidget(this), tr("Reset System"),
tr("Hardcore mode will not be enabled until the system is reset. Do you want to reset the system now?")) !=
QMessageBox::Yes)
{
return;
}
g_emu_thread->resetVM();
}
void AchievementSettingsWidget::onAchievementsNotificationDurationSliderChanged()
{
const float duration = m_dialog->getEffectiveFloatValue("Achievements", "NotificationsDuration",
Pcsx2Config::AchievementsOptions::DEFAULT_NOTIFICATION_DURATION);
m_ui.achievementNotificationsDurationLabel->setText(tr("%n seconds", nullptr, static_cast<int>(duration)));
}
void AchievementSettingsWidget::onLeaderboardsNotificationDurationSliderChanged()
{
const float duration = m_dialog->getEffectiveFloatValue("Achievements", "LeaderboardsDuration",
Pcsx2Config::AchievementsOptions::DEFAULT_LEADERBOARD_DURATION);
m_ui.leaderboardNotificationsDurationLabel->setText(tr("%n seconds", nullptr, static_cast<int>(duration)));
}
void AchievementSettingsWidget::updateLoginState()
{
const std::string username(Host::GetBaseStringSettingValue("Achievements", "Username"));
const bool logged_in = !username.empty();
if (logged_in)
{
const u64 login_unix_timestamp =
StringUtil::FromChars<u64>(Host::GetBaseStringSettingValue("Achievements", "LoginTimestamp", "0")).value_or(0);
const QDateTime login_timestamp(QDateTime::fromSecsSinceEpoch(static_cast<qint64>(login_unix_timestamp)));
m_ui.loginStatus->setText(tr("Username: %1\nLogin token generated on %2.")
.arg(QString::fromStdString(username))
.arg(login_timestamp.toString(Qt::TextDate)));
m_ui.loginButton->setText(tr("Logout"));
}
else
{
m_ui.loginStatus->setText(tr("Not Logged In."));
m_ui.loginButton->setText(tr("Login..."));
}
m_ui.viewProfile->setEnabled(logged_in);
}
void AchievementSettingsWidget::onLoginLogoutPressed()
{
if (!Host::GetBaseStringSettingValue("Achievements", "Username").empty())
{
Host::RunOnCPUThread([]() { Achievements::Logout(); }, true);
updateLoginState();
return;
}
AchievementLoginDialog login(this, Achievements::LoginRequestReason::UserInitiated);
int res = login.exec();
if (res != 0)
return;
updateLoginState();
// Login can enable achievements/hardcore.
if (!m_ui.enable->isChecked() && Host::GetBaseBoolSettingValue("Achievements", "Enabled", false))
{
QSignalBlocker sb(m_ui.enable);
m_ui.enable->setChecked(true);
updateEnableState();
}
if (!m_ui.hardcoreMode->isChecked() && Host::GetBaseBoolSettingValue("Achievements", "ChallengeMode", false))
{
QSignalBlocker sb(m_ui.hardcoreMode);
m_ui.hardcoreMode->setChecked(true);
}
}
void AchievementSettingsWidget::onViewProfilePressed()
{
const std::string username(Host::GetBaseStringSettingValue("Achievements", "Username"));
if (username.empty())
return;
const QByteArray encoded_username(QUrl::toPercentEncoding(QString::fromStdString(username)));
QtUtils::OpenURL(
QtUtils::GetRootWidget(this),
QUrl(QStringLiteral("https://retroachievements.org/user/%1").arg(QString::fromUtf8(encoded_username))));
}
void AchievementSettingsWidget::onAchievementsRefreshed(quint32 id, const QString& game_info_string)
{
m_ui.gameInfo->setText(game_info_string);
}

View File

@@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QWidget>
#include "ui_AchievementSettingsWidget.h"
class SettingsWindow;
class AchievementSettingsWidget : public QWidget
{
Q_OBJECT
public:
explicit AchievementSettingsWidget(SettingsWindow* dialog, QWidget* parent);
~AchievementSettingsWidget();
private Q_SLOTS:
void updateEnableState();
void onHardcoreModeStateChanged();
void onAchievementsNotificationDurationSliderChanged();
void onLeaderboardsNotificationDurationSliderChanged();
void onLoginLogoutPressed();
void onViewProfilePressed();
void onAchievementsRefreshed(quint32 id, const QString& game_info_string);
private:
void updateLoginState();
Ui::AchievementSettingsWidget m_ui;
SettingsWindow* m_dialog;
};

View File

@@ -0,0 +1,391 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AchievementSettingsWidget</class>
<widget class="QWidget" name="AchievementSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>674</width>
<height>481</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaAchievements">
<property name="geometry">
<rect>
<x>0</x>
<y>-2</y>
<width>655</width>
<height>739</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="settingsBox">
<property name="title">
<string>Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="2" column="0">
<widget class="QCheckBox" name="unofficialAchievements">
<property name="text">
<string>Test Unofficial Achievements</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="hardcoreMode">
<property name="text">
<string>Enable Hardcore Mode</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="enable">
<property name="text">
<string>Enable Achievements</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="spectatorMode">
<property name="text">
<string>Enable Spectator Mode</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="encoreMode">
<property name="text">
<string>Enable Encore Mode</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="notificationBox">
<property name="title">
<string>Notifications</string>
</property>
<layout class="QGridLayout" name="gridLayout_2" columnstretch="1,0">
<item row="0" column="1">
<layout class="QHBoxLayout" name="achievementNotificationsDurationSliderContainer" stretch="1,0">
<item>
<widget class="QSlider" name="achievementNotificationsDuration">
<property name="minimum">
<number>3</number>
</property>
<property name="maximum">
<number>30</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>5</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>1</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="achievementNotificationsDurationLabel">
<property name="text">
<string>5 seconds</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="achievementNotifications">
<property name="text">
<string>Show Achievement Notifications</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="leaderboardNotificationsDurationSliderContainer" stretch="1,0">
<item>
<widget class="QSlider" name="leaderboardNotificationsDuration">
<property name="minimum">
<number>3</number>
</property>
<property name="maximum">
<number>30</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>5</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>1</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="leaderboardNotificationsDurationLabel">
<property name="text">
<string>5 seconds</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="leaderboardNotifications">
<property name="text">
<string>Show Leaderboard Notifications</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="soundEffects">
<property name="text">
<string>Enable Sound Effects</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="overlays">
<property name="text">
<string>Enable In-Game Overlays</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="soundEffectsBox">
<property name="title">
<string>Sound Effects</string>
</property>
<layout class="QGridLayout" name="gridLayout_6">
<item row="5" column="0">
<widget class="QLineEdit" name="lbSoundPath"/>
</item>
<item row="1" column="0">
<widget class="QLineEdit" name="notificationSoundPath"/>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="unlockSoundBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="notificationSoundReset">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="unlockSound">
<property name="text">
<string>Achievement Unlock Sound</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="notificationSoundBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLineEdit" name="unlockSoundPath"/>
</item>
<item row="5" column="1">
<widget class="QPushButton" name="lbSoundBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="unlockSoundOpen">
<property name="text">
<string>Preview</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="notificationSoundOpen">
<property name="text">
<string>Preview</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="notificationSound">
<property name="text">
<string>Notification Sound</string>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QPushButton" name="lbSoundOpen">
<property name="text">
<string>Preview</string>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QPushButton" name="unlockSoundReset">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item row="5" column="3">
<widget class="QPushButton" name="lbSoundReset">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="lbSound">
<property name="text">
<string>Leaderboard Submit Sound</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="loginBox">
<property name="title">
<string>Account</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
<item>
<widget class="QLabel" name="loginStatus">
<property name="text">
<string>Username:
Login token generated at:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="achievementButtons">
<item>
<widget class="QPushButton" name="viewProfile">
<property name="text">
<string>View Profile...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="loginButton">
<property name="text">
<string>Login...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gameInfoBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>75</height>
</size>
</property>
<property name="title">
<string>Game Info</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="gameInfo">
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="rcheevosDisclaimer">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;justify&quot;&gt;PCSX2 uses RetroAchievements as an achievement database and for tracking progress. To use achievements, please sign up for an account at &lt;a href=&quot;https://retroachievements.org/&quot;&gt;retroachievements.org&lt;/a&gt;.&lt;/p&gt;&lt;p align=&quot;justify&quot;&gt;To view the achievement list in-game, press the hotkey for &lt;span style=&quot; font-weight:600;&quot;&gt;Open Pause Menu&lt;/span&gt; and select &lt;span style=&quot; font-weight:600;&quot;&gt;Achievements&lt;/span&gt; from the menu.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::TextFormat::RichText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="margin">
<number>8</number>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,228 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include <QtWidgets/QMessageBox>
#include <algorithm>
#include "AdvancedSettingsWidget.h"
#include "QtHost.h"
#include "QtUtils.h"
#include "SettingWidgetBinder.h"
#include "SettingsWindow.h"
AdvancedSettingsWidget::AdvancedSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: QWidget(parent)
, m_dialog(dialog)
{
SettingsInterface* sif = dialog->getSettingsInterface();
m_ui.setupUi(this);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.eeRecompiler, "EmuCore/CPU/Recompiler", "EnableEE", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.eeCache, "EmuCore/CPU/Recompiler", "EnableEECache", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.eeINTCSpinDetection, "EmuCore/Speedhacks", "IntcStat", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.eeWaitLoopDetection, "EmuCore/Speedhacks", "WaitLoop", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.eeFastmem, "EmuCore/CPU/Recompiler", "EnableFastmem", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pauseOnTLBMiss, "EmuCore/CPU/Recompiler", "PauseOnTLBMiss", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.extraMemory, "EmuCore/CPU", "ExtraMemory", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.vu0Recompiler, "EmuCore/CPU/Recompiler", "EnableVU0", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.vu1Recompiler, "EmuCore/CPU/Recompiler", "EnableVU1", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.vuFlagHack, "EmuCore/Speedhacks", "vuFlagHack", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.instantVU1, "EmuCore/Speedhacks", "vu1Instant", true);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.eeRoundingMode, "EmuCore/CPU", "FPU.Roundmode", static_cast<int>(FPRoundMode::ChopZero));
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.eeDivRoundingMode, "EmuCore/CPU", "FPUDiv.Roundmode", static_cast<int>(FPRoundMode::Nearest));
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.vu0RoundingMode, "EmuCore/CPU", "VU0.Roundmode", static_cast<int>(FPRoundMode::ChopZero));
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.vu1RoundingMode, "EmuCore/CPU", "VU1.Roundmode", static_cast<int>(FPRoundMode::ChopZero));
if (m_dialog->isPerGameSettings())
{
m_ui.eeClampMode->insertItem(0, tr("Use Global Setting [%1]").arg(m_ui.eeClampMode->itemText(getGlobalClampingModeIndex(-1))));
m_ui.vu0ClampMode->insertItem(0, tr("Use Global Setting [%1]").arg(m_ui.vu0ClampMode->itemText(getGlobalClampingModeIndex(0))));
m_ui.vu1ClampMode->insertItem(0, tr("Use Global Setting [%1]").arg(m_ui.vu1ClampMode->itemText(getGlobalClampingModeIndex(1))));
}
m_ui.eeClampMode->setCurrentIndex(getClampingModeIndex(-1));
m_ui.vu0ClampMode->setCurrentIndex(getClampingModeIndex(0));
m_ui.vu1ClampMode->setCurrentIndex(getClampingModeIndex(1));
connect(m_ui.eeClampMode, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int index) { setClampingMode(-1, index); });
connect(m_ui.vu0ClampMode, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int index) { setClampingMode(0, index); });
connect(m_ui.vu1ClampMode, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int index) { setClampingMode(1, index); });
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.iopRecompiler, "EmuCore/CPU/Recompiler", "EnableIOP", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.gameFixes, "EmuCore", "EnableGameFixes", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.patches, "EmuCore", "EnablePatches", true);
SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_ui.ntscFrameRate, "EmuCore/GS", "FramerateNTSC", 59.94f);
SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_ui.palFrameRate, "EmuCore/GS", "FrameratePAL", 50.00f);
dialog->registerWidgetHelp(m_ui.savestateSelector, tr("Use Save State Selector"), tr("Checked"),
tr("Show a save state selector UI when switching slots instead of showing a notification bubble."));
SettingWidgetBinder::BindWidgetToIntSetting(
sif, m_ui.savestateCompressionMethod, "EmuCore", "SavestateCompressionType", static_cast<int>(SavestateCompressionMethod::Zstandard));
SettingWidgetBinder::BindWidgetToIntSetting(
sif, m_ui.savestateCompressionLevel, "EmuCore", "SavestateCompressionRatio", static_cast<int>(SavestateCompressionLevel::Medium));
connect(m_ui.savestateCompressionMethod, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&AdvancedSettingsWidget::onSavestateCompressionTypeChanged);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.backupSaveStates, "EmuCore", "BackupSavestate", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.saveStateOnShutdown, "EmuCore", "SaveStateOnShutdown", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.savestateSelector, "EmuCore", "UseSavestateSelector", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pineEnable, "EmuCore", "EnablePINE", false);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.pineSlot, "EmuCore", "PINESlot", 28011);
dialog->registerWidgetHelp(m_ui.eeRoundingMode, tr("Rounding Mode"), tr("Chop/Zero (Default)"), tr("Changes how PCSX2 handles rounding while emulating the Emotion Engine's Floating Point Unit (EE FPU). "
"Because the various FPUs in the PS2 are non-compliant with international standards, some games may need different modes to do math correctly. The default value handles the vast majority of games; <b>modifying this setting when a game is not having a visible problem can cause instability.</b>"));
dialog->registerWidgetHelp(m_ui.eeDivRoundingMode, tr("Division Rounding Mode"), tr("Nearest (Default)"), tr("Determines how the results of floating-point division are rounded. Some games need specific settings; <b>modifying this setting when a game is not having a visible problem can cause instability.</b>"));
dialog->registerWidgetHelp(m_ui.eeClampMode, tr("Clamping Mode"), tr("Normal (Default)"), tr("Changes how PCSX2 handles keeping floats in a standard x86 range. "
"The default value handles the vast majority of games; <b>modifying this setting when a game is not having a visible problem can cause instability.</b>"));
dialog->registerWidgetHelp(m_ui.eeRecompiler, tr("Enable Recompiler"), tr("Checked"),
tr("Performs just-in-time binary translation of 64-bit MIPS-IV machine code to x86."));
//: Wait loop: When the game makes the CPU do nothing (loop/spin) while it waits for something to happen (usually an interrupt).
dialog->registerWidgetHelp(m_ui.eeWaitLoopDetection, tr("Wait Loop Detection"), tr("Checked"),
tr("Moderate speedup for some games, with no known side effects."));
dialog->registerWidgetHelp(m_ui.eeCache, tr("Enable Cache (Slow)"), tr("Unchecked"), tr("Interpreter only, provided for diagnostic."));
//: INTC = Name of a PS2 register, leave as-is. "spin" = to make a cpu (or gpu) actively do nothing while you wait for something. Like spinning in a circle, you're moving but not actually going anywhere.
dialog->registerWidgetHelp(m_ui.eeINTCSpinDetection, tr("INTC Spin Detection"), tr("Checked"),
tr("Huge speedup for some games, with almost no compatibility side effects."));
dialog->registerWidgetHelp(m_ui.eeFastmem, tr("Enable Fast Memory Access"), tr("Checked"),
//: "Backpatching" = To edit previously generated code to change what it does (in this case, we generate direct memory accesses, then backpatch them to jump to a fancier handler function when we realize they need the fancier handler function)
tr("Uses backpatching to avoid register flushing on every memory access."));
dialog->registerWidgetHelp(m_ui.pauseOnTLBMiss, tr("Pause On TLB Miss"), tr("Unchecked"),
tr("Pauses the virtual machine when a TLB miss occurs, instead of ignoring it and continuing. Note that the VM will pause after the "
"end of the block, not on the instruction which caused the exception. Refer to the console to see the address where the invalid "
"access occurred."));
dialog->registerWidgetHelp(m_ui.extraMemory, tr("Enable 128MB RAM (Dev Console)"), tr("Unchecked"),
tr("Exposes an additional 96MB of memory to the virtual machine."));
dialog->registerWidgetHelp(m_ui.vu0RoundingMode, tr("VU0 Rounding Mode"), tr("Chop/Zero (Default)"), tr("Changes how PCSX2 handles rounding while emulating the Emotion Engine's Vector Unit 0 (EE VU0). "
"The default value handles the vast majority of games; <b>modifying this setting when a game is not having a visible problem will cause stability issues and/or crashes.</b>"));
dialog->registerWidgetHelp(m_ui.vu1RoundingMode, tr("VU1 Rounding Mode"), tr("Chop/Zero (Default)"), tr("Changes how PCSX2 handles rounding while emulating the Emotion Engine's Vector Unit 1 (EE VU1). "
"The default value handles the vast majority of games; <b>modifying this setting when a game is not having a visible problem will cause stability issues and/or crashes.</b>"));
dialog->registerWidgetHelp(m_ui.vu0ClampMode, tr("VU0 Clamping Mode"), tr("Normal (Default)"), tr("Changes how PCSX2 handles keeping floats in a standard x86 range in the Emotion Engine's Vector Unit 0 (EE VU0). "
"The default value handles the vast majority of games; <b>modifying this setting when a game is not having a visible problem can cause instability.</b>"));
dialog->registerWidgetHelp(m_ui.vu1ClampMode, tr("VU1 Clamping Mode"), tr("Normal (Default)"), tr("Changes how PCSX2 handles keeping floats in a standard x86 range in the Emotion Engine's Vector Unit 1 (EE VU1). "
"The default value handles the vast majority of games; <b>modifying this setting when a game is not having a visible problem can cause instability.</b>"));
dialog->registerWidgetHelp(m_ui.instantVU1, tr("Enable Instant VU1"), tr("Checked"), tr("Runs VU1 instantly. Provides a modest speed improvement in most games. "
"Safe for most games, but a few games may exhibit graphical errors."));
//: VU0 = Vector Unit 0. One of the PS2's processors.
dialog->registerWidgetHelp(m_ui.vu0Recompiler, tr("Enable VU0 Recompiler (Micro Mode)"), tr("Checked"), tr("Enables VU0 Recompiler."));
//: VU1 = Vector Unit 1. One of the PS2's processors.
dialog->registerWidgetHelp(m_ui.vu1Recompiler, tr("Enable VU1 Recompiler"), tr("Checked"), tr("Enables VU1 Recompiler."));
dialog->registerWidgetHelp(
//: mVU = PCSX2's recompiler for VU (Vector Unit) code (full name: microVU)
m_ui.vuFlagHack, tr("mVU Flag Hack"), tr("Checked"), tr("Good speedup and high compatibility, may cause graphical errors."));
dialog->registerWidgetHelp(m_ui.iopRecompiler, tr("Enable Recompiler"), tr("Checked"),
tr("Performs just-in-time binary translation of 32-bit MIPS-I machine code to x86."));
dialog->registerWidgetHelp(m_ui.gameFixes, tr("Enable Game Fixes"), tr("Checked"),
tr("Automatically loads and applies fixes to known problematic games on game start."));
dialog->registerWidgetHelp(m_ui.patches, tr("Enable Compatibility Patches"), tr("Checked"),
tr("Automatically loads and applies compatibility patches to known problematic games."));
dialog->registerWidgetHelp(m_ui.savestateCompressionMethod, tr("Savestate Compression Method"), tr("Zstandard"),
tr("Determines the algorithm to be used when compressing savestates."));
dialog->registerWidgetHelp(m_ui.savestateCompressionLevel, tr("Savestate Compression Level"), tr("Medium"),
tr("Determines the level to be used when compressing savestates."));
dialog->registerWidgetHelp(m_ui.saveStateOnShutdown, tr("Save State On Shutdown"), tr("Unchecked"),
tr("Automatically saves the emulator state when powering down or exiting. You can then "
"resume directly from where you left off next time."));
dialog->registerWidgetHelp(m_ui.backupSaveStates, tr("Create Save State Backups"), tr("Checked"),
//: Do not translate the ".backup" extension.
tr("Creates a backup copy of a save state if it already exists when the save is created. The backup copy has a .backup suffix."));
}
AdvancedSettingsWidget::~AdvancedSettingsWidget() = default;
int AdvancedSettingsWidget::getGlobalClampingModeIndex(int vunum) const
{
if (Host::GetBaseBoolSettingValue(
"EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0SignOverflow" : "vu1SignOverflow") : "fpuFullMode"), false))
return 3;
if (Host::GetBaseBoolSettingValue(
"EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0ExtraOverflow" : "vu1ExtraOverflow") : "fpuExtraOverflow"), false))
return 2;
if (Host::GetBaseBoolSettingValue(
"EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0Overflow" : "vu1Overflow") : "fpuOverflow"), true))
return 1;
return 0;
}
int AdvancedSettingsWidget::getClampingModeIndex(int vunum) const
{
// This is so messy... maybe we should just make the mode an int in the settings too...
const bool base = m_dialog->isPerGameSettings() ? 1 : 0;
std::optional<bool> default_false = m_dialog->isPerGameSettings() ? std::nullopt : std::optional<bool>(false);
std::optional<bool> default_true = m_dialog->isPerGameSettings() ? std::nullopt : std::optional<bool>(true);
std::optional<bool> third = m_dialog->getBoolValue(
"EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0SignOverflow" : "vu1SignOverflow") : "fpuFullMode"), default_false);
std::optional<bool> second = m_dialog->getBoolValue("EmuCore/CPU/Recompiler",
(vunum >= 0 ? ((vunum == 0) ? "vu0ExtraOverflow" : "vu1ExtraOverflow") : "fpuExtraOverflow"), default_false);
std::optional<bool> first = m_dialog->getBoolValue(
"EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0Overflow" : "vu1Overflow") : "fpuOverflow"), default_true);
if (third.has_value() && third.value())
return base + 3;
if (second.has_value() && second.value())
return base + 2;
if (first.has_value() && first.value())
return base + 1;
else if (first.has_value())
return base + 0; // none
else
return 0; // no per game override
}
void AdvancedSettingsWidget::setClampingMode(int vunum, int index)
{
std::optional<bool> first, second, third;
if (!m_dialog->isPerGameSettings() || index > 0)
{
const bool base = m_dialog->isPerGameSettings() ? 1 : 0;
third = (index >= (base + 3));
second = (index >= (base + 2));
first = (index >= (base + 1));
}
m_dialog->setBoolSettingValue(
"EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0SignOverflow" : "vu1SignOverflow") : "fpuFullMode"), third);
m_dialog->setBoolSettingValue(
"EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0ExtraOverflow" : "vu1ExtraOverflow") : "fpuExtraOverflow"), second);
m_dialog->setBoolSettingValue(
"EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0Overflow" : "vu1Overflow") : "fpuOverflow"), first);
}
void AdvancedSettingsWidget::onSavestateCompressionTypeChanged()
{
const bool uncompressed = (m_dialog->getEffectiveIntValue("EmuCore", "SavestateCompressionType", static_cast<int>(SavestateCompressionMethod::Zstandard)) ==
static_cast<int>(SavestateCompressionMethod::Uncompressed));
m_ui.savestateCompressionLevel->setDisabled(uncompressed);
}

View File

@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QWidget>
#include "ui_AdvancedSettingsWidget.h"
class SettingsWindow;
class AdvancedSettingsWidget : public QWidget
{
Q_OBJECT
public:
AdvancedSettingsWidget(SettingsWindow* dialog, QWidget* parent);
~AdvancedSettingsWidget();
private:
int getGlobalClampingModeIndex(int vunum) const;
int getClampingModeIndex(int vunum) const;
void setClampingMode(int vunum, int index);
void onSavestateCompressionTypeChanged();
SettingsWindow* m_dialog;
Ui::AdvancedSystemSettingsWidget m_ui;
};

View File

@@ -0,0 +1,628 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AdvancedSystemSettingsWidget</class>
<widget class="QWidget" name="AdvancedSystemSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>809</width>
<height>602</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>-449</y>
<width>790</width>
<height>1051</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="disclaimerLabel">
<property name="text">
<string>Changing these options may cause games to become non-functional. Modify at your own risk, the PCSX2 team will not provide support for configurations with these settings changed.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="eeSettings">
<property name="title">
<string extracomment="Emotion Engine = Commercial name of one of PS2's processors. Leave as-is unless there's an official name (like for Japanese).">EmotionEngine (MIPS-IV)</string>
</property>
<layout class="QGridLayout" name="gridLayout_7">
<item row="0" column="0">
<widget class="QLabel" name="eeRoundingLabel">
<property name="text">
<string extracomment="Rounding refers here to the mathematical term.">Rounding Mode:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="eeRoundingMode">
<item>
<property name="text">
<string>Nearest</string>
</property>
</item>
<item>
<property name="text">
<string>Negative</string>
</property>
</item>
<item>
<property name="text">
<string>Positive</string>
</property>
</item>
<item>
<property name="text">
<string>Chop/Zero (Default)</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="eeDivRoundingLabel">
<property name="text">
<string extracomment="Rounding refers here to the mathematical term.">Division Rounding Mode:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="eeDivRoundingMode">
<item>
<property name="text">
<string>Nearest (Default)</string>
</property>
</item>
<item>
<property name="text">
<string>Negative</string>
</property>
</item>
<item>
<property name="text">
<string>Positive</string>
</property>
</item>
<item>
<property name="text">
<string>Chop/Zero</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="eeClampLabel">
<property name="text">
<string extracomment="Clamping: Forcing out of bounds things in bounds by changing them to the closest possible value. In this case, this refers to clamping large PS2 floating point values (which map to infinity or NaN in PCs' IEEE754 floats) to non-infinite ones.">Clamping Mode:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="eeClampMode">
<item>
<property name="text">
<string comment="ClampMode">None</string>
</property>
</item>
<item>
<property name="text">
<string>Normal (Default)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Sign: refers here to the mathematical meaning (plus/minus).">Extra + Preserve Sign</string>
</property>
</item>
<item>
<property name="text">
<string>Full</string>
</property>
</item>
</widget>
</item>
<item row="3" column="0" colspan="2">
<layout class="QGridLayout" name="eeSettingsMisc">
<item row="1" column="0">
<widget class="QCheckBox" name="eeWaitLoopDetection">
<property name="text">
<string>Wait Loop Detection</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="eeRecompiler">
<property name="text">
<string>Enable Recompiler</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="eeFastmem">
<property name="text">
<string>Enable Fast Memory Access</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="eeCache">
<property name="text">
<string>Enable Cache (Slow)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="eeINTCSpinDetection">
<property name="text">
<string>INTC Spin Detection</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="pauseOnTLBMiss">
<property name="text">
<string>Pause On TLB Miss</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="extraMemory">
<property name="text">
<string>Enable 128MB RAM (Dev Console)</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="vuSettings">
<property name="title">
<string extracomment="Vector Unit/VU: refers to two of PS2's processors. Do not translate the full text or do so as a comment. Leave the acronym as-is.">Vector Units (VU)</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="2" column="0">
<widget class="QLabel" name="vu1RoundingLabel">
<property name="text">
<string>VU1 Rounding Mode:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="vu0RoundingMode">
<item>
<property name="text">
<string>Nearest</string>
</property>
</item>
<item>
<property name="text">
<string>Negative</string>
</property>
</item>
<item>
<property name="text">
<string>Positive</string>
</property>
</item>
<item>
<property name="text">
<string>Chop/Zero (Default)</string>
</property>
</item>
</widget>
</item>
<item row="4" column="0" colspan="2">
<layout class="QGridLayout" name="vuSettingsLayout">
<item row="1" column="0">
<widget class="QCheckBox" name="vuFlagHack">
<property name="text">
<string>mVU Flag Hack</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="vu1Recompiler">
<property name="text">
<string>Enable VU1 Recompiler</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="vu0Recompiler">
<property name="text">
<string>Enable VU0 Recompiler (Micro Mode)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="instantVU1">
<property name="text">
<string>Enable Instant VU1</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="vu0ClampMode">
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>Normal (Default)</string>
</property>
</item>
<item>
<property name="text">
<string>Extra</string>
</property>
</item>
<item>
<property name="text">
<string>Extra + Preserve Sign</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="vu0ClampLabel">
<property name="text">
<string>VU0 Clamping Mode:</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="vu0RoundingLabel">
<property name="text">
<string>VU0 Rounding Mode:</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="vu1ClampLabel">
<property name="text">
<string>VU1 Clamping Mode:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="vu1RoundingMode">
<item>
<property name="text">
<string>Nearest</string>
</property>
</item>
<item>
<property name="text">
<string>Negative</string>
</property>
</item>
<item>
<property name="text">
<string>Positive</string>
</property>
</item>
<item>
<property name="text">
<string>Chop/Zero (Default)</string>
</property>
</item>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="vu1ClampMode">
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>Normal (Default)</string>
</property>
</item>
<item>
<property name="text">
<string>Extra</string>
</property>
</item>
<item>
<property name="text">
<string>Extra + Preserve Sign</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="iopSettings">
<property name="title">
<string>I/O Processor (IOP, MIPS-I)</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QCheckBox" name="iopRecompiler">
<property name="text">
<string>Enable Recompiler</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gameSettings">
<property name="title">
<string>Game Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QCheckBox" name="gameFixes">
<property name="text">
<string>Enable Game Fixes</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="patches">
<property name="text">
<string>Enable Compatibility Patches</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="savestateSettings">
<property name="title">
<string>Savestate Settings</string>
</property>
<layout class="QGridLayout" name="savestateSettingsLayout">
<item row="3" column="1">
<widget class="QCheckBox" name="saveStateOnShutdown">
<property name="text">
<string>Save State On Shutdown</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="savestateCompressionLevel">
<item>
<property name="text">
<string>Low (Fast)</string>
</property>
</item>
<item>
<property name="text">
<string>Medium (Recommended)</string>
</property>
</item>
<item>
<property name="text">
<string>High</string>
</property>
</item>
<item>
<property name="text">
<string>Very High (Slow, Not Recommended)</string>
</property>
</item>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="savestateCompressionMethod">
<item>
<property name="text">
<string>Uncompressed</string>
</property>
</item>
<item>
<property name="text">
<string>Deflate64</string>
</property>
</item>
<item>
<property name="text">
<string>Zstandard</string>
</property>
</item>
<item>
<property name="text">
<string>LZMA2</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="savestateCompressionMethodLabel">
<property name="text">
<string>Compression Level:</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="backupSaveStates">
<property name="text">
<string>Create Save State Backups</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="savestateCompressionLabel">
<property name="text">
<string>Compression Method:</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="savestateSelector">
<property name="text">
<string>Use Save State Selector</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="framerateControlSettings">
<property name="title">
<string>Frame Rate Control</string>
</property>
<layout class="QGridLayout" name="framerateControlLayout">
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="palFrameRate">
<property name="suffix">
<string extracomment="hz=Hertz, as in the measuring unit. Shown after the corresponding number. Those languages who'd need to remove the space or do something in between should do so."> hz</string>
</property>
<property name="minimum">
<double>10.000000000000000</double>
</property>
<property name="maximum">
<double>300.000000000000000</double>
</property>
<property name="singleStep">
<double>0.010000000000000</double>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="ntscFrameRate">
<property name="suffix">
<string> hz</string>
</property>
<property name="minimum">
<double>10.000000000000000</double>
</property>
<property name="maximum">
<double>300.000000000000000</double>
</property>
<property name="singleStep">
<double>0.010000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="palLabel">
<property name="text">
<string>PAL Frame Rate:</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="ntscLabel">
<property name="text">
<string>NTSC Frame Rate:</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="pineSettings">
<property name="title">
<string>PINE Settings</string>
</property>
<layout class="QGridLayout" name="pineSettingsLayout">
<item row="1" column="1">
<widget class="QLineEdit" name="pineSlot">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="pineSlotLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Slot:</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="pineEnable">
<property name="text">
<string>Enable</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacerBottom">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>3</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,476 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AudioExpansionSettingsDialog</class>
<widget class="QDialog" name="AudioExpansionSettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>614</width>
<height>371</height>
</rect>
</property>
<property name="windowTitle">
<string>Audio Expansion Settings</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Circular Wrap:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QSlider" name="circularWrap">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>360</number>
</property>
<property name="value">
<number>90</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="circularWrapLabel">
<property name="text">
<string>30</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Shift:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QSlider" name="shift">
<property name="minimum">
<number>-100</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>20</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>5</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="shiftLabel">
<property name="text">
<string>20</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Depth:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QSlider" name="depth">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>50</number>
</property>
<property name="value">
<number>10</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>2</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="depthLabel">
<property name="text">
<string>10</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Focus:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QSlider" name="focus">
<property name="minimum">
<number>-100</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>20</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>5</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="focusLabel">
<property name="text">
<string>20</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Center Image:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QSlider" name="centerImage">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>20</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>5</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="centerImageLabel">
<property name="text">
<string>20</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Front Separation:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QSlider" name="frontSeparation">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>20</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>5</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="frontSeparationLabel">
<property name="text">
<string>20</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Rear Separation:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QSlider" name="rearSeparation">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>20</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>5</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="rearSeparationLabel">
<property name="text">
<string>20</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Low Cutoff:</string>
</property>
</widget>
</item>
<item row="9" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QSlider" name="lowCutoff">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>20</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>5</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lowCutoffLabel">
<property name="text">
<string>20</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>High Cutoff:</string>
</property>
</widget>
</item>
<item row="10" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<widget class="QSlider" name="highCutoff">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>20</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>5</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="highCutoffLabel">
<property name="text">
<string>20</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="11" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Close|QDialogButtonBox::StandardButton::RestoreDefaults</set>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="icon">
<property name="minimumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Audio Expansion Settings&lt;/span&gt;&lt;br/&gt;These settings fine-tune the behavior of the FreeSurround-based channel expander.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::TextFormat::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Block Size:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QSlider" name="blockSize">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>8192</number>
</property>
<property name="singleStep">
<number>16</number>
</property>
<property name="pageStep">
<number>128</number>
</property>
<property name="value">
<number>1024</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>128</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="blockSizeLabel">
<property name="text">
<string>30</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,506 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "AudioSettingsWidget.h"
#include "QtHost.h"
#include "QtUtils.h"
#include "SettingWidgetBinder.h"
#include "SettingsWindow.h"
#include "ui_AudioExpansionSettingsDialog.h"
#include "ui_AudioStretchSettingsDialog.h"
#include "pcsx2/Host/AudioStream.h"
#include "pcsx2/SPU2/spu2.h"
#include "pcsx2/VMManager.h"
#include <QtWidgets/QMessageBox>
#include <algorithm>
#include <bit>
AudioSettingsWidget::AudioSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: QWidget(parent)
, m_dialog(dialog)
{
SettingsInterface* sif = dialog->getSettingsInterface();
m_ui.setupUi(this);
for (u32 i = 0; i < static_cast<u32>(AudioBackend::Count); i++)
m_ui.audioBackend->addItem(QString::fromUtf8(AudioStream::GetBackendDisplayName(static_cast<AudioBackend>(i))));
for (u32 i = 0; i < static_cast<u32>(AudioExpansionMode::Count); i++)
{
m_ui.expansionMode->addItem(
QString::fromUtf8(AudioStream::GetExpansionModeDisplayName(static_cast<AudioExpansionMode>(i))));
}
for (u32 i = 0; i < static_cast<u32>(Pcsx2Config::SPU2Options::SPU2SyncMode::Count); i++)
{
m_ui.syncMode->addItem(
QString::fromUtf8(Pcsx2Config::SPU2Options::GetSyncModeDisplayName(
static_cast<Pcsx2Config::SPU2Options::SPU2SyncMode>(i))));
}
SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.audioBackend, "SPU2/Output", "Backend",
&AudioStream::ParseBackendName, &AudioStream::GetBackendName,
Pcsx2Config::SPU2Options::DEFAULT_BACKEND);
SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.expansionMode, "SPU2/Output", "ExpansionMode",
&AudioStream::ParseExpansionMode, &AudioStream::GetExpansionModeName,
AudioStreamParameters::DEFAULT_EXPANSION_MODE);
SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.syncMode, "SPU2/Output", "SyncMode",
&Pcsx2Config::SPU2Options::ParseSyncMode, &Pcsx2Config::SPU2Options::GetSyncModeName,
Pcsx2Config::SPU2Options::DEFAULT_SYNC_MODE);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.bufferMS, "SPU2/Output", "BufferMS",
AudioStreamParameters::DEFAULT_BUFFER_MS);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.outputLatencyMS, "SPU2/Output", "OutputLatencyMS",
AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.outputLatencyMinimal, "SPU2/Output", "OutputLatencyMinimal", false);
connect(m_ui.audioBackend, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::updateDriverNames);
connect(m_ui.expansionMode, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::onExpansionModeChanged);
connect(m_ui.expansionSettings, &QToolButton::clicked, this, &AudioSettingsWidget::onExpansionSettingsClicked);
connect(m_ui.syncMode, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::onSyncModeChanged);
connect(m_ui.stretchSettings, &QToolButton::clicked, this, &AudioSettingsWidget::onStretchSettingsClicked);
onExpansionModeChanged();
onSyncModeChanged();
updateDriverNames();
connect(m_ui.bufferMS, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabel);
connect(m_ui.outputLatencyMS, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabel);
connectCheckStateChanged(m_ui.outputLatencyMinimal, this, &AudioSettingsWidget::onMinimalOutputLatencyChanged);
onMinimalOutputLatencyChanged();
updateLatencyLabel();
// for per-game, just use the normal path, since it needs to re-read/apply
if (!dialog->isPerGameSettings())
{
m_ui.volume->setValue(m_dialog->getEffectiveIntValue("SPU2/Output", "OutputVolume", 100));
m_ui.fastForwardVolume->setValue(m_dialog->getEffectiveIntValue("SPU2/Output", "FastForwardVolume", 100));
m_ui.muted->setChecked(m_dialog->getEffectiveBoolValue("SPU2/Output", "OutputMuted", false));
connect(m_ui.volume, &QSlider::valueChanged, this, &AudioSettingsWidget::onOutputVolumeChanged);
connect(m_ui.fastForwardVolume, &QSlider::valueChanged, this, &AudioSettingsWidget::onFastForwardVolumeChanged);
connectCheckStateChanged(m_ui.muted, this, &AudioSettingsWidget::onOutputMutedChanged);
updateVolumeLabel();
}
else
{
SettingWidgetBinder::BindWidgetAndLabelToIntSetting(sif, m_ui.volume, m_ui.volumeLabel, tr("%"), "SPU2/Output", "OutputVolume", 100);
SettingWidgetBinder::BindWidgetAndLabelToIntSetting(sif, m_ui.fastForwardVolume, m_ui.fastForwardVolumeLabel, tr("%"), "SPU2/Output", "FastForwardVolume", 100);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.muted, "SPU2/Output", "OutputMuted", false);
}
connect(m_ui.resetVolume, &QToolButton::clicked, this, [this]() { resetVolume(false); });
connect(m_ui.resetFastForwardVolume, &QToolButton::clicked, this, [this]() { resetVolume(true); });
dialog->registerWidgetHelp(
m_ui.audioBackend, tr("Audio Backend"), QStringLiteral("Cubeb"),
tr("The audio backend determines how frames produced by the emulator are submitted to the host. Cubeb provides the "
"lowest latency, if you encounter issues, try the SDL backend. The null backend disables all host audio "
"output."));
dialog->registerWidgetHelp(
m_ui.bufferMS, tr("Buffer Size"), tr("%1 ms").arg(AudioStreamParameters::DEFAULT_BUFFER_MS),
tr("Determines the buffer size which the time stretcher will try to keep filled. It effectively selects the "
"average latency, as audio will be stretched/shrunk to keep the buffer size within check."));
dialog->registerWidgetHelp(
m_ui.outputLatencyMS, tr("Output Latency"), tr("%1 ms").arg(AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS),
tr("Determines the latency from the buffer to the host audio output. This can be set lower than the target latency "
"to reduce audio delay."));
dialog->registerWidgetHelp(m_ui.volume, tr("Output Volume"), "100%",
tr("Controls the volume of the audio played on the host."));
dialog->registerWidgetHelp(m_ui.fastForwardVolume, tr("Fast Forward Volume"), "100%",
tr("Controls the volume of the audio played on the host when fast forwarding."));
dialog->registerWidgetHelp(m_ui.muted, tr("Mute All Sound"), tr("Unchecked"),
tr("Prevents the emulator from producing any audible sound."));
dialog->registerWidgetHelp(m_ui.expansionMode, tr("Expansion Mode"), tr("Disabled (Stereo)"),
tr("Determines how audio is expanded from stereo to surround for supported games. This "
"includes games that support Dolby Pro Logic/Pro Logic II."));
dialog->registerWidgetHelp(m_ui.expansionSettings, tr("Expansion Settings"), tr("N/A"),
tr("These settings fine-tune the behavior of the FreeSurround-based channel expander."));
dialog->registerWidgetHelp(m_ui.syncMode, tr("Synchronization"), tr("TimeStretch (Recommended)"),
tr("When running outside of 100% speed, adjusts the tempo on audio instead of dropping frames. Produces much nicer fast-forward/slowdown audio."));
dialog->registerWidgetHelp(m_ui.stretchSettings, tr("Stretch Settings"), tr("N/A"),
tr("These settings fine-tune the behavior of the SoundTouch audio time stretcher when running outside of 100% speed."));
dialog->registerWidgetHelp(m_ui.resetVolume, tr("Reset Volume"), tr("N/A"),
m_dialog->isPerGameSettings() ? tr("Resets output volume back to the global/inherited setting.") :
tr("Resets output volume back to the default."));
dialog->registerWidgetHelp(m_ui.resetFastForwardVolume, tr("Reset Fast Forward Volume"), tr("N/A"),
m_dialog->isPerGameSettings() ? tr("Resets fast forward volume back to the global/inherited setting.") :
tr("Resets fast forward volume back to the default."));
}
AudioSettingsWidget::~AudioSettingsWidget() = default;
AudioExpansionMode AudioSettingsWidget::getEffectiveExpansionMode() const
{
return AudioStream::ParseExpansionMode(
m_dialog->getEffectiveStringValue("SPU2/Output", "ExpansionMode",
AudioStream::GetExpansionModeName(AudioStreamParameters::DEFAULT_EXPANSION_MODE))
.c_str())
.value_or(AudioStreamParameters::DEFAULT_EXPANSION_MODE);
}
u32 AudioSettingsWidget::getEffectiveExpansionBlockSize() const
{
const AudioExpansionMode expansion_mode = getEffectiveExpansionMode();
if (expansion_mode == AudioExpansionMode::Disabled)
return 0;
const u32 config_block_size = m_dialog->getEffectiveIntValue("SPU2/Output", "ExpandBlockSize",
AudioStreamParameters::DEFAULT_EXPAND_BLOCK_SIZE);
return std::has_single_bit(config_block_size) ? config_block_size : std::bit_ceil(config_block_size);
}
void AudioSettingsWidget::onExpansionModeChanged()
{
const AudioExpansionMode expansion_mode = getEffectiveExpansionMode();
m_ui.expansionSettings->setEnabled(expansion_mode != AudioExpansionMode::Disabled);
updateLatencyLabel();
}
void AudioSettingsWidget::onSyncModeChanged()
{
const Pcsx2Config::SPU2Options::SPU2SyncMode sync_mode =
Pcsx2Config::SPU2Options::ParseSyncMode(
m_dialog
->getEffectiveStringValue("SPU2/Output", "SyncMode",
Pcsx2Config::SPU2Options::GetSyncModeName(Pcsx2Config::SPU2Options::DEFAULT_SYNC_MODE))
.c_str())
.value_or(Pcsx2Config::SPU2Options::DEFAULT_SYNC_MODE);
m_ui.stretchSettings->setEnabled(sync_mode == Pcsx2Config::SPU2Options::SPU2SyncMode::TimeStretch);
}
AudioBackend AudioSettingsWidget::getEffectiveBackend() const
{
return AudioStream::ParseBackendName(m_dialog->getEffectiveStringValue("SPU2/Output", "Backend",
AudioStream::GetBackendName(Pcsx2Config::SPU2Options::DEFAULT_BACKEND))
.c_str())
.value_or(Pcsx2Config::SPU2Options::DEFAULT_BACKEND);
}
void AudioSettingsWidget::updateDriverNames()
{
const AudioBackend backend = getEffectiveBackend();
const std::vector<std::pair<std::string, std::string>> names = AudioStream::GetDriverNames(backend);
m_ui.driver->disconnect();
m_ui.driver->clear();
if (names.empty())
{
m_ui.driver->addItem(tr("Default"), QString());
m_ui.driver->setEnabled(false);
}
else
{
m_ui.driver->setEnabled(true);
for (const std::pair<std::string, std::string>& it : names)
m_ui.driver->addItem(QString::fromStdString(it.second), QString::fromStdString(it.first));
SettingWidgetBinder::BindWidgetToStringSetting(m_dialog->getSettingsInterface(), m_ui.driver, "SPU2/Output", "DriverName",
std::move(names.front().first));
connect(m_ui.driver, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::updateDeviceNames);
}
updateDeviceNames();
}
void AudioSettingsWidget::updateDeviceNames()
{
const AudioBackend backend = getEffectiveBackend();
const std::string driver_name = m_dialog->getEffectiveStringValue("SPU2/Output", "DriverName", "");
const std::string current_device = m_dialog->getEffectiveStringValue("SPU2/Output", "DeviceName", "");
const std::vector<AudioStream::DeviceInfo> devices = AudioStream::GetOutputDevices(backend, driver_name.c_str());
m_ui.outputDevice->disconnect();
m_ui.outputDevice->clear();
m_output_device_latency = 0;
if (devices.empty())
{
m_ui.outputDevice->addItem(tr("Default"), QString());
m_ui.outputDevice->setEnabled(false);
}
else
{
m_ui.outputDevice->setEnabled(true);
bool is_known_device = false;
for (const AudioStream::DeviceInfo& di : devices)
{
m_ui.outputDevice->addItem(QString::fromStdString(di.display_name), QString::fromStdString(di.name));
if (di.name == current_device)
{
m_output_device_latency = di.minimum_latency_frames;
is_known_device = true;
}
}
if (!is_known_device)
{
m_ui.outputDevice->addItem(tr("Unknown Device \"%1\"").arg(QString::fromStdString(current_device)),
QString::fromStdString(current_device));
}
SettingWidgetBinder::BindWidgetToStringSetting(m_dialog->getSettingsInterface(), m_ui.outputDevice, "SPU2/Output",
"DeviceName", std::move(devices.front().name));
}
updateLatencyLabel();
}
void AudioSettingsWidget::updateLatencyLabel()
{
const u32 expand_buffer_ms = AudioStream::GetMSForBufferSize(SPU2::SAMPLE_RATE, getEffectiveExpansionBlockSize());
const u32 config_buffer_ms = m_dialog->getEffectiveIntValue("SPU2/Output", "BufferMS", AudioStreamParameters::DEFAULT_BUFFER_MS);
const u32 config_output_latency_ms = m_dialog->getEffectiveIntValue("SPU2/Output", "OutputLatencyMS", AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS);
const bool minimal_output = m_dialog->getEffectiveBoolValue("SPU2/Output", "OutputLatencyMinimal", false);
//: Preserve the %1 variable, adapt the latter ms (and/or any possible spaces in between) to your language's ruleset.
m_ui.outputLatencyLabel->setText(minimal_output ? tr("N/A") : tr("%1 ms").arg(config_output_latency_ms));
m_ui.bufferMSLabel->setText(tr("%1 ms").arg(config_buffer_ms));
const u32 output_latency_ms = minimal_output ? AudioStream::GetMSForBufferSize(SPU2::SAMPLE_RATE, m_output_device_latency) : config_output_latency_ms;
if (output_latency_ms > 0)
{
if (expand_buffer_ms > 0)
{
m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 ms (%2 ms buffer + %3 ms expand + %4 ms output)")
.arg(config_buffer_ms + expand_buffer_ms + output_latency_ms)
.arg(config_buffer_ms)
.arg(expand_buffer_ms)
.arg(output_latency_ms));
}
else
{
m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 ms (%2 ms buffer + %3 ms output)")
.arg(config_buffer_ms + output_latency_ms)
.arg(config_buffer_ms)
.arg(output_latency_ms));
}
}
else
{
if (expand_buffer_ms > 0)
{
m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 ms (%2 ms expand, minimum output latency unknown)")
.arg(expand_buffer_ms + config_buffer_ms)
.arg(expand_buffer_ms));
}
else
{
m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 ms (minimum output latency unknown)").arg(config_buffer_ms));
}
}
}
void AudioSettingsWidget::updateVolumeLabel()
{
m_ui.volumeLabel->setText(tr("%1%").arg(m_ui.volume->value()));
m_ui.fastForwardVolumeLabel->setText(tr("%1%").arg(m_ui.fastForwardVolume->value()));
}
void AudioSettingsWidget::onMinimalOutputLatencyChanged()
{
const bool minimal = m_dialog->getEffectiveBoolValue("SPU2/Output", "OutputLatencyMinimal", false);
m_ui.outputLatencyMS->setEnabled(!minimal);
updateLatencyLabel();
}
void AudioSettingsWidget::onOutputVolumeChanged(int new_value)
{
// only called for base settings
pxAssert(!m_dialog->isPerGameSettings());
Host::SetBaseIntSettingValue("SPU2/Output", "OutputVolume", new_value);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
updateVolumeLabel();
}
void AudioSettingsWidget::onFastForwardVolumeChanged(int new_value)
{
// only called for base settings
pxAssert(!m_dialog->isPerGameSettings());
Host::SetBaseIntSettingValue("SPU2/Output", "FastForwardVolume", new_value);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
updateVolumeLabel();
}
void AudioSettingsWidget::onOutputMutedChanged(int new_state)
{
// only called for base settings
pxAssert(!m_dialog->isPerGameSettings());
const bool muted = (new_state != 0);
Host::SetBaseBoolSettingValue("SPU2/Output", "OutputMuted", muted);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
}
void AudioSettingsWidget::onExpansionSettingsClicked()
{
QDialog dlg(QtUtils::GetRootWidget(this));
Ui::AudioExpansionSettingsDialog dlgui;
dlgui.setupUi(&dlg);
QtUtils::SetScalableIcon(dlgui.icon, QIcon::fromTheme(QStringLiteral("volume-up-line")), QSize(32, 32));
SettingsInterface* sif = m_dialog->getSettingsInterface();
SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.blockSize, "SPU2/Output", "ExpandBlockSize",
AudioStreamParameters::DEFAULT_EXPAND_BLOCK_SIZE, 0);
QtUtils::BindLabelToSlider(dlgui.blockSize, dlgui.blockSizeLabel);
SettingWidgetBinder::BindWidgetToFloatSetting(sif, dlgui.circularWrap, "SPU2/Output", "ExpandCircularWrap",
AudioStreamParameters::DEFAULT_EXPAND_CIRCULAR_WRAP);
QtUtils::BindLabelToSlider(dlgui.circularWrap, dlgui.circularWrapLabel);
SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.shift, "SPU2/Output", "ExpandShift", 100.0f,
AudioStreamParameters::DEFAULT_EXPAND_SHIFT);
QtUtils::BindLabelToSlider(dlgui.shift, dlgui.shiftLabel, 100.0f);
SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.depth, "SPU2/Output", "ExpandDepth", 10.0f,
AudioStreamParameters::DEFAULT_EXPAND_DEPTH);
QtUtils::BindLabelToSlider(dlgui.depth, dlgui.depthLabel, 10.0f);
SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.focus, "SPU2/Output", "ExpandFocus", 100.0f,
AudioStreamParameters::DEFAULT_EXPAND_FOCUS);
QtUtils::BindLabelToSlider(dlgui.focus, dlgui.focusLabel, 100.0f);
SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.centerImage, "SPU2/Output", "ExpandCenterImage", 100.0f,
AudioStreamParameters::DEFAULT_EXPAND_CENTER_IMAGE);
QtUtils::BindLabelToSlider(dlgui.centerImage, dlgui.centerImageLabel, 100.0f);
SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.frontSeparation, "SPU2/Output", "ExpandFrontSeparation",
10.0f, AudioStreamParameters::DEFAULT_EXPAND_FRONT_SEPARATION);
QtUtils::BindLabelToSlider(dlgui.frontSeparation, dlgui.frontSeparationLabel, 10.0f);
SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.rearSeparation, "SPU2/Output", "ExpandRearSeparation", 10.0f,
AudioStreamParameters::DEFAULT_EXPAND_REAR_SEPARATION);
QtUtils::BindLabelToSlider(dlgui.rearSeparation, dlgui.rearSeparationLabel, 10.0f);
SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.lowCutoff, "SPU2/Output", "ExpandLowCutoff",
AudioStreamParameters::DEFAULT_EXPAND_LOW_CUTOFF);
QtUtils::BindLabelToSlider(dlgui.lowCutoff, dlgui.lowCutoffLabel);
SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.highCutoff, "SPU2/Output", "ExpandHighCutoff",
AudioStreamParameters::DEFAULT_EXPAND_HIGH_CUTOFF);
QtUtils::BindLabelToSlider(dlgui.highCutoff, dlgui.highCutoffLabel);
connect(dlgui.buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, &dlg, &QDialog::accept);
connect(dlgui.buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, [this, &dlg]() {
m_dialog->setIntSettingValue("SPU2/Output", "ExpandBlockSize",
m_dialog->isPerGameSettings() ?
std::nullopt :
std::optional<int>(AudioStreamParameters::DEFAULT_EXPAND_BLOCK_SIZE));
m_dialog->setFloatSettingValue("SPU2/Output", "ExpandCircularWrap",
m_dialog->isPerGameSettings() ?
std::nullopt :
std::optional<float>(AudioStreamParameters::DEFAULT_EXPAND_CIRCULAR_WRAP));
m_dialog->setFloatSettingValue(
"SPU2/Output", "ExpandShift",
m_dialog->isPerGameSettings() ? std::nullopt : std::optional<float>(AudioStreamParameters::DEFAULT_EXPAND_SHIFT));
m_dialog->setFloatSettingValue(
"SPU2/Output", "ExpandDepth",
m_dialog->isPerGameSettings() ? std::nullopt : std::optional<float>(AudioStreamParameters::DEFAULT_EXPAND_DEPTH));
m_dialog->setFloatSettingValue(
"SPU2/Output", "ExpandFocus",
m_dialog->isPerGameSettings() ? std::nullopt : std::optional<float>(AudioStreamParameters::DEFAULT_EXPAND_FOCUS));
m_dialog->setFloatSettingValue("SPU2/Output", "ExpandCenterImage",
m_dialog->isPerGameSettings() ?
std::nullopt :
std::optional<float>(AudioStreamParameters::DEFAULT_EXPAND_CENTER_IMAGE));
m_dialog->setFloatSettingValue("SPU2/Output", "ExpandFrontSeparation",
m_dialog->isPerGameSettings() ?
std::nullopt :
std::optional<float>(AudioStreamParameters::DEFAULT_EXPAND_FRONT_SEPARATION));
m_dialog->setFloatSettingValue("SPU2/Output", "ExpandRearSeparation",
m_dialog->isPerGameSettings() ?
std::nullopt :
std::optional<float>(AudioStreamParameters::DEFAULT_EXPAND_REAR_SEPARATION));
m_dialog->setIntSettingValue("SPU2/Output", "ExpandLowCutoff",
m_dialog->isPerGameSettings() ?
std::nullopt :
std::optional<int>(AudioStreamParameters::DEFAULT_EXPAND_LOW_CUTOFF));
m_dialog->setIntSettingValue("SPU2/Output", "ExpandHighCutoff",
m_dialog->isPerGameSettings() ?
std::nullopt :
std::optional<int>(AudioStreamParameters::DEFAULT_EXPAND_HIGH_CUTOFF));
dlg.done(0);
QMetaObject::invokeMethod(this, &AudioSettingsWidget::onExpansionSettingsClicked, Qt::QueuedConnection);
});
dlg.exec();
updateLatencyLabel();
}
void AudioSettingsWidget::onStretchSettingsClicked()
{
QDialog dlg(QtUtils::GetRootWidget(this));
Ui::AudioStretchSettingsDialog dlgui;
dlgui.setupUi(&dlg);
QtUtils::SetScalableIcon(dlgui.icon, QIcon::fromTheme(QStringLiteral("volume-up-line")), QSize(32, 32));
SettingsInterface* sif = m_dialog->getSettingsInterface();
SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.sequenceLength, "SPU2/Output", "StretchSequenceLengthMS",
AudioStreamParameters::DEFAULT_STRETCH_SEQUENCE_LENGTH, 0);
QtUtils::BindLabelToSlider(dlgui.sequenceLength, dlgui.sequenceLengthLabel);
SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.seekWindowSize, "SPU2/Output", "StretchSeekWindowMS",
AudioStreamParameters::DEFAULT_STRETCH_SEEKWINDOW, 0);
QtUtils::BindLabelToSlider(dlgui.seekWindowSize, dlgui.seekWindowSizeLabel);
SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.overlap, "SPU2/Output", "StretchOverlapMS",
AudioStreamParameters::DEFAULT_STRETCH_OVERLAP, 0);
QtUtils::BindLabelToSlider(dlgui.overlap, dlgui.overlapLabel);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.useQuickSeek, "SPU2/Output", "StretchUseQuickSeek",
AudioStreamParameters::DEFAULT_STRETCH_USE_QUICKSEEK);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.useAAFilter, "SPU2/Output", "StretchUseAAFilter",
AudioStreamParameters::DEFAULT_STRETCH_USE_AA_FILTER);
connect(dlgui.buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, &dlg, &QDialog::accept);
connect(dlgui.buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, [this, &dlg]() {
m_dialog->setIntSettingValue("SPU2/Output", "StretchSequenceLengthMS",
m_dialog->isPerGameSettings() ?
std::nullopt :
std::optional<int>(AudioStreamParameters::DEFAULT_STRETCH_SEQUENCE_LENGTH));
m_dialog->setIntSettingValue("SPU2/Output", "StretchSeekWindowMS",
m_dialog->isPerGameSettings() ?
std::nullopt :
std::optional<int>(AudioStreamParameters::DEFAULT_STRETCH_SEEKWINDOW));
m_dialog->setIntSettingValue("SPU2/Output", "StretchOverlapMS",
m_dialog->isPerGameSettings() ?
std::nullopt :
std::optional<int>(AudioStreamParameters::DEFAULT_STRETCH_OVERLAP));
m_dialog->setBoolSettingValue("SPU2/Output", "StretchUseQuickSeek",
m_dialog->isPerGameSettings() ?
std::nullopt :
std::optional<bool>(AudioStreamParameters::DEFAULT_STRETCH_USE_QUICKSEEK));
m_dialog->setBoolSettingValue("SPU2/Output", "StretchUseAAFilter",
m_dialog->isPerGameSettings() ?
std::nullopt :
std::optional<bool>(AudioStreamParameters::DEFAULT_STRETCH_USE_AA_FILTER));
dlg.done(0);
QMetaObject::invokeMethod(this, &AudioSettingsWidget::onStretchSettingsClicked, Qt::QueuedConnection);
});
dlg.exec();
}
void AudioSettingsWidget::resetVolume(bool fast_forward)
{
const char* key = fast_forward ? "FastForwardVolume" : "OutputVolume";
QSlider* const slider = fast_forward ? m_ui.fastForwardVolume : m_ui.volume;
QLabel* const label = fast_forward ? m_ui.fastForwardVolumeLabel : m_ui.volumeLabel;
if (m_dialog->isPerGameSettings())
{
m_dialog->removeSettingValue("SPU2/Output", key);
const int value = m_dialog->getEffectiveIntValue("SPU2/Output", key, 100);
QSignalBlocker sb(slider);
slider->setValue(value);
label->setText(QStringLiteral("%1%2").arg(value).arg(tr("%")));
// remove bold font if it was previously overridden
QFont font(label->font());
font.setBold(false);
label->setFont(font);
}
else
{
slider->setValue(100);
}
}

View File

@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "ui_AudioSettingsWidget.h"
#include "common/Pcsx2Defs.h"
#include <QtWidgets/QWidget>
enum class AudioBackend : u8;
enum class AudioExpansionMode : u8;
class SettingsWindow;
class AudioSettingsWidget : public QWidget
{
Q_OBJECT
public:
AudioSettingsWidget(SettingsWindow* dialog, QWidget* parent);
~AudioSettingsWidget();
private Q_SLOTS:
void onExpansionModeChanged();
void onSyncModeChanged();
void updateDriverNames();
void updateDeviceNames();
void updateLatencyLabel();
void updateVolumeLabel();
void onMinimalOutputLatencyChanged();
void onOutputVolumeChanged(int new_value);
void onFastForwardVolumeChanged(int new_value);
void onOutputMutedChanged(int new_state);
void onExpansionSettingsClicked();
void onStretchSettingsClicked();
private:
AudioBackend getEffectiveBackend() const;
AudioExpansionMode getEffectiveExpansionMode() const;
u32 getEffectiveExpansionBlockSize() const;
void resetVolume(bool fast_forward);
Ui::AudioSettingsWidget m_ui;
SettingsWindow* m_dialog;
u32 m_output_device_latency = 0;
};

View File

@@ -0,0 +1,360 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AudioSettingsWidget</class>
<widget class="QWidget" name="AudioSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>523</width>
<height>504</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Configuration</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Driver:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="driver"/>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0">
<item>
<widget class="QComboBox" name="expansionMode"/>
</item>
<item>
<widget class="QToolButton" name="expansionSettings">
<property name="toolTip">
<string>Expansion Settings</string>
</property>
<property name="icon">
<iconset theme="settings-3-line"/>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4" stretch="1,0">
<item>
<widget class="QSlider" name="bufferMS">
<property name="minimum">
<number>15</number>
</property>
<property name="maximum">
<number>500</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="pageStep">
<number>5</number>
</property>
<property name="value">
<number>50</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBothSides</enum>
</property>
<property name="tickInterval">
<number>20</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="bufferMSLabel">
<property name="text">
<string>0 ms</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,0">
<item>
<widget class="QComboBox" name="syncMode"/>
</item>
<item>
<widget class="QToolButton" name="stretchSettings">
<property name="toolTip">
<string>Stretch Settings</string>
</property>
<property name="icon">
<iconset theme="settings-3-line"/>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Buffer Size:</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QLabel" name="bufferingLabel">
<property name="text">
<string>Maximum latency: 0 frames (0.00ms)</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Backend:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QSlider" name="outputLatencyMS">
<property name="minimum">
<number>15</number>
</property>
<property name="maximum">
<number>200</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBothSides</enum>
</property>
<property name="tickInterval">
<number>20</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="outputLatencyLabel">
<property name="text">
<string>0 ms</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="outputLatencyMinimal">
<property name="text">
<string>Minimal</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="audioBackend"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Output Latency:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="outputDevice"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Output Device:</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Expansion:</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Synchronization:</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Controls</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Output Volume:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSlider" name="volume">
<property name="maximum">
<number>200</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBothSides</enum>
</property>
<property name="tickInterval">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="volumeLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>100%</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="resetVolume">
<property name="toolTip">
<string>Reset Volume</string>
</property>
<property name="icon">
<iconset theme="restart-line"/>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QSlider" name="fastForwardVolume">
<property name="maximum">
<number>200</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBothSides</enum>
</property>
<property name="tickInterval">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="fastForwardVolumeLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>100%</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="resetFastForwardVolume">
<property name="toolTip">
<string>Reset Fast Forward Volume</string>
</property>
<property name="icon">
<iconset theme="restart-line"/>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Fast Forward Volume:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="muted">
<property name="text">
<string>Mute All Sound</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,204 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AudioStretchSettingsDialog</class>
<widget class="QDialog" name="AudioStretchSettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>501</width>
<height>248</height>
</rect>
</property>
<property name="windowTitle">
<string>Audio Stretch Settings</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Sequence Length:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QSlider" name="sequenceLength">
<property name="minimum">
<number>20</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>30</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="sequenceLengthLabel">
<property name="text">
<string>30</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Seekwindow Size:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QSlider" name="seekWindowSize">
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>30</number>
</property>
<property name="value">
<number>20</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>2</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="seekWindowSizeLabel">
<property name="text">
<string>20</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Overlap:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QSlider" name="overlap">
<property name="minimum">
<number>5</number>
</property>
<property name="maximum">
<number>15</number>
</property>
<property name="value">
<number>10</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>1</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="overlapLabel">
<property name="text">
<string>10</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Close|QDialogButtonBox::StandardButton::RestoreDefaults</set>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="icon">
<property name="minimumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Audio Stretch Settings&lt;/span&gt;&lt;br/&gt;These settings fine-tune the behavior of the SoundTouch audio time stretcher when running outside of 100% speed.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::TextFormat::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="useQuickSeek">
<property name="text">
<string>Use Quickseek</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="useAAFilter">
<property name="text">
<string>Use Anti-Aliasing Filter</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,145 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "BIOSSettingsWidget.h"
#include "QtHost.h"
#include "QtUtils.h"
#include "SettingWidgetBinder.h"
#include "SettingsWindow.h"
#include "pcsx2/Host.h"
#include "pcsx2/ps2/BiosTools.h"
#include "common/FileSystem.h"
#include <QtGui/QIcon>
#include <QtWidgets/QFileDialog>
#include <algorithm>
BIOSSettingsWidget::BIOSSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: QWidget(parent)
, m_dialog(dialog)
{
SettingsInterface* sif = dialog->getSettingsInterface();
m_ui.setupUi(this);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.fastBoot, "EmuCore", "EnableFastBoot", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.fastBootFastForward, "EmuCore", "EnableFastBootFastForward", false);
SettingWidgetBinder::BindWidgetToFolderSetting(sif, m_ui.searchDirectory, m_ui.browseSearchDirectory, m_ui.openSearchDirectory,
m_ui.resetSearchDirectory, "Folders", "Bios", Path::Combine(EmuFolders::DataRoot, "bios"));
dialog->registerWidgetHelp(m_ui.fastBoot, tr("Fast Boot"), tr("Checked"),
tr("Patches the BIOS to skip the console's boot animation."));
dialog->registerWidgetHelp(m_ui.fastBootFastForward, tr("Fast Forward Boot"), tr("Unchecked"),
tr("Removes emulation speed throttle until the game starts to reduce startup time."));
refreshList();
connect(m_ui.searchDirectory, &QLineEdit::textChanged, this, &BIOSSettingsWidget::refreshList);
connect(m_ui.refresh, &QPushButton::clicked, this, &BIOSSettingsWidget::refreshList);
connect(m_ui.fileList, &QTreeWidget::currentItemChanged, this, &BIOSSettingsWidget::listItemChanged);
connectCheckStateChanged(m_ui.fastBoot, this, &BIOSSettingsWidget::fastBootChanged);
}
BIOSSettingsWidget::~BIOSSettingsWidget() = default;
void BIOSSettingsWidget::refreshList()
{
const std::string search_dir = m_ui.searchDirectory->text().toStdString();
populateList(m_ui.fileList, search_dir);
}
void BIOSSettingsWidget::populateList(QTreeWidget* list, const std::string& directory)
{
const std::string selected_bios = Host::GetBaseStringSettingValue("Filenames", "BIOS");
const QString res_path = QtHost::GetResourcesBasePath();
QSignalBlocker blocker(list);
list->clear();
list->setEnabled(false);
qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
FileSystem::FindResultsArray files;
FileSystem::FindFiles(directory.c_str(), "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES, &files);
u32 bios_version, bios_region;
std::string bios_description, bios_zone;
for (const FILESYSTEM_FIND_DATA& fd : files)
{
if (!IsBIOS(fd.FileName.c_str(), bios_version, bios_description, bios_region, bios_zone))
continue;
const std::string_view bios_name = Path::GetFileName(fd.FileName);
QTreeWidgetItem* item = new QTreeWidgetItem();
item->setText(0, QtUtils::StringViewToQString(bios_name));
item->setText(1, QString::fromStdString(bios_description));
switch (bios_region)
{
case 0: // Japan
item->setIcon(0, QIcon(QStringLiteral("%1/icons/flags/NTSC-J.svg").arg(res_path)));
break;
case 1: // USA
item->setIcon(0, QIcon(QStringLiteral("%1/icons/flags/NTSC-U.svg").arg(res_path)));
break;
case 2: // Europe
item->setIcon(0, QIcon(QStringLiteral("%1/icons/flags/PAL-E.svg").arg(res_path)));
break;
case 3: // Oceania
item->setIcon(0, QIcon(QStringLiteral("%1/icons/flags/PAL-A.svg").arg(res_path)));
break;
case 4: // Asia
item->setIcon(0, QIcon(QStringLiteral("%1/icons/flags/NTSC-HK.svg").arg(res_path)));
break;
case 5: // Russia
item->setIcon(0, QIcon(QStringLiteral("%1/icons/flags/PAL-R.svg").arg(res_path)));
break;
case 6: // China
item->setIcon(0, QIcon(QStringLiteral("%1/icons/flags/NTSC-C.svg").arg(res_path)));
break;
case 7: // Mexico, flag is missing
case 8: // T10K
case 9: // Test
case 10: // Free
default:
item->setIcon(0, QIcon(QStringLiteral("%1/icons/flags/NTSC-J.svg").arg(res_path)));
break;
}
list->addTopLevelItem(item);
if (selected_bios == bios_name)
{
list->selectionModel()->setCurrentIndex(list->indexFromItem(item), QItemSelectionModel::Select);
item->setSelected(true);
}
}
list->setEnabled(true);
}
void BIOSSettingsWidget::listItemChanged(const QTreeWidgetItem* current, const QTreeWidgetItem* previous)
{
Host::SetBaseStringSettingValue("Filenames", "BIOS", current->text(0).toUtf8().constData());
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
}
void BIOSSettingsWidget::fastBootChanged()
{
const bool enabled = m_dialog->getEffectiveBoolValue("EmuCore", "EnableFastBoot", true);
m_ui.fastBootFastForward->setEnabled(enabled);
}

View File

@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtCore/QDir>
#include <QtCore/QPair>
#include <QtCore/QString>
#include <QtCore/QThread>
#include <QtCore/QVector>
#include <QtWidgets/QWidget>
#include <string>
#include "ui_BIOSSettingsWidget.h"
class SettingsWindow;
class QThread;
class BIOSSettingsWidget : public QWidget
{
Q_OBJECT
public:
BIOSSettingsWidget(SettingsWindow* dialog, QWidget* parent);
~BIOSSettingsWidget();
static void populateList(QTreeWidget* list, const std::string& directory);
private Q_SLOTS:
void refreshList();
void listItemChanged(const QTreeWidgetItem* current, const QTreeWidgetItem* previous);
void fastBootChanged();
private:
Ui::BIOSSettingsWidget m_ui;
SettingsWindow* m_dialog;
};

View File

@@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>BIOSSettingsWidget</class>
<widget class="QWidget" name="BIOSSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>618</width>
<height>408</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>BIOS Directory</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_3">
<property name="text">
<string>PCSX2 will search for BIOS images in this directory.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="searchDirectory"/>
</item>
<item>
<widget class="QPushButton" name="browseSearchDirectory">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="resetSearchDirectory">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>BIOS Selection</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="openSearchDirectory">
<property name="text">
<string>Open BIOS Folder...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="refresh">
<property name="text">
<string>Refresh List</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QTreeWidget" name="fileList">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<attribute name="headerMinimumSectionSize">
<number>250</number>
</attribute>
<attribute name="headerDefaultSectionSize">
<number>250</number>
</attribute>
<column>
<property name="text">
<string>Filename</string>
</property>
</column>
<column>
<property name="text">
<string>Version</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Options and Patches</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QCheckBox" name="fastBoot">
<property name="text">
<string>Fast Boot</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="fastBootFastForward">
<property name="text">
<string>Fast Forward Boot</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,319 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "pcsx2/SIO/Pad/PadTypes.h"
#include <QtWidgets/QWidget>
#include <span>
#include "ui_ControllerBindingWidget.h"
#include "ui_ControllerBindingWidget_DualShock2.h"
#include "ui_ControllerBindingWidget_Guitar.h"
#include "ui_ControllerBindingWidget_Jogcon.h"
#include "ui_ControllerBindingWidget_Negcon.h"
#include "ui_ControllerBindingWidget_Popn.h"
#include "ui_ControllerMacroWidget.h"
#include "ui_ControllerMacroEditWidget.h"
#include "ui_USBDeviceWidget.h"
class InputBindingWidget;
class ControllerSettingsWindow;
class ControllerCustomSettingsWidget;
class ControllerMacroWidget;
class ControllerMacroEditWidget;
class ControllerBindingWidget_Base;
class USBBindingWidget;
class ControllerBindingWidget final : public QWidget
{
Q_OBJECT
public:
ControllerBindingWidget(QWidget* parent, ControllerSettingsWindow* dialog, u32 port);
~ControllerBindingWidget();
QIcon getIcon() const;
__fi ControllerSettingsWindow* getDialog() const { return m_dialog; }
__fi const std::string& getConfigSection() const { return m_config_section; }
__fi Pad::ControllerType getControllerType() const { return m_controller_type; }
__fi u32 getPortNumber() const { return m_port_number; }
private Q_SLOTS:
void onTypeChanged();
void onAutomaticBindingClicked();
void onClearBindingsClicked();
void onBindingsClicked();
void onSettingsClicked();
void onMacrosClicked();
private:
void populateControllerTypes();
void updateHeaderToolButtons();
void doDeviceAutomaticBinding(const QString& device);
Ui::ControllerBindingWidget m_ui;
ControllerSettingsWindow* m_dialog;
std::string m_config_section;
Pad::ControllerType m_controller_type;
u32 m_port_number;
ControllerBindingWidget_Base* m_bindings_widget = nullptr;
ControllerCustomSettingsWidget* m_settings_widget = nullptr;
ControllerMacroWidget* m_macros_widget = nullptr;
};
//////////////////////////////////////////////////////////////////////////
class ControllerMacroWidget : public QWidget
{
Q_OBJECT
public:
ControllerMacroWidget(ControllerBindingWidget* parent);
~ControllerMacroWidget();
void updateListItem(u32 index);
private:
static constexpr u32 NUM_MACROS = Pad::NUM_MACRO_BUTTONS_PER_CONTROLLER;
void createWidgets(ControllerBindingWidget* parent);
Ui::ControllerMacroWidget m_ui;
ControllerSettingsWindow* m_dialog;
std::array<ControllerMacroEditWidget*, NUM_MACROS> m_macros;
};
//////////////////////////////////////////////////////////////////////////
class ControllerMacroEditWidget : public QWidget
{
Q_OBJECT
public:
ControllerMacroEditWidget(ControllerMacroWidget* parent, ControllerBindingWidget* bwidget, u32 index);
~ControllerMacroEditWidget();
QString getSummary() const;
private Q_SLOTS:
void onPressureChanged();
void onDeadzoneChanged();
void onSetFrequencyClicked();
void updateBinds();
private:
void modFrequency(s32 delta);
void updateFrequency();
void updateFrequencyText();
Ui::ControllerMacroEditWidget m_ui;
ControllerMacroWidget* m_parent;
ControllerBindingWidget* m_bwidget;
u32 m_index;
std::vector<const InputBindingInfo*> m_binds;
u32 m_frequency = 0;
};
//////////////////////////////////////////////////////////////////////////
class ControllerCustomSettingsWidget : public QWidget
{
Q_OBJECT
public:
ControllerCustomSettingsWidget(std::span<const SettingInfo> settings, std::string config_section, std::string config_prefix,
const char* translation_ctx, ControllerSettingsWindow* dialog, QWidget* parent_widget);
~ControllerCustomSettingsWidget();
private Q_SLOTS:
void restoreDefaults();
private:
void createSettingWidgets(const char* translation_ctx, QWidget* widget_parent, QGridLayout* layout);
std::span<const SettingInfo> m_settings;
std::string m_config_section;
std::string m_config_prefix;
ControllerSettingsWindow* m_dialog;
};
//////////////////////////////////////////////////////////////////////////
class ControllerBindingWidget_Base : public QWidget
{
Q_OBJECT
public:
ControllerBindingWidget_Base(ControllerBindingWidget* parent);
virtual ~ControllerBindingWidget_Base();
__fi ControllerSettingsWindow* getDialog() const { return static_cast<ControllerBindingWidget*>(parent())->getDialog(); }
__fi const std::string& getConfigSection() const { return static_cast<ControllerBindingWidget*>(parent())->getConfigSection(); }
__fi Pad::ControllerType getControllerType() const { return static_cast<ControllerBindingWidget*>(parent())->getControllerType(); }
__fi u32 getPortNumber() const { return static_cast<ControllerBindingWidget*>(parent())->getPortNumber(); }
virtual QIcon getIcon() const;
protected:
void initBindingWidgets();
};
class ControllerBindingWidget_DualShock2 final : public ControllerBindingWidget_Base
{
Q_OBJECT
public:
ControllerBindingWidget_DualShock2(ControllerBindingWidget* parent);
~ControllerBindingWidget_DualShock2();
QIcon getIcon() const override;
static ControllerBindingWidget_Base* createInstance(ControllerBindingWidget* parent);
private:
Ui::ControllerBindingWidget_DualShock2 m_ui;
};
class ControllerBindingWidget_Guitar final : public ControllerBindingWidget_Base
{
Q_OBJECT
public:
ControllerBindingWidget_Guitar(ControllerBindingWidget* parent);
~ControllerBindingWidget_Guitar();
QIcon getIcon() const override;
static ControllerBindingWidget_Base* createInstance(ControllerBindingWidget* parent);
private:
Ui::ControllerBindingWidget_Guitar m_ui;
};
class ControllerBindingWidget_Jogcon final : public ControllerBindingWidget_Base
{
Q_OBJECT
public:
ControllerBindingWidget_Jogcon(ControllerBindingWidget* parent);
~ControllerBindingWidget_Jogcon();
QIcon getIcon() const override;
static ControllerBindingWidget_Base* createInstance(ControllerBindingWidget* parent);
private:
Ui::ControllerBindingWidget_Jogcon m_ui;
};
class ControllerBindingWidget_Negcon final : public ControllerBindingWidget_Base
{
Q_OBJECT
public:
ControllerBindingWidget_Negcon(ControllerBindingWidget* parent);
~ControllerBindingWidget_Negcon();
QIcon getIcon() const override;
static ControllerBindingWidget_Base* createInstance(ControllerBindingWidget* parent);
private:
Ui::ControllerBindingWidget_Negcon m_ui;
};
class ControllerBindingWidget_Popn final : public ControllerBindingWidget_Base
{
Q_OBJECT
public:
ControllerBindingWidget_Popn(ControllerBindingWidget* parent);
~ControllerBindingWidget_Popn();
QIcon getIcon() const override;
static ControllerBindingWidget_Base* createInstance(ControllerBindingWidget* parent);
private:
Ui::ControllerBindingWidget_Popn m_ui;
};
//////////////////////////////////////////////////////////////////////////
class USBDeviceWidget final : public QWidget
{
Q_OBJECT
public:
USBDeviceWidget(QWidget* parent, ControllerSettingsWindow* dialog, u32 port);
~USBDeviceWidget();
QIcon getIcon() const;
__fi ControllerSettingsWindow* getDialog() const { return m_dialog; }
__fi const std::string& getConfigSection() const { return m_config_section; }
__fi const std::string& getDeviceType() const { return m_device_type; }
__fi u32 getPortNumber() const { return m_port_number; }
private Q_SLOTS:
void onTypeChanged();
void onSubTypeChanged(int new_index);
void onAutomaticBindingClicked();
void onClearBindingsClicked();
void onBindingsClicked();
void onSettingsClicked();
private:
void populateDeviceTypes();
void populatePages();
void updateHeaderToolButtons();
void doDeviceAutomaticBinding(const QString& device);
Ui::USBDeviceWidget m_ui;
ControllerSettingsWindow* m_dialog;
std::string m_config_section;
std::string m_device_type;
u32 m_device_subtype;
u32 m_port_number;
USBBindingWidget* m_bindings_widget = nullptr;
ControllerCustomSettingsWidget* m_settings_widget = nullptr;
};
class USBBindingWidget : public QWidget
{
Q_OBJECT
public:
USBBindingWidget(USBDeviceWidget* parent);
~USBBindingWidget() override;
__fi ControllerSettingsWindow* getDialog() const { return static_cast<USBDeviceWidget*>(parent())->getDialog(); }
__fi const std::string& getConfigSection() const { return static_cast<USBDeviceWidget*>(parent())->getConfigSection(); }
__fi const std::string& getDeviceType() const { return static_cast<USBDeviceWidget*>(parent())->getDeviceType(); }
__fi u32 getPortNumber() const { return static_cast<USBDeviceWidget*>(parent())->getPortNumber(); }
QIcon getIcon() const;
static USBBindingWidget* createInstance(const std::string& type, u32 subtype, std::span<const InputBindingInfo> bindings, USBDeviceWidget* parent);
protected:
std::string getBindingKey(const char* binding_name) const;
void createWidgets(std::span<const InputBindingInfo> bindings);
void bindWidgets(std::span<const InputBindingInfo> bindings);
};

View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerBindingWidget</class>
<widget class="QWidget" name="ControllerBindingWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>833</width>
<height>617</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,1">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Virtual Controller Type</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QComboBox" name="controllerType"/>
</item>
<item>
<widget class="QToolButton" name="bindings">
<property name="text">
<string>Bindings</string>
</property>
<property name="icon">
<iconset theme="controller-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="settings">
<property name="text">
<string>Settings</string>
</property>
<property name="icon">
<iconset theme="checkbox-multiple-blank-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="macros">
<property name="text">
<string>Macros</string>
</property>
<property name="icon">
<iconset theme="flashlight-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>460</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QToolButton" name="automaticBinding">
<property name="text">
<string>Automatic Mapping</string>
</property>
<property name="icon">
<iconset theme="controller-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="clearBindings">
<property name="text">
<string>Clear Mapping</string>
</property>
<property name="icon">
<iconset theme="trash-fill">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="stackedWidget"/>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,314 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerBindingWidget_Guitar</class>
<widget class="QWidget" name="ControllerBindingWidget_Guitar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1100</width>
<height>500</height>
</rect>
</property>
<widget class="QWidget" name="horizontalLayoutWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1101</width>
<height>322</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="maximumSize">
<size>
<width>800</width>
<height>320</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../resources/resources.qrc">:/images/Guitar.svg</pixmap>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="gridLayoutWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>320</y>
<width>1101</width>
<height>181</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="6">
<widget class="QGroupBox" name="groupBox_7">
<property name="title">
<string>Yellow</string>
</property>
<widget class="InputBindingWidget" name="Yellow">
<property name="geometry">
<rect>
<x>24</x>
<y>30</y>
<width>90</width>
<height>40</height>
</rect>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</widget>
</item>
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Start</string>
</property>
<widget class="InputBindingWidget" name="Start">
<property name="geometry">
<rect>
<x>24</x>
<y>30</y>
<width>90</width>
<height>40</height>
</rect>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</widget>
</item>
<item row="0" column="7">
<widget class="QGroupBox" name="groupBox_8">
<property name="title">
<string>Red</string>
</property>
<widget class="InputBindingWidget" name="Red">
<property name="geometry">
<rect>
<x>24</x>
<y>30</y>
<width>90</width>
<height>40</height>
</rect>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</widget>
</item>
<item row="0" column="8">
<widget class="QGroupBox" name="groupBox_9">
<property name="title">
<string>Green</string>
</property>
<widget class="InputBindingWidget" name="Green">
<property name="geometry">
<rect>
<x>24</x>
<y>30</y>
<width>90</width>
<height>40</height>
</rect>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</widget>
</item>
<item row="0" column="3">
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Orange</string>
</property>
<widget class="InputBindingWidget" name="Orange">
<property name="geometry">
<rect>
<x>24</x>
<y>30</y>
<width>90</width>
<height>40</height>
</rect>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</widget>
</item>
<item row="0" column="1">
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Select</string>
</property>
<widget class="InputBindingWidget" name="Select">
<property name="geometry">
<rect>
<x>24</x>
<y>30</y>
<width>90</width>
<height>40</height>
</rect>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</widget>
</item>
<item row="0" column="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Strum Up</string>
</property>
<widget class="InputBindingWidget" name="Up">
<property name="geometry">
<rect>
<x>24</x>
<y>30</y>
<width>90</width>
<height>40</height>
</rect>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</widget>
</item>
<item row="1" column="2">
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Strum Down</string>
</property>
<widget class="InputBindingWidget" name="Down">
<property name="geometry">
<rect>
<x>24</x>
<y>30</y>
<width>90</width>
<height>40</height>
</rect>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</widget>
</item>
<item row="0" column="5">
<widget class="QGroupBox" name="groupBox_6">
<property name="title">
<string>Blue</string>
</property>
<widget class="InputBindingWidget" name="Blue">
<property name="geometry">
<rect>
<x>24</x>
<y>30</y>
<width>90</width>
<height>40</height>
</rect>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="groupBox_10">
<property name="title">
<string>Whammy Bar</string>
</property>
<widget class="InputBindingWidget" name="Whammy">
<property name="geometry">
<rect>
<x>24</x>
<y>30</y>
<width>90</width>
<height>40</height>
</rect>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</widget>
</item>
<item row="1" column="1">
<widget class="QGroupBox" name="groupBox_11">
<property name="title">
<string>Tilt</string>
</property>
<widget class="InputBindingWidget" name="Tilt">
<property name="geometry">
<rect>
<x>20</x>
<y>30</y>
<width>90</width>
<height>40</height>
</rect>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>InputBindingWidget</class>
<extends>QPushButton</extends>
<header>Settings/InputBindingWidget.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,832 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerBindingWidget_Jogcon</class>
<widget class="QWidget" name="ControllerBindingWidget_Jogcon">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1232</width>
<height>644</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>1100</width>
<height>500</height>
</size>
</property>
<layout class="QGridLayout" name="gridLayout_0">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0" rowspan="3">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox_0">
<property name="title">
<string>D-Pad</string>
</property>
<layout class="QGridLayout" name="gridLayout_1">
<item row="3" column="1" colspan="2">
<widget class="QGroupBox" name="groupBox_1">
<property name="title">
<string>Down</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Down">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Left</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Left">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Up</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Up">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="2" colspan="2">
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Right</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Right">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_29">
<property name="title">
<string>Large Motor</string>
</property>
<layout class="QGridLayout" name="gridLayout_30">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputVibrationBindingWidget" name="LargeMotor">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="1">
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>L2</string>
</property>
<layout class="QGridLayout" name="gridLayout_7">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="L2">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="3">
<widget class="QGroupBox" name="groupBox_6">
<property name="title">
<string>R2</string>
</property>
<layout class="QGridLayout" name="gridLayout_8">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="R2">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="groupBox_7">
<property name="title">
<string>L1</string>
</property>
<layout class="QGridLayout" name="gridLayout_9">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="L1">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="3">
<widget class="QGroupBox" name="groupBox_8">
<property name="title">
<string>R1</string>
</property>
<layout class="QGridLayout" name="gridLayout_10">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="R1">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="2">
<widget class="QGroupBox" name="groupBox_9">
<property name="title">
<string>Start</string>
</property>
<layout class="QGridLayout" name="gridLayout_11">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Start">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="1">
<widget class="QGroupBox" name="groupBox_10">
<property name="title">
<string>Select</string>
</property>
<layout class="QGridLayout" name="gridLayout_12">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Select">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item row="0" column="2" rowspan="3">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox_11">
<property name="title">
<string>Face Buttons</string>
</property>
<layout class="QGridLayout" name="gridLayout_13">
<item row="3" column="1" colspan="2">
<widget class="QGroupBox" name="groupBox_12">
<property name="title">
<string>Cross</string>
</property>
<layout class="QGridLayout" name="gridLayout_14">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Cross">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_13">
<property name="title">
<string>Square</string>
</property>
<layout class="QGridLayout" name="gridLayout_15">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Square">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QGroupBox" name="groupBox_14">
<property name="title">
<string>Triangle</string>
</property>
<layout class="QGridLayout" name="gridLayout_16">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Triangle">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="2" colspan="2">
<widget class="QGroupBox" name="groupBox_15">
<property name="title">
<string>Circle</string>
</property>
<layout class="QGridLayout" name="gridLayout_17">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Circle">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_32">
<property name="title">
<string>Small Motor</string>
</property>
<layout class="QGridLayout" name="gridLayout_31">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputVibrationBindingWidget" name="SmallMotor">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>400</width>
<height>266</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../resources/resources.qrc">:/images/Jogcon.svg</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="2" column="1">
<layout class="QGridLayout" name="gridLayout_18">
<item row="0" column="1">
<widget class="QGroupBox" name="groupBox_16">
<property name="title">
<string>Dial Left</string>
</property>
<layout class="QGridLayout" name="gridLayout_19">
<item row="0" column="0">
<widget class="InputBindingWidget" name="DialLeft">
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="2">
<widget class="QGroupBox" name="groupBox_17">
<property name="title">
<string>Dial Right</string>
</property>
<layout class="QGridLayout" name="gridLayout_20">
<item row="0" column="0">
<widget class="InputBindingWidget" name="DialRight">
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0" colspan="4">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>InputBindingWidget</class>
<extends>QPushButton</extends>
<header>Settings/InputBindingWidget.h</header>
</customwidget>
<customwidget>
<class>InputVibrationBindingWidget</class>
<extends>QPushButton</extends>
<header>Settings/InputBindingWidget.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,730 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerBindingWidget_Negcon</class>
<widget class="QWidget" name="ControllerBindingWidget_Negcon">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1232</width>
<height>644</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>1100</width>
<height>500</height>
</size>
</property>
<layout class="QGridLayout" name="gridLayout_35">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0" rowspan="3">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox_1">
<property name="title">
<string>D-Pad</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="3" column="1" colspan="2">
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Down</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Down">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Left</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Left">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Up</string>
</property>
<layout class="QGridLayout" name="gridLayout_1">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Up">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="2" colspan="2">
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Right</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Right">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_29">
<property name="title">
<string>Large Motor</string>
</property>
<layout class="QGridLayout" name="gridLayout_30">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputVibrationBindingWidget" name="LargeMotor">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="1">
<layout class="QGridLayout" name="gridLayout_27">
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox_22">
<property name="title">
<string>L</string>
</property>
<layout class="QGridLayout" name="gridLayout_22">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="L">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1">
<widget class="QGroupBox" name="groupBox_26">
<property name="title">
<string>Start</string>
</property>
<layout class="QGridLayout" name="gridLayout_26">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Start">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="2">
<widget class="QGroupBox" name="groupBox_24">
<property name="title">
<string>R</string>
</property>
<layout class="QGridLayout" name="gridLayout_24">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="R">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item row="0" column="2" rowspan="3">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox_16">
<property name="title">
<string>Face Buttons</string>
</property>
<layout class="QGridLayout" name="gridLayout_16">
<item row="3" column="1" colspan="2">
<widget class="QGroupBox" name="groupBox_17">
<property name="title">
<string>I</string>
</property>
<layout class="QGridLayout" name="gridLayout_17">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="I">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_18">
<property name="title">
<string>II</string>
</property>
<layout class="QGridLayout" name="gridLayout_18">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="II">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QGroupBox" name="groupBox_19">
<property name="title">
<string>B</string>
</property>
<layout class="QGridLayout" name="gridLayout_19">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="B">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="2" colspan="2">
<widget class="QGroupBox" name="groupBox_20">
<property name="title">
<string>A</string>
</property>
<layout class="QGridLayout" name="gridLayout_20">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="A">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_32">
<property name="title">
<string>Small Motor</string>
</property>
<layout class="QGridLayout" name="gridLayout_31">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputVibrationBindingWidget" name="SmallMotor">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>400</width>
<height>266</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../resources/resources.qrc">:/images/Negcon.svg</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="2" column="1">
<layout class="QGridLayout" name="gridLayout_32">
<item row="0" column="1">
<widget class="QGroupBox" name="groupBox_31">
<property name="title">
<string>Twist Left</string>
</property>
<layout class="QGridLayout" name="gridLayout_33">
<item row="0" column="0">
<widget class="InputBindingWidget" name="TwistLeft">
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="2">
<widget class="QGroupBox" name="groupBox_6">
<property name="title">
<string>Twist Right</string>
</property>
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0">
<widget class="InputBindingWidget" name="TwistRight">
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0" colspan="4">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>InputBindingWidget</class>
<extends>QPushButton</extends>
<header>Settings/InputBindingWidget.h</header>
</customwidget>
<customwidget>
<class>InputVibrationBindingWidget</class>
<extends>QPushButton</extends>
<header>Settings/InputBindingWidget.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,531 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerBindingWidget_Popn</class>
<widget class="QWidget" name="ControllerBindingWidget_Popn">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1232</width>
<height>644</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>1100</width>
<height>500</height>
</size>
</property>
<layout class="QGridLayout" name="gridLayout_35">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="2" rowspan="3">
<layout class="QVBoxLayout" name="verticalLayout"/>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>400</width>
<height>266</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../resources/resources.qrc">:/images/Popn.svg</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="0" rowspan="3">
<layout class="QVBoxLayout" name="verticalLayout_2"/>
</item>
<item row="0" column="1">
<layout class="QGridLayout" name="gridLayout_27">
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox_21">
<property name="title">
<string extracomment="Leave this button name as-is or uppercase it entirely.">Select</string>
</property>
<layout class="QGridLayout" name="gridLayout_21">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Select">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="groupBox_22">
<property name="title">
<string>Yellow (Left)</string>
</property>
<layout class="QGridLayout" name="gridLayout_22">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="YellowL">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="3">
<widget class="QGroupBox" name="groupBox_24">
<property name="title">
<string>Yellow (Right)</string>
</property>
<layout class="QGridLayout" name="gridLayout_24">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="YellowR">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="2">
<widget class="QGroupBox" name="groupBox_26">
<property name="title">
<string>Blue (Right)</string>
</property>
<layout class="QGridLayout" name="gridLayout_26">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="BlueR">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="1">
<widget class="QGroupBox" name="groupBox_25">
<property name="title">
<string>Blue (Left)</string>
</property>
<layout class="QGridLayout" name="gridLayout_25">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="BlueL">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1">
<widget class="QGroupBox" name="groupBox_23">
<property name="title">
<string extracomment="Leave this button name as-is or uppercase it entirely.">Start</string>
</property>
<layout class="QGridLayout" name="gridLayout_23">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="Start">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item row="2" column="1">
<layout class="QGridLayout" name="gridLayout_32">
<item row="0" column="2">
<widget class="QGroupBox" name="groupBox_34">
<property name="title">
<string>Red</string>
</property>
<layout class="QGridLayout" name="gridLayout_34">
<item row="0" column="0">
<widget class="InputBindingWidget" name="Red">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0" colspan="5">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="3">
<widget class="QGroupBox" name="groupBox_28">
<property name="title">
<string>Green (Right)</string>
</property>
<layout class="QGridLayout" name="gridLayout_29">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="GreenR">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox_27">
<property name="title">
<string>White (Left)</string>
</property>
<layout class="QGridLayout" name="gridLayout_28">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="WhiteL">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1">
<widget class="QGroupBox" name="groupBox_29">
<property name="title">
<string>Green (Left)</string>
</property>
<layout class="QGridLayout" name="gridLayout_30">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="GreenL">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="4">
<widget class="QGroupBox" name="groupBox_30">
<property name="title">
<string>White (Right)</string>
</property>
<layout class="QGridLayout" name="gridLayout_31">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="InputBindingWidget" name="WhiteR">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>InputBindingWidget</class>
<extends>QPushButton</extends>
<header>Settings/InputBindingWidget.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,211 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "Settings/ControllerGlobalSettingsWidget.h"
#include "Settings/ControllerSettingsWindow.h"
#include "Settings/ControllerSettingWidgetBinder.h"
#include "QtUtils.h"
#include "SettingWidgetBinder.h"
#include "pcsx2/Input/InputManager.h"
#include "pcsx2/Input/SDLInputSource.h"
ControllerGlobalSettingsWidget::ControllerGlobalSettingsWidget(QWidget* parent, ControllerSettingsWindow* dialog)
: QWidget(parent)
, m_dialog(dialog)
{
m_ui.setupUi(this);
SettingsInterface* sif = dialog->getProfileSettingsInterface();
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.enableSDLSource, "InputSources", "SDL", true);
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.enableSDLEnhancedMode, "InputSources", "SDLControllerEnhancedMode", true);
connectCheckStateChanged(m_ui.enableSDLSource, this, &ControllerGlobalSettingsWidget::updateSDLOptionsEnabled);
connect(m_ui.ledSettings, &QToolButton::clicked, this, &ControllerGlobalSettingsWidget::ledSettingsClicked);
#ifdef _WIN32
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.enableSDLRawInput, "InputSources", "SDLRawInput", false);
#else
m_ui.sdlGridLayout->removeWidget(m_ui.enableSDLRawInput);
m_ui.enableSDLRawInput->deleteLater();
m_ui.enableSDLRawInput = nullptr;
#endif
#ifdef __APPLE__
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.enableSDLIOKitDriver, "InputSources", "SDLIOKitDriver", true);
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.enableSDLMFIDriver, "InputSources", "SDLMFIDriver", true);
#else
m_ui.sdlGridLayout->removeWidget(m_ui.enableSDLIOKitDriver);
m_ui.enableSDLIOKitDriver->deleteLater();
m_ui.enableSDLIOKitDriver = nullptr;
m_ui.sdlGridLayout->removeWidget(m_ui.enableSDLMFIDriver);
m_ui.enableSDLMFIDriver->deleteLater();
m_ui.enableSDLMFIDriver = nullptr;
#endif
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.enableMouseMapping, "UI", "EnableMouseMapping", false);
connect(m_ui.mouseSettings, &QToolButton::clicked, this, &ControllerGlobalSettingsWidget::mouseSettingsClicked);
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.multitapPort1, "Pad", "MultitapPort1", false);
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.multitapPort2, "Pad", "MultitapPort2", false);
#ifdef _WIN32
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.enableXInputSource, "InputSources", "XInput", false);
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.enableDInputSource, "InputSources", "DInput", false);
#else
m_ui.mainLayout->removeWidget(m_ui.xinputGroup);
m_ui.xinputGroup->deleteLater();
m_ui.xinputGroup = nullptr;
m_ui.mainLayout->removeWidget(m_ui.dinputGroup);
m_ui.dinputGroup->deleteLater();
m_ui.dinputGroup = nullptr;
#endif
if (dialog->isEditingProfile())
{
m_ui.useProfileHotkeyBindings->setChecked(m_dialog->getBoolValue("Pad", "UseProfileHotkeyBindings", false));
connectCheckStateChanged(m_ui.useProfileHotkeyBindings, this, [this](int new_state) {
m_dialog->setBoolValue("Pad", "UseProfileHotkeyBindings", (new_state == Qt::Checked));
emit bindingSetupChanged();
});
}
else
{
// remove profile options from the UI.
m_ui.mainLayout->removeWidget(m_ui.profileSettings);
m_ui.profileSettings->deleteLater();
m_ui.profileSettings = nullptr;
}
for (QCheckBox* cb : {m_ui.multitapPort1, m_ui.multitapPort2})
connectCheckStateChanged(cb, this, [this]() { emit bindingSetupChanged(); });
updateSDLOptionsEnabled();
}
ControllerGlobalSettingsWidget::~ControllerGlobalSettingsWidget() = default;
void ControllerGlobalSettingsWidget::addDeviceToList(const QString& identifier, const QString& name)
{
QListWidgetItem* item = new QListWidgetItem();
item->setText(QStringLiteral("%1: %2").arg(identifier).arg(name));
item->setData(Qt::UserRole, identifier);
m_ui.deviceList->addItem(item);
}
void ControllerGlobalSettingsWidget::removeDeviceFromList(const QString& identifier)
{
const int count = m_ui.deviceList->count();
for (int i = 0; i < count; i++)
{
QListWidgetItem* item = m_ui.deviceList->item(i);
if (item->data(Qt::UserRole) != identifier)
continue;
delete m_ui.deviceList->takeItem(i);
break;
}
}
void ControllerGlobalSettingsWidget::updateSDLOptionsEnabled()
{
const bool enabled = m_ui.enableSDLSource->isChecked();
m_ui.enableSDLEnhancedMode->setEnabled(enabled);
m_ui.ledSettings->setEnabled(enabled);
#ifdef _WIN32
m_ui.enableSDLRawInput->setEnabled(enabled);
#endif
#ifdef __APPLE__
m_ui.enableSDLIOKitDriver->setEnabled(enabled);
m_ui.enableSDLMFIDriver->setEnabled(enabled);
#endif
}
void ControllerGlobalSettingsWidget::ledSettingsClicked()
{
ControllerLEDSettingsDialog dialog(this, m_dialog);
dialog.exec();
}
void ControllerGlobalSettingsWidget::mouseSettingsClicked()
{
ControllerMouseSettingsDialog dialog(this, m_dialog);
dialog.exec();
}
ControllerLEDSettingsDialog::ControllerLEDSettingsDialog(QWidget* parent, ControllerSettingsWindow* dialog)
: QDialog(parent)
, m_dialog(dialog)
{
m_ui.setupUi(this);
linkButton(m_ui.SDL0LED, 0);
linkButton(m_ui.SDL1LED, 1);
linkButton(m_ui.SDL2LED, 2);
linkButton(m_ui.SDL3LED, 3);
SettingsInterface* sif = dialog->getProfileSettingsInterface();
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.enableSDLPS5PlayerLED, "InputSources", "SDLPS5PlayerLED", true);
connect(m_ui.buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &QDialog::accept);
}
ControllerLEDSettingsDialog::~ControllerLEDSettingsDialog() = default;
void ControllerLEDSettingsDialog::linkButton(ColorPickerButton* button, u32 player_id)
{
std::string key(fmt::format("Player{}LED", player_id));
const u32 current_value = SDLInputSource::ParseRGBForPlayerId(m_dialog->getStringValue("SDLExtra", key.c_str(), ""), player_id);
button->setColor(current_value);
connect(button, &ColorPickerButton::colorChanged, this, [this, key = std::move(key)](u32 new_rgb) {
m_dialog->setStringValue("SDLExtra", key.c_str(), fmt::format("{:06X}", new_rgb).c_str());
});
}
ControllerMouseSettingsDialog::ControllerMouseSettingsDialog(QWidget* parent, ControllerSettingsWindow* dialog)
: QDialog(parent)
{
m_ui.setupUi(this);
SettingsInterface* sif = dialog->getProfileSettingsInterface();
QtUtils::SetScalableIcon(m_ui.icon, QIcon::fromTheme(QStringLiteral("mouse-line")), QSize(32, 32));
ControllerSettingWidgetBinder::BindWidgetToInputProfileFloat(sif, m_ui.pointerXSpeedSlider, "Pad", "PointerXSpeed", 40.0f);
ControllerSettingWidgetBinder::BindWidgetToInputProfileFloat(sif, m_ui.pointerYSpeedSlider, "Pad", "PointerYSpeed", 40.0f);
ControllerSettingWidgetBinder::BindWidgetToInputProfileFloat(sif, m_ui.pointerXDeadZoneSlider, "Pad", "PointerXDeadZone", 20.0f);
ControllerSettingWidgetBinder::BindWidgetToInputProfileFloat(sif, m_ui.pointerYDeadZoneSlider, "Pad", "PointerYDeadZone", 20.0f);
ControllerSettingWidgetBinder::BindWidgetToInputProfileFloat(sif, m_ui.pointerInertiaSlider, "Pad", "PointerInertia", 10.0f);
connect(m_ui.pointerXSpeedSlider, &QSlider::valueChanged, this, [this](int value) { m_ui.pointerXSpeedVal->setText(QStringLiteral("%1").arg(value)); });
connect(m_ui.pointerYSpeedSlider, &QSlider::valueChanged, this, [this](int value) { m_ui.pointerYSpeedVal->setText(QStringLiteral("%1").arg(value)); });
connect(m_ui.pointerXDeadZoneSlider, &QSlider::valueChanged, this, [this](int value) { m_ui.pointerXDeadZoneVal->setText(QStringLiteral("%1").arg(value)); });
connect(m_ui.pointerYDeadZoneSlider, &QSlider::valueChanged, this, [this](int value) { m_ui.pointerYDeadZoneVal->setText(QStringLiteral("%1").arg(value)); });
connect(m_ui.pointerInertiaSlider, &QSlider::valueChanged, this, [this](int value) { m_ui.pointerInertiaVal->setText(QStringLiteral("%1").arg(value)); });
m_ui.pointerXSpeedVal->setText(QStringLiteral("%1").arg(m_ui.pointerXSpeedSlider->value()));
m_ui.pointerYSpeedVal->setText(QStringLiteral("%1").arg(m_ui.pointerYSpeedSlider->value()));
m_ui.pointerXDeadZoneVal->setText(QStringLiteral("%1").arg(m_ui.pointerXDeadZoneSlider->value()));
m_ui.pointerYDeadZoneVal->setText(QStringLiteral("%1").arg(m_ui.pointerYDeadZoneSlider->value()));
m_ui.pointerInertiaVal->setText(QStringLiteral("%1").arg(m_ui.pointerInertiaSlider->value()));
connect(m_ui.buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &QDialog::accept);
}
ControllerMouseSettingsDialog::~ControllerMouseSettingsDialog() = default;
ControllerMappingSettingsDialog::ControllerMappingSettingsDialog(ControllerSettingsWindow* parent)
: QDialog(parent)
{
m_ui.setupUi(this);
SettingsInterface* sif = parent->getProfileSettingsInterface();
QtUtils::SetScalableIcon(m_ui.icon, QIcon::fromTheme(QStringLiteral("settings-3-line")), QSize(32, 32));
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.ignoreInversion, "InputSources", "IgnoreInversion", false);
connect(m_ui.buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &QDialog::accept);
}
ControllerMappingSettingsDialog::~ControllerMappingSettingsDialog() = default;

View File

@@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QWidget>
#include <QtCore/QMap>
#include <array>
#include <vector>
#include "ColorPickerButton.h"
#include "ui_ControllerGlobalSettingsWidget.h"
#include "ui_ControllerLEDSettingsDialog.h"
#include "ui_ControllerMappingSettingsDialog.h"
#include "ui_ControllerMouseSettingsDialog.h"
class ControllerSettingsWindow;
class ControllerGlobalSettingsWidget : public QWidget
{
Q_OBJECT
public:
ControllerGlobalSettingsWidget(QWidget* parent, ControllerSettingsWindow* dialog);
~ControllerGlobalSettingsWidget();
void addDeviceToList(const QString& identifier, const QString& name);
void removeDeviceFromList(const QString& identifier);
Q_SIGNALS:
void bindingSetupChanged();
private Q_SLOTS:
void updateSDLOptionsEnabled();
void ledSettingsClicked();
void mouseSettingsClicked();
private:
Ui::ControllerGlobalSettingsWidget m_ui;
ControllerSettingsWindow* m_dialog;
};
class ControllerLEDSettingsDialog : public QDialog
{
Q_OBJECT
public:
ControllerLEDSettingsDialog(QWidget* parent, ControllerSettingsWindow* dialog);
~ControllerLEDSettingsDialog();
private:
void linkButton(ColorPickerButton* button, u32 player_id);
Ui::ControllerLEDSettingsDialog m_ui;
ControllerSettingsWindow* m_dialog;
};
class ControllerMouseSettingsDialog : public QDialog
{
Q_OBJECT
public:
ControllerMouseSettingsDialog(QWidget* parent, ControllerSettingsWindow* dialog);
~ControllerMouseSettingsDialog();
private:
Ui::ControllerMouseSettingsDialog m_ui;
};
class ControllerMappingSettingsDialog : public QDialog
{
Q_OBJECT
public:
ControllerMappingSettingsDialog(ControllerSettingsWindow* parent);
~ControllerMappingSettingsDialog();
private:
Ui::ControllerMappingSettingsDialog m_ui;
};

View File

@@ -0,0 +1,288 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerGlobalSettingsWidget</class>
<widget class="QWidget" name="ControllerGlobalSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>902</width>
<height>665</height>
</rect>
</property>
<layout class="QGridLayout" name="mainLayout" columnstretch="1,0">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="7" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="0">
<widget class="QGroupBox" name="multitapGroup">
<property name="title">
<string>Controller Multitap</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="1" column="0">
<widget class="QCheckBox" name="multitapPort1">
<property name="text">
<string>Multitap on Console Port 1</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="multitapPort2">
<property name="text">
<string>Multitap on Console Port 2</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label">
<property name="text">
<string>The multitap enables up to 8 controllers to be connected to the console. Each multitap provides 4 ports. Multitap is not supported by all games.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0">
<widget class="QGroupBox" name="dinputGroup">
<property name="title">
<string>DInput Source</string>
</property>
<layout class="QGridLayout" name="gridLayout_6">
<item row="2" column="0">
<widget class="QCheckBox" name="enableDInputSource">
<property name="text">
<string>Enable DInput Input Source</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="label_9">
<property name="text">
<string>The DInput source provides support for legacy controllers which do not support XInput. Accessing these controllers via SDL instead is recommended, but DirectInput can be used if they are not compatible with SDL.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="6" column="0">
<widget class="QGroupBox" name="profileSettings">
<property name="title">
<string>Profile Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>When this option is enabled, hotkeys can be set in this input profile, and will be used instead of the global hotkeys. By default, hotkeys are always shared between all profiles.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="useProfileHotkeyBindings">
<property name="text">
<string>Use Per-Profile Hotkeys</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="0">
<widget class="QGroupBox" name="sdlGroup">
<property name="title">
<string>SDL Input Source</string>
</property>
<layout class="QGridLayout" name="sdlGridLayout">
<item row="1" column="0">
<widget class="QCheckBox" name="enableSDLSource">
<property name="text">
<string>Enable SDL Input Source</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_2">
<property name="text">
<string>The SDL input source supports most controllers, and provides advanced functionality for DualShock 4 / DualSense pads in Bluetooth mode (Vibration / LED Control).</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QCheckBox" name="enableSDLEnhancedMode">
<property name="text">
<string>DualShock 4 / DualSense Enhanced Mode</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="ledSettings">
<property name="toolTip">
<string>Controller LED Settings</string>
</property>
<property name="icon">
<iconset theme="lightbulb-line">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="enableSDLRawInput">
<property name="text">
<string>Enable SDL Raw Input</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="enableSDLIOKitDriver">
<property name="text">
<string>Enable IOKit Driver</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="enableSDLMFIDriver">
<property name="text">
<string>Enable MFI Driver</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="4" column="0">
<widget class="QGroupBox" name="mouseGroup">
<property name="title">
<string>Mouse/Pointer Source</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_7">
<property name="text">
<string>PCSX2 allows you to use your mouse to simulate analog stick movement.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="1,0">
<item>
<widget class="QCheckBox" name="enableMouseMapping">
<property name="text">
<string>Enable Mouse Mapping</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="mouseSettings">
<property name="toolTip">
<string>Controller LED Settings</string>
</property>
<property name="text">
<string>Settings...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="xinputGroup">
<property name="title">
<string>XInput Source</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>The XInput source provides support for Xbox 360 / Xbox One / Xbox Series controllers, and third party controllers which implement the XInput protocol.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="enableXInputSource">
<property name="text">
<string>Enable XInput Input Source</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" rowspan="8">
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Detected Devices</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QListWidget" name="deviceList">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerLEDSettingsDialog</class>
<widget class="QDialog" name="ControllerLEDSettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>501</width>
<height>128</height>
</rect>
</property>
<property name="windowTitle">
<string>Controller LED Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>SDL-0 LED</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="ColorPickerButton" name="SDL0LED"/>
</item>
</layout>
</widget>
</item>
<item row="0" column="3">
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>SDL-3 LED</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="ColorPickerButton" name="SDL3LED"/>
</item>
</layout>
</widget>
</item>
<item row="0" column="2">
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>SDL-2 LED</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="ColorPickerButton" name="SDL2LED"/>
</item>
</layout>
</widget>
</item>
<item row="0" column="1">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>SDL-1 LED</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="ColorPickerButton" name="SDL1LED"/>
</item>
</layout>
</widget>
</item>
<item row="3" column="0" colspan="4">
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="enableSDLPS5PlayerLED">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Enable DualSense Player LED</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ColorPickerButton</class>
<extends>QPushButton</extends>
<header>ColorPickerButton.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,244 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerMacroEditWidget</class>
<widget class="QWidget" name="ControllerMacroEditWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>691</width>
<height>433</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="1,0,0">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Binds/Buttons</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<widget class="QListWidget" name="bindList"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Select the buttons which you want to trigger with this macro. All buttons are activated concurrently.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Pressure</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>For buttons which are pressure sensitive, this slider controls how much force will be simulated when the macro is active.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="pressureLayout">
<item>
<widget class="QSlider" name="pressure">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pressureValue">
<property name="text">
<string>100%</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Trigger</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Select the trigger to activate this macro. This can be a single button, or combination of buttons (chord). Shift-click for multiple triggers.</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="triggerToggle">
<property name="text">
<string>Press To Toggle</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<widget class="InputBindingWidget" name="trigger">
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="deadzoneLayout" stretch="0,1,0">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Deadzone:</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="deadzone">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="deadzoneValue">
<property name="text">
<string>100%</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Frequency</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0,0,0">
<item>
<widget class="QLabel" name="frequencyText">
<property name="text">
<string>Macro will toggle every N frames.</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="setFrequency">
<property name="text">
<string>Set...</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="increaseFrequency">
<property name="minimumSize">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
<property name="arrowType">
<enum>Qt::UpArrow</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="decreateFrequency">
<property name="minimumSize">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
<property name="arrowType">
<enum>Qt::DownArrow</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>InputBindingWidget</class>
<extends>QPushButton</extends>
<header>Settings/InputBindingWidget.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerMacroWidget</class>
<widget class="QWidget" name="ControllerMacroWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>799</width>
<height>493</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QListWidget" name="portList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QStackedWidget" name="container"/>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerMappingSettingsDialog</class>
<widget class="QDialog" name="ControllerMappingSettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>654</width>
<height>275</height>
</rect>
</property>
<property name="windowTitle">
<string>Controller Mapping Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="icon">
<property name="minimumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Controller Mapping Settings&lt;/span&gt;&lt;br/&gt;These settings fine-tune the behavior when mapping physical controllers to the emulated controllers.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="ignoreInversion">
<property name="text">
<string>Ignore Inversion</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Some third party controllers incorrectly flag their analog sticks as inverted on the positive component, but not negative.&lt;/p&gt;&lt;p&gt;As a result, the analog stick will be &amp;quot;stuck on&amp;quot; even while resting at neutral position. &lt;/p&gt;&lt;p&gt;Enabling this setting will tell PCSX2 to ignore inversion flags when creating mappings, allowing such controllers to function normally.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>28</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,417 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerMouseSettingsDialog</class>
<widget class="QDialog" name="ControllerMouseSettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>654</width>
<height>169</height>
</rect>
</property>
<property name="windowTitle">
<string>Mouse Mapping Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="5" column="1">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="pointerYSpeed">
<item>
<widget class="QLabel" name="pointerYSpeedLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Y Speed</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="pointerYSpeedSlider">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pointerYSpeedVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>10</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="pointerXSpeed">
<item>
<widget class="QLabel" name="pointerXSpeedLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>X Speed</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="pointerXSpeedSlider">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pointerXSpeedVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>10</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="icon">
<property name="minimumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Mouse Mapping Settings&lt;/span&gt;&lt;br/&gt;These settings fine-tune the behavior when mapping a mouse to the emulated controller.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="0">
<layout class="QHBoxLayout" name="pointerInertia">
<item>
<widget class="QLabel" name="pointerInertiaLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Inertia</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="pointerInertiaSlider">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pointerInertiaVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>10</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="pointerXDeadZone">
<item>
<widget class="QLabel" name="pointerXDeadZoneLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>X Dead Zone</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="pointerXDeadZoneSlider">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pointerXDeadZoneVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>10</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="pointerYDeadZone">
<item>
<widget class="QLabel" name="pointerYDeadZoneLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Y Dead Zone</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="pointerYDeadZoneSlider">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pointerYDeadZoneVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>10</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,202 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "pcsx2/Host.h"
#include "QtHost.h"
#include "SettingWidgetBinder.h"
#include <optional>
#include <type_traits>
#include <QtCore/QtCore>
#include <QtGui/QAction>
#include <QtWidgets/QCheckBox>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QDoubleSpinBox>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QSlider>
#include <QtWidgets/QSpinBox>
/// This nastyness is required because input profiles aren't overlaid settings like the rest of them, it's
/// input profile *or* global, not both.
namespace ControllerSettingWidgetBinder
{
/// Interface specific method of BindWidgetToBoolSetting().
template <typename WidgetType>
static inline void BindWidgetToInputProfileBool(
SettingsInterface* sif, WidgetType* widget, std::string section, std::string key, bool default_value)
{
using Accessor = SettingWidgetBinder::SettingAccessor<WidgetType>;
if (sif)
{
const bool value = sif->GetBoolValue(section.c_str(), key.c_str(), default_value);
Accessor::setBoolValue(widget, value);
Accessor::connectValueChanged(widget, [sif, widget, section = std::move(section), key = std::move(key)]() {
const bool new_value = Accessor::getBoolValue(widget);
sif->SetBoolValue(section.c_str(), key.c_str(), new_value);
QtHost::SaveGameSettings(sif, false);
g_emu_thread->reloadGameSettings();
});
}
else
{
const bool value = Host::GetBaseBoolSettingValue(section.c_str(), key.c_str(), default_value);
Accessor::setBoolValue(widget, value);
Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key)]() {
const bool new_value = Accessor::getBoolValue(widget);
Host::SetBaseBoolSettingValue(section.c_str(), key.c_str(), new_value);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
});
}
}
/// Interface specific method of BindWidgetToIntSetting().
template <typename WidgetType>
static inline void BindWidgetToInputProfileInt(
SettingsInterface* sif, WidgetType* widget, std::string section, std::string key, s32 default_value, s32 option_offset = 0)
{
using Accessor = SettingWidgetBinder::SettingAccessor<WidgetType>;
if (sif)
{
const s32 value = sif->GetIntValue(section.c_str(), key.c_str(), default_value);
Accessor::setIntValue(widget, value - option_offset);
Accessor::connectValueChanged(widget, [sif, widget, section = std::move(section), key = std::move(key), option_offset]() {
const float new_value = Accessor::getIntValue(widget);
sif->SetIntValue(section.c_str(), key.c_str(), new_value + option_offset);
QtHost::SaveGameSettings(sif, false);
g_emu_thread->reloadGameSettings();
});
}
else
{
const s32 value = Host::GetBaseIntSettingValue(section.c_str(), key.c_str(), default_value);
Accessor::setIntValue(widget, value - option_offset);
Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key), option_offset]() {
const s32 new_value = Accessor::getIntValue(widget);
Host::SetBaseIntSettingValue(section.c_str(), key.c_str(), new_value + option_offset);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
});
}
}
/// Interface specific method of BindWidgetToFloatSetting().
template <typename WidgetType>
static inline void BindWidgetToInputProfileFloat(
SettingsInterface* sif, WidgetType* widget, std::string section, std::string key, float default_value, float multiplier = 1.0f)
{
using Accessor = SettingWidgetBinder::SettingAccessor<WidgetType>;
if (sif)
{
const float value = sif->GetFloatValue(section.c_str(), key.c_str(), default_value);
Accessor::setFloatValue(widget, value * multiplier);
Accessor::connectValueChanged(widget, [sif, widget, section = std::move(section), key = std::move(key), multiplier]() {
const float new_value = Accessor::getFloatValue(widget) / multiplier;
sif->SetFloatValue(section.c_str(), key.c_str(), new_value);
QtHost::SaveGameSettings(sif, false);
g_emu_thread->reloadGameSettings();
});
}
else
{
const float value = Host::GetBaseFloatSettingValue(section.c_str(), key.c_str(), default_value);
Accessor::setFloatValue(widget, value * multiplier);
Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key), multiplier]() {
const float new_value = Accessor::getFloatValue(widget) / multiplier;
Host::SetBaseFloatSettingValue(section.c_str(), key.c_str(), new_value);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
});
}
}
/// Interface specific method of BindWidgetToNormalizedSetting().
template <typename WidgetType>
static inline void BindWidgetToInputProfileNormalized(
SettingsInterface* sif, WidgetType* widget, std::string section, std::string key, float range, float default_value)
{
using Accessor = SettingWidgetBinder::SettingAccessor<WidgetType>;
if (sif)
{
const float value = sif->GetFloatValue(section.c_str(), key.c_str(), default_value);
Accessor::setIntValue(widget, static_cast<int>(value * range));
Accessor::connectValueChanged(widget, [sif, widget, section = std::move(section), key = std::move(key), range]() {
const int new_value = Accessor::getIntValue(widget);
sif->SetFloatValue(section.c_str(), key.c_str(), static_cast<float>(new_value) / range);
QtHost::SaveGameSettings(sif, false);
g_emu_thread->reloadGameSettings();
});
}
else
{
const float value = Host::GetBaseFloatSettingValue(section.c_str(), key.c_str(), default_value);
Accessor::setIntValue(widget, static_cast<int>(value * range));
Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key), range]() {
const float new_value = (static_cast<float>(Accessor::getIntValue(widget)) / range);
Host::SetBaseFloatSettingValue(section.c_str(), key.c_str(), new_value);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
});
}
}
/// Interface specific method of BindWidgetToStringSetting().
template <typename WidgetType>
static inline void BindWidgetToInputProfileString(
SettingsInterface* sif, WidgetType* widget, std::string section, std::string key, std::string default_value = std::string())
{
using Accessor = SettingWidgetBinder::SettingAccessor<WidgetType>;
if (sif)
{
const QString value(QString::fromStdString(sif->GetStringValue(section.c_str(), key.c_str(), default_value.c_str())));
Accessor::setStringValue(widget, value);
Accessor::connectValueChanged(widget, [widget, sif, section = std::move(section), key = std::move(key)]() {
const QString new_value = Accessor::getStringValue(widget);
if (!new_value.isEmpty())
sif->SetStringValue(section.c_str(), key.c_str(), new_value.toUtf8().constData());
else
sif->DeleteValue(section.c_str(), key.c_str());
QtHost::SaveGameSettings(sif, false);
g_emu_thread->reloadGameSettings();
});
}
else
{
const QString value(
QString::fromStdString(Host::GetBaseStringSettingValue(section.c_str(), key.c_str(), default_value.c_str())));
Accessor::setStringValue(widget, value);
Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key)]() {
const QString new_value = Accessor::getStringValue(widget);
if (!new_value.isEmpty())
Host::SetBaseStringSettingValue(section.c_str(), key.c_str(), new_value.toUtf8().constData());
else
Host::RemoveBaseSettingValue(section.c_str(), key.c_str());
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
});
}
}
} // namespace ControllerSettingWidgetBinder

View File

@@ -0,0 +1,590 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "QtHost.h"
#include "Settings/ControllerSettingsWindow.h"
#include "Settings/ControllerGlobalSettingsWidget.h"
#include "Settings/ControllerBindingWidget.h"
#include "Settings/HotkeySettingsWidget.h"
#include "pcsx2/INISettingsInterface.h"
#include "pcsx2/SIO/Pad/Pad.h"
#include "pcsx2/SIO/Sio.h"
#include "pcsx2/VMManager.h"
#include "common/Assertions.h"
#include "common/FileSystem.h"
#include <array>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QTextEdit>
static constexpr const std::array<char, 4> s_mtap_slot_names = {{'A', 'B', 'C', 'D'}};
ControllerSettingsWindow::ControllerSettingsWindow()
: QWidget(nullptr)
{
m_ui.setupUi(this);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
refreshProfileList();
createWidgets();
m_ui.settingsCategory->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
connect(m_ui.settingsCategory, &QListWidget::currentRowChanged, this, &ControllerSettingsWindow::onCategoryCurrentRowChanged);
connect(m_ui.currentProfile, &QComboBox::currentIndexChanged, this, &ControllerSettingsWindow::onCurrentProfileChanged);
connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &ControllerSettingsWindow::close);
connect(m_ui.newProfile, &QPushButton::clicked, this, &ControllerSettingsWindow::onNewProfileClicked);
connect(m_ui.applyProfile, &QPushButton::clicked, this, &ControllerSettingsWindow::onApplyProfileClicked);
connect(m_ui.renameProfile, &QPushButton::clicked, this, &ControllerSettingsWindow::onRenameProfileClicked);
connect(m_ui.deleteProfile, &QPushButton::clicked, this, &ControllerSettingsWindow::onDeleteProfileClicked);
connect(m_ui.mappingSettings, &QPushButton::clicked, this, &ControllerSettingsWindow::onMappingSettingsClicked);
connect(m_ui.restoreDefaults, &QPushButton::clicked, this, &ControllerSettingsWindow::onRestoreDefaultsClicked);
connect(g_emu_thread, &EmuThread::onInputDevicesEnumerated, this, &ControllerSettingsWindow::onInputDevicesEnumerated);
connect(g_emu_thread, &EmuThread::onInputDeviceConnected, this, &ControllerSettingsWindow::onInputDeviceConnected);
connect(g_emu_thread, &EmuThread::onInputDeviceDisconnected, this, &ControllerSettingsWindow::onInputDeviceDisconnected);
connect(g_emu_thread, &EmuThread::onVibrationMotorsEnumerated, this, &ControllerSettingsWindow::onVibrationMotorsEnumerated);
// trigger a device enumeration to populate the device list
g_emu_thread->enumerateInputDevices();
g_emu_thread->enumerateVibrationMotors();
}
ControllerSettingsWindow::~ControllerSettingsWindow() = default;
void ControllerSettingsWindow::setCategory(Category category)
{
switch (category)
{
case Category::GlobalSettings:
m_ui.settingsCategory->setCurrentRow(0);
break;
// TODO: These will need to take multitap into consideration in the future.
case Category::FirstControllerSettings:
m_ui.settingsCategory->setCurrentRow(1);
break;
case Category::HotkeySettings:
m_ui.settingsCategory->setCurrentRow(5);
break;
default:
break;
}
}
void ControllerSettingsWindow::onCategoryCurrentRowChanged(int row)
{
m_ui.settingsContainer->setCurrentIndex(row);
}
void ControllerSettingsWindow::onCurrentProfileChanged(int index)
{
switchProfile((index == 0) ? 0 : m_ui.currentProfile->itemText(index));
}
void ControllerSettingsWindow::onNewProfileClicked()
{
const QString profile_name(QInputDialog::getText(this, tr("Create Input Profile"),
tr("Custom input profiles are used to override the Shared input profile for specific games.\n"
"To apply a custom input profile to a game, go to its Game Properties, then change the 'Input Profile' on the Summary tab.\n\n"
"Enter the name for the new input profile:")));
if (profile_name.isEmpty())
return;
std::string profile_path(VMManager::GetInputProfilePath(profile_name.toStdString()));
if (FileSystem::FileExists(profile_path.c_str()))
{
QMessageBox::critical(this, tr("Error"), tr("A profile with the name '%1' already exists.").arg(profile_name));
return;
}
const int res = QMessageBox::question(this, tr("Create Input Profile"),
tr("Do you want to copy all bindings from the currently-selected profile to the new profile? Selecting No will create a completely "
"empty profile."),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if (res == QMessageBox::Cancel)
return;
INISettingsInterface temp_si(std::move(profile_path));
if (res == QMessageBox::Yes)
{
// copy from global or the current profile
if (!m_profile_interface)
{
const int hkres = QMessageBox::question(this, tr("Create Input Profile"),
tr("Do you want to copy the current hotkey bindings from global settings to the new input profile?"),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if (hkres == QMessageBox::Cancel)
return;
const bool copy_hotkey_bindings = (hkres == QMessageBox::Yes);
if (copy_hotkey_bindings)
temp_si.SetBoolValue("Pad", "UseProfileHotkeyBindings", true);
// from global
auto lock = Host::GetSettingsLock();
Pad::CopyConfiguration(&temp_si, *Host::Internal::GetBaseSettingsLayer(), true, true, copy_hotkey_bindings);
USB::CopyConfiguration(&temp_si, *Host::Internal::GetBaseSettingsLayer(), true, true);
}
else
{
// from profile
const bool copy_hotkey_bindings = m_profile_interface->GetBoolValue("Pad", "UseProfileHotkeyBindings", false);
temp_si.SetBoolValue("Pad", "UseProfileHotkeyBindings", copy_hotkey_bindings);
Pad::CopyConfiguration(&temp_si, *m_profile_interface, true, true, copy_hotkey_bindings);
USB::CopyConfiguration(&temp_si, *m_profile_interface, true, true);
}
}
if (!temp_si.Save())
{
QMessageBox::critical(
this, tr("Error"), tr("Failed to save the new profile to '%1'.").arg(QString::fromStdString(temp_si.GetFileName())));
return;
}
refreshProfileList();
switchProfile(profile_name);
}
void ControllerSettingsWindow::onApplyProfileClicked()
{
if (QMessageBox::question(this, tr("Load Input Profile"),
tr("Are you sure you want to load the input profile named '%1'?\n\n"
"All current global bindings will be removed, and the profile bindings loaded.\n\n"
"You cannot undo this action.")
.arg(m_profile_name)) != QMessageBox::Yes)
{
return;
}
{
const bool copy_hotkey_bindings = m_profile_interface->GetBoolValue("Pad", "UseProfileHotkeyBindings", false);
auto lock = Host::GetSettingsLock();
Pad::CopyConfiguration(Host::Internal::GetBaseSettingsLayer(), *m_profile_interface, true, true, copy_hotkey_bindings);
USB::CopyConfiguration(Host::Internal::GetBaseSettingsLayer(), *m_profile_interface, true, true);
}
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
// make it visible
switchProfile({});
}
void ControllerSettingsWindow::onRenameProfileClicked()
{
const QString profile_name(QInputDialog::getText(this, tr("Rename Input Profile"),
tr("Enter the new name for the input profile:").arg(m_profile_name)));
if (profile_name.isEmpty())
return;
std::string old_profile_name(m_profile_name.toStdString());
std::string old_profile_path(VMManager::GetInputProfilePath(m_profile_name.toStdString()));
std::string profile_path(VMManager::GetInputProfilePath(profile_name.toStdString()));
if (FileSystem::FileExists(profile_path.c_str()))
{
QMessageBox::critical(this, tr("Error"), tr("A profile with the name '%1' already exists.").arg(profile_name));
return;
}
if (!FileSystem::RenamePath(old_profile_path.c_str(), profile_path.c_str()))
{
QMessageBox::critical(this, tr("Error"), tr("Failed to rename '%1'.").arg(QString::fromStdString(old_profile_path)));
return;
}
FileSystem::FindResultsArray files;
FileSystem::FindFiles(EmuFolders::GameSettings.c_str(), "*", FILESYSTEM_FIND_FILES, &files);
for (const auto& game_settings : files)
{
std::string game_settings_path(game_settings.FileName.c_str());
std::unique_ptr<INISettingsInterface> update_sif(std::make_unique<INISettingsInterface>(std::move(game_settings_path)));
update_sif->Load();
if (!old_profile_name.compare(update_sif->GetStringValue("EmuCore", "InputProfileName")))
{
update_sif->SetStringValue("EmuCore", "InputProfileName", profile_name.toUtf8());
}
}
refreshProfileList();
switchProfile({profile_name});
}
void ControllerSettingsWindow::onDeleteProfileClicked()
{
if (QMessageBox::question(this, tr("Delete Input Profile"),
tr("Are you sure you want to delete the input profile named '%1'?\n\n"
"You cannot undo this action.")
.arg(m_profile_name)) != QMessageBox::Yes)
{
return;
}
std::string profile_path(VMManager::GetInputProfilePath(m_profile_name.toStdString()));
if (!FileSystem::DeleteFilePath(profile_path.c_str()))
{
QMessageBox::critical(this, tr("Error"), tr("Failed to delete '%1'.").arg(QString::fromStdString(profile_path)));
return;
}
// switch back to global
refreshProfileList();
switchProfile({});
}
void ControllerSettingsWindow::onMappingSettingsClicked()
{
ControllerMappingSettingsDialog dialog(this);
dialog.exec();
}
void ControllerSettingsWindow::onRestoreDefaultsClicked()
{
if (QMessageBox::question(this, tr("Restore Defaults"),
tr("Are you sure you want to restore the default controller configuration?\n\n"
"All shared bindings and configuration will be lost, but your input profiles will remain.\n\n"
"You cannot undo this action.")) != QMessageBox::Yes)
{
return;
}
// actually restore it
{
auto lock = Host::GetSettingsLock();
VMManager::SetDefaultSettings(*Host::Internal::GetBaseSettingsLayer(), false, false, true, true, false);
}
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
// reload all settings
switchProfile({});
}
void ControllerSettingsWindow::onInputDevicesEnumerated(const QList<QPair<QString, QString>>& devices)
{
m_device_list = devices;
for (const QPair<QString, QString>& device : devices)
m_global_settings->addDeviceToList(device.first, device.second);
}
void ControllerSettingsWindow::onInputDeviceConnected(const QString& identifier, const QString& device_name)
{
m_device_list.emplace_back(identifier, device_name);
m_global_settings->addDeviceToList(identifier, device_name);
g_emu_thread->enumerateVibrationMotors();
}
void ControllerSettingsWindow::onInputDeviceDisconnected(const QString& identifier)
{
for (auto iter = m_device_list.begin(); iter != m_device_list.end(); ++iter)
{
if (iter->first == identifier)
{
m_device_list.erase(iter);
break;
}
}
m_global_settings->removeDeviceFromList(identifier);
g_emu_thread->enumerateVibrationMotors();
}
void ControllerSettingsWindow::onVibrationMotorsEnumerated(const QList<InputBindingKey>& motors)
{
m_vibration_motors.clear();
m_vibration_motors.reserve(motors.size());
for (const InputBindingKey key : motors)
{
const std::string key_str(InputManager::ConvertInputBindingKeyToString(InputBindingInfo::Type::Motor, key));
if (!key_str.empty())
m_vibration_motors.push_back(QString::fromStdString(key_str));
}
}
bool ControllerSettingsWindow::getBoolValue(const char* section, const char* key, bool default_value) const
{
if (m_profile_interface)
return m_profile_interface->GetBoolValue(section, key, default_value);
else
return Host::GetBaseBoolSettingValue(section, key, default_value);
}
s32 ControllerSettingsWindow::getIntValue(const char* section, const char* key, s32 default_value) const
{
if (m_profile_interface)
return m_profile_interface->GetIntValue(section, key, default_value);
else
return Host::GetBaseIntSettingValue(section, key, default_value);
}
std::string ControllerSettingsWindow::getStringValue(const char* section, const char* key, const char* default_value) const
{
std::string value;
if (m_profile_interface)
value = m_profile_interface->GetStringValue(section, key, default_value);
else
value = Host::GetBaseStringSettingValue(section, key, default_value);
return value;
}
void ControllerSettingsWindow::setBoolValue(const char* section, const char* key, bool value)
{
if (m_profile_interface)
{
m_profile_interface->SetBoolValue(section, key, value);
saveAndReloadGameSettings();
}
else
{
Host::SetBaseBoolSettingValue(section, key, value);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
}
}
void ControllerSettingsWindow::setIntValue(const char* section, const char* key, s32 value)
{
if (m_profile_interface)
{
m_profile_interface->SetIntValue(section, key, value);
saveAndReloadGameSettings();
}
else
{
Host::SetBaseIntSettingValue(section, key, value);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
}
}
void ControllerSettingsWindow::setStringValue(const char* section, const char* key, const char* value)
{
if (m_profile_interface)
{
m_profile_interface->SetStringValue(section, key, value);
saveAndReloadGameSettings();
}
else
{
Host::SetBaseStringSettingValue(section, key, value);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
}
}
void ControllerSettingsWindow::clearSettingValue(const char* section, const char* key)
{
if (m_profile_interface)
{
m_profile_interface->DeleteValue(section, key);
saveAndReloadGameSettings();
}
else
{
Host::RemoveBaseSettingValue(section, key);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
}
}
void ControllerSettingsWindow::saveAndReloadGameSettings()
{
pxAssert(m_profile_interface);
QtHost::SaveGameSettings(m_profile_interface.get(), false);
g_emu_thread->reloadGameSettings();
}
void ControllerSettingsWindow::createWidgets()
{
QSignalBlocker sb(m_ui.settingsContainer);
QSignalBlocker sb2(m_ui.settingsCategory);
while (m_ui.settingsContainer->count() > 0)
{
QWidget* widget = m_ui.settingsContainer->widget(m_ui.settingsContainer->count() - 1);
m_ui.settingsContainer->removeWidget(widget);
widget->deleteLater();
}
m_ui.settingsCategory->clear();
m_global_settings = nullptr;
m_hotkey_settings = nullptr;
{
// global settings
QListWidgetItem* item = new QListWidgetItem();
item->setText(tr("Global Settings"));
item->setIcon(QIcon::fromTheme("settings-3-line"));
m_ui.settingsCategory->addItem(item);
m_ui.settingsCategory->setCurrentRow(0);
m_global_settings = new ControllerGlobalSettingsWidget(m_ui.settingsContainer, this);
m_ui.settingsContainer->addWidget(m_global_settings);
connect(m_global_settings, &ControllerGlobalSettingsWidget::bindingSetupChanged, this, &ControllerSettingsWindow::createWidgets);
for (const QPair<QString, QString>& dev : m_device_list)
m_global_settings->addDeviceToList(dev.first, dev.second);
}
// load mtap settings
const std::array<bool, 2> mtap_enabled = {{getBoolValue("Pad", "MultitapPort1", false), getBoolValue("Pad", "MultitapPort2", false)}};
// we reorder things a little to make it look less silly for mtap
static constexpr const std::array<u32, MAX_PORTS> mtap_port_order = {{0, 2, 3, 4, 1, 5, 6, 7}};
// create the ports
for (u32 global_slot : mtap_port_order)
{
const bool is_mtap_port = sioPadIsMultitapSlot(global_slot);
const auto [port, slot] = sioConvertPadToPortAndSlot(global_slot);
if (is_mtap_port && !mtap_enabled[port])
continue;
m_port_bindings[global_slot] = new ControllerBindingWidget(m_ui.settingsContainer, this, global_slot);
m_ui.settingsContainer->addWidget(m_port_bindings[global_slot]);
const Pad::ControllerInfo* ci = Pad::GetControllerInfo(m_port_bindings[global_slot]->getControllerType());
const QString display_name(QString::fromUtf8(ci ? ci->GetLocalizedName() : "Unknown"));
QListWidgetItem* item = new QListWidgetItem();
//: Controller Port is an official term from Sony. Find the official translation for your language inside the console's manual.
item->setText(mtap_enabled[port] ? (tr("Controller Port %1%2\n%3").arg(port + 1).arg(s_mtap_slot_names[slot]).arg(display_name)) :
//: Controller Port is an official term from Sony. Find the official translation for your language inside the console's manual.
tr("Controller Port %1\n%2").arg(port + 1).arg(display_name));
item->setIcon(m_port_bindings[global_slot]->getIcon());
item->setData(Qt::UserRole, QVariant(global_slot));
m_ui.settingsCategory->addItem(item);
}
// USB ports
for (u32 port = 0; port < USB::NUM_PORTS; port++)
{
m_usb_bindings[port] = new USBDeviceWidget(m_ui.settingsContainer, this, port);
m_ui.settingsContainer->addWidget(m_usb_bindings[port]);
const std::string dtype(getStringValue(fmt::format("USB{}", port + 1).c_str(), "Type", "None"));
const QString display_name(qApp->translate("USB", USB::GetDeviceName(dtype)));
QListWidgetItem* item = new QListWidgetItem();
item->setText(tr("USB Port %1\n%2").arg(port + 1).arg(display_name));
item->setIcon(m_usb_bindings[port]->getIcon());
item->setData(Qt::UserRole, QVariant(MAX_PORTS + port));
m_ui.settingsCategory->addItem(item);
}
// only add hotkeys if we're editing global settings
if (!m_profile_interface || m_profile_interface->GetBoolValue("Pad", "UseProfileHotkeyBindings", false))
{
QListWidgetItem* item = new QListWidgetItem();
item->setText(tr("Hotkeys"));
item->setIcon(QIcon::fromTheme("keyboard-line"));
m_ui.settingsCategory->addItem(item);
m_hotkey_settings = new HotkeySettingsWidget(m_ui.settingsContainer, this);
m_ui.settingsContainer->addWidget(m_hotkey_settings);
}
m_ui.applyProfile->setEnabled(isEditingProfile());
m_ui.renameProfile->setEnabled(isEditingProfile());
m_ui.deleteProfile->setEnabled(isEditingProfile());
m_ui.restoreDefaults->setEnabled(isEditingGlobalSettings());
}
void ControllerSettingsWindow::updateListDescription(u32 global_slot, ControllerBindingWidget* widget)
{
for (int i = 0; i < m_ui.settingsCategory->count(); i++)
{
QListWidgetItem* item = m_ui.settingsCategory->item(i);
const QVariant data(item->data(Qt::UserRole));
if (data.metaType().id() == QMetaType::UInt && data.toUInt() == global_slot)
{
const auto [port, slot] = sioConvertPadToPortAndSlot(global_slot);
const bool mtap_enabled = getBoolValue("Pad", (port == 0) ? "MultitapPort1" : "MultitapPort2", false);
const Pad::ControllerInfo* ci = Pad::GetControllerInfo(widget->getControllerType());
const QString display_name = QString::fromUtf8(ci ? ci->GetLocalizedName() : "Unknown");
//: Controller Port is an official term from Sony. Find the official translation for your language inside the console's manual.
item->setText(mtap_enabled ? (tr("Controller Port %1%2\n%3").arg(port + 1).arg(s_mtap_slot_names[slot]).arg(display_name)) :
//: Controller Port is an official term from Sony. Find the official translation for your language inside the console's manual.
tr("Controller Port %1\n%2").arg(port + 1).arg(display_name));
item->setIcon(widget->getIcon());
break;
}
}
}
void ControllerSettingsWindow::updateListDescription(u32 port, USBDeviceWidget* widget)
{
for (int i = 0; i < m_ui.settingsCategory->count(); i++)
{
QListWidgetItem* item = m_ui.settingsCategory->item(i);
const QVariant data(item->data(Qt::UserRole));
if (data.metaType().id() == QMetaType::UInt && data.toUInt() == (MAX_PORTS + port))
{
const std::string dtype(getStringValue(fmt::format("USB{}", port + 1).c_str(), "Type", "None"));
const QString display_name(qApp->translate("USB", USB::GetDeviceName(dtype)));
item->setText(tr("USB Port %1\n%2").arg(port + 1).arg(display_name));
item->setIcon(widget->getIcon());
break;
}
}
}
void ControllerSettingsWindow::refreshProfileList()
{
const std::vector<std::string> names = Pad::GetInputProfileNames();
QSignalBlocker sb(m_ui.currentProfile);
m_ui.currentProfile->clear();
//: "Shared" refers here to the shared input profile.
m_ui.currentProfile->addItem(tr("Shared"));
if (isEditingGlobalSettings())
m_ui.currentProfile->setCurrentIndex(0);
for (const std::string& name : names)
{
const QString qname(QString::fromStdString(name));
m_ui.currentProfile->addItem(qname);
if (qname == m_profile_name)
m_ui.currentProfile->setCurrentIndex(m_ui.currentProfile->count() - 1);
}
}
void ControllerSettingsWindow::switchProfile(const QString& name)
{
QSignalBlocker sb(m_ui.currentProfile);
if (!name.isEmpty())
{
std::string path(VMManager::GetInputProfilePath(name.toStdString()));
if (!FileSystem::FileExists(path.c_str()))
{
QMessageBox::critical(this, tr("Error"), tr("The input profile named '%1' cannot be found.").arg(name));
return;
}
std::unique_ptr<INISettingsInterface> sif(std::make_unique<INISettingsInterface>(std::move(path)));
sif->Load();
m_profile_interface = std::move(sif);
m_ui.currentProfile->setCurrentIndex(m_ui.currentProfile->findText(name));
}
else
{
m_profile_interface.reset();
m_ui.currentProfile->setCurrentIndex(0);
}
m_profile_name = name;
createWidgets();
}

View File

@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "ui_ControllerSettingsWindow.h"
#include "pcsx2/Input/InputManager.h"
#include "pcsx2/USB/USB.h"
#include <QtCore/QList>
#include <QtCore/QPair>
#include <QtCore/QString>
#include <QtCore/QStringList>
#include <QtWidgets/QWidget>
#include <array>
#include <string>
class ControllerGlobalSettingsWidget;
class ControllerBindingWidget;
class HotkeySettingsWidget;
class USBDeviceWidget;
class SettingsInterface;
class ControllerSettingsWindow final : public QWidget
{
Q_OBJECT
public:
enum class Category
{
GlobalSettings,
FirstControllerSettings,
HotkeySettings,
Count
};
enum : u32
{
MAX_PORTS = 8
};
ControllerSettingsWindow();
~ControllerSettingsWindow();
__fi HotkeySettingsWidget* getHotkeySettingsWidget() const { return m_hotkey_settings; }
__fi const QList<QPair<QString, QString>>& getDeviceList() const { return m_device_list; }
__fi const QStringList& getVibrationMotors() const { return m_vibration_motors; }
__fi bool isEditingGlobalSettings() const { return m_profile_name.isEmpty(); }
__fi bool isEditingProfile() const { return !m_profile_name.isEmpty(); }
__fi SettingsInterface* getProfileSettingsInterface() { return m_profile_interface.get(); }
void updateListDescription(u32 global_slot, ControllerBindingWidget* widget);
void updateListDescription(u32 port, USBDeviceWidget* widget);
// Helper functions for updating setting values globally or in the profile.
bool getBoolValue(const char* section, const char* key, bool default_value) const;
s32 getIntValue(const char* section, const char* key, s32 default_value) const;
std::string getStringValue(const char* section, const char* key, const char* default_value) const;
void setBoolValue(const char* section, const char* key, bool value);
void setIntValue(const char* section, const char* key, s32 value);
void setStringValue(const char* section, const char* key, const char* value);
void clearSettingValue(const char* section, const char* key);
void saveAndReloadGameSettings();
Q_SIGNALS:
void inputProfileSwitched();
public Q_SLOTS:
void setCategory(Category category);
private Q_SLOTS:
void onCategoryCurrentRowChanged(int row);
void onCurrentProfileChanged(int index);
void onNewProfileClicked();
void onApplyProfileClicked();
void onRenameProfileClicked();
void onDeleteProfileClicked();
void onMappingSettingsClicked();
void onRestoreDefaultsClicked();
void onInputDevicesEnumerated(const QList<QPair<QString, QString>>& devices);
void onInputDeviceConnected(const QString& identifier, const QString& device_name);
void onInputDeviceDisconnected(const QString& identifier);
void onVibrationMotorsEnumerated(const QList<InputBindingKey>& motors);
void createWidgets();
private:
void refreshProfileList();
void switchProfile(const QString& name);
Ui::ControllerSettingsWindow m_ui;
ControllerGlobalSettingsWidget* m_global_settings = nullptr;
std::array<ControllerBindingWidget*, MAX_PORTS> m_port_bindings{};
std::array<USBDeviceWidget*, USB::NUM_PORTS> m_usb_bindings{};
HotkeySettingsWidget* m_hotkey_settings = nullptr;
QList<QPair<QString, QString>> m_device_list;
QStringList m_vibration_motors;
QString m_profile_name;
std::unique_ptr<SettingsInterface> m_profile_interface;
};

View File

@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerSettingsWindow</class>
<widget class="QWidget" name="ControllerSettingsWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1318</width>
<height>690</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>PCSX2 Controller Settings</string>
</property>
<property name="windowIcon">
<iconset>
<normalon>:/icons/AppIcon64.png</normalon>
</iconset>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QListWidget" name="settingsCategory">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>180</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QStackedWidget" name="settingsContainer">
<property name="minimumSize">
<size>
<width>1100</width>
<height>620</height>
</size>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<layout class="QHBoxLayout" name="profileLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="profileLabel">
<property name="text">
<string>Editing Profile:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="currentProfile">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>220</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="newProfile">
<property name="text">
<string>New Profile</string>
</property>
<property name="icon">
<iconset theme="plus-line"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="applyProfile">
<property name="text">
<string>Apply Profile</string>
</property>
<property name="icon">
<iconset theme="folder-open-line"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="renameProfile">
<property name="text">
<string>Rename Profile</string>
</property>
<property name="icon">
<iconset theme="pencil-line"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="deleteProfile">
<property name="text">
<string>Delete Profile</string>
</property>
<property name="icon">
<iconset theme="minus-line"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="mappingSettings">
<property name="text">
<string>Mapping Settings</string>
</property>
<property name="icon">
<iconset theme="settings-3-line"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="restoreDefaults">
<property name="text">
<string>Restore Defaults</string>
</property>
<property name="icon">
<iconset theme="restart-line"/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Close</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,132 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QFileDialog>
#include <QtGui/QStandardItemModel>
#include <algorithm>
#include "common/StringUtil.h"
#include "DEV9DnsHostDialog.h"
#include "QtHost.h"
#include "QtUtils.h"
#include "SettingWidgetBinder.h"
#include "SettingsWindow.h"
//Figure out lists
//On export, we take list from settings (or are given it from the DEV9 panel)
//We display, then export
//On import, we read file
//we display, then pass list back to main DEV9 panel
DEV9DnsHostDialog::DEV9DnsHostDialog(std::vector<HostEntryUi> hosts, QWidget* parent)
: QDialog(parent)
{
m_ui.setupUi(this);
m_ethHost_model = new QStandardItemModel(0, 5, m_ui.hostList);
QStringList headers;
headers.push_back(tr("Selected"));
headers.push_back(tr("Name"));
headers.push_back(tr("Url"));
headers.push_back(tr("Address"));
headers.push_back(tr("Enabled"));
m_ethHost_model->setHorizontalHeaderLabels(headers);
m_ethHosts_proxy = new QSortFilterProxyModel(m_ui.hostList);
m_ethHosts_proxy->setSourceModel(m_ethHost_model);
m_ui.hostList->setModel(m_ethHosts_proxy);
m_ui.hostList->setItemDelegateForColumn(3, new IPItemDelegate(m_ui.hostList));
for (size_t i = 0; i < hosts.size(); i++)
{
HostEntryUi entry = hosts[i];
const int row = m_ethHost_model->rowCount();
m_ethHost_model->insertRow(row);
QSignalBlocker sb(m_ethHost_model);
QStandardItem* includeItem = new QStandardItem();
includeItem->setEditable(false);
includeItem->setCheckable(true);
includeItem->setCheckState(Qt::CheckState::Checked);
m_ethHost_model->setItem(row, 0, includeItem);
QStandardItem* nameItem = new QStandardItem();
nameItem->setText(QString::fromStdString(entry.Desc));
nameItem->setEnabled(false);
m_ethHost_model->setItem(row, 1, nameItem);
QStandardItem* urlItem = new QStandardItem();
urlItem->setText(QString::fromStdString(entry.Url));
urlItem->setEnabled(false);
m_ethHost_model->setItem(row, 2, urlItem);
QStandardItem* addressItem = new QStandardItem();
addressItem->setText(QString::fromStdString(entry.Address));
addressItem->setEnabled(false);
m_ethHost_model->setItem(row, 3, addressItem);
QStandardItem* enabledItem = new QStandardItem();
enabledItem->setEditable(false);
enabledItem->setCheckable(true);
enabledItem->setCheckState(entry.Enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked);
enabledItem->setEnabled(false);
m_ethHost_model->setItem(row, 4, enabledItem);
}
m_ui.hostList->sortByColumn(1, Qt::AscendingOrder);
m_ui.hostList->installEventFilter(this);
connect(m_ui.btnOK, &QPushButton::clicked, this, &DEV9DnsHostDialog::onOK);
connect(m_ui.btnCancel, &QPushButton::clicked, this, &DEV9DnsHostDialog::onCancel);
m_hosts = hosts;
}
std::optional<std::vector<HostEntryUi>> DEV9DnsHostDialog::PromptList()
{
int ret = exec();
if (ret != Accepted)
return std::nullopt;
std::vector<HostEntryUi> selectedList;
for (int i = 0; i < m_ethHost_model->rowCount(); i++)
{
if (m_ethHost_model->item(i, 0)->checkState() == Qt::CheckState::Checked)
selectedList.push_back(m_hosts[i]);
}
return selectedList;
}
void DEV9DnsHostDialog::onOK()
{
accept();
}
void DEV9DnsHostDialog::onCancel()
{
reject();
}
bool DEV9DnsHostDialog::eventFilter(QObject* object, QEvent* event)
{
if (object == m_ui.hostList)
{
//Check isVisible to avoind an unnessecery call to ResizeColumnsForTableView()
if (event->type() == QEvent::Resize && m_ui.hostList->isVisible())
QtUtils::ResizeColumnsForTableView(m_ui.hostList, {80, -1, 170, 90, 80});
else if (event->type() == QEvent::Show)
QtUtils::ResizeColumnsForTableView(m_ui.hostList, {80, -1, 170, 90, 80});
}
return false;
}
DEV9DnsHostDialog::~DEV9DnsHostDialog() = default;

View File

@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtCore/QSortFilterProxyModel>
#include <QtGui/QStandardItemModel>
#include <QtWidgets/QDialog>
#include "ui_DEV9DnsHostDialog.h"
#include "DEV9UiCommon.h"
class SettingsWindow;
class DEV9DnsHostDialog : public QDialog
{
Q_OBJECT
private Q_SLOTS:
void onOK();
void onCancel();
public:
DEV9DnsHostDialog(std::vector<HostEntryUi> hosts, QWidget* parent);
~DEV9DnsHostDialog();
std::optional<std::vector<HostEntryUi>> PromptList();
protected:
bool eventFilter(QObject* object, QEvent* event);
private:
Ui::DEV9DnsHostDialog m_ui;
std::vector<HostEntryUi> m_hosts;
QStandardItemModel* m_ethHost_model;
QSortFilterProxyModel* m_ethHosts_proxy;
};

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DEV9DnsHostDialog</class>
<widget class="QWidget" name="DEV9DnsHostDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Network DNS Hosts Import/Export</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Select Hosts</string>
</property>
</widget>
</item>
<item>
<widget class="QTableView" name="hostList">
<attribute name="horizontalHeaderHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btnOK">
<property name="text">
<string>OK</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnCancel">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QWidget>
#include <QtGui/QStandardItemModel>
#include "ui_DEV9SettingsWidget.h"
#include "DEV9UiCommon.h"
#include "DEV9DnsHostDialog.h"
#include "DEV9/net.h"
class SettingsWindow;
class DEV9SettingsWidget : public QWidget
{
Q_OBJECT
private Q_SLOTS:
void onEthEnabledChanged(Qt::CheckState state);
void onEthDeviceTypeChanged(int index);
void onEthDeviceChanged(int index);
void onEthDHCPInterceptChanged(Qt::CheckState state);
void onEthIPChanged(QLineEdit* sender, const char* section, const char* key);
void onEthAutoChanged(QCheckBox* sender, Qt::CheckState state, QLineEdit* input, const char* section, const char* key);
void onEthDNSModeChanged(QComboBox* sender, int index, QLineEdit* input, const char* section, const char* key);
void onEthHostAdd();
void onEthHostDel();
void onEthHostExport();
void onEthHostImport();
void onEthHostPerGame();
void onEthHostEdit(QStandardItem* item);
void onHddEnabledChanged(Qt::CheckState state);
void onHddBrowseFileClicked();
void onHddFileTextChange();
void onHddFileEdit();
void onHddSizeSlide(int i);
void onHddSizeAccessorSpin();
void onHddLBA48Changed(Qt::CheckState state);
void onHddCreateClicked();
public:
DEV9SettingsWidget(SettingsWindow* dialog, QWidget* parent);
~DEV9SettingsWidget();
protected:
void showEvent(QShowEvent* event);
bool eventFilter(QObject* object, QEvent* event);
private:
void AddAdapter(const AdapterEntry& adapter);
void LoadAdapters();
void RefreshHostList();
int CountHostsConfig();
std::optional<std::vector<HostEntryUi>> ListHostsConfig();
std::vector<HostEntryUi> ListBaseHostsConfig();
void AddNewHostConfig(const HostEntryUi& host);
void DeleteHostConfig(int index);
void UpdateHddSizeUIEnabled();
void UpdateHddSizeUIValues();
SettingsWindow* m_dialog;
Ui::DEV9SettingsWidget m_ui;
bool m_firstShow{true};
QStandardItemModel* m_ethHost_model;
QSortFilterProxyModel* m_ethHosts_proxy;
bool m_adaptersLoaded{false};
std::vector<Pcsx2Config::DEV9Options::NetApi> m_api_list;
std::vector<const char*> m_api_namelist;
std::vector<const char*> m_api_valuelist;
std::vector<std::vector<AdapterEntry>> m_adapter_list;
AdapterOptions m_adapter_options{AdapterOptions::None};
//Use by per-game ui only
Pcsx2Config::DEV9Options::NetApi m_global_api{Pcsx2Config::DEV9Options::NetApi::Unset};
};

View File

@@ -0,0 +1,399 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DEV9SettingsWidget</class>
<widget class="QWidget" name="DEV9SettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>482</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Ethernet</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="1" colspan="2">
<widget class="QComboBox" name="ethDev"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="ethDevLabel">
<property name="text">
<string>Ethernet Device:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="ethDevTypeLabel">
<property name="text">
<string>Ethernet Device Type:</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="3">
<widget class="QTabWidget" name="ethTabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<property name="documentMode">
<bool>true</bool>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Intercept DHCP</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<item row="4" column="2">
<widget class="QComboBox" name="ethDNS1Mode"/>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="ethInterceptDHCP">
<property name="text">
<string comment="InterceptDHCP">Enabled</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="ethNetMask">
<property name="inputMask">
<string/>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="ethNetMaskLabel">
<property name="text">
<string>Subnet Mask:</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="ethGatewayAddrLabel">
<property name="text">
<string>Gateway Address:</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QCheckBox" name="ethGatewayAuto">
<property name="text">
<string>Auto</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="ethInterceptDHCPLabel">
<property name="text">
<string>Intercept DHCP:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="ethPS2Addr">
<property name="inputMask">
<string/>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QCheckBox" name="ethNetMaskAuto">
<property name="text">
<string>Auto</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="ethPS2AddrLabel">
<property name="text">
<string>PS2 Address:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="ethGatewayAddr">
<property name="inputMask">
<string/>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="ethDNS1AddrLabel">
<property name="text">
<string>DNS1 Address:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="ethDNS1Addr">
<property name="inputMask">
<string/>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="ethDNS2AddrLabel">
<property name="text">
<string>DNS2 Address:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="ethDNS2Addr">
<property name="inputMask">
<string/>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QComboBox" name="ethDNS2Mode"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Internal DNS</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<item row="1" column="1">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QPushButton" name="ethHostAdd">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ethHostDel">
<property name="text">
<string>Delete</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ethHostExport">
<property name="text">
<string>Export</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ethHostImport">
<property name="text">
<string>Import</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ethHostPerGame">
<property name="text">
<string>Per game</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Internal DNS can be selected using the DNS1/2 dropdowns, or by setting them to 192.0.2.1</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QTableView" name="ethHosts">
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<attribute name="horizontalHeaderHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QComboBox" name="ethDevType"/>
</item>
<item row="0" column="0" colspan="3">
<widget class="QCheckBox" name="ethEnabled">
<property name="text">
<string comment="InternalDNSTable">Enabled</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Hard Disk Drive</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="2">
<widget class="QPushButton" name="hddBrowseFile">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="hddFile"/>
</item>
<item row="0" column="0" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="hddEnabled">
<property name="text">
<string comment="HDD">Enabled</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="hddLBA48">
<property name="text">
<string>Enable 48-Bit LBA</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="hddSizeMinLabel">
<property name="text">
<string>40</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="hddSizeSlider">
<property name="minimum">
<number>40</number>
</property>
<property name="maximum">
<number>120</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>5</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="hddSizeMaxLabel">
<property name="text">
<string>120</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="hddSizeSpinBox">
<property name="minimum">
<number>40</number>
</property>
<property name="maximum">
<number>120</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="hddFileLabel">
<property name="text">
<string>HDD File:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="hddSizeLabel">
<property name="text">
<string>HDD Size (GiB):</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="hddCreate">
<property name="text">
<string>Create Image</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>68</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include <QtWidgets/QLineEdit>
#include "DEV9UiCommon.h"
#define IP_RANGE_INTER "([0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5]|)"
#define IP_RANGE_FINAL "([0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])"
// clang-format off
const QRegularExpression IPValidator::intermediateRegex{QStringLiteral("^" IP_RANGE_INTER "\\." IP_RANGE_INTER "\\." IP_RANGE_INTER "\\." IP_RANGE_INTER "$")};
const QRegularExpression IPValidator::finalRegex {QStringLiteral("^" IP_RANGE_FINAL "\\." IP_RANGE_FINAL "\\." IP_RANGE_FINAL "\\." IP_RANGE_FINAL "$")};
// clang-format on
IPValidator::IPValidator(QObject* parent, bool allowEmpty)
: QValidator(parent)
, m_allowEmpty{allowEmpty}
{
}
QValidator::State IPValidator::validate(QString& input, int& pos) const
{
if (input.isEmpty())
return m_allowEmpty ? Acceptable : Intermediate;
QRegularExpressionMatch m = finalRegex.match(input, 0, QRegularExpression::NormalMatch);
if (m.hasMatch())
return Acceptable;
m = intermediateRegex.match(input, 0, QRegularExpression::PartialPreferCompleteMatch);
if (m.hasMatch() || m.hasPartialMatch())
return Intermediate;
else
{
pos = input.size();
return Invalid;
}
}
IPItemDelegate::IPItemDelegate(QObject* parent)
: QStyledItemDelegate(parent)
{
}
QWidget* IPItemDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
QLineEdit* editor = new QLineEdit(parent);
editor->setValidator(new IPValidator());
return editor;
}
void IPItemDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
{
QString value = index.model()->data(index, Qt::EditRole).toString();
QLineEdit* line = static_cast<QLineEdit*>(editor);
line->setText(value);
}
void IPItemDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
{
QLineEdit* line = static_cast<QLineEdit*>(editor);
QString value = line->text();
model->setData(index, value);
}
void IPItemDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
editor->setGeometry(option.rect);
}

View File

@@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtGui/QValidator>
#include <QtWidgets/QStyledItemDelegate>
struct HostEntryUi
{
std::string Url;
std::string Desc;
std::string Address = "0.0.0.0";
bool Enabled;
};
class IPValidator : public QValidator
{
Q_OBJECT
public:
explicit IPValidator(QObject* parent = nullptr, bool allowEmpty = false);
virtual State validate(QString& input, int& pos) const override;
private:
static const QRegularExpression intermediateRegex;
static const QRegularExpression finalRegex;
bool m_allowEmpty;
};
class IPItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit IPItemDelegate(QObject* parent = nullptr);
protected:
QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const;
void setEditorData(QWidget* editor, const QModelIndex& index) const;
void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const;
void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const;
};

View File

@@ -0,0 +1,521 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "DebugAnalysisSettingsWidget.h"
#include "SettingsWindow.h"
#include "SettingWidgetBinder.h"
#include "DebugTools/SymbolImporter.h"
#include <QtWidgets/QFileDialog>
DebugAnalysisSettingsWidget::DebugAnalysisSettingsWidget(QWidget* parent)
: QWidget(parent)
{
m_ui.setupUi(this);
m_ui.automaticallyClearSymbols->setChecked(Host::GetBoolSettingValue("Debugger/Analysis", "AutomaticallySelectSymbolsToClear", true));
setupSymbolSourceGrid();
m_ui.importFromElf->setChecked(
Host::GetBoolSettingValue("Debugger/Analysis", "ImportSymbolsFromELF", true));
m_ui.importSymFileFromDefaultLocation->setChecked(
Host::GetBoolSettingValue("Debugger/Analysis", "ImportSymFileFromDefaultLocation", true));
m_ui.demangleSymbols->setChecked(
Host::GetBoolSettingValue("Debugger/Analysis", "DemangleSymbols", true));
m_ui.demangleParameters->setChecked(
Host::GetBoolSettingValue("Debugger/Analysis", "DemangleParameters", true));
setupSymbolFileList();
std::string function_scan_mode = Host::GetStringSettingValue("Debugger/Analysis", "FunctionScanMode");
for (int i = 0;; i++)
{
if (Pcsx2Config::DebugAnalysisOptions::FunctionScanModeNames[i] == nullptr)
break;
if (function_scan_mode == Pcsx2Config::DebugAnalysisOptions::FunctionScanModeNames[i])
m_ui.functionScanMode->setCurrentIndex(i);
}
m_ui.customAddressRange->setChecked(
Host::GetBoolSettingValue("Debugger/Analysis", "CustomFunctionScanRange", false));
m_ui.addressRangeStart->setText(QString::fromStdString(
Host::GetStringSettingValue("Debugger/Analysis", "FunctionScanStartAddress", "")));
m_ui.addressRangeEnd->setText(QString::fromStdString(
Host::GetStringSettingValue("Debugger/Analysis", "FunctionScanEndAddress", "")));
m_ui.grayOutOverwrittenFunctions->setChecked(
Host::GetBoolSettingValue("Debugger/Analysis", "GenerateFunctionHashes", true));
connectCheckStateChanged(m_ui.automaticallyClearSymbols, this, &DebugAnalysisSettingsWidget::updateEnabledStates);
connectCheckStateChanged(m_ui.demangleSymbols, this, &DebugAnalysisSettingsWidget::updateEnabledStates);
connectCheckStateChanged(m_ui.customAddressRange, this, &DebugAnalysisSettingsWidget::updateEnabledStates);
updateEnabledStates();
}
DebugAnalysisSettingsWidget::DebugAnalysisSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: QWidget(parent)
, m_dialog(dialog)
{
SettingsInterface* sif = dialog->getSettingsInterface();
m_ui.setupUi(this);
// Make sure the user doesn't select symbol sources from both the global
// settings and the per-game settings, as these settings will conflict with
// each other. It only really makes sense to modify these settings on a
// per-game basis anyway.
if (dialog->isPerGameSettings())
{
SettingWidgetBinder::BindWidgetToBoolSetting(
sif, m_ui.automaticallyClearSymbols, "Debugger/Analysis", "AutomaticallySelectSymbolsToClear", true);
m_dialog->registerWidgetHelp(m_ui.automaticallyClearSymbols, tr("Automatically Select Symbols To Clear"), tr("Checked"),
tr("Automatically delete symbols that were generated by any previous analysis runs."));
setupSymbolSourceGrid();
}
else
{
m_ui.clearExistingSymbolsGroup->hide();
}
SettingWidgetBinder::BindWidgetToBoolSetting(
sif, m_ui.importFromElf, "Debugger/Analysis", "ImportSymbolsFromELF", true);
SettingWidgetBinder::BindWidgetToBoolSetting(
sif, m_ui.importSymFileFromDefaultLocation, "Debugger/Analysis", "ImportSymFileFromDefaultLocation", true);
SettingWidgetBinder::BindWidgetToBoolSetting(
sif, m_ui.demangleSymbols, "Debugger/Analysis", "DemangleSymbols", true);
SettingWidgetBinder::BindWidgetToBoolSetting(
sif, m_ui.demangleParameters, "Debugger/Analysis", "DemangleParameters", true);
m_dialog->registerWidgetHelp(m_ui.importFromElf, tr("Import From ELF"), tr("Checked"),
tr("Import symbol tables stored in the game's boot ELF."));
m_dialog->registerWidgetHelp(m_ui.importSymFileFromDefaultLocation, tr("Import Default .sym File"), tr("Checked"),
tr("Import symbols from a .sym file with the same name as the loaded ISO file on disk if such a file exists."));
m_dialog->registerWidgetHelp(m_ui.demangleSymbols, tr("Demangle Symbols"), tr("Checked"),
tr("Demangle C++ symbols during the import process so that the function and global variable names shown in the "
"debugger are more readable."));
m_dialog->registerWidgetHelp(m_ui.demangleParameters, tr("Demangle Parameters"), tr("Checked"),
tr("Include parameter lists in demangled function names."));
// Same as above. It only makes sense to load extra symbol files on a
// per-game basis.
if (dialog->isPerGameSettings())
{
setupSymbolFileList();
}
else
{
m_ui.symbolFileLabel->hide();
m_ui.symbolFileTable->hide();
m_ui.importSymbolFileButtons->hide();
}
SettingWidgetBinder::BindWidgetToEnumSetting(
sif, m_ui.functionScanMode, "Debugger/Analysis", "FunctionScanMode",
Pcsx2Config::DebugAnalysisOptions::FunctionScanModeNames, DebugFunctionScanMode::SCAN_ELF);
m_dialog->registerWidgetHelp(m_ui.functionScanMode, tr("Scan Mode"), tr("Scan ELF"),
tr("Choose where the function scanner looks to find functions. This option can be useful if the application "
"loads additional code at runtime."));
// Same as above. It only makes sense to set a custom memory range on a
// per-game basis.
if (dialog->isPerGameSettings())
{
SettingWidgetBinder::BindWidgetToBoolSetting(
sif, m_ui.customAddressRange, "Debugger/Analysis", "CustomFunctionScanRange", false);
m_ui.addressRangeStart->setText(QString::fromStdString(
getStringSettingValue("Debugger/Analysis", "FunctionScanStartAddress", "")));
m_ui.addressRangeEnd->setText(QString::fromStdString(
getStringSettingValue("Debugger/Analysis", "FunctionScanEndAddress", "")));
connect(m_ui.addressRangeStart, &QLineEdit::textChanged,
this, &DebugAnalysisSettingsWidget::saveFunctionScanRange);
connect(m_ui.addressRangeEnd, &QLineEdit::textChanged,
this, &DebugAnalysisSettingsWidget::saveFunctionScanRange);
m_dialog->registerWidgetHelp(m_ui.customAddressRange, tr("Custom Address Range"), tr("Unchecked"),
tr("Whether to look for functions from the address range specified (Checked), or from the ELF segment "
"containing the entry point (Unchecked)."));
}
else
{
m_ui.customAddressRange->hide();
m_ui.customAddressRangeLineEdits->hide();
}
SettingWidgetBinder::BindWidgetToBoolSetting(
sif, m_ui.grayOutOverwrittenFunctions, "Debugger/Analysis", "GenerateFunctionHashes", true);
m_dialog->registerWidgetHelp(m_ui.grayOutOverwrittenFunctions, tr("Gray Out Symbols For Overwritten Functions"), tr("Checked"),
tr("Generate hashes for all the detected functions, and gray out the symbols displayed in the debugger for "
"functions that no longer match."));
connectCheckStateChanged(m_ui.automaticallyClearSymbols, this, &DebugAnalysisSettingsWidget::updateEnabledStates);
connectCheckStateChanged(m_ui.demangleSymbols, this, &DebugAnalysisSettingsWidget::updateEnabledStates);
connectCheckStateChanged(m_ui.customAddressRange, this, &DebugAnalysisSettingsWidget::updateEnabledStates);
updateEnabledStates();
}
void DebugAnalysisSettingsWidget::parseSettingsFromWidgets(Pcsx2Config::DebugAnalysisOptions& output)
{
output.AutomaticallySelectSymbolsToClear = m_ui.automaticallyClearSymbols->isChecked();
for (const auto& [name, temp] : m_symbol_sources)
{
DebugSymbolSource& source = output.SymbolSources.emplace_back();
source.Name = name;
source.ClearDuringAnalysis = temp.check_box->isChecked();
}
output.ImportSymbolsFromELF = m_ui.importFromElf->isChecked();
output.ImportSymFileFromDefaultLocation = m_ui.importSymFileFromDefaultLocation->isChecked();
output.DemangleSymbols = m_ui.demangleSymbols->isChecked();
output.DemangleParameters = m_ui.demangleParameters->isChecked();
for (int i = 0; i < m_symbol_file_model->rowCount(); i++)
{
DebugExtraSymbolFile& file = output.ExtraSymbolFiles.emplace_back();
file.Path = m_symbol_file_model->item(i, PATH_COLUMN)->text().toStdString();
file.BaseAddress = m_symbol_file_model->item(i, BASE_ADDRESS_COLUMN)->text().toStdString();
file.Condition = m_symbol_file_model->item(i, CONDITION_COLUMN)->text().toStdString();
}
output.FunctionScanMode = static_cast<DebugFunctionScanMode>(m_ui.functionScanMode->currentIndex());
output.CustomFunctionScanRange = m_ui.customAddressRange->isChecked();
output.FunctionScanStartAddress = m_ui.addressRangeStart->text().toStdString();
output.FunctionScanEndAddress = m_ui.addressRangeEnd->text().toStdString();
}
void DebugAnalysisSettingsWidget::setupSymbolSourceGrid()
{
QGridLayout* layout = new QGridLayout(m_ui.symbolSourceGrid);
if (!m_dialog || m_dialog->getSerial() == QtHost::GetCurrentGameSerial().toStdString())
{
// Add symbol sources for which the user has already selected whether or
// not they should be cleared.
int existing_symbol_source_count = getIntSettingValue("Debugger/Analysis/SymbolSources", "Count", 0);
for (int i = 0; i < existing_symbol_source_count; i++)
{
std::string section = "Debugger/Analysis/SymbolSources/" + std::to_string(i);
std::string name = getStringSettingValue(section.c_str(), "Name", "");
bool value = getBoolSettingValue(section.c_str(), "ClearDuringAnalysis", false);
SymbolSourceTemp& source = m_symbol_sources[name];
source.previous_value = value;
source.modified_by_user = true;
}
// Add any more symbol sources for which the user hasn't made a
// selection. These are separate since we don't want to have to store
// configuration data for them.
R5900SymbolGuardian.Read([&](const ccc::SymbolDatabase& database) {
for (const ccc::SymbolSource& symbol_source : database.symbol_sources)
{
if (m_symbol_sources.find(symbol_source.name()) == m_symbol_sources.end() && symbol_source.name() != "Built-In")
{
SymbolSourceTemp& source = m_symbol_sources[symbol_source.name()];
source.previous_value = SymbolImporter::ShouldClearSymbolsFromSourceByDefault(symbol_source.name());
source.modified_by_user = false;
}
}
});
if (m_symbol_sources.empty())
{
m_ui.symbolSourceErrorMessage->setText(tr("<i>No symbol sources in database.</i>"));
m_ui.symbolSourceScrollArea->hide();
return;
}
// Create the check boxes.
int i = 0;
for (auto& [name, temp] : m_symbol_sources)
{
temp.check_box = new QCheckBox(QString::fromStdString(name));
temp.check_box->setChecked(temp.previous_value);
layout->addWidget(temp.check_box, i / 2, i % 2);
connectCheckStateChanged(temp.check_box, this, &DebugAnalysisSettingsWidget::symbolSourceCheckStateChanged);
i++;
}
}
else
{
m_ui.symbolSourceErrorMessage->setText(tr("<i>Start this game to modify the symbol sources list.</i>"));
m_ui.symbolSourceScrollArea->hide();
return;
}
m_ui.symbolSourceErrorMessage->hide();
}
void DebugAnalysisSettingsWidget::symbolSourceCheckStateChanged()
{
QCheckBox* check_box = qobject_cast<QCheckBox*>(sender());
if (!check_box)
return;
auto temp = m_symbol_sources.find(check_box->text().toStdString());
if (temp == m_symbol_sources.end())
return;
temp->second.modified_by_user = true;
saveSymbolSources();
}
void DebugAnalysisSettingsWidget::saveSymbolSources()
{
if (!m_dialog)
return;
SettingsInterface* sif = m_dialog->getSettingsInterface();
if (!sif)
return;
// Clean up old configuration entries.
int old_count = sif->GetIntValue("Debugger/Analysis/SymbolSources", "Count");
for (int i = 0; i < old_count; i++)
{
std::string section = "Debugger/Analysis/SymbolSources/" + std::to_string(i);
sif->RemoveSection(section.c_str());
}
sif->RemoveSection("Debugger/Analysis/SymbolSources");
int symbol_sources_to_save = 0;
for (auto& [name, temp] : m_symbol_sources)
if (temp.modified_by_user)
symbol_sources_to_save++;
if (symbol_sources_to_save == 0)
return;
// Make new configuration entries.
sif->SetIntValue("Debugger/Analysis/SymbolSources", "Count", symbol_sources_to_save);
int i = 0;
for (auto& [name, temp] : m_symbol_sources)
{
if (!temp.modified_by_user)
continue;
std::string section = "Debugger/Analysis/SymbolSources/" + std::to_string(i);
sif->SetStringValue(section.c_str(), "Name", name.c_str());
sif->SetBoolValue(section.c_str(), "ClearDuringAnalysis", temp.check_box->isChecked());
i++;
}
QtHost::SaveGameSettings(sif, true);
g_emu_thread->reloadGameSettings();
}
void DebugAnalysisSettingsWidget::setupSymbolFileList()
{
m_symbol_file_model = new QStandardItemModel(0, SYMBOL_FILE_COLUMN_COUNT, m_ui.symbolFileTable);
QStringList headers;
headers.emplace_back(tr("Path"));
headers.emplace_back(tr("Base Address"));
headers.emplace_back(tr("Condition"));
m_symbol_file_model->setHorizontalHeaderLabels(headers);
m_ui.symbolFileTable->setModel(m_symbol_file_model);
m_ui.symbolFileTable->horizontalHeader()->setSectionResizeMode(PATH_COLUMN, QHeaderView::Stretch);
m_ui.symbolFileTable->horizontalHeader()->setSectionResizeMode(BASE_ADDRESS_COLUMN, QHeaderView::Fixed);
m_ui.symbolFileTable->horizontalHeader()->setSectionResizeMode(CONDITION_COLUMN, QHeaderView::Fixed);
m_ui.symbolFileTable->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
int extra_symbol_file_count = getIntSettingValue("Debugger/Analysis/ExtraSymbolFiles", "Count", 0);
for (int i = 0; i < extra_symbol_file_count; i++)
{
std::string section = "Debugger/Analysis/ExtraSymbolFiles/" + std::to_string(i);
int row = m_symbol_file_model->rowCount();
if (!m_symbol_file_model->insertRow(row))
continue;
QStandardItem* path_item = new QStandardItem();
path_item->setText(QString::fromStdString(getStringSettingValue(section.c_str(), "Path", "")));
m_symbol_file_model->setItem(row, PATH_COLUMN, path_item);
QStandardItem* base_address_item = new QStandardItem();
base_address_item->setText(QString::fromStdString(getStringSettingValue(section.c_str(), "BaseAddress")));
m_symbol_file_model->setItem(row, BASE_ADDRESS_COLUMN, base_address_item);
QStandardItem* condition_item = new QStandardItem();
condition_item->setText(QString::fromStdString(getStringSettingValue(section.c_str(), "Condition")));
m_symbol_file_model->setItem(row, CONDITION_COLUMN, condition_item);
}
connect(m_ui.addSymbolFile, &QPushButton::clicked, this, &DebugAnalysisSettingsWidget::addSymbolFile);
connect(m_ui.removeSymbolFile, &QPushButton::clicked, this, &DebugAnalysisSettingsWidget::removeSymbolFile);
connect(m_ui.symbolFileTable->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &DebugAnalysisSettingsWidget::updateEnabledStates);
connect(m_symbol_file_model, &QStandardItemModel::dataChanged,
this, &DebugAnalysisSettingsWidget::saveSymbolFiles);
connect(m_symbol_file_model, &QStandardItemModel::dataChanged,
this, &DebugAnalysisSettingsWidget::updateEnabledStates);
}
void DebugAnalysisSettingsWidget::addSymbolFile()
{
std::string path = Path::ToNativePath(QFileDialog::getOpenFileName(this, tr("Add Symbol File")).toStdString());
if (path.empty())
return;
std::string relative_path = Path::MakeRelative(path, EmuFolders::GameSettings);
if (!relative_path.starts_with(".."))
path = std::move(relative_path);
int row = m_symbol_file_model->rowCount();
if (!m_symbol_file_model->insertRow(row))
return;
QStandardItem* path_item = new QStandardItem();
path_item->setText(QString::fromStdString(path));
m_symbol_file_model->setItem(row, PATH_COLUMN, path_item);
QStandardItem* base_address_item = new QStandardItem();
base_address_item->setText("");
m_symbol_file_model->setItem(row, BASE_ADDRESS_COLUMN, base_address_item);
QStandardItem* condition_item = new QStandardItem();
condition_item->setText("");
m_symbol_file_model->setItem(row, CONDITION_COLUMN, condition_item);
saveSymbolFiles();
updateEnabledStates();
}
void DebugAnalysisSettingsWidget::removeSymbolFile()
{
QItemSelectionModel* selection_model = m_ui.symbolFileTable->selectionModel();
if (!selection_model)
return;
while (!selection_model->selectedIndexes().isEmpty())
{
QModelIndex index = selection_model->selectedIndexes().first();
m_symbol_file_model->removeRow(index.row(), index.parent());
}
saveSymbolFiles();
updateEnabledStates();
}
void DebugAnalysisSettingsWidget::saveSymbolFiles()
{
if (!m_dialog)
return;
SettingsInterface* sif = m_dialog->getSettingsInterface();
if (!sif)
return;
// Clean up old configuration entries.
int old_count = sif->GetIntValue("Debugger/Analysis/ExtraSymbolFiles", "Count");
for (int i = 0; i < old_count; i++)
{
std::string section = "Debugger/Analysis/ExtraSymbolFiles/" + std::to_string(i);
sif->RemoveSection(section.c_str());
}
sif->RemoveSection("Debugger/Analysis/ExtraSymbolFiles");
if (m_symbol_file_model->rowCount() == 0)
return;
// Make new configuration entries.
sif->SetIntValue("Debugger/Analysis/ExtraSymbolFiles", "Count", m_symbol_file_model->rowCount());
for (int i = 0; i < m_symbol_file_model->rowCount(); i++)
{
std::string section = "Debugger/Analysis/ExtraSymbolFiles/" + std::to_string(i);
if (QStandardItem* path_item = m_symbol_file_model->item(i, PATH_COLUMN))
sif->SetStringValue(section.c_str(), "Path", path_item->text().toStdString().c_str());
if (QStandardItem* base_address_item = m_symbol_file_model->item(i, BASE_ADDRESS_COLUMN))
sif->SetStringValue(section.c_str(), "BaseAddress", base_address_item->text().toStdString().c_str());
if (QStandardItem* condition_item = m_symbol_file_model->item(i, CONDITION_COLUMN))
sif->SetStringValue(section.c_str(), "Condition", condition_item->text().toStdString().c_str());
}
QtHost::SaveGameSettings(sif, true);
g_emu_thread->reloadGameSettings();
}
void DebugAnalysisSettingsWidget::saveFunctionScanRange()
{
if (!m_dialog)
return;
SettingsInterface* sif = m_dialog->getSettingsInterface();
if (!sif)
return;
QString start_address = m_ui.addressRangeStart->text();
QString end_address = m_ui.addressRangeEnd->text();
sif->SetStringValue("Debugger/Analysis", "FunctionScanStartAddress", start_address.toStdString().c_str());
sif->SetStringValue("Debugger/Analysis", "FunctionScanEndAddress", end_address.toStdString().c_str());
QtHost::SaveGameSettings(sif, true);
g_emu_thread->reloadGameSettings();
}
void DebugAnalysisSettingsWidget::updateEnabledStates()
{
m_ui.symbolSourceScrollArea->setEnabled(!m_ui.automaticallyClearSymbols->isChecked());
m_ui.symbolSourceErrorMessage->setEnabled(!m_ui.automaticallyClearSymbols->isChecked());
m_ui.demangleParameters->setEnabled(m_ui.demangleSymbols->isChecked());
m_ui.removeSymbolFile->setEnabled(
m_ui.symbolFileTable->selectionModel() && m_ui.symbolFileTable->selectionModel()->hasSelection());
m_ui.customAddressRangeLineEdits->setEnabled(m_ui.customAddressRange->isChecked());
}
std::string DebugAnalysisSettingsWidget::getStringSettingValue(
const char* section, const char* key, const char* default_value)
{
if (m_dialog)
return m_dialog->getEffectiveStringValue(section, key, default_value);
return Host::GetStringSettingValue(section, key, default_value);
}
bool DebugAnalysisSettingsWidget::getBoolSettingValue(
const char* section, const char* key, bool default_value)
{
if (m_dialog)
return m_dialog->getEffectiveBoolValue(section, key, default_value);
return Host::GetBoolSettingValue(section, key, default_value);
}
int DebugAnalysisSettingsWidget::getIntSettingValue(
const char* section, const char* key, int default_value)
{
if (m_dialog)
return m_dialog->getEffectiveIntValue(section, key, default_value);
return Host::GetIntSettingValue(section, key, default_value);
}

View File

@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "ui_DebugAnalysisSettingsWidget.h"
#include "Config.h"
#include <QtGui/QStandardItemModel>
class SettingsWindow;
class DebugAnalysisSettingsWidget : public QWidget
{
Q_OBJECT
public:
// Create a widget that will discard any settings changed after it is
// closed, for use in the dialog opened by the "Analyze" button.
DebugAnalysisSettingsWidget(QWidget* parent = nullptr);
// Create a widget that will write back any settings changed to the config
// system, for use in the settings dialog.
DebugAnalysisSettingsWidget(SettingsWindow* dialog, QWidget* parent = nullptr);
// Read all the analysis settings from the widget tree and store them in the
// output object. This is used by the analysis options dialog to start an
// analysis run manually.
void parseSettingsFromWidgets(Pcsx2Config::DebugAnalysisOptions& output);
protected:
void setupSymbolSourceGrid();
void symbolSourceCheckStateChanged();
void saveSymbolSources();
void setupSymbolFileList();
void addSymbolFile();
void removeSymbolFile();
void saveSymbolFiles();
void saveFunctionScanRange();
void updateEnabledStates();
std::string getStringSettingValue(const char* section, const char* key, const char* default_value = "");
bool getBoolSettingValue(const char* section, const char* key, bool default_value = false);
int getIntSettingValue(const char* section, const char* key, int default_value = 0);
struct SymbolSourceTemp
{
QCheckBox* check_box = nullptr;
bool previous_value = false;
bool modified_by_user = false;
};
enum SymbolFileColumn
{
PATH_COLUMN = 0,
BASE_ADDRESS_COLUMN = 1,
CONDITION_COLUMN = 2,
SYMBOL_FILE_COLUMN_COUNT = 3
};
SettingsWindow* m_dialog = nullptr;
std::map<std::string, SymbolSourceTemp> m_symbol_sources;
QStandardItemModel* m_symbol_file_model;
Ui::DebugAnalysisSettingsWidget m_ui;
};

View File

@@ -0,0 +1,425 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DebugAnalysisSettingsWidget</class>
<widget class="QWidget" name="DebugAnalysisSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>750</height>
</rect>
</property>
<property name="windowTitle">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="clearExistingSymbolsGroup">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Clear Existing Symbols</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QCheckBox" name="automaticallyClearSymbols">
<property name="text">
<string>Automatically Select Symbols To Clear</string>
</property>
</widget>
</item>
<item>
<widget class="QScrollArea" name="symbolSourceScrollArea">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>150</height>
</size>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="symbolSourceGrid">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<height>83</height>
</rect>
</property>
</widget>
</widget>
</item>
<item>
<widget class="QLabel" name="symbolSourceErrorMessage">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="importSymbolTablesGroup">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Import Symbols</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QWidget" name="importSymbolTableCheckBoxes" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QGridLayout" name="importSymbolTableCheckBoxesLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QCheckBox" name="importFromElf">
<property name="text">
<string>Import From ELF</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="demangleSymbols">
<property name="text">
<string>Demangle Symbols</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="demangleParameters">
<property name="text">
<string>Demangle Parameters</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="importSymFileFromDefaultLocation">
<property name="text">
<string>Import Default .sym File</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="symbolFileLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Import from file (.elf, .sym, etc):</string>
</property>
</widget>
</item>
<item>
<widget class="QTableView" name="symbolFileTable">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>100</height>
</size>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="textElideMode">
<enum>Qt::ElideLeft</enum>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="QWidget" name="importSymbolFileButtons" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="importSymbolFileButtonsLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<spacer name="importSymbolFileSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="addSymbolFile">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeSymbolFile">
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="scanForFunctionsGroup">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Scan For Functions</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="functionScanForm">
<item row="0" column="0">
<widget class="QLabel" name="functionScanLabel">
<property name="text">
<string>Scan Mode:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="functionScanMode">
<item>
<property name="text">
<string>Scan ELF</string>
</property>
</item>
<item>
<property name="text">
<string>Scan Memory</string>
</property>
</item>
<item>
<property name="text">
<string>Skip</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="customAddressRange">
<property name="text">
<string>Custom Address Range:</string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="customAddressRangeLineEdits" native="true">
<layout class="QHBoxLayout" name="memoryRangeLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<spacer name="startSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Maximum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="addressRangeStartLabel">
<property name="text">
<string>Start:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addressRangeStart"/>
</item>
<item>
<widget class="QLabel" name="addressRangeEndLabel">
<property name="text">
<string>End:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addressRangeEnd"/>
</item>
<item>
<spacer name="endSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Maximum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Hash Functions</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QCheckBox" name="grayOutOverwrittenFunctions">
<property name="text">
<string>Gray Out Symbols For Overwritten Functions</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,229 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "DebugSettingsWidget.h"
#include "DebugUserInterfaceSettingsWidget.h"
#include "DebugAnalysisSettingsWidget.h"
#include "QtUtils.h"
#include "SettingWidgetBinder.h"
#include "SettingsWindow.h"
#include "pcsx2/Host.h"
#include <QtWidgets/QMessageBox>
DebugSettingsWidget::DebugSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: QWidget(parent)
, m_dialog(dialog)
{
SettingsInterface* sif = dialog->getSettingsInterface();
m_ui.setupUi(this);
//////////////////////////////////////////////////////////////////////////
// User Interface Settings
//////////////////////////////////////////////////////////////////////////
if (!dialog->isPerGameSettings())
{
m_user_interface_settings = new DebugUserInterfaceSettingsWidget(dialog);
m_ui.userInterfaceTabWidget->setLayout(new QVBoxLayout());
m_ui.userInterfaceTabWidget->layout()->addWidget(m_user_interface_settings);
}
else
{
m_ui.debugTabs->removeTab(m_ui.debugTabs->indexOf(m_ui.userInterfaceTabWidget));
}
//////////////////////////////////////////////////////////////////////////
// Analysis Settings
//////////////////////////////////////////////////////////////////////////
SettingWidgetBinder::BindWidgetToEnumSetting(
sif, m_ui.analysisCondition, "Debugger/Analysis", "RunCondition",
Pcsx2Config::DebugAnalysisOptions::RunConditionNames, DebugAnalysisCondition::IF_DEBUGGER_IS_OPEN);
SettingWidgetBinder::BindWidgetToBoolSetting(
sif, m_ui.generateSymbolsForIRXExportTables, "Debugger/Analysis", "GenerateSymbolsForIRXExports", true);
dialog->registerWidgetHelp(m_ui.analysisCondition, tr("Analyze Program"), tr("If Debugger Is Open"),
tr("Choose when the analysis passes should be run: Always (to save time when opening the debugger), If "
"Debugger Is Open (to save memory if you never open the debugger), or Never."));
dialog->registerWidgetHelp(m_ui.generateSymbolsForIRXExportTables, tr("Generate Symbols for IRX Export Tables"), tr("Checked"),
tr("Hook IRX module loading/unloading and generate symbols for exported functions on the fly."));
m_analysis_settings = new DebugAnalysisSettingsWidget(dialog);
m_ui.analysisSettings->setLayout(new QVBoxLayout());
m_ui.analysisSettings->layout()->setContentsMargins(0, 0, 0, 0);
m_ui.analysisSettings->layout()->addWidget(m_analysis_settings);
//////////////////////////////////////////////////////////////////////////
// GS Settings
//////////////////////////////////////////////////////////////////////////
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.dumpGSData, "EmuCore/GS", "DumpGSData", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.saveRT, "EmuCore/GS", "SaveRT", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.saveFrame, "EmuCore/GS", "SaveFrame", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.saveTexture, "EmuCore/GS", "SaveTexture", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.saveDepth, "EmuCore/GS", "SaveDepth", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.saveAlpha, "EmuCore/GS", "SaveAlpha", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.saveInfo, "EmuCore/GS", "SaveInfo", false);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.saveDrawStart, "EmuCore/GS", "SaveDrawStart", 0);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.saveDrawCount, "EmuCore/GS", "SaveDrawCount", 5000);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.saveFrameStart, "EmuCore/GS", "SaveFrameStart", 0);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.saveFrameCount, "EmuCore/GS", "SaveFrameCount", 999999);
SettingWidgetBinder::BindWidgetToFolderSetting(
sif, m_ui.hwDumpDirectory, m_ui.hwDumpBrowse, m_ui.hwDumpOpen, nullptr, "EmuCore/GS", "HWDumpDirectory", std::string(), false);
SettingWidgetBinder::BindWidgetToFolderSetting(
sif, m_ui.swDumpDirectory, m_ui.swDumpBrowse, m_ui.swDumpOpen, nullptr, "EmuCore/GS", "SWDumpDirectory", std::string(), false);
connectCheckStateChanged(m_ui.dumpGSData, this, &DebugSettingsWidget::onDrawDumpingChanged);
onDrawDumpingChanged();
#ifdef PCSX2_DEVBUILD
//////////////////////////////////////////////////////////////////////////
// Trace Logging Settings
//////////////////////////////////////////////////////////////////////////
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEnable, "EmuCore/TraceLog", "Enabled", false);
dialog->registerWidgetHelp(m_ui.chkEnable, tr("Enable Trace Logging"), tr("Unchecked"), tr("Globally enable / disable trace logging."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEEBIOS, "EmuCore/TraceLog", "EE.bios", false);
dialog->registerWidgetHelp(m_ui.chkEEBIOS, tr("EE BIOS"), tr("Unchecked"), tr("Log SYSCALL and DECI2 activity."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEEMemory, "EmuCore/TraceLog", "EE.memory", false);
dialog->registerWidgetHelp(m_ui.chkEEMemory, tr("EE Memory"), tr("Unchecked"), tr("Log memory access to unknown or unmapped EE memory."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEER5900, "EmuCore/TraceLog", "EE.r5900", false);
dialog->registerWidgetHelp(m_ui.chkEER5900, tr("EE R5900"), tr("Unchecked"), tr("Log R5900 core instructions (excluding COPs). Requires modifying the PCSX2 source and enabling the interpreter."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEECOP0, "EmuCore/TraceLog", "EE.cop0", false);
dialog->registerWidgetHelp(m_ui.chkEECOP0, tr("EE COP0"), tr("Unchecked"), tr("Log COP0 (MMU, CPU status, etc) instructions."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEECOP1, "EmuCore/TraceLog", "EE.cop1", false);
dialog->registerWidgetHelp(m_ui.chkEECOP1, tr("EE COP1"), tr("Unchecked"), tr("Log COP1 (FPU) instructions."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEECOP2, "EmuCore/TraceLog", "EE.cop2", false);
dialog->registerWidgetHelp(m_ui.chkEECOP2, tr("EE COP2"), tr("Unchecked"), tr("Log COP2 (VU0 Macro mode) instructions."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEECache, "EmuCore/TraceLog", "EE.cache", false);
dialog->registerWidgetHelp(m_ui.chkEECache, tr("EE Cache"), tr("Unchecked"), tr("Log EE cache activity."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEEMMIO, "EmuCore/TraceLog", "EE.knownhw", false);
dialog->registerWidgetHelp(m_ui.chkEEMMIO, tr("EE Known MMIO"), tr("Unchecked"), tr("Log known MMIO accesses."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEEUNKNWNMMIO, "EmuCore/TraceLog", "EE.unknownhw", false);
dialog->registerWidgetHelp(m_ui.chkEEUNKNWNMMIO, tr("EE Unknown MMIO"), tr("Unchecked"), tr("Log unknown or unimplemented MMIO accesses."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEEDMARegs, "EmuCore/TraceLog", "EE.dmahw", false);
dialog->registerWidgetHelp(m_ui.chkEEDMARegs, tr("EE DMA Registers"), tr("Unchecked"), tr("Log DMA-related MMIO accesses."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEEIPU, "EmuCore/TraceLog", "EE.ipu", false);
dialog->registerWidgetHelp(m_ui.chkEEIPU, tr("EE IPU"), tr("Unchecked"), tr("Log IPU activity; MMIO, decoding operations, DMA status, etc."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEEGIFTags, "EmuCore/TraceLog", "EE.giftag", false);
dialog->registerWidgetHelp(m_ui.chkEEGIFTags, tr("EE GIF Tags"), tr("Unchecked"), tr("Log GIFtag parsing activity."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEEVIFCodes, "EmuCore/TraceLog", "EE.vifcode", false);
dialog->registerWidgetHelp(m_ui.chkEEVIFCodes, tr("EE VIF Codes"), tr("Unchecked"), tr("Log VIFcode processing; command, tag style, interrupts."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEEMSKPATH3, "EmuCore/TraceLog", "EE.mskpath3", false);
dialog->registerWidgetHelp(m_ui.chkEEMSKPATH3, tr("EE MSKPATH3"), tr("Unchecked"), tr("Log Path3 Masking processing."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEEMFIFO, "EmuCore/TraceLog", "EE.spr", false);
dialog->registerWidgetHelp(m_ui.chkEEMFIFO, tr("EE MFIFO"), tr("Unchecked"), tr("Log Scratchpad MFIFO activity."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEEDMACTRL, "EmuCore/TraceLog", "EE.dmac", false);
dialog->registerWidgetHelp(m_ui.chkEEDMACTRL, tr("EE DMA Controller"), tr("Unchecked"), tr("Log DMA transfer activity. Stalls, bus right arbitration, etc."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEECounters, "EmuCore/TraceLog", "EE.counters", false);
dialog->registerWidgetHelp(m_ui.chkEECounters, tr("EE Counters"), tr("Unchecked"), tr("Log all EE counters events and some counter register activity."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEEVIF, "EmuCore/TraceLog", "EE.vif", false);
dialog->registerWidgetHelp(m_ui.chkEEVIF, tr("EE VIF"), tr("Unchecked"), tr("Log various VIF and VIFcode processing data."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEEGIF, "EmuCore/TraceLog", "EE.gif", false);
dialog->registerWidgetHelp(m_ui.chkEEGIF, tr("EE GIF"), tr("Unchecked"), tr("Log various GIF and GIFtag parsing data."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkIOPBIOS, "EmuCore/TraceLog", "IOP.Bios", false);
dialog->registerWidgetHelp(m_ui.chkIOPBIOS, tr("IOP BIOS"), tr("Unchecked"), tr("Log SYSCALL and IRX activity."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkIOPMemcards, "EmuCore/TraceLog", "IOP.memcards", false);
dialog->registerWidgetHelp(m_ui.chkIOPMemcards, tr("IOP Memcards"), tr("Unchecked"), tr("Log memory card activity. Reads, Writes, erases, etc."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkIOPR3000A, "EmuCore/TraceLog", "IOP.r3000a", false);
dialog->registerWidgetHelp(m_ui.chkIOPR3000A, tr("IOP R3000A"), tr("Unchecked"), tr("Log R3000A core instructions (excluding COPs)."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkIOPCOP2, "EmuCore/TraceLog", "IOP.cop2", false);
dialog->registerWidgetHelp(m_ui.chkIOPCOP2, tr("IOP COP2"), tr("Unchecked"), tr("Log IOP GPU co-processor instructions."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkIOPMMIO, "EmuCore/TraceLog", "IOP.knownhw", false);
dialog->registerWidgetHelp(m_ui.chkIOPMMIO, tr("IOP Known MMIO"), tr("Unchecked"), tr("Log known MMIO accesses."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkIOPUNKNWNMMIO, "EmuCore/TraceLog", "IOP.unknownhw", false);
dialog->registerWidgetHelp(m_ui.chkIOPUNKNWNMMIO, tr("IOP Unknown MMIO"), tr("Unchecked"), tr("Log unknown or unimplemented MMIO accesses."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkIOPDMARegs, "EmuCore/TraceLog", "IOP.dmahw", false);
dialog->registerWidgetHelp(m_ui.chkIOPDMARegs, tr("IOP DMA Registers"), tr("Unchecked"), tr("Log DMA-related MMIO accesses."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkIOPPad, "EmuCore/TraceLog", "IOP.pad", false);
dialog->registerWidgetHelp(m_ui.chkIOPPad, tr("IOP PAD"), tr("Unchecked"), tr("Log PAD activity."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkIOPDMACTRL, "EmuCore/TraceLog", "IOP.dmac", false);
dialog->registerWidgetHelp(m_ui.chkIOPDMACTRL, tr("IOP DMA Controller"), tr("Unchecked"), tr("Log DMA transfer activity. Stalls, bus right arbitration, etc."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkIOPCounters, "EmuCore/TraceLog", "IOP.counters", false);
dialog->registerWidgetHelp(m_ui.chkIOPCounters, tr("IOP Counters"), tr("Unchecked"), tr("Log all IOP counters events and some counter register activity."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkIOPCDVD, "EmuCore/TraceLog", "IOP.cdvd", false);
dialog->registerWidgetHelp(m_ui.chkIOPCDVD, tr("IOP CDVD"), tr("Unchecked"), tr("Log CDVD hardware activity."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkIOPMDEC, "EmuCore/TraceLog", "IOP.mdec", false);
dialog->registerWidgetHelp(m_ui.chkIOPMDEC, tr("IOP MDEC"), tr("Unchecked"), tr("Log Motion (FMV) Decoder hardware unit activity."));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.chkEESIF, "EmuCore/TraceLog", "MISC.sif", false);
dialog->registerWidgetHelp(m_ui.chkEESIF, tr("EE SIF"), tr("Unchecked"), tr("Log SIF (EE <-> IOP) activity."));
connectCheckStateChanged(m_ui.chkEnable, this, &DebugSettingsWidget::onLoggingEnableChanged);
onLoggingEnableChanged();
#else
m_ui.debugTabs->removeTab(m_ui.debugTabs->indexOf(m_ui.traceLogTabWidget));
#endif
}
DebugSettingsWidget::~DebugSettingsWidget() = default;
void DebugSettingsWidget::onDrawDumpingChanged()
{
const bool enabled = m_dialog->getEffectiveBoolValue("EmuCore/GS", "DumpGSData", false);
m_ui.saveRT->setEnabled(enabled);
m_ui.saveFrame->setEnabled(enabled);
m_ui.saveTexture->setEnabled(enabled);
m_ui.saveDepth->setEnabled(enabled);
m_ui.saveAlpha->setEnabled(enabled);
m_ui.saveInfo->setEnabled(enabled);
m_ui.saveDrawStart->setEnabled(enabled);
m_ui.saveDrawCount->setEnabled(enabled);
m_ui.saveFrameStart->setEnabled(enabled);
m_ui.saveFrameCount->setEnabled(enabled);
m_ui.hwDumpDirectory->setEnabled(enabled);
m_ui.hwDumpBrowse->setEnabled(enabled);
m_ui.hwDumpOpen->setEnabled(enabled);
m_ui.swDumpDirectory->setEnabled(enabled);
m_ui.swDumpBrowse->setEnabled(enabled);
m_ui.swDumpOpen->setEnabled(enabled);
}
#ifdef PCSX2_DEVBUILD
void DebugSettingsWidget::onLoggingEnableChanged()
{
const bool enabled = m_dialog->getEffectiveBoolValue("EmuCore/TraceLog", "Enabled", false);
m_ui.chkEEBIOS->setEnabled(enabled);
m_ui.chkEEMemory->setEnabled(enabled);
m_ui.chkEER5900->setEnabled(enabled);
m_ui.chkEECOP0->setEnabled(enabled);
m_ui.chkEECOP1->setEnabled(enabled);
m_ui.chkEECOP2->setEnabled(enabled);
m_ui.chkEECache->setEnabled(enabled);
m_ui.chkEEMMIO->setEnabled(enabled);
m_ui.chkEEUNKNWNMMIO->setEnabled(enabled);
m_ui.chkEEDMARegs->setEnabled(enabled);
m_ui.chkEEIPU->setEnabled(enabled);
m_ui.chkEEGIFTags->setEnabled(enabled);
m_ui.chkEEVIFCodes->setEnabled(enabled);
m_ui.chkEEMSKPATH3->setEnabled(enabled);
m_ui.chkEEMFIFO->setEnabled(enabled);
m_ui.chkEEDMACTRL->setEnabled(enabled);
m_ui.chkEECounters->setEnabled(enabled);
m_ui.chkEEVIF->setEnabled(enabled);
m_ui.chkEEGIF->setEnabled(enabled);
m_ui.chkEESIF->setEnabled(enabled);
m_ui.chkIOPBIOS->setEnabled(enabled);
m_ui.chkIOPMemcards->setEnabled(enabled);
m_ui.chkIOPR3000A->setEnabled(enabled);
m_ui.chkIOPCOP2->setEnabled(enabled);
m_ui.chkIOPMMIO->setEnabled(enabled);
m_ui.chkIOPUNKNWNMMIO->setEnabled(enabled);
m_ui.chkIOPDMARegs->setEnabled(enabled);
m_ui.chkIOPMemcards->setEnabled(enabled);
m_ui.chkIOPPad->setEnabled(enabled);
m_ui.chkIOPDMACTRL->setEnabled(enabled);
m_ui.chkIOPCounters->setEnabled(enabled);
m_ui.chkIOPCDVD->setEnabled(enabled);
m_ui.chkIOPMDEC->setEnabled(enabled);
g_emu_thread->applySettings();
}
#endif

View File

@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QWidget>
#include "ui_DebugSettingsWidget.h"
class SettingsWindow;
class DebugUserInterfaceSettingsWidget;
class DebugAnalysisSettingsWidget;
class DebugSettingsWidget : public QWidget
{
Q_OBJECT
public:
DebugSettingsWidget(SettingsWindow* dialog, QWidget* parent);
~DebugSettingsWidget();
private Q_SLOTS:
void onDrawDumpingChanged();
#ifdef PCSX2_DEVBUILD
void onLoggingEnableChanged();
#endif
private:
SettingsWindow* m_dialog;
DebugUserInterfaceSettingsWidget* m_user_interface_settings;
DebugAnalysisSettingsWidget* m_analysis_settings;
Ui::DebugSettingsWidget m_ui;
};

View File

@@ -0,0 +1,642 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DebugSettingsWidget</class>
<widget class="QWidget" name="DebugSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>647</width>
<height>501</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTabWidget" name="debugTabs">
<property name="currentIndex">
<number>0</number>
</property>
<property name="documentMode">
<bool>true</bool>
</property>
<widget class="QWidget" name="userInterfaceTabWidget">
<attribute name="title">
<string>User Interface</string>
</attribute>
</widget>
<widget class="QWidget" name="analysisTabWidget">
<attribute name="title">
<string>Analysis</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="analysisScrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="analysisScrollAreaContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>645</width>
<height>469</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="analysisLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>These settings control what and when analysis passes should be performed on the program running in the virtual machine so that the resultant information can be shown in the debugger.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="analysisGroupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Analysis</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QFormLayout" name="analysisForm">
<item row="0" column="0">
<widget class="QLabel" name="analysisConditionLabel">
<property name="text">
<string>Automatically Analyze Program:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="analysisCondition">
<item>
<property name="text">
<string>Always</string>
</property>
</item>
<item>
<property name="text">
<string>If Debugger Is Open</string>
</property>
</item>
<item>
<property name="text">
<string>Never</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="generateSymbolsForIRXExportTables">
<property name="text">
<string>Generate Symbols For IRX Exports</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="analysisSettings" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="gsTabWidget">
<attribute name="title">
<string>GS</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="drawDumpingGroupBox">
<property name="title">
<string>Draw Dumping</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QCheckBox" name="dumpGSData">
<property name="text">
<string>Dump GS Draws</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="saveRT">
<property name="text">
<string>Save RT</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="saveFrame">
<property name="text">
<string>Save Frame</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="saveTexture">
<property name="text">
<string>Save Texture</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="saveDepth">
<property name="text">
<string>Save Depth</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="saveAlpha">
<property name="text">
<string>Save Alpha</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="saveInfo">
<property name="text">
<string>Save Info</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_1">
<property name="text">
<string>Save Draw Start:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="saveDrawStart">
<property name="maximum">
<number>99999999</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Save Draw Count:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="saveDrawCount">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>99999999</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Save Frame Start:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="saveFrameStart">
<property name="maximum">
<number>99999999</number>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Save Frame Count:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="saveFrameCount">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>99999999</number>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Hardware Dump Directory:</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Software Dump Directory:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0,0">
<item>
<widget class="QLineEdit" name="hwDumpDirectory"/>
</item>
<item>
<widget class="QPushButton" name="hwDumpBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="hwDumpOpen">
<property name="text">
<string>Open...</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0,0">
<item>
<widget class="QLineEdit" name="swDumpDirectory"/>
</item>
<item>
<widget class="QPushButton" name="swDumpBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="swDumpOpen">
<property name="text">
<string>Open...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="traceLogTabWidget">
<attribute name="title">
<string>Trace Logging</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<widget class="QCheckBox" name="chkEnable">
<property name="text">
<string>Enable</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="traceLogHorizontalLayout">
<item>
<widget class="QGroupBox" name="grpEELogging">
<property name="title">
<string>EE</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QGridLayout" name="grpEELoggingGrid">
<item row="4" column="1">
<widget class="QCheckBox" name="chkEEDMACTRL">
<property name="text">
<string>DMA Control</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="chkEEMFIFO">
<property name="text">
<string>SPR / MFIFO</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QCheckBox" name="chkEEVIF">
<property name="text">
<string>VIF</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="chkEECOP1">
<property name="text">
<string>COP1 (FPU)</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="chkEEMSKPATH3">
<property name="text">
<string>MSKPATH3</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="chkEECache">
<property name="text">
<string>Cache</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QCheckBox" name="chkEEGIF">
<property name="text">
<string>GIF</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="chkEER5900">
<property name="text">
<string>R5900</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="chkEECOP0">
<property name="text">
<string>COP0</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="chkEEMMIO">
<property name="text">
<string>HW Regs (MMIO)</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="chkEECounters">
<property name="text">
<string>Counters</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QCheckBox" name="chkEESIF">
<property name="text">
<string>SIF</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="chkEECOP2">
<property name="text">
<string>COP2 (VU0 Macro)</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QCheckBox" name="chkEEVIFCodes">
<property name="text">
<string>VIFCodes</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="chkEEMemory">
<property name="text">
<string>Memory</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="chkEEUNKNWNMMIO">
<property name="text">
<string>Unknown MMIO</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QCheckBox" name="chkEEIPU">
<property name="text">
<string>IPU</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QCheckBox" name="chkEEBIOS">
<property name="text">
<string>BIOS</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="chkEEDMARegs">
<property name="text">
<string>DMA Registers</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="chkEEGIFTags">
<property name="text">
<string>GIFTags</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="eeLoggingSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="grpIOPLogging">
<property name="title">
<string>IOP</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<layout class="QGridLayout" name="grpIOPLoggingGrid">
<item row="4" column="0">
<widget class="QCheckBox" name="chkIOPCounters">
<property name="text">
<string>Counters</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="chkIOPUNKNWNMMIO">
<property name="text">
<string>Unknown MMIO</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="chkIOPMMIO">
<property name="text">
<string>HW Regs (MMIO)</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="chkIOPCDVD">
<property name="text">
<string>CDVD</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="chkIOPR3000A">
<property name="text">
<string>R3000A</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="chkIOPMemcards">
<property name="text">
<string>Memcards</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="chkIOPDMARegs">
<property name="text">
<string>DMA Registers</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="chkIOPPad">
<property name="text">
<string>Pad</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="chkIOPBIOS">
<property name="text">
<string>BIOS</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="chkIOPMDEC">
<property name="text">
<string>MDEC</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="chkIOPDMACTRL">
<property name="text">
<string>DMA Control</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="chkIOPCOP2">
<property name="text">
<string>COP2 (GPU)</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="iopLoggingSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "DebugUserInterfaceSettingsWidget.h"
#include "SettingWidgetBinder.h"
#include "Debugger/DebuggerWindow.h"
static const char* s_drop_indicators[] = {
QT_TRANSLATE_NOOP("DebugUserInterfaceSettingsWidget", "Classic"),
QT_TRANSLATE_NOOP("DebugUserInterfaceSettingsWidget", "Segmented"),
QT_TRANSLATE_NOOP("DebugUserInterfaceSettingsWidget", "Minimalistic"),
nullptr,
};
DebugUserInterfaceSettingsWidget::DebugUserInterfaceSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: QWidget(parent)
{
SettingsInterface* sif = dialog->getSettingsInterface();
m_ui.setupUi(this);
SettingWidgetBinder::BindWidgetToIntSetting(
sif, m_ui.refreshInterval, "Debugger/UserInterface", "RefreshInterval", 1000);
connect(m_ui.refreshInterval, &QSpinBox::valueChanged, this, []() {
if (g_debugger_window)
g_debugger_window->updateFromSettings();
});
dialog->registerWidgetHelp(
m_ui.refreshInterval, tr("Refresh Interval"), tr("1000ms"),
tr("The amount of time to wait between subsequent attempts to update the user interface to reflect the state "
"of the virtual machine."));
SettingWidgetBinder::BindWidgetToBoolSetting(
sif, m_ui.showOnStartup, "Debugger/UserInterface", "ShowOnStartup", false);
dialog->registerWidgetHelp(
m_ui.showOnStartup, tr("Show On Startup"), tr("Unchecked"),
tr("Open the debugger window automatically when PCSX2 starts."));
SettingWidgetBinder::BindWidgetToBoolSetting(
sif, m_ui.saveWindowGeometry, "Debugger/UserInterface", "SaveWindowGeometry", true);
dialog->registerWidgetHelp(
m_ui.saveWindowGeometry, tr("Save Window Geometry"), tr("Checked"),
tr("Save the position and size of the debugger window when it is closed so that it can be restored later."));
SettingWidgetBinder::BindWidgetToEnumSetting(
sif,
m_ui.dropIndicator,
"Debugger/UserInterface",
"DropIndicatorStyle",
s_drop_indicators,
s_drop_indicators,
s_drop_indicators[0],
"DebugUserInterfaceSettingsWidget");
dialog->registerWidgetHelp(
m_ui.dropIndicator, tr("Drop Indicator Style"), tr("Classic"),
tr("Choose how the drop indicators that appear when you drag dock windows in the debugger are styled. "
"You will have to restart the debugger for this option to take effect."));
}

View File

@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "ui_DebugUserInterfaceSettingsWidget.h"
class SettingsWindow;
class DebugUserInterfaceSettingsWidget : public QWidget
{
Q_OBJECT
public:
DebugUserInterfaceSettingsWidget(SettingsWindow* dialog, QWidget* parent = nullptr);
private:
Ui::DebugUserInterfaceSettingsWidget m_ui;
};

View File

@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DebugUserInterfaceSettingsWidget</class>
<widget class="QWidget" name="DebugUserInterfaceSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>750</height>
</rect>
</property>
<property name="windowTitle">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="windowGroup">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Debugger Window</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="0">
<widget class="QCheckBox" name="showOnStartup">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Show On Startup</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="saveWindowGeometry">
<property name="text">
<string>Save Window Geometry</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="refreshIntervalLabel">
<property name="text">
<string>Refresh Interval:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="refreshInterval">
<property name="suffix">
<string>ms</string>
</property>
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>100000</number>
</property>
<property name="value">
<number>1000</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="dockingGroup">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Docking</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="dropIndicatorLabel">
<property name="text">
<string>Drop Indicator Style:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="dropIndicator"/>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,321 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QMessageBox>
#include <limits>
#include "pcsx2/Host.h"
#include "EmulationSettingsWidget.h"
#include "QtUtils.h"
#include "SettingWidgetBinder.h"
#include "SettingsWindow.h"
static constexpr int MINIMUM_EE_CYCLE_RATE = -3;
static constexpr int MAXIMUM_EE_CYCLE_RATE = 3;
static constexpr int DEFAULT_EE_CYCLE_RATE = 0;
static constexpr int DEFAULT_EE_CYCLE_SKIP = 0;
static constexpr u32 DEFAULT_FRAME_LATENCY = 2;
EmulationSettingsWidget::EmulationSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: QWidget(parent)
, m_dialog(dialog)
{
SettingsInterface* sif = dialog->getSettingsInterface();
m_ui.setupUi(this);
initializeSpeedCombo(m_ui.normalSpeed, "Framerate", "NominalScalar", 1.0f);
initializeSpeedCombo(m_ui.fastForwardSpeed, "Framerate", "TurboScalar", 2.0f);
initializeSpeedCombo(m_ui.slowMotionSpeed, "Framerate", "SlomoScalar", 0.5f);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.maxFrameLatency, "EmuCore/GS", "VsyncQueueSize", DEFAULT_FRAME_LATENCY);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.vsync, "EmuCore/GS", "VsyncEnable", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.syncToHostRefreshRate, "EmuCore/GS", "SyncToHostRefreshRate", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.useVSyncForTiming, "EmuCore/GS", "UseVSyncForTiming", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.skipPresentingDuplicateFrames, "EmuCore/GS", "SkipDuplicateFrames", false);
connectCheckStateChanged(m_ui.optimalFramePacing, this, &EmulationSettingsWidget::onOptimalFramePacingChanged);
connectCheckStateChanged(m_ui.vsync, this, &EmulationSettingsWidget::updateUseVSyncForTimingEnabled);
connectCheckStateChanged(m_ui.syncToHostRefreshRate, this, &EmulationSettingsWidget::updateUseVSyncForTimingEnabled);
m_ui.optimalFramePacing->setTristate(dialog->isPerGameSettings());
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.eeCycleSkipping, "EmuCore/Speedhacks", "EECycleSkip", DEFAULT_EE_CYCLE_SKIP);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.MTVU, "EmuCore/Speedhacks", "vuThread", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.threadPinning, "EmuCore", "EnableThreadPinning", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.fastCDVD, "EmuCore/Speedhacks", "fastCDVD", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.precacheCDVD, "EmuCore", "CdvdPrecache", false);
if (m_dialog->isPerGameSettings())
{
SettingWidgetBinder::BindWidgetToDateTimeSetting(sif, m_ui.rtcDateTime, "EmuCore");
m_ui.rtcDateTime->setDateRange(QDate(2000, 1, 1), QDate(2099, 12, 31));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.manuallySetRealTimeClock, "EmuCore", "ManuallySetRealTimeClock", false);
connectCheckStateChanged(m_ui.manuallySetRealTimeClock, this, &EmulationSettingsWidget::onManuallySetRealTimeClockChanged);
EmulationSettingsWidget::onManuallySetRealTimeClockChanged();
m_ui.eeCycleRate->insertItem(
0, tr("Use Global Setting [%1]")
.arg(m_ui.eeCycleRate->itemText(
std::clamp(Host::GetBaseIntSettingValue("EmuCore/Speedhacks", "EECycleRate", DEFAULT_EE_CYCLE_RATE) - MINIMUM_EE_CYCLE_RATE,
0, MAXIMUM_EE_CYCLE_RATE - MINIMUM_EE_CYCLE_RATE))));
// Disable cheats, use the cheats panel instead (move fastcvd up in its spot).
const int count = m_ui.systemSettingsLayout->count();
for (int i = 0; i < count; i++)
{
QLayoutItem* item = m_ui.systemSettingsLayout->itemAt(i);
if (item && item->widget() == m_ui.cheats)
{
int row, col, rowSpan, colSpan;
m_ui.systemSettingsLayout->getItemPosition(i, &row, &col, &rowSpan, &colSpan);
delete m_ui.systemSettingsLayout->takeAt(i);
m_ui.systemSettingsLayout->removeWidget(m_ui.fastCDVD);
m_ui.systemSettingsLayout->addWidget(m_ui.fastCDVD, row, col);
delete m_ui.cheats;
m_ui.cheats = nullptr;
break;
}
}
}
else
{
m_ui.rtcGroup->hide();
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.cheats, "EmuCore", "EnableCheats", false);
// Allow for FastCDVD for per-game settings only
m_ui.systemSettingsLayout->removeWidget(m_ui.fastCDVD);
m_ui.fastCDVD->deleteLater();
}
const std::optional<int> cycle_rate =
m_dialog->getIntValue("EmuCore/Speedhacks", "EECycleRate", sif ? std::nullopt : std::optional<int>(DEFAULT_EE_CYCLE_RATE));
m_ui.eeCycleRate->setCurrentIndex(cycle_rate.has_value() ? (std::clamp(cycle_rate.value(), MINIMUM_EE_CYCLE_RATE, MAXIMUM_EE_CYCLE_RATE) +
(0 - MINIMUM_EE_CYCLE_RATE) + static_cast<int>(m_dialog->isPerGameSettings())) :
0);
connect(m_ui.eeCycleRate, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
std::optional<int> value;
if (!m_dialog->isPerGameSettings() || index > 0)
value = MINIMUM_EE_CYCLE_RATE + index - static_cast<int>(m_dialog->isPerGameSettings());
m_dialog->setIntSettingValue("EmuCore/Speedhacks", "EECycleRate", value);
});
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.hostFilesystem, "EmuCore", "HostFs", false);
dialog->registerWidgetHelp(m_ui.normalSpeed, tr("Normal Speed"), tr("100%"),
tr("Sets the target emulation speed. It is not guaranteed that this speed will be reached, "
"and if not, the emulator will run as fast as it can manage."));
//: The "User Preference" string will appear after the text "Recommended Value:"
dialog->registerWidgetHelp(m_ui.fastForwardSpeed, tr("Fast-Forward Speed"), tr("User Preference"),
tr("Sets the fast-forward speed. This speed will be used when the fast-forward hotkey is pressed/toggled."));
//: The "User Preference" string will appear after the text "Recommended Value:"
dialog->registerWidgetHelp(m_ui.slowMotionSpeed, tr("Slow-Motion Speed"), tr("User Preference"),
tr("Sets the slow-motion speed. This speed will be used when the slow-motion hotkey is pressed/toggled."));
dialog->registerWidgetHelp(m_ui.eeCycleRate, tr("EE Cycle Rate"), tr("100% (Normal Speed)"),
tr("Higher values may increase internal framerate in games, but will increase CPU requirements substantially. "
"Lower values will reduce the CPU load allowing lightweight games to run full speed on weaker CPUs."));
dialog->registerWidgetHelp(m_ui.eeCycleSkipping, tr("EE Cycle Skip"), tr("Disabled"),
tr("Makes the emulated Emotion Engine skip cycles. "
//: SOTC = Shadow of the Colossus. A game's title, should not be translated unless an official translation exists.
"Helps a small subset of games like SOTC. Most of the time it's harmful to performance."));
dialog->registerWidgetHelp(m_ui.threadPinning, tr("Enable Thread Pinning"), tr("Unchecked"),
tr("Sets the priority for specific threads in a specific order ignoring the system scheduler. "
//: P-Core = Performance Core, E-Core = Efficiency Core. See if Intel has official translations for these terms.
"May help CPUs with big (P) and little (E) cores (e.g. Intel 12th or newer generation CPUs from Intel or other vendors such as AMD)."));
dialog->registerWidgetHelp(m_ui.MTVU, tr("Enable Multithreaded VU1 (MTVU1)"), tr("Checked"),
tr("Generally a speedup on CPUs with 4 or more cores. "
"Safe for most games, but a few are incompatible and may hang."));
dialog->registerWidgetHelp(m_ui.fastCDVD, tr("Enable Fast CDVD"), tr("Unchecked"),
tr("Fast disc access, less loading times. Check HDLoader compatibility lists for known games that have issues with this."));
dialog->registerWidgetHelp(m_ui.precacheCDVD, tr("Enable CDVD Precaching"), tr("Unchecked"),
tr("Loads the disc image into RAM before starting the virtual machine. Can reduce stutter on systems with hard drives that "
"have long wake times, but significantly increases boot times."));
dialog->registerWidgetHelp(m_ui.cheats, tr("Enable Cheats"), tr("Unchecked"),
tr("Automatically loads and applies cheats on game start."));
dialog->registerWidgetHelp(m_ui.hostFilesystem, tr("Enable Host Filesystem"), tr("Unchecked"),
tr("Allows games and homebrew to access files / folders directly on the host computer."));
dialog->registerWidgetHelp(m_ui.optimalFramePacing, tr("Optimal Frame Pacing"), tr("Unchecked"),
tr("Sets the VSync queue size to 0, making every frame be completed and presented by the GS before input is polled and the next frame begins. "
"Using this setting can reduce input lag at the cost of measurably higher CPU and GPU requirements."));
dialog->registerWidgetHelp(m_ui.maxFrameLatency, tr("Maximum Frame Latency"), tr("2 Frames"),
tr("Sets the maximum number of frames that can be queued up to the GS, before the CPU thread will wait for one of them to complete before continuing. "
"Higher values can assist with smoothing out irregular frame times, but add additional input lag."));
dialog->registerWidgetHelp(m_ui.syncToHostRefreshRate, tr("Sync to Host Refresh Rate"), tr("Unchecked"),
tr("Speeds up emulation so that the guest refresh rate matches the host. This results in the smoothest animations possible, at the cost of "
"potentially increasing the emulation speed by less than 1%. Sync to Host Refresh Rate will not take effect if "
"the console's refresh rate is too far from the host's refresh rate. Users with variable refresh rate displays "
"should disable this option."));
dialog->registerWidgetHelp(m_ui.vsync, tr("Vertical Sync (VSync)"), tr("Unchecked"),
tr("Enable this option to match PCSX2's refresh rate with your current monitor or screen. VSync is automatically disabled when "
"it is not possible (eg. running at non-100% speed)."));
dialog->registerWidgetHelp(m_ui.useVSyncForTiming, tr("Use Host VSync Timing"), tr("Unchecked"),
tr("When synchronizing with the host refresh rate, this option disable's PCSX2's internal frame timing, and uses the host instead. "
"Can result in smoother frame pacing, <strong>but at the cost of increased input latency</strong>."));
dialog->registerWidgetHelp(m_ui.skipPresentingDuplicateFrames, tr("Skip Presenting Duplicate Frames"), tr("Checked"),
tr("Detects when idle frames are being presented in 25/30fps games, and skips presenting those frames. The frame is still "
"rendered, it just means the GPU has more time to complete it (this is NOT frame skipping). Can smooth out frame time "
"fluctuations when the CPU/GPU are near maximum utilization, but makes frame pacing more inconsistent and can increase "
"input lag. Helps when using frame generation on 25/30fps games."));
dialog->registerWidgetHelp(m_ui.manuallySetRealTimeClock, tr("Manually Set Real-Time Clock"), tr("Unchecked"),
tr("Manually set a real-time clock to use for the virtual PlayStation 2 instead of using your OS' system clock."));
dialog->registerWidgetHelp(m_ui.rtcDateTime, tr("Real-Time Clock"), tr("Current date and time"),
tr("Real-time clock (RTC) used by the virtual PlayStation 2. Date format is the same as the one used by your OS. "
"This time is only applied upon booting the PS2; changing it while in-game will have no effect. "
"NOTE: This assumes you have your PS2 set to the default timezone of GMT+0 and default DST of Summer Time. "
"Some games require an RTC date/time set after their release date."));
updateOptimalFramePacing();
updateUseVSyncForTimingEnabled();
}
EmulationSettingsWidget::~EmulationSettingsWidget() = default;
void EmulationSettingsWidget::initializeSpeedCombo(QComboBox* cb, const char* section, const char* key, float default_value)
{
float value = Host::GetBaseFloatSettingValue(section, key, default_value);
if (m_dialog->isPerGameSettings())
{
cb->addItem(tr("Use Global Setting [%1%]").arg(value * 100.0f, 0, 'f', 0));
if (!m_dialog->getSettingsInterface()->GetFloatValue(section, key, &value))
{
// set to something without data
value = -1.0f;
cb->setCurrentIndex(0);
}
}
static const int speeds[] = {2, 10, 25, 50, 75, 90, 100, 110, 120, 150, 175, 200, 300, 400, 500, 1000};
for (const int speed : speeds)
{
cb->addItem(tr("%1% [%2 FPS (NTSC) / %3 FPS (PAL)]")
.arg(speed)
.arg((60 * speed) / 100)
.arg((50 * speed) / 100),
QVariant(static_cast<float>(speed) / 100.0f));
}
//: Every case that uses this particular string seems to refer to speeds: Normal Speed/Fast Forward Speed/Slow Motion Speed.
cb->addItem(tr("Unlimited"), QVariant(0.0f));
const int custom_index = cb->count();
//: Every case that uses this particular string seems to refer to speeds: Normal Speed/Fast Forward Speed/Slow Motion Speed.
cb->addItem(tr("Custom"));
if (const int index = cb->findData(QVariant(value)); index >= 0)
{
cb->setCurrentIndex(index);
}
else if (value > 0.0f)
{
cb->setItemText(custom_index, tr("Custom [%1% / %2 FPS (NTSC) / %3 FPS (PAL)]")
.arg(value * 100)
.arg(60 * value)
.arg(50 * value));
cb->setCurrentIndex(custom_index);
}
connect(cb, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this, cb, section, key](int index) { handleSpeedComboChange(cb, section, key); });
}
void EmulationSettingsWidget::handleSpeedComboChange(QComboBox* cb, const char* section, const char* key)
{
const int custom_index = cb->count() - 1;
const int current_index = cb->currentIndex();
std::optional<float> new_value;
if (current_index == custom_index)
{
bool ok = false;
const double custom_value = QInputDialog::getDouble(
QtUtils::GetRootWidget(this), tr("Custom Speed"), tr("Enter Custom Speed"), cb->currentData().toFloat(), 0.0f, 5000.0f, 1, &ok);
if (!ok)
{
// we need to set back to the old value
float value = m_dialog->getEffectiveFloatValue(section, key, 1.0f);
QSignalBlocker sb(cb);
if (m_dialog->isPerGameSettings() && !m_dialog->getSettingsInterface()->GetFloatValue(section, key, &value))
cb->setCurrentIndex(0);
else if (const int index = cb->findData(QVariant(value)); index >= 0)
cb->setCurrentIndex(index);
return;
}
cb->setItemText(custom_index, tr("Custom [%1% / %2 FPS (NTSC) / %3 FPS (PAL)]")
.arg(custom_value)
.arg((60 * custom_value) / 100)
.arg((50 * custom_value) / 100));
new_value = static_cast<float>(custom_value / 100.0);
}
else if (current_index > 0 || !m_dialog->isPerGameSettings())
{
new_value = cb->currentData().toFloat();
}
m_dialog->setFloatSettingValue(section, key, new_value);
}
void EmulationSettingsWidget::onOptimalFramePacingChanged()
{
const QSignalBlocker sb(m_ui.maxFrameLatency);
std::optional<int> value;
bool optimal = false;
if (m_ui.optimalFramePacing->checkState() != Qt::PartiallyChecked)
{
optimal = m_ui.optimalFramePacing->isChecked();
value = optimal ? 0 : DEFAULT_FRAME_LATENCY;
}
else
{
value = m_dialog->getEffectiveIntValue("EmuCore/GS", "VsyncQueueSize", DEFAULT_FRAME_LATENCY);
optimal = (value == 0);
}
m_ui.maxFrameLatency->setMinimum(optimal ? 0 : 1);
m_ui.maxFrameLatency->setValue(optimal ? 0 : DEFAULT_FRAME_LATENCY);
m_ui.maxFrameLatency->setEnabled(!m_dialog->isPerGameSettings() && !m_ui.optimalFramePacing->isChecked());
m_dialog->setIntSettingValue("EmuCore/GS", "VsyncQueueSize", value);
}
void EmulationSettingsWidget::updateOptimalFramePacing()
{
const QSignalBlocker sb(m_ui.optimalFramePacing);
const QSignalBlocker sb2(m_ui.maxFrameLatency);
int value = m_dialog->getEffectiveIntValue("EmuCore/GS", "VsyncQueueSize", DEFAULT_FRAME_LATENCY);
bool optimal = (value == 0);
if (m_dialog->isPerGameSettings() && !m_dialog->getSettingsInterface()->GetIntValue("EmuCore/GS", "VsyncQueueSize", &value))
{
m_ui.optimalFramePacing->setCheckState(Qt::PartiallyChecked);
m_ui.maxFrameLatency->setEnabled(false);
}
else
{
m_ui.optimalFramePacing->setChecked(optimal);
m_ui.maxFrameLatency->setEnabled(!optimal);
}
m_ui.maxFrameLatency->setMinimum(optimal ? 0 : 1);
m_ui.maxFrameLatency->setValue(optimal ? 0 : value);
}
void EmulationSettingsWidget::updateUseVSyncForTimingEnabled()
{
const bool vsync = m_dialog->getEffectiveBoolValue("EmuCore/GS", "VsyncEnable", false);
const bool sync_to_host_refresh = m_dialog->getEffectiveBoolValue("EmuCore/GS", "SyncToHostRefreshRate", false);
m_ui.useVSyncForTiming->setEnabled(vsync && sync_to_host_refresh);
}
void EmulationSettingsWidget::onManuallySetRealTimeClockChanged()
{
const bool enabled = m_dialog->getEffectiveBoolValue("EmuCore", "ManuallySetRealTimeClock", false);
m_ui.rtcDateTime->setEnabled(enabled);
}

View File

@@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QWidget>
#include "ui_EmulationSettingsWidget.h"
class SettingsWindow;
class EmulationSettingsWidget : public QWidget
{
Q_OBJECT
public:
EmulationSettingsWidget(SettingsWindow* dialog, QWidget* parent);
~EmulationSettingsWidget();
private Q_SLOTS:
void onOptimalFramePacingChanged();
private:
void initializeSpeedCombo(QComboBox* cb, const char* section, const char* key, float default_value);
void handleSpeedComboChange(QComboBox* cb, const char* section, const char* key);
void updateOptimalFramePacing();
void updateUseVSyncForTimingEnabled();
void onManuallySetRealTimeClockChanged();
SettingsWindow* m_dialog;
Ui::EmulationSettingsWidget m_ui;
};

View File

@@ -0,0 +1,303 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EmulationSettingsWidget</class>
<widget class="QWidget" name="EmulationSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>672</width>
<height>500</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="speedGroup">
<property name="title">
<string>Speed Control</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Slow-Motion Speed:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="fastForwardSpeed"/>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="slowMotionSpeed"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Normal Speed:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Fast-Forward Speed:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="normalSpeed"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="systemSettingsGroup">
<property name="title">
<string>System Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="1">
<widget class="QComboBox" name="eeCycleRate">
<item>
<property name="text">
<string>50% (Underclock)</string>
</property>
</item>
<item>
<property name="text">
<string>60% (Underclock)</string>
</property>
</item>
<item>
<property name="text">
<string>75% (Underclock)</string>
</property>
</item>
<item>
<property name="text">
<string>100% (Normal Speed)</string>
</property>
</item>
<item>
<property name="text">
<string>130% (Overclock)</string>
</property>
</item>
<item>
<property name="text">
<string>180% (Overclock)</string>
</property>
</item>
<item>
<property name="text">
<string>300% (Overclock)</string>
</property>
</item>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="eeCycleSkipping">
<item>
<property name="text">
<string>Disabled</string>
</property>
</item>
<item>
<property name="text">
<string>Mild Underclock</string>
</property>
</item>
<item>
<property name="text">
<string>Moderate Underclock</string>
</property>
</item>
<item>
<property name="text">
<string>Maximum Underclock</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0" colspan="2">
<layout class="QGridLayout" name="systemSettingsLayout">
<item row="1" column="0">
<widget class="QCheckBox" name="cheats">
<property name="text">
<string>Enable Cheats</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="MTVU">
<property name="text">
<string>Enable Multithreaded VU1 (MTVU)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="hostFilesystem">
<property name="text">
<string>Enable Host Filesystem</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="fastCDVD">
<property name="text">
<string>Enable Fast CDVD</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="precacheCDVD">
<property name="text">
<string>Enable CDVD Precaching</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="threadPinning">
<property name="text">
<string>Enable Thread Pinning</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>EE Cycle Rate:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>EE Cycle Skipping:</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="pacingGroup">
<property name="title">
<string>Frame Pacing / Latency Control</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="1">
<widget class="QSpinBox" name="maxFrameLatency">
<property name="suffix">
<string extracomment="This string will appear next to the amount of frames selected, in a dropdown box."> frames</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>5</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Maximum Frame Latency:</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<layout class="QGridLayout" name="basicCheckboxGridLayout">
<item row="0" column="1">
<widget class="QCheckBox" name="syncToHostRefreshRate">
<property name="text">
<string>Sync to Host Refresh Rate</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="vsync">
<property name="text">
<string>Vertical Sync (VSync)</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="optimalFramePacing">
<property name="text">
<string>Optimal Frame Pacing</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="useVSyncForTiming">
<property name="text">
<string>Use Host VSync Timing</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="skipPresentingDuplicateFrames">
<property name="text">
<string>Skip Presenting Duplicate Frames</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QGroupBox" name="rtcGroup">
<property name="title">
<string>Real-Time Clock</string>
</property>
<layout class="QGridLayout" name="gridLayoutRTC">
<item row="0" column="0">
<widget class="QCheckBox" name="manuallySetRealTimeClock">
<property name="text">
<string>Manually Set Real-Time Clock</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDateTimeEdit" name="rtcDateTime"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include <QtWidgets/QMessageBox>
#include <algorithm>
#include "FolderSettingsWidget.h"
#include "SettingWidgetBinder.h"
#include "SettingsWindow.h"
FolderSettingsWidget::FolderSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: QWidget(parent)
{
SettingsInterface* sif = dialog->getSettingsInterface();
m_ui.setupUi(this);
SettingWidgetBinder::BindWidgetToFolderSetting(sif, m_ui.cache, m_ui.cacheBrowse, m_ui.cacheOpen, m_ui.cacheReset, "Folders", "Cache", Path::Combine(EmuFolders::DataRoot, "cache"));
SettingWidgetBinder::BindWidgetToFolderSetting(sif, m_ui.cheats, m_ui.cheatsBrowse, m_ui.cheatsOpen, m_ui.cheatsReset, "Folders", "Cheats", Path::Combine(EmuFolders::DataRoot, "cheats"));
SettingWidgetBinder::BindWidgetToFolderSetting(sif, m_ui.covers, m_ui.coversBrowse, m_ui.coversOpen, m_ui.coversReset, "Folders", "Covers", Path::Combine(EmuFolders::DataRoot, "covers"));
SettingWidgetBinder::BindWidgetToFolderSetting(sif, m_ui.snapshots, m_ui.snapshotsBrowse, m_ui.snapshotsOpen, m_ui.snapshotsReset, "Folders", "Snapshots", Path::Combine(EmuFolders::DataRoot, "snaps"));
SettingWidgetBinder::BindWidgetToFolderSetting(sif, m_ui.saveStates, m_ui.saveStatesBrowse, m_ui.saveStatesOpen, m_ui.saveStatesReset, "Folders", "SaveStates", Path::Combine(EmuFolders::DataRoot, "sstates"));
}
FolderSettingsWidget::~FolderSettingsWidget() = default;

View File

@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QWidget>
#include "ui_FolderSettingsWidget.h"
class SettingsWindow;
class FolderSettingsWidget : public QWidget
{
Q_OBJECT
public:
FolderSettingsWidget(SettingsWindow* dialog, QWidget* parent);
~FolderSettingsWidget();
private:
Ui::FolderSettingsWidget m_ui;
};

View File

@@ -0,0 +1,245 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FolderSettingsWidget</class>
<widget class="QWidget" name="FolderSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>648</width>
<height>487</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Cache Directory</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="0">
<widget class="QLineEdit" name="cache"/>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="cacheBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="cacheOpen">
<property name="text">
<string>Open...</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="cacheReset">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="4">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Used for storing shaders, game list, and achievement data.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Cheats Directory</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="1" column="0">
<widget class="QLineEdit" name="cheats"/>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="cheatsBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="cheatsOpen">
<property name="text">
<string>Open...</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="cheatsReset">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="4">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Used for storing .pnach files containing game cheats.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Covers Directory</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="1" column="0">
<widget class="QLineEdit" name="covers"/>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="coversBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="coversOpen">
<property name="text">
<string>Open...</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="coversReset">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="4">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Used for storing covers in the game grid/Big Picture UIs.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Snapshots Directory</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QLineEdit" name="snapshots"/>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="snapshotsBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="snapshotsOpen">
<property name="text">
<string>Open...</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="snapshotsReset">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="4">
<widget class="QLabel" name="label">
<property name="text">
<string>Used for screenshots and saving GS dumps.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Save States Directory</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<widget class="QLineEdit" name="saveStates"/>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="saveStatesBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="saveStatesOpen">
<property name="text">
<string>Open...</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="saveStatesReset">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="4">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Used for storing save states.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>132</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,292 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "MainWindow.h"
#include "QtHost.h"
#include "QtUtils.h"
#include "SettingWidgetBinder.h"
#include "Settings/GameCheatSettingsWidget.h"
#include "Settings/SettingsWindow.h"
#include "pcsx2/GameList.h"
#include "pcsx2/Patch.h"
#include "common/HeterogeneousContainers.h"
#include <QtCore/QSortFilterProxyModel>
#include <QtGui/QStandardItemModel>
GameCheatSettingsWidget::GameCheatSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: m_dialog(dialog)
{
m_ui.setupUi(this);
m_model = new QStandardItemModel(this);
QStringList headers;
headers.push_back(tr("Name"));
headers.push_back(tr("Author"));
headers.push_back(tr("Description"));
m_model->setHorizontalHeaderLabels(headers);
m_model_proxy = new QSortFilterProxyModel(this);
m_model_proxy->setSourceModel(m_model);
m_model_proxy->setRecursiveFilteringEnabled(true);
m_model_proxy->setAutoAcceptChildRows(true);
m_model_proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_ui.cheatList->setModel(m_model_proxy);
reloadList();
m_ui.cheatList->expandAll();
SettingsInterface* sif = m_dialog->getSettingsInterface();
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableCheats, "EmuCore", "EnableCheats", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.allCRCsCheckbox, "EmuCore", "ShowCheatsForAllCRCs", false);
updateListEnabled();
connectCheckStateChanged(m_ui.enableCheats, this, &GameCheatSettingsWidget::updateListEnabled);
connect(m_ui.cheatList, &QTreeView::doubleClicked, this, &GameCheatSettingsWidget::onCheatListItemDoubleClicked);
connect(m_model, &QStandardItemModel::itemChanged, this, &GameCheatSettingsWidget::onCheatListItemChanged);
connect(m_ui.reloadCheats, &QPushButton::clicked, this, &GameCheatSettingsWidget::onReloadClicked);
connect(m_ui.enableAll, &QPushButton::clicked, this, [this]() { setStateForAll(true); });
connect(m_ui.disableAll, &QPushButton::clicked, this, [this]() { setStateForAll(false); });
connectCheckStateChanged(m_ui.allCRCsCheckbox, this, &GameCheatSettingsWidget::onReloadClicked);
connect(m_ui.searchText, &QLineEdit::textChanged, this, [this](const QString& text) {
m_model_proxy->setFilterFixedString(text);
m_ui.cheatList->expandAll();
});
connect(m_dialog, &SettingsWindow::discSerialChanged, this, &GameCheatSettingsWidget::reloadList);
dialog->registerWidgetHelp(m_ui.allCRCsCheckbox, tr("Show Cheats For All CRCs"), tr("Checked"),
tr("Toggles scanning patch files for all CRCs of the game. With this enabled available patches for the game serial with different CRCs will also be loaded."));
}
GameCheatSettingsWidget::~GameCheatSettingsWidget() = default;
void GameCheatSettingsWidget::onCheatListItemDoubleClicked(const QModelIndex& index)
{
const QModelIndex source_index = m_model_proxy->mapToSource(index);
const QModelIndex sibling_index = source_index.sibling(source_index.row(), 0);
QStandardItem* item = m_model->itemFromIndex(sibling_index);
if (!item)
return;
if (item->hasChildren() && index.column() != 0)
{
const QModelIndex view_sibling_index = index.sibling(index.row(), 0);
const bool isExpanded = m_ui.cheatList->isExpanded(view_sibling_index);
if (isExpanded)
m_ui.cheatList->collapse(view_sibling_index);
else
m_ui.cheatList->expand(view_sibling_index);
return;
}
QVariant data = item->data(Qt::UserRole);
if (!data.isValid())
return;
std::string cheat_name = data.toString().toStdString();
const bool new_state = !(item->checkState() == Qt::Checked);
item->setCheckState(new_state ? Qt::Checked : Qt::Unchecked);
setCheatEnabled(std::move(cheat_name), new_state, true);
}
void GameCheatSettingsWidget::onCheatListItemChanged(QStandardItem* item)
{
QVariant data = item->data(Qt::UserRole);
if (!data.isValid())
return;
std::string cheat_name = data.toString().toStdString();
const bool current_enabled =
(std::find(m_enabled_patches.begin(), m_enabled_patches.end(), cheat_name) != m_enabled_patches.end());
const bool current_checked = (item->checkState() == Qt::Checked);
if (current_enabled == current_checked)
return;
setCheatEnabled(std::move(cheat_name), current_checked, true);
}
void GameCheatSettingsWidget::onReloadClicked()
{
reloadList();
m_ui.cheatList->expandAll();
// reload it on the emu thread too, so it picks up any changes
g_emu_thread->reloadPatches();
}
void GameCheatSettingsWidget::updateListEnabled()
{
const bool cheats_enabled = m_dialog->getEffectiveBoolValue("EmuCore", "EnableCheats", false);
m_ui.cheatList->setEnabled(cheats_enabled);
m_ui.enableAll->setEnabled(cheats_enabled);
m_ui.disableAll->setEnabled(cheats_enabled);
m_ui.reloadCheats->setEnabled(cheats_enabled);
m_ui.allCRCsCheckbox->setEnabled(cheats_enabled && !m_dialog->getSerial().empty());
m_ui.searchText->setEnabled(cheats_enabled);
}
void GameCheatSettingsWidget::disableAllCheats()
{
SettingsInterface* si = m_dialog->getSettingsInterface();
si->ClearSection(Patch::CHEATS_CONFIG_SECTION);
si->Save();
}
void GameCheatSettingsWidget::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
QtUtils::ResizeColumnsForTreeView(m_ui.cheatList, {320, 100, -1});
}
void GameCheatSettingsWidget::setCheatEnabled(std::string name, bool enabled, bool save_and_reload_settings)
{
SettingsInterface* si = m_dialog->getSettingsInterface();
auto it = std::find(m_enabled_patches.begin(), m_enabled_patches.end(), name);
if (enabled)
{
si->AddToStringList(Patch::CHEATS_CONFIG_SECTION, Patch::PATCH_ENABLE_CONFIG_KEY, name.c_str());
if (it == m_enabled_patches.end())
m_enabled_patches.push_back(std::move(name));
}
else
{
si->RemoveFromStringList(Patch::CHEATS_CONFIG_SECTION, Patch::PATCH_ENABLE_CONFIG_KEY, name.c_str());
if (it != m_enabled_patches.end())
m_enabled_patches.erase(it);
}
if (save_and_reload_settings)
{
si->Save();
g_emu_thread->reloadGameSettings();
}
}
void GameCheatSettingsWidget::setStateForAll(bool enabled)
{
// Temporarily disconnect from itemChanged to prevent redundant saves
disconnect(m_model, &QStandardItemModel::itemChanged, this, &GameCheatSettingsWidget::onCheatListItemChanged);
setStateRecursively(nullptr, enabled);
m_dialog->getSettingsInterface()->Save();
g_emu_thread->reloadGameSettings();
connect(m_model, &QStandardItemModel::itemChanged, this, &GameCheatSettingsWidget::onCheatListItemChanged);
}
void GameCheatSettingsWidget::setStateRecursively(QStandardItem* parent, bool enabled)
{
const int count = parent ? parent->rowCount() : m_model->rowCount();
for (int i = 0; i < count; i++)
{
QStandardItem* item = parent ? parent->child(i, 0) : m_model->item(i, 0);
QVariant data = item->data(Qt::UserRole);
if (data.isValid())
{
if ((item->checkState() == Qt::Checked) != enabled)
{
item->setCheckState(enabled ? Qt::Checked : Qt::Unchecked);
setCheatEnabled(data.toString().toStdString(), enabled, false);
}
}
else
{
setStateRecursively(item, enabled);
}
}
}
void GameCheatSettingsWidget::reloadList()
{
u32 num_unlabelled_codes = 0;
bool showAllCRCS = m_ui.allCRCsCheckbox->isChecked();
m_patches = Patch::GetPatchInfo(m_dialog->getSerial(), m_dialog->getDiscCRC(), true, showAllCRCS, & num_unlabelled_codes);
m_enabled_patches =
m_dialog->getSettingsInterface()->GetStringList(Patch::CHEATS_CONFIG_SECTION, Patch::PATCH_ENABLE_CONFIG_KEY);
m_parent_map.clear();
m_model->removeRows(0, m_model->rowCount());
m_ui.allCRCsCheckbox->setEnabled(!m_dialog->getSerial().empty() && m_ui.cheatList->isEnabled());
for (const Patch::PatchInfo& pi : m_patches)
{
const bool enabled =
(std::find(m_enabled_patches.begin(), m_enabled_patches.end(), pi.name) != m_enabled_patches.end());
const std::string_view parent_part = pi.GetNameParentPart();
QStandardItem* parent = getTreeViewParent(parent_part);
QList<QStandardItem*> items = populateTreeViewRow(pi, enabled);
if (parent)
parent->appendRow(items);
else
m_model->appendRow(items);
}
// Hide root indicator when there's no groups, frees up some whitespace.
m_ui.cheatList->setRootIsDecorated(!m_parent_map.empty());
if (num_unlabelled_codes > 0)
{
QStandardItem* item = new QStandardItem();
item->setText(tr("%1 unlabelled patch codes will automatically activate.").arg(num_unlabelled_codes));
m_model->appendRow(item);
}
}
QStandardItem* GameCheatSettingsWidget::getTreeViewParent(const std::string_view parent)
{
if (parent.empty())
return nullptr;
auto it = m_parent_map.find(parent);
if (it != m_parent_map.end())
return it->second;
std::string_view this_part = parent;
QStandardItem* parent_to_this = nullptr;
const std::string_view::size_type pos = parent.rfind('\\');
if (pos != std::string::npos && pos != (parent.size() - 1))
{
// go up the chain until we find the real parent, then back down
parent_to_this = getTreeViewParent(parent.substr(0, pos));
this_part = parent.substr(pos + 1);
}
QStandardItem* item = new QStandardItem();
item->setText(QString::fromUtf8(this_part.data(), this_part.length()));
if (parent_to_this)
parent_to_this->appendRow(item);
else
m_model->appendRow(item);
m_parent_map.emplace(parent, item);
return item;
}
QList<QStandardItem*> GameCheatSettingsWidget::populateTreeViewRow(const Patch::PatchInfo& pi, bool enabled)
{
QList<QStandardItem*> items;
QStandardItem* nameItem = new QStandardItem();
const std::string_view name_part = pi.GetNamePart();
nameItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemNeverHasChildren | Qt::ItemIsEnabled);
nameItem->setCheckState(enabled ? Qt::Checked : Qt::Unchecked);
nameItem->setData(QString::fromStdString(pi.name), Qt::UserRole);
if (!name_part.empty())
nameItem->setText(QString::fromUtf8(name_part.data(), name_part.length()));
QStandardItem* authorItem = new QStandardItem(QString::fromStdString(pi.author));
QStandardItem* descriptionItem = new QStandardItem(QString::fromStdString(pi.description));
descriptionItem->setToolTip(QString::fromStdString(pi.description));
items.push_back(nameItem);
items.push_back(authorItem);
items.push_back(descriptionItem);
return items;
}

View File

@@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtGui/QStandardItemModel>
#include <QtCore/QSortFilterProxyModel>
#include "ui_GameCheatSettingsWidget.h"
#include "pcsx2/Patch.h"
#include "common/HeterogeneousContainers.h"
#include <string>
#include <string_view>
#include <vector>
namespace GameList
{
struct Entry;
}
class SettingsWindow;
class GameCheatSettingsWidget : public QWidget
{
Q_OBJECT
public:
GameCheatSettingsWidget(SettingsWindow* dialog, QWidget* parent);
~GameCheatSettingsWidget();
void disableAllCheats();
protected:
void resizeEvent(QResizeEvent* event) override;
private Q_SLOTS:
void onCheatListItemDoubleClicked(const QModelIndex& index);
void onCheatListItemChanged(QStandardItem* item);
void onReloadClicked();
void updateListEnabled();
void reloadList();
private:
QStandardItem* getTreeViewParent(const std::string_view parent);
QList<QStandardItem*> populateTreeViewRow(const Patch::PatchInfo& pi, bool enabled);
void setCheatEnabled(std::string name, bool enabled, bool save_and_reload_settings);
void setStateForAll(bool enabled);
void setStateRecursively(QStandardItem* parent, bool enabled);
Ui::GameCheatSettingsWidget m_ui;
SettingsWindow* m_dialog;
QStandardItemModel* m_model = nullptr;
QSortFilterProxyModel* m_model_proxy = nullptr;
UnorderedStringMap<QStandardItem*> m_parent_map;
std::vector<Patch::PatchInfo> m_patches;
std::vector<std::string> m_enabled_patches;
};

View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GameCheatSettingsWidget</class>
<widget class="QWidget" name="GameCheatSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>697</width>
<height>361</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Activating cheats can cause unpredictable behavior, crashing, soft-locks, or broken saved games. Use cheats at your own risk, the PCSX2 team will provide no support for users who have enabled cheats.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="enableCheats">
<property name="text">
<string>Enable Cheats</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="searchText">
<property name="placeholderText">
<string>Search...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTreeView" name="cheatList">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectItems</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="enableAll">
<property name="text">
<string>Enable All</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="disableAll">
<property name="text">
<string>Disable All</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="allCRCsCheckbox">
<property name="text">
<string>All CRCs</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="reloadCheats">
<property name="text">
<string>Reload Cheats</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include <QtWidgets/QMessageBox>
#include <algorithm>
#include "GameFixSettingsWidget.h"
#include "QtHost.h"
#include "QtUtils.h"
#include "SettingWidgetBinder.h"
#include "SettingsWindow.h"
GameFixSettingsWidget::GameFixSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: QWidget(parent)
{
SettingsInterface* sif = dialog->getSettingsInterface();
m_ui.setupUi(this);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.FpuMulHack, "EmuCore/Gamefixes", "FpuMulHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.GoemonTlbHack, "EmuCore/Gamefixes", "GoemonTlbHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.SoftwareRendererFMVHack, "EmuCore/Gamefixes", "SoftwareRendererFMVHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.SkipMPEGHack, "EmuCore/Gamefixes", "SkipMPEGHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.OPHFlagHack, "EmuCore/Gamefixes", "OPHFlagHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.EETimingHack, "EmuCore/Gamefixes", "EETimingHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.InstantDMAHack, "EmuCore/Gamefixes", "InstantDMAHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.DMABusyHack, "EmuCore/Gamefixes", "DMABusyHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.GIFFIFOHack, "EmuCore/Gamefixes", "GIFFIFOHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.VIFFIFOHack, "EmuCore/Gamefixes", "VIFFIFOHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.VIF1StallHack, "EmuCore/Gamefixes", "VIF1StallHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.VuAddSubHack, "EmuCore/Gamefixes", "VuAddSubHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.IbitHack, "EmuCore/Gamefixes", "IbitHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.FullVU0SyncHack, "EmuCore/Gamefixes", "FullVU0SyncHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.VUSyncHack, "EmuCore/Gamefixes", "VUSyncHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.VUOverflowHack, "EmuCore/Gamefixes", "VUOverflowHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.XgKickHack, "EmuCore/Gamefixes", "XgKickHack", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.BlitInternalFPSHack, "EmuCore/Gamefixes", "BlitInternalFPSHack", false);
dialog->registerWidgetHelp(m_ui.FpuMulHack, tr("FPU Multiply Hack"), tr("Unchecked"), tr("For Tales of Destiny."));
dialog->registerWidgetHelp(m_ui.GoemonTlbHack, tr("Preload TLB Hack"), tr("Unchecked"), tr("To avoid TLB miss on Goemon."));
dialog->registerWidgetHelp(m_ui.SoftwareRendererFMVHack, tr("Use Software Renderer For FMVs"), tr("Unchecked"), tr("Needed for some games with complex FMV rendering."));
dialog->registerWidgetHelp(m_ui.SkipMPEGHack, tr("Skip MPEG Hack"), tr("Unchecked"), tr("Skips videos/FMVs in games to avoid game hanging/freezes."));
dialog->registerWidgetHelp(m_ui.OPHFlagHack, tr("OPH Flag Hack"), tr("Unchecked"), tr("Known to affect following games: Bleach Blade Battlers, Growlanser II and III, Wizardry."));
dialog->registerWidgetHelp(m_ui.EETimingHack, tr("EE Timing Hack"), tr("Unchecked"), tr("General-purpose timing hack. Known to affect following games: Digital Devil Saga, SSX."));
dialog->registerWidgetHelp(m_ui.InstantDMAHack, tr("Instant DMA Hack"), tr("Unchecked"), tr("Good for cache emulation problems. Known to affect following games: Fire Pro Wrestling Z."));
dialog->registerWidgetHelp(m_ui.DMABusyHack, tr("DMA Busy Hack"), tr("Unchecked"), tr("Known to affect following games: Mana Khemia 1, Metal Saga, Pilot Down Behind Enemy Lines."));
dialog->registerWidgetHelp(m_ui.GIFFIFOHack, tr("Emulate GIF FIFO"), tr("Unchecked"), tr("Correct but slower. Known to affect the following games: Fifa Street 2."));
dialog->registerWidgetHelp(m_ui.VIFFIFOHack, tr("Emulate VIF FIFO"), tr("Unchecked"), tr("Simulate VIF1 FIFO read ahead. Known to affect following games: Test Drive Unlimited, Transformers."));
dialog->registerWidgetHelp(m_ui.VIF1StallHack, tr("Delay VIF1 Stalls"), tr("Unchecked"), tr("For SOCOM 2 HUD and Spy Hunter loading hang."));
dialog->registerWidgetHelp(m_ui.VuAddSubHack, tr("VU Add Hack"), tr("Unchecked"), tr("For Tri-Ace Games: Star Ocean 3, Radiata Stories, Valkyrie Profile 2."));
dialog->registerWidgetHelp(m_ui.IbitHack, tr("VU I Bit Hack"), tr("Unchecked"), tr("Avoids constant recompilation in some games. Known to affect the following games: Scarface The World is Yours, Crash Tag Team Racing."));
dialog->registerWidgetHelp(m_ui.FullVU0SyncHack, tr("Full VU0 Synchronization"), tr("Unchecked"), tr("Forces tight VU0 sync on every COP2 instruction."));
dialog->registerWidgetHelp(m_ui.VUSyncHack, tr("VU Sync"), tr("Unchecked"), tr("Run behind. To avoid sync problems when reading or writing VU registers."));
dialog->registerWidgetHelp(m_ui.VUOverflowHack, tr("VU Overflow Hack"), tr("Unchecked"), tr("To check for possible float overflows (Superman Returns)."));
dialog->registerWidgetHelp(m_ui.XgKickHack, tr("VU XGKick Sync"), tr("Unchecked"), tr("Use accurate timing for VU XGKicks (slower)."));
dialog->registerWidgetHelp(m_ui.BlitInternalFPSHack, tr("Force Blit Internal FPS Detection"), tr("Unchecked"), tr("Use alternative method to calculate internal FPS to avoid false readings in some games."));
}
GameFixSettingsWidget::~GameFixSettingsWidget() = default;

View File

@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QWidget>
#include "ui_GameFixSettingsWidget.h"
class SettingsWindow;
class GameFixSettingsWidget : public QWidget
{
Q_OBJECT
public:
GameFixSettingsWidget(SettingsWindow* dialog, QWidget* parent);
~GameFixSettingsWidget();
private:
Ui::GameFixSettingsWidget m_ui;
};

View File

@@ -0,0 +1,211 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GameFixSettingsWidget</class>
<widget class="QWidget" name="GameFixSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>676</width>
<height>535</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>674</width>
<height>533</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Game Fixes</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="FpuMulHack">
<property name="text">
<string extracomment="FPU = Floating Point Unit. A part of the PS2's CPU. Do not translate.\nMultiply: mathematical term.\nTales of Destiny: a game's name. Leave as-is or use an official translation.">FPU Multiply Hack</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="SoftwareRendererFMVHack">
<property name="text">
<string extracomment="FMV: Full Motion Video. Find the common used term in your language.">Use Software Renderer For FMVs</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="SkipMPEGHack">
<property name="text">
<string extracomment="MPEG: video codec, leave as-is. FMV: Full Motion Video. Find the common used term in your language.">Skip MPEG Hack</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="GoemonTlbHack">
<property name="text">
<string extracomment="TLB: Translation Lookaside Buffer. Leave as-is. Goemon: name of a character from the series with his name. Leave as-is or use an official translation.">Preload TLB Hack</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="EETimingHack">
<property name="text">
<string extracomment="EE: Emotion Engine. Leave as-is.">EE Timing Hack</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="InstantDMAHack">
<property name="text">
<string extracomment="DMA: Direct Memory Access. Leave as-is.">Instant DMA Hack</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="OPHFlagHack">
<property name="text">
<string extracomment="OPH: Name of a flag (Output PatH) in the GIF_STAT register in the EE. Leave as-is.\nBleach Blade Battles: a game's name. Leave as-is or use an official translation.">OPH Flag Hack</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="GIFFIFOHack">
<property name="text">
<string extracomment="GIF = GS (Graphics Synthesizer, the GPU) Interface. Leave as-is.\nFIFO = First-In-First-Out, a type of buffer. Leave as-is.">Emulate GIF FIFO</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="DMABusyHack">
<property name="text">
<string extracomment="DMA: Direct Memory Access. Leave as-is.">DMA Busy Hack</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="VIF1StallHack">
<property name="text">
<string extracomment="VIF = VU (Vector Unit) Interface. Leave as-is. SOCOM 2 and Spy Hunter: names of two different games. Leave as-is or use an official translation.\nHUD = Heads-Up Display. The games' interfaces.">Delay VIF1 Stalls</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="VIFFIFOHack">
<property name="text">
<string extracomment="VIF = VU (Vector Unit) Interface. Leave as-is.\nFIFO = First-In-First-Out, a type of buffer. Leave as-is.">Emulate VIF FIFO</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="FullVU0SyncHack">
<property name="text">
<string extracomment="VU0 = VU (Vector Unit) 0. Leave as-is.">Full VU0 Synchronization</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="IbitHack">
<property name="text">
<string extracomment="VU = Vector Unit. Leave as-is.\nI Bit = A bit referred as I, not as 1.\nScarface The World is Yours and Crash Tag Team Racing: names of two different games. Leave as-is or use an official translation.">VU I Bit Hack</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="VuAddSubHack">
<property name="text">
<string extracomment="VU = Vector Unit. Leave as-is.\nTri-Ace: a game development company name. Leave as-is.">VU Add Hack</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="VUOverflowHack">
<property name="text">
<string extracomment="VU = Vector Unit. Leave as-is.\nSuperman Returns: a game's name. Leave as-is or use an official translation.">VU Overflow Hack</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="VUSyncHack">
<property name="text">
<string extracomment="VU = Vector Unit. Leave as-is.\nRun Behind: watch out for misleading capitalization for non-English: this refers to making the VUs run behind (delayed relative to) the EE.\nM-Bit: a bitflag in VU instructions that tells VU0 to synchronize with the EE. M-Bit Game: A game that uses instructions with the M-Bit enabled (unofficial PCSX2 name).">VU Sync</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="XgKickHack">
<property name="text">
<string extracomment="VU = Vector Unit. Leave as-is.\nXGKick: the name of one of the VU's instructions. Leave as-is.">VU XGKick Sync</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="BlitInternalFPSHack">
<property name="text">
<string extracomment="Blit = a data operation. You might want to write it as-is, but fully uppercased. More information: https://en.wikipedia.org/wiki/Bit_blit This option tells PCSX2 to estimate internal FPS by detecting blits (image copies) onto visible display memory.">Force Blit Internal FPS Detection</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,281 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include <QtCore/QAbstractTableModel>
#include <QtCore/QDebug>
#include <QtCore/QSettings>
#include <QtCore/QUrl>
#include <QtWidgets/QCheckBox>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QMenu>
#include <QtWidgets/QMessageBox>
#include <algorithm>
#include "GameListSettingsWidget.h"
#include "MainWindow.h"
#include "QtHost.h"
#include "QtUtils.h"
#include "SettingWidgetBinder.h"
GameListSettingsWidget::GameListSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: QWidget(parent)
{
SettingsInterface* sif = dialog->getSettingsInterface();
m_ui.setupUi(this);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.preferEnglishGameList, "UI", "PreferEnglishGameList", false);
connectCheckStateChanged(m_ui.preferEnglishGameList, this, [this]{ emit preferEnglishGameListChanged(); });
dialog->registerWidgetHelp(m_ui.preferEnglishGameList, tr("Prefer English Titles"), tr("Unchecked"),
tr("For games with both a title in the game's native language and one in English, prefer the English title."));
m_ui.searchDirectoryList->setSelectionMode(QAbstractItemView::SingleSelection);
m_ui.searchDirectoryList->setSelectionBehavior(QAbstractItemView::SelectRows);
m_ui.searchDirectoryList->setAlternatingRowColors(true);
m_ui.searchDirectoryList->setShowGrid(false);
m_ui.searchDirectoryList->horizontalHeader()->setHighlightSections(false);
m_ui.searchDirectoryList->verticalHeader()->hide();
m_ui.searchDirectoryList->setCurrentIndex({});
m_ui.searchDirectoryList->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
connect(m_ui.searchDirectoryList, &QTableWidget::customContextMenuRequested, this, &GameListSettingsWidget::onDirectoryListContextMenuRequested);
connect(m_ui.searchDirectoryList, &QTableWidget::itemSelectionChanged, this, &GameListSettingsWidget::onDirectoryListSelectionChanged);
connect(m_ui.addSearchDirectoryButton, &QPushButton::clicked, this, &GameListSettingsWidget::onAddSearchDirectoryButtonClicked);
connect(m_ui.removeSearchDirectoryButton, &QPushButton::clicked, this, &GameListSettingsWidget::onRemoveSearchDirectoryButtonClicked);
connect(m_ui.addExcludedFile, &QPushButton::clicked, this, &GameListSettingsWidget::onAddExcludedFileButtonClicked);
connect(m_ui.addExcludedPath, &QPushButton::clicked, this, &GameListSettingsWidget::onAddExcludedPathButtonClicked);
connect(m_ui.removeExcludedPath, &QPushButton::clicked, this, &GameListSettingsWidget::onRemoveExcludedPathButtonClicked);
connect(m_ui.excludedPaths, &QListWidget::itemSelectionChanged, this, &GameListSettingsWidget::onExcludedPathsSelectionChanged);
connect(m_ui.rescanAllGames, &QPushButton::clicked, this, &GameListSettingsWidget::onRescanAllGamesClicked);
connect(m_ui.scanForNewGames, &QPushButton::clicked, this, &GameListSettingsWidget::onScanForNewGamesClicked);
refreshDirectoryList();
refreshExclusionList();
}
GameListSettingsWidget::~GameListSettingsWidget() = default;
bool GameListSettingsWidget::addExcludedPath(const std::string& path)
{
if (!Host::AddBaseValueToStringList("GameList", "ExcludedPaths", path.c_str()))
return false;
Host::CommitBaseSettingChanges();
m_ui.excludedPaths->addItem(QString::fromStdString(path));
g_main_window->refreshGameList(false);
return true;
}
void GameListSettingsWidget::refreshExclusionList()
{
m_ui.excludedPaths->clear();
const std::vector<std::string> paths(Host::GetBaseStringListSetting("GameList", "ExcludedPaths"));
for (const std::string& path : paths)
m_ui.excludedPaths->addItem(QString::fromStdString(path));
m_ui.removeExcludedPath->setEnabled(false);
}
bool GameListSettingsWidget::event(QEvent* event)
{
bool res = QWidget::event(event);
switch (event->type())
{
case QEvent::LayoutRequest:
case QEvent::Resize:
QtUtils::ResizeColumnsForTableView(m_ui.searchDirectoryList, {-1, 100});
break;
default:
break;
}
return res;
}
void GameListSettingsWidget::addPathToTable(const std::string& path, bool recursive)
{
const int row = m_ui.searchDirectoryList->rowCount();
m_ui.searchDirectoryList->insertRow(row);
QTableWidgetItem* item = new QTableWidgetItem();
item->setText(QString::fromStdString(path));
item->setFlags(item->flags() & ~(Qt::ItemIsEditable));
m_ui.searchDirectoryList->setItem(row, 0, item);
QCheckBox* cb = new QCheckBox(m_ui.searchDirectoryList);
m_ui.searchDirectoryList->setCellWidget(row, 1, cb);
cb->setChecked(recursive);
connectCheckStateChanged(cb, this, [item](Qt::CheckState state) {
const std::string path(item->text().toStdString());
if (state == Qt::Checked)
{
Host::RemoveBaseValueFromStringList("GameList", "Paths", path.c_str());
Host::AddBaseValueToStringList("GameList", "RecursivePaths", path.c_str());
}
else
{
Host::RemoveBaseValueFromStringList("GameList", "RecursivePaths", path.c_str());
Host::AddBaseValueToStringList("GameList", "Paths", path.c_str());
}
Host::CommitBaseSettingChanges();
});
}
void GameListSettingsWidget::refreshDirectoryList()
{
QSignalBlocker sb(m_ui.searchDirectoryList);
while (m_ui.searchDirectoryList->rowCount() > 0)
m_ui.searchDirectoryList->removeRow(0);
std::vector<std::string> path_list = Host::GetBaseStringListSetting("GameList", "Paths");
for (const std::string& entry : path_list)
addPathToTable(entry, false);
path_list = Host::GetBaseStringListSetting("GameList", "RecursivePaths");
for (const std::string& entry : path_list)
addPathToTable(entry, true);
m_ui.searchDirectoryList->sortByColumn(0, Qt::AscendingOrder);
m_ui.removeSearchDirectoryButton->setEnabled(false);
}
void GameListSettingsWidget::addSearchDirectory(const QString& path, bool recursive)
{
const std::string spath(path.toStdString());
Host::RemoveBaseValueFromStringList("GameList", recursive ? "Paths" : "RecursivePaths", spath.c_str());
Host::AddBaseValueToStringList("GameList", recursive ? "RecursivePaths" : "Paths", spath.c_str());
Host::CommitBaseSettingChanges();
refreshDirectoryList();
g_main_window->refreshGameList(false);
}
void GameListSettingsWidget::removeSearchDirectory(const QString& path)
{
const std::string spath(path.toStdString());
if (!Host::RemoveBaseValueFromStringList("GameList", "Paths", spath.c_str()) &&
!Host::RemoveBaseValueFromStringList("GameList", "RecursivePaths", spath.c_str()))
{
return;
}
Host::CommitBaseSettingChanges();
refreshDirectoryList();
g_main_window->refreshGameList(false);
}
void GameListSettingsWidget::onDirectoryListContextMenuRequested(const QPoint& point)
{
QModelIndexList selection = m_ui.searchDirectoryList->selectionModel()->selectedIndexes();
if (selection.size() < 1)
return;
const int row = selection[0].row();
QMenu menu;
menu.addAction(tr("Remove"), [this]() { onRemoveSearchDirectoryButtonClicked(); });
menu.addSeparator();
menu.addAction(tr("Open Directory..."), [this, row]() {
QtUtils::OpenURL(this, QUrl::fromLocalFile(m_ui.searchDirectoryList->item(row, 0)->text()));
});
menu.exec(m_ui.searchDirectoryList->mapToGlobal(point));
}
void GameListSettingsWidget::onDirectoryListSelectionChanged()
{
m_ui.removeSearchDirectoryButton->setEnabled(m_ui.searchDirectoryList->selectionModel()->hasSelection());
}
void GameListSettingsWidget::addSearchDirectory(QWidget* parent_widget)
{
QString dir =
QDir::toNativeSeparators(QFileDialog::getExistingDirectory(parent_widget, tr("Select Search Directory")));
if (dir.isEmpty())
return;
QMessageBox::StandardButton selection =
QMessageBox::question(this, tr("Scan Recursively?"),
tr("Would you like to scan the directory \"%1\" recursively?\n\nScanning recursively takes "
"more time, but will identify files in subdirectories.")
.arg(dir),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if (selection == QMessageBox::Cancel)
return;
const bool recursive = (selection == QMessageBox::Yes);
addSearchDirectory(dir, recursive);
}
void GameListSettingsWidget::onAddSearchDirectoryButtonClicked()
{
addSearchDirectory(this);
}
void GameListSettingsWidget::onRemoveSearchDirectoryButtonClicked()
{
const int row = m_ui.searchDirectoryList->currentRow();
QTableWidgetItem* item = (row >= 0) ? m_ui.searchDirectoryList->takeItem(row, 0) : nullptr;
if (!item)
return;
removeSearchDirectory(item->text());
delete item;
}
void GameListSettingsWidget::onAddExcludedFileButtonClicked()
{
QString path =
QDir::toNativeSeparators(QFileDialog::getOpenFileName(QtUtils::GetRootWidget(this), tr("Select File")));
if (path.isEmpty())
return;
addExcludedPath(path.toStdString());
}
void GameListSettingsWidget::onAddExcludedPathButtonClicked()
{
QString path =
QDir::toNativeSeparators(QFileDialog::getExistingDirectory(QtUtils::GetRootWidget(this), tr("Select Directory")));
if (path.isEmpty())
return;
addExcludedPath(path.toStdString());
}
void GameListSettingsWidget::onRemoveExcludedPathButtonClicked()
{
const int row = m_ui.excludedPaths->currentRow();
QListWidgetItem* item = (row >= 0) ? m_ui.excludedPaths->takeItem(row) : 0;
if (!item)
return;
if (Host::RemoveBaseValueFromStringList("GameList", "ExcludedPaths", item->text().toUtf8().constData()))
Host::CommitBaseSettingChanges();
delete item;
g_main_window->refreshGameList(false);
}
void GameListSettingsWidget::onExcludedPathsSelectionChanged()
{
m_ui.removeExcludedPath->setEnabled(!m_ui.excludedPaths->selectedItems().isEmpty());
}
void GameListSettingsWidget::onRescanAllGamesClicked()
{
g_main_window->refreshGameList(true);
}
void GameListSettingsWidget::onScanForNewGamesClicked()
{
g_main_window->refreshGameList(false);
}

View File

@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <string>
#include <QtWidgets/QWidget>
#include "ui_GameListSettingsWidget.h"
class SettingsWindow;
class GameListSettingsWidget : public QWidget
{
Q_OBJECT
public:
GameListSettingsWidget(SettingsWindow* dialog, QWidget* parent);
~GameListSettingsWidget();
bool addExcludedPath(const std::string& path);
void refreshExclusionList();
Q_SIGNALS:
void preferEnglishGameListChanged();
public Q_SLOTS:
void addSearchDirectory(QWidget* parent_widget);
private Q_SLOTS:
void onDirectoryListContextMenuRequested(const QPoint& point);
void onDirectoryListSelectionChanged();
void onAddSearchDirectoryButtonClicked();
void onRemoveSearchDirectoryButtonClicked();
void onAddExcludedFileButtonClicked();
void onAddExcludedPathButtonClicked();
void onRemoveExcludedPathButtonClicked();
void onExcludedPathsSelectionChanged();
void onScanForNewGamesClicked();
void onRescanAllGamesClicked();
protected:
bool event(QEvent* event);
private:
void addPathToTable(const std::string& path, bool recursive);
void refreshDirectoryList();
void addSearchDirectory(const QString& path, bool recursive);
void removeSearchDirectory(const QString& path);
Ui::GameListSettingsWidget m_ui;
};

View File

@@ -0,0 +1,273 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GameListSettingsWidget</class>
<widget class="QWidget" name="GameListSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>532</width>
<height>376</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,2,0,1,0">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="gameScanningGroup">
<property name="title">
<string>Game Scanning</string>
</property>
<layout class="QVBoxLayout" name="gameScanningVBox">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Search Directories (will be scanned for games)</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="addSearchDirectoryButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Add...</string>
</property>
<property name="icon">
<iconset theme="folder-add-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="removeSearchDirectoryButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Remove</string>
</property>
<property name="icon">
<iconset theme="folder-reduce-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="searchDirectoryList">
<column>
<property name="text">
<string>Search Directory</string>
</property>
</column>
<column>
<property name="text">
<string>Scan Recursively</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Excluded Paths (will not be scanned)</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="addExcludedPath">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Directory...</string>
</property>
<property name="icon">
<iconset theme="folder-add-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="addExcludedFile">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>File...</string>
</property>
<property name="icon">
<iconset theme="file-add-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="removeExcludedPath">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Remove</string>
</property>
<property name="icon">
<iconset theme="file-reduce-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListWidget" name="excludedPaths"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="scanForNewGames">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Scan For New Games</string>
</property>
<property name="icon">
<iconset theme="file-search-line">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="rescanAllGames">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Rescan All Games</string>
</property>
<property name="icon">
<iconset theme="refresh-line">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gameListGroup">
<property name="title">
<string>Display</string>
</property>
<layout class="QGridLayout" name="gridLayout_gameList">
<item row="0" column="0">
<widget class="QCheckBox" name="preferEnglishGameList">
<property name="text">
<string>Prefer English Titles</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GamePatchDetailsWidget</class>
<widget class="QWidget" name="GamePatchDetailsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>541</width>
<height>112</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,1">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="name">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Patch Title</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="enabled">
<property name="text">
<string>Enabled</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="description">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Author: &lt;/span&gt;Patch Author&lt;/p&gt;&lt;p&gt;Description would go here&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,208 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "MainWindow.h"
#include "QtHost.h"
#include "QtUtils.h"
#include "Settings/GamePatchSettingsWidget.h"
#include "SettingWidgetBinder.h"
#include "Settings/SettingsWindow.h"
#include "pcsx2/GameList.h"
#include "pcsx2/Patch.h"
#include "common/Assertions.h"
#include <algorithm>
GamePatchDetailsWidget::GamePatchDetailsWidget(std::string name, const std::string& author,
const std::string& description, bool tristate, Qt::CheckState checkState, SettingsWindow* dialog, QWidget* parent)
: QWidget(parent)
, m_dialog(dialog)
, m_name(name)
{
m_ui.setupUi(this);
m_ui.name->setText(QString::fromStdString(name));
m_ui.description->setText(
tr("<strong>Author: </strong>%1<br>%2")
.arg(author.empty() ? tr("Unknown") : QString::fromStdString(author))
.arg(description.empty() ? tr("No description provided.") : QString::fromStdString(description)));
pxAssert(dialog->getSettingsInterface());
m_ui.enabled->setTristate(tristate);
m_ui.enabled->setCheckState(checkState);
connectCheckStateChanged(m_ui.enabled, this, &GamePatchDetailsWidget::onEnabledStateChanged);
}
GamePatchDetailsWidget::~GamePatchDetailsWidget() = default;
void GamePatchDetailsWidget::onEnabledStateChanged(int state)
{
SettingsInterface* si = m_dialog->getSettingsInterface();
if (state == Qt::Checked)
{
si->AddToStringList(Patch::PATCHES_CONFIG_SECTION, Patch::PATCH_ENABLE_CONFIG_KEY, m_name.c_str());
si->RemoveFromStringList(Patch::PATCHES_CONFIG_SECTION, Patch::PATCH_DISABLE_CONFIG_KEY, m_name.c_str());
}
else
{
si->RemoveFromStringList(Patch::PATCHES_CONFIG_SECTION, Patch::PATCH_ENABLE_CONFIG_KEY, m_name.c_str());
if (m_ui.enabled->isTristate())
{
if (state == Qt::Unchecked)
{
si->AddToStringList(Patch::PATCHES_CONFIG_SECTION, Patch::PATCH_DISABLE_CONFIG_KEY, m_name.c_str());
}
else
{
si->RemoveFromStringList(Patch::PATCHES_CONFIG_SECTION, Patch::PATCH_DISABLE_CONFIG_KEY, m_name.c_str());
}
}
}
si->Save();
g_emu_thread->reloadGameSettings();
}
GamePatchSettingsWidget::GamePatchSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: m_dialog(dialog)
{
m_ui.setupUi(this);
m_ui.scrollArea->setFrameShape(QFrame::WinPanel);
m_ui.scrollArea->setFrameShadow(QFrame::Sunken);
setUnlabeledPatchesWarningVisibility(false);
setGlobalWsPatchNoteVisibility(false);
setGlobalNiPatchNoteVisibility(false);
SettingsInterface* sif = m_dialog->getSettingsInterface();
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.allCRCsCheckbox, "EmuCore", "ShowPatchesForAllCRCs", false);
connect(m_ui.reload, &QPushButton::clicked, this, &GamePatchSettingsWidget::onReloadClicked);
connectCheckStateChanged(m_ui.allCRCsCheckbox, this, &GamePatchSettingsWidget::reloadList);
connect(m_dialog, &SettingsWindow::discSerialChanged, this, &GamePatchSettingsWidget::reloadList);
dialog->registerWidgetHelp(m_ui.allCRCsCheckbox, tr("Show Patches For All CRCs"), tr("Checked"),
tr("Toggles scanning patch files for all CRCs of the game. With this enabled available patches for the game serial with different CRCs will also be loaded."));
reloadList();
}
GamePatchSettingsWidget::~GamePatchSettingsWidget() = default;
void GamePatchSettingsWidget::onReloadClicked()
{
reloadList();
// reload it on the emu thread too, so it picks up any changes
g_emu_thread->reloadPatches();
}
void GamePatchSettingsWidget::disableAllPatches()
{
SettingsInterface* si = m_dialog->getSettingsInterface();
si->ClearSection(Patch::PATCHES_CONFIG_SECTION);
si->Save();
}
void GamePatchSettingsWidget::reloadList()
{
const SettingsInterface* si = m_dialog->getSettingsInterface();
// Patches shouldn't have any unlabelled patch groups, because they're new.
u32 number_of_unlabeled_patches = 0;
bool showAllCRCS = m_ui.allCRCsCheckbox->isChecked();
std::vector<Patch::PatchInfo> patches = Patch::GetPatchInfo(m_dialog->getSerial(), m_dialog->getDiscCRC(), false, showAllCRCS, &number_of_unlabeled_patches);
std::vector<std::string> enabled_list =
si->GetStringList(Patch::PATCHES_CONFIG_SECTION, Patch::PATCH_ENABLE_CONFIG_KEY);
std::vector<std::string> disabled_list =
si->GetStringList(Patch::PATCHES_CONFIG_SECTION, Patch::PATCH_DISABLE_CONFIG_KEY);
const bool ws_patches_enabled_globally = m_dialog->getEffectiveBoolValue("EmuCore", "EnableWideScreenPatches", false);
const bool ni_patches_enabled_globally = m_dialog->getEffectiveBoolValue("EmuCore", "EnableNoInterlacingPatches", false);
setUnlabeledPatchesWarningVisibility(number_of_unlabeled_patches > 0);
setGlobalWsPatchNoteVisibility(ws_patches_enabled_globally);
setGlobalNiPatchNoteVisibility(ni_patches_enabled_globally);
delete m_ui.scrollArea->takeWidget();
m_ui.allCRCsCheckbox->setEnabled(!m_dialog->getSerial().empty());
QWidget* container = new QWidget(m_ui.scrollArea);
QVBoxLayout* layout = new QVBoxLayout(container);
layout->setContentsMargins(0, 0, 0, 0);
if (!patches.empty())
{
bool first = true;
for (const Patch::PatchInfo& pi : patches)
{
if (!first)
{
QFrame* frame = new QFrame(container);
frame->setFrameShape(QFrame::HLine);
frame->setFrameShadow(QFrame::Sunken);
layout->addWidget(frame);
}
else
{
first = false;
}
const bool is_on_enable_list = std::find(enabled_list.begin(), enabled_list.end(), pi.name) != enabled_list.end();
const bool is_on_disable_list = std::find(disabled_list.begin(), disabled_list.end(), pi.name) != disabled_list.end();
const bool globally_toggleable_option = Patch::IsGloballyToggleablePatch(pi);
Qt::CheckState check_state;
if (!globally_toggleable_option)
{
// Normal patches
check_state = is_on_enable_list && !is_on_disable_list ? Qt::CheckState::Checked : Qt::CheckState::Unchecked;
}
else
{
// WS/NI patches
if (is_on_disable_list)
{
check_state = Qt::CheckState::Unchecked;
}
else if (is_on_enable_list)
{
check_state = Qt::CheckState::Checked;
}
else
{
check_state = Qt::CheckState::PartiallyChecked;
}
}
GamePatchDetailsWidget* it =
new GamePatchDetailsWidget(std::move(pi.name), pi.author, pi.description, globally_toggleable_option, check_state, m_dialog, container);
layout->addWidget(it);
}
}
else
{
QLabel* label = new QLabel(tr("There are no patches available for this game."), container);
layout->addWidget(label);
}
layout->addStretch(1);
m_ui.scrollArea->setWidget(container);
}
void GamePatchSettingsWidget::setUnlabeledPatchesWarningVisibility(bool visible)
{
m_ui.unlabeledPatchWarning->setVisible(visible);
}
void GamePatchSettingsWidget::setGlobalWsPatchNoteVisibility(bool visible)
{
m_ui.globalWsPatchState->setVisible(visible);
}
void GamePatchSettingsWidget::setGlobalNiPatchNoteVisibility(bool visible)
{
m_ui.globalNiPatchState->setVisible(visible);
}

View File

@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QWidget>
#include "ui_GamePatchDetailsWidget.h"
#include "ui_GamePatchSettingsWidget.h"
#include "pcsx2/Patch.h"
namespace GameList
{
struct Entry;
}
class SettingsWindow;
class GamePatchDetailsWidget : public QWidget
{
Q_OBJECT
public:
GamePatchDetailsWidget(std::string name, const std::string& author, const std::string& description, bool tristate, Qt::CheckState checkState,
SettingsWindow* dialog, QWidget* parent);
~GamePatchDetailsWidget();
private Q_SLOTS:
void onEnabledStateChanged(int state);
private:
Ui::GamePatchDetailsWidget m_ui;
SettingsWindow* m_dialog;
std::string m_name;
};
class GamePatchSettingsWidget : public QWidget
{
Q_OBJECT
public:
GamePatchSettingsWidget(SettingsWindow* dialog, QWidget* parent);
void disableAllPatches();
~GamePatchSettingsWidget();
private Q_SLOTS:
void onReloadClicked();
private:
void reloadList();
void setUnlabeledPatchesWarningVisibility(bool visible);
void setGlobalWsPatchNoteVisibility(bool visible);
void setGlobalNiPatchNoteVisibility(bool visible);
Ui::GamePatchSettingsWidget m_ui;
SettingsWindow* m_dialog;
};

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GamePatchSettingsWidget</class>
<widget class="QWidget" name="GamePatchSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>766</width>
<height>392</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Activating game patches can cause unpredictable behavior, crashing, soft-locks, or broken saved games. Use patches at your own risk, the PCSX2 team will provide no support for users who have enabled game patches.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="unlabeledPatchWarning">
<property name="text">
<string>Any patches bundled with PCSX2 for this game will be disabled since you have unlabeled patches loaded.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="globalWsPatchState">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Widescreen patches are currently &lt;span style=&quot; font-weight:600;&quot;&gt;ENABLED&lt;/span&gt; globally.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="globalNiPatchState">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;No-Interlacing patches are currently &lt;span style=&quot; font-weight:600;&quot;&gt;ENABLED&lt;/span&gt; globally.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="allCRCsCheckbox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>All CRCs</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="reload">
<property name="text">
<string>Reload Patches</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,419 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "pcsx2/SIO/Pad/Pad.h"
#include "GameSummaryWidget.h"
#include "SettingsWindow.h"
#include "MainWindow.h"
#include "QtCompatibility.h"
#include "QtHost.h"
#include "QtProgressCallback.h"
#include "QtUtils.h"
#include "pcsx2/CDVD/IsoHasher.h"
#include "pcsx2/GameDatabase.h"
#include "pcsx2/GameList.h"
#include "common/Error.h"
#include "common/MD5Digest.h"
#include "common/ScopedGuard.h"
#include "common/StringUtil.h"
#include "fmt/format.h"
#include <QtCore/QDir>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QMessageBox>
GameSummaryWidget::GameSummaryWidget(const GameList::Entry* entry, SettingsWindow* dialog, QWidget* parent)
: m_dialog(dialog)
{
m_ui.setupUi(this);
const QString base_path(QtHost::GetResourcesBasePath());
for (int i = 0; i < m_ui.region->count(); i++)
{
m_ui.region->setItemIcon(i,
QIcon(QStringLiteral("%1/icons/flags/%2.svg").arg(base_path).arg(GameList::RegionToString(static_cast<GameList::Region>(i), false))));
}
m_entry_path = entry->path;
populateInputProfiles();
populateDetails(entry);
populateDiscPath(entry);
populateTrackList(entry);
connect(m_ui.inputProfile, &QComboBox::currentIndexChanged, this, &GameSummaryWidget::onInputProfileChanged);
connect(m_ui.verify, &QAbstractButton::clicked, this, &GameSummaryWidget::onVerifyClicked);
connect(m_ui.searchHash, &QAbstractButton::clicked, this, &GameSummaryWidget::onSearchHashClicked);
connect(m_ui.checkWiki, &QAbstractButton::clicked, this, [this, entry]() { onCheckWikiClicked(entry); });
bool has_custom_title = false, has_custom_region = false;
GameList::CheckCustomAttributesForPath(m_entry_path, has_custom_title, has_custom_region);
m_ui.restoreTitle->setEnabled(has_custom_title);
m_ui.restoreRegion->setEnabled(has_custom_region);
m_ui.checkWiki->setEnabled(!entry->serial.empty());
}
GameSummaryWidget::~GameSummaryWidget() = default;
void GameSummaryWidget::populateInputProfiles()
{
for (const std::string& name : Pad::GetInputProfileNames())
m_ui.inputProfile->addItem(QString::fromStdString(name));
}
void GameSummaryWidget::populateDetails(const GameList::Entry* entry)
{
m_ui.title->setText(QString::fromStdString(entry->title));
m_ui.titleSort->setText(QString::fromStdString(entry->title_sort));
m_ui.titleEN->setText(QString::fromStdString(entry->title_en));
m_ui.path->setText(QString::fromStdString(entry->path));
m_ui.serial->setText(QString::fromStdString(entry->serial));
m_ui.crc->setText(QString::fromStdString(fmt::format("{:08X}", entry->crc)));
m_ui.type->setCurrentIndex(static_cast<int>(entry->type));
m_ui.region->setCurrentIndex(static_cast<int>(entry->region));
//: First arg is a GameList compat; second is a string with space followed by star rating OR empty if Unknown compat
m_ui.compatibility->setText(
tr("%0%1")
.arg(GameList::EntryCompatibilityRatingToString(entry->compatibility_rating, true))
.arg([entry]() {
if (entry->compatibility_rating == GameList::CompatibilityRating::Unknown)
return QStringLiteral("");
const qsizetype compatibility_value = static_cast<qsizetype>(entry->compatibility_rating);
//: First arg is filled-in stars for game compatibility; second is empty stars; should be swapped for RTL languages
return tr(" %0%1").arg(QStringLiteral("").repeated(compatibility_value - 1)).arg(QStringLiteral("").repeated(6 - compatibility_value));
}()));
int row = 0;
m_ui.detailsFormLayout->getWidgetPosition(m_ui.titleSort, &row, nullptr);
setFormRowVisible(m_ui.detailsFormLayout, row, !entry->title_sort.empty());
m_ui.detailsFormLayout->getWidgetPosition(m_ui.titleEN, &row, nullptr);
setFormRowVisible(m_ui.detailsFormLayout, row, !entry->title_en.empty());
std::optional<std::string> profile(m_dialog->getStringValue("EmuCore", "InputProfileName", std::nullopt));
if (profile.has_value())
m_ui.inputProfile->setCurrentIndex(m_ui.inputProfile->findText(QString::fromStdString(profile.value())));
else
m_ui.inputProfile->setCurrentIndex(0);
connect(m_ui.title, &QLineEdit::editingFinished, this, [this]() {
if (m_ui.title->isModified())
{
setCustomTitle(m_ui.title->text().toStdString());
m_ui.title->setModified(false);
}
});
connect(m_ui.restoreTitle, &QAbstractButton::clicked, this, [this]() {
setCustomTitle("");
});
connect(m_ui.region, &QComboBox::currentIndexChanged, this, [this](int index) {
setCustomRegion(index);
});
connect(m_ui.restoreRegion, &QAbstractButton::clicked, this, [this]() {
setCustomRegion(-1);
});
}
void GameSummaryWidget::populateDiscPath(const GameList::Entry* entry)
{
if (entry->type == GameList::EntryType::ELF)
{
std::optional<std::string> iso_path(m_dialog->getStringValue("EmuCore", "DiscPath", std::nullopt));
if (iso_path.has_value() && !iso_path->empty())
m_ui.discPath->setText(QString::fromStdString(iso_path.value()));
connect(m_ui.discPath, &QLineEdit::textChanged, this, &GameSummaryWidget::onDiscPathChanged);
connect(m_ui.discPathBrowse, &QPushButton::clicked, this, &GameSummaryWidget::onDiscPathBrowseClicked);
connect(m_ui.discPathClear, &QPushButton::clicked, m_ui.discPath, &QLineEdit::clear);
}
else
{
// Makes no sense to have disc override for a disc.
int row = 0;
m_ui.detailsFormLayout->getWidgetPosition(m_ui.label_discPath, &row, nullptr);
m_ui.detailsFormLayout->removeRow(row);
m_ui.discPath = nullptr;
m_ui.discPathBrowse = nullptr;
m_ui.discPathClear = nullptr;
}
}
void GameSummaryWidget::onInputProfileChanged(int index)
{
if (index == 0)
m_dialog->setStringSettingValue("EmuCore", "InputProfileName", std::nullopt);
else
m_dialog->setStringSettingValue("EmuCore", "InputProfileName", m_ui.inputProfile->itemText(index).toUtf8());
}
void GameSummaryWidget::onDiscPathChanged(const QString& value)
{
if (value.isEmpty())
m_dialog->removeSettingValue("EmuCore", "DiscPath");
else
m_dialog->setStringSettingValue("EmuCore", "DiscPath", value.toStdString().c_str());
// force rescan of elf to update the serial
g_main_window->rescanFile(m_entry_path);
auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryForPath(m_entry_path.c_str());
if (entry)
{
populateDetails(entry);
m_dialog->setSerial(entry->serial);
m_ui.checkWiki->setEnabled(!entry->serial.empty());
}
}
void GameSummaryWidget::onDiscPathBrowseClicked()
{
const QString filename(QFileDialog::getOpenFileName(
QtUtils::GetRootWidget(this), tr("Select Disc Path"), QString(), qApp->translate("MainWindow", MainWindow::DISC_IMAGE_FILTER)));
if (filename.isEmpty())
return;
// let the signal take care of it
m_ui.discPath->setText(QDir::toNativeSeparators(filename));
}
void GameSummaryWidget::populateTrackList(const GameList::Entry* entry)
{
if (entry->type != GameList::EntryType::PS1Disc && entry->type != GameList::EntryType::PS2Disc)
{
m_ui.verify->setEnabled(false);
m_ui.verifyResult->setPlainText(tr("Game is not a CD/DVD."));
return;
}
if (QtHost::IsVMValid())
{
m_ui.verify->setEnabled(false);
m_ui.verifyResult->setPlainText(tr("Track list unavailable while virtual machine is running."));
return;
}
IsoHasher hasher;
Error error;
if (!hasher.Open(m_entry_path, &error))
{
m_ui.verify->setEnabled(false);
m_ui.verifyResult->setPlainText(QString::fromStdString(error.GetDescription()));
return;
}
const auto AddColumn = [this](const QString& text) {
QTableWidgetItem* item = new QTableWidgetItem(text);
const int column = m_ui.tracks->columnCount();
m_ui.tracks->insertColumn(column);
m_ui.tracks->setHorizontalHeaderItem(column, item);
};
const auto SetColumn = [this](int row, int column, const QString& text) {
QTableWidgetItem* item = new QTableWidgetItem(text);
m_ui.tracks->setItem(row, column, item);
};
// columns depend on CD vs DVD.
AddColumn(tr("#"));
if (hasher.IsCD())
{
AddColumn(tr("Mode"));
AddColumn(tr("Start"));
AddColumn(tr("Sectors"));
AddColumn(tr("Size"));
AddColumn(tr("MD5"));
AddColumn(tr("Status"));
}
else
{
AddColumn(tr("Start"));
AddColumn(tr("Sectors"));
AddColumn(tr("Size"));
AddColumn(tr("MD5"));
AddColumn(tr("Status"));
}
for (const IsoHasher::Track& track : hasher.GetTracks())
{
const int row = m_ui.tracks->rowCount();
m_ui.tracks->insertRow(row);
SetColumn(row, 0, tr("%1").arg(track.number));
if (hasher.IsCD())
{
SetColumn(row, 1, QtUtils::StringViewToQString(IsoHasher::GetTrackTypeString(track.type)));
SetColumn(row, 2, tr("%1").arg(track.start_lsn));
SetColumn(row, 3, tr("%1").arg(track.sectors));
SetColumn(row, 4, tr("%1").arg(track.size));
SetColumn(row, 5, tr("<not computed>"));
SetColumn(row, 6, QString());
}
else
{
SetColumn(row, 1, tr("%1").arg(track.start_lsn));
SetColumn(row, 2, tr("%1").arg(track.sectors));
SetColumn(row, 3, tr("%1").arg(track.size));
SetColumn(row, 4, tr("<not computed>"));
SetColumn(row, 5, QString());
}
}
if (hasher.IsCD())
QtUtils::ResizeColumnsForTableView(m_ui.tracks, {20, 60, 70, 70, 100, 220, 40});
else
QtUtils::ResizeColumnsForTableView(m_ui.tracks, {20, 100, 100, 100, 220, 40});
}
void GameSummaryWidget::onVerifyClicked()
{
// Can't do this while a VM is running because of stupid CDVD.
if (QtHost::IsVMValid())
{
QMessageBox::critical(QtUtils::GetRootWidget(this), tr("Error"), tr("Cannot verify image while a game is running."));
return;
}
IsoHasher hasher;
Error error;
if (!hasher.Open(m_entry_path, &error))
{
setVerifyResult(QString::fromStdString(error.GetDescription()));
return;
}
QtModalProgressCallback callback(this);
hasher.ComputeHashes(&callback);
if (callback.IsCancelled())
return;
const int hash_column = hasher.IsCD() ? 5 : 4;
int row = 0;
// convert to database format
std::vector<GameDatabase::TrackHash> thashes;
thashes.reserve(hasher.GetTrackCount());
for (const IsoHasher::Track& track : hasher.GetTracks())
{
GameDatabase::TrackHash thash;
thash.size = track.size;
if (track.hash.empty() || !thash.parseHash(track.hash))
{
m_ui.verify->setEnabled(false);
m_ui.verifyResult->setPlainText(tr("One or more tracks is missing."));
return;
}
// Use the first track's hash as the redump search term.
if (m_redump_search_keyword.empty())
m_redump_search_keyword = thash.toString();
thashes.push_back(thash);
}
// match the hashes. can't use vector<bool> here because it's not an actual array
std::unique_ptr<bool[]> val_results = std::make_unique<bool[]>(hasher.GetTrackCount());
std::string match_error;
const GameDatabase::HashDatabaseEntry* hentry =
GameDatabase::lookupHash(thashes.data(), thashes.size(), val_results.get(), &match_error);
// fill the UI with both the hashes and validation results
for (u32 i = 0; i < hasher.GetTrackCount(); i++)
{
QTableWidgetItem* const hash_item = m_ui.tracks->item(row, hash_column);
QTableWidgetItem* const status_item = m_ui.tracks->item(row, hash_column + 1);
const bool result = val_results[i];
const QBrush brush(result ? QColor(0, 200, 0) : QColor(200, 0, 0));
hash_item->setText(QString::fromStdString(hasher.GetTrack(i).hash));
hash_item->setForeground(brush);
status_item->setText(result ? QStringLiteral("\u2713") : QStringLiteral("\u2715"));
status_item->setForeground(brush);
row++;
}
if (hentry)
{
if (!hentry->version.empty())
{
setVerifyResult(tr("Verified as %1 [%2] (Version %3).")
.arg(QString::fromStdString(hentry->name))
.arg(QString::fromStdString(hentry->serial))
.arg(QString::fromStdString(hentry->version)));
}
else
{
setVerifyResult(tr("Verified as %1 [%2].")
.arg(QString::fromStdString(hentry->name))
.arg(QString::fromStdString(hentry->serial)));
}
}
else
{
setVerifyResult(QString::fromStdString(match_error));
}
}
void GameSummaryWidget::onSearchHashClicked()
{
if (m_redump_search_keyword.empty())
return;
QtUtils::OpenURL(this, fmt::format("http://redump.org/discs/quicksearch/{}", m_redump_search_keyword).c_str());
}
void GameSummaryWidget::onCheckWikiClicked(const GameList::Entry* entry)
{
QtUtils::OpenURL(this, fmt::format("https://wiki.pcsx2.net/{}", entry->serial).c_str());
}
void GameSummaryWidget::setVerifyResult(QString error)
{
m_ui.verify->setVisible(false);
m_ui.verifyButtonLayout->removeWidget(m_ui.verify);
m_ui.verify->deleteLater();
m_ui.verify = nullptr;
m_ui.verifyButtonLayout->removeItem(m_ui.verifyButtonSpacer);
delete m_ui.verifyButtonSpacer;
m_ui.verifyButtonSpacer = nullptr;
m_ui.verifyLayout->removeItem(m_ui.verifyButtonLayout);
m_ui.verifyButtonLayout->deleteLater();
m_ui.verifyButtonLayout = nullptr;
m_ui.verifyLayout->update();
updateGeometry();
m_ui.verifyResult->setPlainText(error);
m_ui.verifyResult->setVisible(true);
m_ui.searchHash->setVisible(true);
}
void GameSummaryWidget::repopulateCurrentDetails()
{
auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryForPath(m_entry_path.c_str());
if (entry)
{
populateDetails(entry);
m_dialog->setWindowTitle(QString::fromStdString(entry->title));
}
}
void GameSummaryWidget::setCustomTitle(const std::string& text)
{
m_ui.restoreTitle->setEnabled(!text.empty());
GameList::SaveCustomTitleForPath(m_entry_path, text);
repopulateCurrentDetails();
}
void GameSummaryWidget::setCustomRegion(int region)
{
m_ui.restoreRegion->setEnabled(region >= 0);
GameList::SaveCustomRegionForPath(m_entry_path, region);
repopulateCurrentDetails();
}

View File

@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QWidget>
#include "ui_GameSummaryWidget.h"
namespace GameList
{
struct Entry;
}
class SettingsWindow;
class GameSummaryWidget : public QWidget
{
Q_OBJECT
public:
GameSummaryWidget(const GameList::Entry* entry, SettingsWindow* dialog, QWidget* parent);
~GameSummaryWidget();
private Q_SLOTS:
void onInputProfileChanged(int index);
void onDiscPathChanged(const QString& value);
void onDiscPathBrowseClicked();
void onVerifyClicked();
void onSearchHashClicked();
void onCheckWikiClicked(const GameList::Entry* entry);
private:
void populateInputProfiles();
void populateDetails(const GameList::Entry* entry);
void populateDiscPath(const GameList::Entry* entry);
void populateTrackList(const GameList::Entry* entry);
void setVerifyResult(QString error);
void repopulateCurrentDetails();
void setCustomTitle(const std::string& text);
void setCustomRegion(int region);
Ui::GameSummaryWidget m_ui;
SettingsWindow* m_dialog;
std::string m_entry_path;
std::string m_redump_search_keyword;
};

View File

@@ -0,0 +1,536 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GameSummaryWidget</class>
<widget class="QWidget" name="GameSummaryWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>641</width>
<height>573</height>
</rect>
</property>
<layout class="QFormLayout" name="detailsFormLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::FieldGrowthPolicy::ExpandingFieldsGrow</enum>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_title">
<property name="text">
<string>Title:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="titleLayout">
<item>
<widget class="QLineEdit" name="title">
<property name="placeholderText">
<string>Clear the line to restore the original title...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="restoreTitle">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Restore</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_titleSort">
<property name="text">
<string extracomment="Name for use in sorting (e.g. &quot;XXX, The&quot; for a game called &quot;The XXX&quot;)">Sorting Title:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="titleSort">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_titleEN">
<property name="text">
<string>English Title:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="titleEN">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_path">
<property name="text">
<string>Path:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="path">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_serial">
<property name="text">
<string>Serial:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="serialLayout">
<item>
<widget class="QLineEdit" name="serial">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="checkWiki">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Check Wiki</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_crc">
<property name="text">
<string>CRC:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="crc">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_type">
<property name="text">
<string>Type:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QComboBox" name="type">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>true</bool>
</property>
<item>
<property name="text">
<string>PS2 Disc</string>
</property>
<property name="icon">
<iconset theme="disc-2-line"/>
</property>
</item>
<item>
<property name="text">
<string>PS1 Disc</string>
</property>
<property name="icon">
<iconset theme="disc-2-line"/>
</property>
</item>
<item>
<property name="text">
<string>ELF (PS2 Executable)</string>
</property>
<property name="icon">
<iconset theme="file-settings-line"/>
</property>
</item>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_region">
<property name="text">
<string>Region:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<layout class="QHBoxLayout" name="regionLayout">
<item>
<widget class="QComboBox" name="region">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-B (Brazil)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-C (China)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-HK (Hong Kong)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-J (Japan)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-K (Korea)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-T (Taiwan)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-U (US)</string>
</property>
</item>
<item>
<property name="text">
<string>Other</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-A (Australia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-AF (South Africa)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-AU (Austria)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-BE (Belgium)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-E (Europe/Australia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-F (France)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-FI (Finland)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-G (Germany)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-GR (Greece)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-I (Italy)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-IN (India)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-M (Europe/Australia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-NL (Netherlands)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-NO (Norway)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-P (Portugal)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-PL (Poland)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-R (Russia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-S (Spain)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-SC (Scandinavia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-SW (Sweden)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-SWI (Switzerland)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-UK (United Kingdom)</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QPushButton" name="restoreRegion">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Restore</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_compat">
<property name="text">
<string>Compatibility:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QLineEdit" name="compatibility">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_profile">
<property name="text">
<string>Input Profile:</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QComboBox" name="inputProfile">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string extracomment="Refers to the shared settings profile.">Shared</string>
</property>
</item>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_discPath">
<property name="text">
<string>Disc Path:</string>
</property>
</widget>
</item>
<item row="10" column="1">
<layout class="QHBoxLayout" name="discLayout">
<item>
<widget class="QLineEdit" name="discPath"/>
</item>
<item>
<widget class="QPushButton" name="discPathBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="discPathClear">
<property name="text">
<string>Clear</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="11" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="12" column="0" colspan="2">
<layout class="QVBoxLayout" name="verifyLayout">
<item>
<widget class="QTableWidget" name="tracks">
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="cornerButtonEnabled">
<bool>false</bool>
</property>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="verifyButtonLayout">
<item>
<spacer name="verifyButtonSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="verify">
<property name="text">
<string>Verify</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="2">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0" rowspan="3">
<widget class="QPlainTextEdit" name="verifyResult">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>60</height>
</size>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="searchHash">
<property name="visible">
<bool>false</bool>
</property>
<property name="text">
<string>Search on Redump.org...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QWidget>
#include "ui_GraphicsSettingsWidget.h"
#include "common/Pcsx2Defs.h"
enum class GSRendererType : s8;
class SettingsWindow;
class GraphicsSettingsWidget : public QWidget
{
Q_OBJECT
public:
GraphicsSettingsWidget(SettingsWindow* dialog, QWidget* parent);
~GraphicsSettingsWidget();
Q_SIGNALS:
void fullscreenModesChanged(const QStringList& modes);
private Q_SLOTS:
void onTextureFilteringChange();
void onSWTextureFilteringChange();
void onRendererChanged(int index);
void onAdapterChanged(int index);
void onUpscaleMultiplierChanged();
void onTrilinearFilteringChanged();
void onGpuPaletteConversionChanged(int state);
void onCPUSpriteRenderBWChanged();
void onFullscreenModeChanged(int index);
void onTextureDumpChanged();
void onTextureReplacementChanged();
void onShadeBoostChanged();
void onMessagesPosChanged();
void onPerformancePosChanged();
void onCaptureContainerChanged();
void onCaptureCodecChanged();
void onEnableVideoCaptureChanged();
void onEnableVideoCaptureArgumentsChanged();
void onVideoCaptureAutoResolutionChanged();
void onEnableAudioCaptureChanged();
void onEnableAudioCaptureArgumentsChanged();
private:
GSRendererType getEffectiveRenderer() const;
void updateRendererDependentOptions();
void populateUpscaleMultipliers(u32 max_upscale_multiplier);
void resetManualHardwareFixes();
SettingsWindow* m_dialog;
Ui::GraphicsSettingsWidget m_ui;
bool m_hardware_renderer_visible = false;
bool m_software_renderer_visible = false;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include <QtWidgets/QMessageBox>
#include "HddCreateQt.h"
HddCreateQt::HddCreateQt(QWidget* parent)
: m_parent{parent}
, progressDialog{nullptr}
{
}
void HddCreateQt::Init()
{
reqMiB = (neededSize + ((1024 * 1024) - 1)) / (1024 * 1024);
progressDialog = new QProgressDialog(QObject::tr("Creating HDD file \n %1 / %2 MiB").arg(0).arg(reqMiB), QObject::tr("Cancel"), 0, reqMiB, m_parent);
progressDialog->setWindowTitle("HDD Creator");
progressDialog->setWindowModality(Qt::WindowModal);
}
void HddCreateQt::SetFileProgress(u64 currentSize)
{
const int writtenMB = (currentSize + ((1024 * 1024) - 1)) / (1024 * 1024);
progressDialog->setValue(writtenMB);
progressDialog->setLabelText(QObject::tr("Creating HDD file \n %1 / %2 MiB").arg(writtenMB).arg(reqMiB));
if (progressDialog->wasCanceled())
SetCanceled();
}
void HddCreateQt::SetError()
{
QMessageBox::warning(progressDialog, QObject::tr("HDD Creator"),
QObject::tr("Failed to create HDD image"),
QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
}
void HddCreateQt::Cleanup()
{
delete progressDialog;
}

View File

@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QProgressDialog>
#include "DEV9/ATA/HddCreate.h"
class HddCreateQt : public HddCreate
{
public:
HddCreateQt(QWidget* parent);
virtual ~HddCreateQt(){};
private:
QWidget* m_parent;
QProgressDialog* progressDialog;
int reqMiB;
protected:
virtual void Init();
virtual void Cleanup();
virtual void SetFileProgress(u64 currentSize);
virtual void SetError();
};

View File

@@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "Settings/HotkeySettingsWidget.h"
#include "Settings/ControllerSettingsWindow.h"
#include "InputBindingWidget.h"
#include "QtUtils.h"
#include "SettingWidgetBinder.h"
#include "pcsx2/Input/InputManager.h"
#include <QtWidgets/QGridLayout>
#include <QtWidgets/QLabel>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QScrollArea>
#include <QtWidgets/QVBoxLayout>
HotkeySettingsWidget::HotkeySettingsWidget(QWidget* parent, ControllerSettingsWindow* dialog)
: QWidget(parent)
, m_dialog(dialog)
{
createUi();
}
HotkeySettingsWidget::~HotkeySettingsWidget() = default;
void HotkeySettingsWidget::createUi()
{
QGridLayout* layout = new QGridLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
m_scroll_area = new QScrollArea(this);
m_container = new QWidget(m_scroll_area);
m_layout = new QVBoxLayout(m_container);
m_scroll_area->setWidget(m_container);
m_scroll_area->setWidgetResizable(true);
m_scroll_area->setBackgroundRole(QPalette::Base);
createButtons();
layout->addWidget(m_scroll_area, 0, 0, 1, 1);
setLayout(layout);
}
void HotkeySettingsWidget::createButtons()
{
const std::vector<const HotkeyInfo*> hotkeys(InputManager::GetHotkeyList());
for (const HotkeyInfo* hotkey : hotkeys)
{
const QString category(qApp->translate("Hotkeys", hotkey->category));
auto iter = m_categories.find(category);
if (iter == m_categories.end())
{
QLabel* label = new QLabel(category, m_container);
QFont label_font(label->font());
label_font.setPointSizeF(14.0f);
label->setFont(label_font);
m_layout->addWidget(label);
QLabel* line = new QLabel(m_container);
line->setFrameShape(QFrame::HLine);
line->setFixedHeight(4);
m_layout->addWidget(line);
QGridLayout* layout = new QGridLayout();
layout->setContentsMargins(0, 0, 0, 0);
m_layout->addLayout(layout);
iter = m_categories.insert(category, layout);
}
QGridLayout* layout = *iter;
const int target_row = layout->count() / 2;
QLabel* label = new QLabel(qApp->translate("Hotkeys", hotkey->display_name), m_container);
layout->addWidget(label, target_row, 0);
InputBindingWidget* bind = new InputBindingWidget(
m_container, m_dialog->getProfileSettingsInterface(), InputBindingInfo::Type::Button, "Hotkeys", hotkey->name);
bind->setMinimumWidth(300);
layout->addWidget(bind, target_row, 1);
}
// Fill remaining space.
m_layout->addStretch(1);
}

View File

@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QWidget>
#include <QtCore/QMap>
#include <array>
#include <vector>
class QScrollArea;
class QGridLayout;
class QVBoxLayout;
class ControllerSettingsWindow;
class HotkeySettingsWidget : public QWidget
{
Q_OBJECT
public:
HotkeySettingsWidget(QWidget* parent, ControllerSettingsWindow* dialog);
~HotkeySettingsWidget();
private:
void createUi();
void createButtons();
ControllerSettingsWindow* m_dialog;
QScrollArea* m_scroll_area = nullptr;
QWidget* m_container = nullptr;
QVBoxLayout* m_layout = nullptr;
QMap<QString, QGridLayout*> m_categories;
};

View File

@@ -0,0 +1,386 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "QtHost.h"
#include "QtUtils.h"
#include "Settings/ControllerSettingWidgetBinder.h"
#include "Settings/InputBindingDialog.h"
#include "Settings/InputBindingWidget.h"
#include <QtCore/QTimer>
#include <QtGui/QKeyEvent>
#include <QtGui/QMouseEvent>
#include <QtGui/QWheelEvent>
#include "fmt/format.h"
#include <bit>
InputBindingDialog::InputBindingDialog(SettingsInterface* sif, InputBindingInfo::Type bind_type, std::string section_name,
std::string key_name, std::vector<std::string> bindings_settings, std::vector<std::string> bindings_ui, QWidget* parent)
: QDialog(parent)
, m_sif(sif)
, m_bind_type(bind_type)
, m_section_name(std::move(section_name))
, m_key_name(std::move(key_name))
, m_bindings_settings(std::move(bindings_settings))
, m_bindings_ui(std::move(bindings_ui))
{
m_ui.setupUi(this);
m_ui.title->setText(tr("Bindings for %1 %2").arg(QString::fromStdString(m_section_name)).arg(QString::fromStdString(m_key_name)));
m_ui.buttonBox->button(QDialogButtonBox::Close)->setText(tr("Close"));
connect(m_ui.addBinding, &QPushButton::clicked, this, &InputBindingDialog::onAddBindingButtonClicked);
connect(m_ui.removeBinding, &QPushButton::clicked, this, &InputBindingDialog::onRemoveBindingButtonClicked);
connect(m_ui.clearBindings, &QPushButton::clicked, this, &InputBindingDialog::onClearBindingsButtonClicked);
connect(m_ui.buttonBox, &QDialogButtonBox::rejected, [this]() { done(0); });
connect(g_emu_thread, &EmuThread::onInputDeviceConnected, this, &InputBindingDialog::onInputDeviceConnected);
connect(g_emu_thread, &EmuThread::onInputDeviceDisconnected, this, &InputBindingDialog::onInputDeviceDisconnected);
updateList();
// Only show the sensitivity controls for binds where it's applicable.
if (bind_type == InputBindingInfo::Type::Button || bind_type == InputBindingInfo::Type::Axis ||
bind_type == InputBindingInfo::Type::HalfAxis)
{
ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(
sif, m_ui.sensitivity, m_section_name, fmt::format("{}Scale", m_key_name), 100.0f, 1.0f);
ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(
sif, m_ui.deadzone, m_section_name, fmt::format("{}Deadzone", m_key_name), 100.0f, 0.0f);
connect(m_ui.sensitivity, &QSlider::valueChanged, this, &InputBindingDialog::onSensitivityChanged);
connect(m_ui.deadzone, &QSlider::valueChanged, this, &InputBindingDialog::onDeadzoneChanged);
onSensitivityChanged(m_ui.sensitivity->value());
onDeadzoneChanged(m_ui.deadzone->value());
}
else
{
m_ui.verticalLayout->removeWidget(m_ui.sensitivityWidget);
delete m_ui.sensitivityWidget;
m_ui.sensitivityWidget = nullptr;
}
}
InputBindingDialog::~InputBindingDialog()
{
Q_ASSERT(!isListeningForInput());
}
bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event)
{
const QEvent::Type event_type = event->type();
// if the key is being released, set the input
if (event_type == QEvent::KeyRelease || event_type == QEvent::MouseButtonRelease)
{
addNewBinding();
stopListeningForInput();
return true;
}
else if (event_type == QEvent::KeyPress)
{
const QKeyEvent* key_event = static_cast<const QKeyEvent*>(event);
m_new_bindings.push_back(InputManager::MakeHostKeyboardKey(QtUtils::KeyEventToCode(key_event)));
return true;
}
else if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonDblClick)
{
// double clicks get triggered if we click bind, then click again quickly.
if (const u32 button_mask = static_cast<u32>(static_cast<const QMouseEvent*>(event)->button()))
m_new_bindings.push_back(InputManager::MakePointerButtonKey(0, std::countr_zero(button_mask)));
return true;
}
else if (event_type == QEvent::Wheel)
{
const QPoint delta_angle(static_cast<QWheelEvent*>(event)->angleDelta());
const float dx = std::clamp(static_cast<float>(delta_angle.x()) / QtUtils::MOUSE_WHEEL_DELTA, -1.0f, 1.0f);
if (dx != 0.0f)
{
InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelX));
key.modifier = dx < 0.0f ? InputModifier::Negate : InputModifier::None;
m_new_bindings.push_back(key);
}
const float dy = std::clamp(static_cast<float>(delta_angle.y()) / QtUtils::MOUSE_WHEEL_DELTA, -1.0f, 1.0f);
if (dy != 0.0f)
{
InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelY));
key.modifier = dy < 0.0f ? InputModifier::Negate : InputModifier::None;
m_new_bindings.push_back(key);
}
if (dx != 0.0f || dy != 0.0f)
{
addNewBinding();
stopListeningForInput();
}
return true;
}
else if (event_type == QEvent::MouseMove && m_mouse_mapping_enabled)
{
// if we've moved more than a decent distance from the center of the widget, bind it.
// this is so we don't accidentally bind to the mouse if you bump it while reaching for your pad.
static constexpr const s32 THRESHOLD = 50;
const QPoint diff(static_cast<QMouseEvent*>(event)->globalPosition().toPoint() - m_input_listen_start_position);
bool has_one = false;
if (std::abs(diff.x()) >= THRESHOLD)
{
InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::X));
key.modifier = diff.x() < 0 ? InputModifier::Negate : InputModifier::None;
m_new_bindings.push_back(key);
has_one = true;
}
if (std::abs(diff.y()) >= THRESHOLD)
{
InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::Y));
key.modifier = diff.y() < 0 ? InputModifier::Negate : InputModifier::None;
m_new_bindings.push_back(key);
has_one = true;
}
if (has_one)
{
addNewBinding();
stopListeningForInput();
return true;
}
}
return false;
}
void InputBindingDialog::onInputListenTimerTimeout()
{
m_input_listen_remaining_seconds--;
if (m_input_listen_remaining_seconds == 0)
{
stopListeningForInput();
return;
}
m_ui.status->setText(tr("Push Button/Axis... [%1]").arg(m_input_listen_remaining_seconds));
}
void InputBindingDialog::startListeningForInput(u32 timeout_in_seconds)
{
m_value_ranges.clear();
m_new_bindings.clear();
m_mouse_mapping_enabled = InputBindingWidget::isMouseMappingEnabled(m_sif);
m_input_listen_start_position = QCursor::pos();
m_input_listen_timer = new QTimer(this);
m_input_listen_timer->setSingleShot(false);
m_input_listen_timer->start(1000);
m_input_listen_timer->connect(m_input_listen_timer, &QTimer::timeout, this, &InputBindingDialog::onInputListenTimerTimeout);
m_input_listen_remaining_seconds = timeout_in_seconds;
m_ui.status->setText(tr("Push Button/Axis... [%1]").arg(m_input_listen_remaining_seconds));
m_ui.addBinding->setEnabled(false);
m_ui.removeBinding->setEnabled(false);
m_ui.clearBindings->setEnabled(false);
m_ui.buttonBox->setEnabled(false);
installEventFilter(this);
grabKeyboard();
grabMouse();
setMouseTracking(true);
hookInputManager();
}
void InputBindingDialog::stopListeningForInput()
{
m_ui.status->clear();
m_ui.addBinding->setEnabled(true);
m_ui.removeBinding->setEnabled(true);
m_ui.clearBindings->setEnabled(true);
m_ui.buttonBox->setEnabled(true);
delete m_input_listen_timer;
m_input_listen_timer = nullptr;
unhookInputManager();
releaseMouse();
releaseKeyboard();
setMouseTracking(false);
removeEventFilter(this);
}
void InputBindingDialog::addNewBinding()
{
if (m_new_bindings.empty())
return;
const std::string new_binding(InputManager::ConvertInputBindingKeysToString(m_bind_type, m_new_bindings.data(), m_new_bindings.size()));
if (!new_binding.empty())
{
if (std::find(m_bindings_settings.begin(), m_bindings_settings.end(), new_binding) != m_bindings_settings.end())
return;
m_bindings_settings.push_back(std::move(new_binding));
SmallString new_binding_temp{std::string_view{new_binding}};
InputManager::PrettifyInputBinding(new_binding_temp, false);
std::string new_binding_ui{new_binding_temp};
m_bindings_ui.push_back(new_binding_ui);
m_ui.bindingList->addItem(QString::fromStdString(new_binding_ui));
saveListToSettings();
}
}
void InputBindingDialog::onAddBindingButtonClicked()
{
if (isListeningForInput())
stopListeningForInput();
startListeningForInput(TIMEOUT_FOR_BINDING);
}
void InputBindingDialog::onRemoveBindingButtonClicked()
{
const int row = m_ui.bindingList->currentRow();
if (row < 0 || static_cast<size_t>(row) >= m_bindings_ui.size())
return;
m_bindings_settings.erase(m_bindings_settings.begin() + row);
m_bindings_ui.erase(m_bindings_ui.begin() + row);
delete m_ui.bindingList->takeItem(row);
saveListToSettings();
}
void InputBindingDialog::onClearBindingsButtonClicked()
{
m_bindings_settings.clear();
m_bindings_ui.clear();
m_ui.bindingList->clear();
saveListToSettings();
}
void InputBindingDialog::updateList()
{
m_ui.bindingList->clear();
for (const std::string& binding : m_bindings_ui)
m_ui.bindingList->addItem(QString::fromStdString(binding));
}
void InputBindingDialog::saveListToSettings()
{
if (m_sif)
{
if (!m_bindings_settings.empty())
m_sif->SetStringList(m_section_name.c_str(), m_key_name.c_str(), m_bindings_settings);
else
m_sif->DeleteValue(m_section_name.c_str(), m_key_name.c_str());
m_sif->Save();
g_emu_thread->reloadGameSettings();
}
else
{
if (!m_bindings_settings.empty())
Host::SetBaseStringListSettingValue(m_section_name.c_str(), m_key_name.c_str(), m_bindings_settings);
else
Host::RemoveBaseSettingValue(m_section_name.c_str(), m_key_name.c_str());
Host::CommitBaseSettingChanges();
g_emu_thread->reloadInputBindings();
}
}
void InputBindingDialog::inputManagerHookCallback(InputBindingKey key, float value)
{
if (!isListeningForInput())
return;
float initial_value = value;
float min_value = value;
auto it = std::find_if(m_value_ranges.begin(), m_value_ranges.end(), [key](const auto& it) { return it.first.bits == key.bits; });
if (it != m_value_ranges.end())
{
initial_value = it->second.first;
min_value = it->second.second = std::min(it->second.second, value);
}
else
{
m_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value));
}
const float abs_value = std::abs(value);
const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f);
for (InputBindingKey& other_key : m_new_bindings)
{
if (other_key.MaskDirection() == key.MaskDirection())
{
// for pedals, we wait for it to go back to near its starting point to commit the binding
if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f)))
{
// did we go the full range?
if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f)
other_key.modifier = InputModifier::FullAxis;
// if this key is in our new binding list, it's a "release", and we're done
addNewBinding();
stopListeningForInput();
return;
}
// otherwise, keep waiting
return;
}
}
// new binding, add it to the list, but wait for a decent distance first, and then wait for release
if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f)))
{
InputBindingKey key_to_add = key;
key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None;
key_to_add.invert = reverse_threshold;
m_new_bindings.push_back(key_to_add);
}
}
void InputBindingDialog::onSensitivityChanged(int value)
{
m_ui.sensitivityValue->setText(tr("%1%").arg(value));
}
void InputBindingDialog::onDeadzoneChanged(int value)
{
m_ui.deadzoneValue->setText(tr("%1%").arg(value));
}
void InputBindingDialog::onInputDeviceConnected(const QString& identifier, const QString& device_name)
{
ReloadBindNames();
}
void InputBindingDialog::onInputDeviceDisconnected(const QString& identifier)
{
ReloadBindNames();
}
void InputBindingDialog::hookInputManager()
{
InputManager::SetHook([this](InputBindingKey key, float value) {
QMetaObject::invokeMethod(this, "inputManagerHookCallback", Qt::QueuedConnection, Q_ARG(InputBindingKey, key), Q_ARG(float, value));
return InputInterceptHook::CallbackResult::StopProcessingEvent;
});
}
void InputBindingDialog::unhookInputManager()
{
InputManager::RemoveHook();
}
void InputBindingDialog::ReloadBindNames()
{
m_bindings_ui.clear();
m_bindings_ui.reserve(m_bindings_settings.size());
for (size_t i = 0; i < m_bindings_settings.size(); i++)
{
SmallString binding{std::string_view{m_bindings_settings[i]}};
InputManager::PrettifyInputBinding(binding, false);
m_bindings_ui.push_back(std::string{binding});
}
updateList();
}

View File

@@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "ui_InputBindingDialog.h"
#include "pcsx2/Config.h"
#include "pcsx2/Input/InputManager.h"
#include <QtWidgets/QDialog>
#include <optional>
#include <string>
#include <utility>
#include <vector>
class SettingsInterface;
class InputBindingDialog : public QDialog
{
Q_OBJECT
public:
InputBindingDialog(SettingsInterface* sif, InputBindingInfo::Type bind_type, std::string section_name, std::string key_name,
std::vector<std::string> bindings_settings, std::vector<std::string> bindings_ui, QWidget* parent);
~InputBindingDialog();
protected Q_SLOTS:
void onAddBindingButtonClicked();
void onRemoveBindingButtonClicked();
void onClearBindingsButtonClicked();
void onInputListenTimerTimeout();
void inputManagerHookCallback(InputBindingKey key, float value);
void onSensitivityChanged(int value);
void onDeadzoneChanged(int value);
void onInputDeviceConnected(const QString& identifier, const QString& device_name);
void onInputDeviceDisconnected(const QString& identifier);
protected:
enum : u32
{
TIMEOUT_FOR_BINDING = 5
};
virtual bool eventFilter(QObject* watched, QEvent* event) override;
virtual void startListeningForInput(u32 timeout_in_seconds);
virtual void stopListeningForInput();
bool isListeningForInput() const { return m_input_listen_timer != nullptr; }
void addNewBinding();
void updateList();
void saveListToSettings();
void hookInputManager();
void unhookInputManager();
void ReloadBindNames();
Ui::InputBindingDialog m_ui;
SettingsInterface* m_sif;
InputBindingInfo::Type m_bind_type;
std::string m_section_name;
std::string m_key_name;
std::vector<std::string> m_bindings_settings;
std::vector<std::string> m_bindings_ui;
std::vector<InputBindingKey> m_new_bindings;
std::vector<std::pair<InputBindingKey, std::pair<float, float>>> m_value_ranges;
QTimer* m_input_listen_timer = nullptr;
u32 m_input_listen_remaining_seconds = 0;
QPoint m_input_listen_start_position{};
bool m_mouse_mapping_enabled = false;
};

View File

@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>InputBindingDialog</class>
<widget class="QDialog" name="InputBindingDialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>533</width>
<height>266</height>
</rect>
</property>
<property name="windowTitle">
<string>Edit Bindings</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="title">
<property name="text">
<string>Bindings for Controller0/ButtonCircle</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="bindingList"/>
</item>
<item>
<widget class="QWidget" name="sensitivityWidget" native="true">
<layout class="QGridLayout" name="sensitivityLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="sensitivityLabel">
<property name="text">
<string>Sensitivity:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSlider" name="sensitivity">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>200</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="sensitivityValue">
<property name="text">
<string>100%</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="deadzoneLabel">
<property name="text">
<string>Deadzone:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSlider" name="deadzone">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>1</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>5</number>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="deadzoneValue">
<property name="text">
<string>100%</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="status">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="addBinding">
<property name="text">
<string>Add Binding</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeBinding">
<property name="text">
<string>Remove Binding</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="clearBindings">
<property name="text">
<string>Clear Bindings</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,529 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include <QtCore/QTimer>
#include <QtGui/QKeyEvent>
#include <QtGui/QMouseEvent>
#include <QtGui/QWheelEvent>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QMessageBox>
#include <bit>
#include <cmath>
#include <sstream>
#include "pcsx2/Host.h"
#include "QtHost.h"
#include "QtUtils.h"
#include "Settings/ControllerSettingsWindow.h"
#include "Settings/InputBindingDialog.h"
#include "Settings/InputBindingWidget.h"
InputBindingWidget::InputBindingWidget(QWidget* parent)
: QPushButton(parent)
{
connect(this, &QPushButton::clicked, this, &InputBindingWidget::onClicked);
connect(g_emu_thread, &EmuThread::onInputDeviceConnected, this, &InputBindingWidget::onInputDeviceConnected);
connect(g_emu_thread, &EmuThread::onInputDeviceDisconnected, this, &InputBindingWidget::onInputDeviceDisconnected);
}
InputBindingWidget::InputBindingWidget(
QWidget* parent, SettingsInterface* sif, InputBindingInfo::Type bind_type, std::string section_name, std::string key_name)
: QPushButton(parent)
{
setMinimumWidth(225);
setMaximumWidth(225);
connect(this, &QPushButton::clicked, this, &InputBindingWidget::onClicked);
connect(g_emu_thread, &EmuThread::onInputDeviceConnected, this, &InputBindingWidget::onInputDeviceConnected);
connect(g_emu_thread, &EmuThread::onInputDeviceDisconnected, this, &InputBindingWidget::onInputDeviceDisconnected);
initialize(sif, bind_type, std::move(section_name), std::move(key_name));
}
InputBindingWidget::~InputBindingWidget()
{
Q_ASSERT(!isListeningForInput());
}
bool InputBindingWidget::isMouseMappingEnabled(SettingsInterface* sif)
{
return sif ? sif->GetBoolValue("UI", "EnableMouseMapping", false) : Host::GetBaseBoolSettingValue("UI", "EnableMouseMapping", false);
}
void InputBindingWidget::initialize(
SettingsInterface* sif, InputBindingInfo::Type bind_type, std::string section_name, std::string key_name)
{
m_sif = sif;
m_bind_type = bind_type;
m_section_name = std::move(section_name);
m_key_name = std::move(key_name);
reloadBinding();
}
void InputBindingWidget::updateText()
{
const QString binding_tip(tr("\n\nLeft click to assign a new button\nShift + left click for additional bindings"));
const QString binding_clear_tip(tr("\nRight click to clear binding"));
if (m_bindings_ui.empty())
{
setText(QString());
setToolTip(tr("No bindings registered") + binding_tip);
}
else if (m_bindings_ui.size() > 1)
{
setText(tr("%n bindings", "", static_cast<int>(m_bindings_ui.size())));
// keep the full thing for the tooltip
std::stringstream ss;
bool first = true;
for (const std::string& binding : m_bindings_ui)
{
if (first)
first = false;
else
ss << "\n";
ss << binding;
}
setToolTip(QString::fromStdString(ss.str()) + binding_tip + binding_clear_tip);
}
else
{
QString binding_text(QString::fromStdString(m_bindings_ui[0]));
setToolTip(binding_text + binding_tip + binding_clear_tip);
// fix up accelerators, and if it's too long, ellipsise it
if (binding_text.contains('&'))
binding_text = binding_text.replace(QStringLiteral("&"), QStringLiteral("&&"));
if (binding_text.length() > 35)
binding_text = binding_text.left(35).append(QStringLiteral("..."));
setText(binding_text);
}
}
bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event)
{
const QEvent::Type event_type = event->type();
// if the key is being released, set the input
if (event_type == QEvent::KeyRelease || event_type == QEvent::MouseButtonRelease)
{
setNewBinding();
stopListeningForInput();
return true;
}
else if (event_type == QEvent::KeyPress)
{
const QKeyEvent* key_event = static_cast<const QKeyEvent*>(event);
m_new_bindings.push_back(InputManager::MakeHostKeyboardKey(QtUtils::KeyEventToCode(key_event)));
return true;
}
else if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonDblClick)
{
// double clicks get triggered if we click bind, then click again quickly.
if (const u32 button_mask = static_cast<u32>(static_cast<const QMouseEvent*>(event)->button()))
m_new_bindings.push_back(InputManager::MakePointerButtonKey(0, std::countr_zero(button_mask)));
return true;
}
else if (event_type == QEvent::Wheel)
{
const QPoint delta_angle(static_cast<QWheelEvent*>(event)->angleDelta());
const float dx = std::clamp(static_cast<float>(delta_angle.x()) / QtUtils::MOUSE_WHEEL_DELTA, -1.0f, 1.0f);
if (dx != 0.0f)
{
InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelX));
key.modifier = dx < 0.0f ? InputModifier::Negate : InputModifier::None;
m_new_bindings.push_back(key);
}
const float dy = std::clamp(static_cast<float>(delta_angle.y()) / QtUtils::MOUSE_WHEEL_DELTA, -1.0f, 1.0f);
if (dy != 0.0f)
{
InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelY));
key.modifier = dy < 0.0f ? InputModifier::Negate : InputModifier::None;
m_new_bindings.push_back(key);
}
if (dx != 0.0f || dy != 0.0f)
{
setNewBinding();
stopListeningForInput();
}
return true;
}
else if (event_type == QEvent::MouseMove && m_mouse_mapping_enabled)
{
// if we've moved more than a decent distance from the center of the widget, bind it.
// this is so we don't accidentally bind to the mouse if you bump it while reaching for your pad.
static constexpr const s32 THRESHOLD = 50;
const QPoint diff(static_cast<QMouseEvent*>(event)->globalPosition().toPoint() - m_input_listen_start_position);
bool has_one = false;
if (std::abs(diff.x()) >= THRESHOLD)
{
InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::X));
key.modifier = diff.x() < 0 ? InputModifier::Negate : InputModifier::None;
m_new_bindings.push_back(key);
has_one = true;
}
if (std::abs(diff.y()) >= THRESHOLD)
{
InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::Y));
key.modifier = diff.y() < 0 ? InputModifier::Negate : InputModifier::None;
m_new_bindings.push_back(key);
has_one = true;
}
if (has_one)
{
setNewBinding();
stopListeningForInput();
return true;
}
}
return false;
}
bool InputBindingWidget::event(QEvent* event)
{
if (event->type() == QEvent::MouseButtonRelease)
{
QMouseEvent* mev = static_cast<QMouseEvent*>(event);
if (mev->button() == Qt::LeftButton && mev->modifiers() & Qt::ShiftModifier)
{
openDialog();
return false;
}
}
return QPushButton::event(event);
}
void InputBindingWidget::mouseReleaseEvent(QMouseEvent* e)
{
if (e->button() == Qt::RightButton)
{
clearBinding();
return;
}
QPushButton::mouseReleaseEvent(e);
}
void InputBindingWidget::setNewBinding()
{
if (m_new_bindings.empty())
return;
std::string new_binding(InputManager::ConvertInputBindingKeysToString(m_bind_type, m_new_bindings.data(), m_new_bindings.size()));
if (!new_binding.empty())
{
if (m_sif)
{
m_sif->SetStringValue(m_section_name.c_str(), m_key_name.c_str(), new_binding.c_str());
m_sif->Save();
g_emu_thread->reloadGameSettings();
}
else
{
Host::SetBaseStringSettingValue(m_section_name.c_str(), m_key_name.c_str(), new_binding.c_str());
Host::CommitBaseSettingChanges();
g_emu_thread->reloadInputBindings();
}
}
m_bindings_ui.clear();
m_bindings_ui.push_back(std::move(new_binding));
}
void InputBindingWidget::clearBinding()
{
if (m_sif)
{
m_sif->DeleteValue(m_section_name.c_str(), m_key_name.c_str());
m_sif->Save();
g_emu_thread->reloadGameSettings();
}
else
{
Host::RemoveBaseSettingValue(m_section_name.c_str(), m_key_name.c_str());
Host::CommitBaseSettingChanges();
g_emu_thread->reloadInputBindings();
}
reloadBinding();
}
void InputBindingWidget::reloadBinding()
{
m_bindings_settings = m_sif ? m_sif->GetStringList(m_section_name.c_str(), m_key_name.c_str()) :
Host::GetBaseStringListSetting(m_section_name.c_str(), m_key_name.c_str());
m_bindings_ui.clear();
m_bindings_ui.reserve(m_bindings_settings.size());
for (size_t i = 0; i < m_bindings_settings.size(); i++)
{
SmallString binding{std::string_view{m_bindings_settings[i]}};
InputManager::PrettifyInputBinding(binding, false);
m_bindings_ui.push_back(std::string{binding});
}
updateText();
}
void InputBindingWidget::onClicked()
{
if (m_bindings_ui.size() > 1)
{
openDialog();
return;
}
if (isListeningForInput())
stopListeningForInput();
startListeningForInput(TIMEOUT_FOR_SINGLE_BINDING);
}
void InputBindingWidget::onInputListenTimerTimeout()
{
m_input_listen_remaining_seconds--;
if (m_input_listen_remaining_seconds == 0)
{
stopListeningForInput();
return;
}
setText(tr("Push Button/Axis... [%1]").arg(m_input_listen_remaining_seconds));
}
void InputBindingWidget::startListeningForInput(u32 timeout_in_seconds)
{
m_value_ranges.clear();
m_new_bindings.clear();
m_mouse_mapping_enabled = isMouseMappingEnabled(m_sif);
m_input_listen_start_position = QCursor::pos();
m_input_listen_timer = new QTimer(this);
m_input_listen_timer->setSingleShot(false);
m_input_listen_timer->start(1000);
m_input_listen_timer->connect(m_input_listen_timer, &QTimer::timeout, this, &InputBindingWidget::onInputListenTimerTimeout);
m_input_listen_remaining_seconds = timeout_in_seconds;
setText(tr("Push Button/Axis... [%1]").arg(m_input_listen_remaining_seconds));
installEventFilter(this);
grabKeyboard();
grabMouse();
setMouseTracking(true);
hookInputManager();
}
void InputBindingWidget::stopListeningForInput()
{
reloadBinding();
delete m_input_listen_timer;
m_input_listen_timer = nullptr;
std::vector<InputBindingKey>().swap(m_new_bindings);
unhookInputManager();
setMouseTracking(false);
releaseMouse();
releaseKeyboard();
removeEventFilter(this);
}
void InputBindingWidget::inputManagerHookCallback(InputBindingKey key, float value)
{
if (!isListeningForInput())
return;
float initial_value = value;
float min_value = value;
auto it = std::find_if(m_value_ranges.begin(), m_value_ranges.end(), [key](const auto& it) { return it.first.bits == key.bits; });
if (it != m_value_ranges.end())
{
initial_value = it->second.first;
min_value = it->second.second = std::min(it->second.second, value);
}
else
{
m_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value));
}
const float abs_value = std::abs(value);
const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f);
for (InputBindingKey& other_key : m_new_bindings)
{
if (other_key.MaskDirection() == key.MaskDirection())
{
// for pedals, we wait for it to go back to near its starting point to commit the binding
if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f)))
{
// did we go the full range?
if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f)
other_key.modifier = InputModifier::FullAxis;
// if this key is in our new binding list, it's a "release", and we're done
setNewBinding();
stopListeningForInput();
return;
}
// otherwise, keep waiting
return;
}
}
// new binding, add it to the list, but wait for a decent distance first, and then wait for release
if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f)))
{
InputBindingKey key_to_add = key;
key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None;
key_to_add.invert = reverse_threshold;
m_new_bindings.push_back(key_to_add);
}
}
void InputBindingWidget::onInputDeviceConnected(const QString& identifier, const QString& device_name)
{
reloadBinding();
}
void InputBindingWidget::onInputDeviceDisconnected(const QString& identifier)
{
reloadBinding();
}
void InputBindingWidget::hookInputManager()
{
InputManager::SetHook([this](InputBindingKey key, float value) {
QMetaObject::invokeMethod(this, "inputManagerHookCallback", Qt::QueuedConnection, Q_ARG(InputBindingKey, key), Q_ARG(float, value));
return InputInterceptHook::CallbackResult::StopProcessingEvent;
});
}
void InputBindingWidget::unhookInputManager()
{
InputManager::RemoveHook();
}
void InputBindingWidget::openDialog()
{
InputBindingDialog binding_dialog(m_sif, m_bind_type, m_section_name, m_key_name, m_bindings_settings, m_bindings_ui, QtUtils::GetRootWidget(this));
binding_dialog.exec();
reloadBinding();
}
InputVibrationBindingWidget::InputVibrationBindingWidget(QWidget* parent)
{
connect(this, &QPushButton::clicked, this, &InputVibrationBindingWidget::onClicked);
}
InputVibrationBindingWidget::InputVibrationBindingWidget(
QWidget* parent, ControllerSettingsWindow* dialog, std::string section_name, std::string key_name)
{
setMinimumWidth(225);
setMaximumWidth(225);
connect(this, &QPushButton::clicked, this, &InputVibrationBindingWidget::onClicked);
setKey(dialog, std::move(section_name), std::move(key_name));
}
InputVibrationBindingWidget::~InputVibrationBindingWidget()
{
}
void InputVibrationBindingWidget::setKey(ControllerSettingsWindow* dialog, std::string section_name, std::string key_name)
{
m_dialog = dialog;
m_section_name = std::move(section_name);
m_key_name = std::move(key_name);
m_binding = Host::GetBaseStringSettingValue(m_section_name.c_str(), m_key_name.c_str());
SmallString binding{std::string_view{m_binding}};
if (InputManager::PrettifyInputBinding(binding, false))
m_binding = binding;
setText(QString::fromStdString(m_binding));
}
void InputVibrationBindingWidget::clearBinding()
{
m_binding = {};
Host::RemoveBaseSettingValue(m_section_name.c_str(), m_key_name.c_str());
Host::CommitBaseSettingChanges();
g_emu_thread->reloadInputBindings();
setText(QString());
}
void InputVibrationBindingWidget::onClicked()
{
QInputDialog dialog(QtUtils::GetRootWidget(this));
const QString full_key(QStringLiteral("%1/%2").arg(QString::fromStdString(m_section_name)).arg(QString::fromStdString(m_key_name)));
const QString current(QString::fromStdString(m_binding));
QStringList input_setting_options(m_dialog->getVibrationMotors());
QStringList input_ui_options;
input_ui_options.reserve(input_setting_options.count());
for (QString motor : input_setting_options)
{
SmallString motor_ui{std::string_view{motor.toStdString()}};
InputManager::PrettifyInputBinding(motor_ui, false);
input_ui_options.push_back(QString(motor_ui));
}
if (!current.isEmpty() && input_ui_options.indexOf(current) < 0)
{
input_ui_options.append(current);
}
else if (input_setting_options.isEmpty())
{
QMessageBox::critical(QtUtils::GetRootWidget(this), tr("Error"), tr("No devices with vibration motors were detected."));
return;
}
QInputDialog input_dialog(this);
input_dialog.setWindowTitle(full_key);
input_dialog.setLabelText(tr("Select vibration motor for %1.").arg(full_key));
input_dialog.setInputMode(QInputDialog::TextInput);
input_dialog.setOptions(QInputDialog::UseListViewForComboBoxItems);
input_dialog.setComboBoxEditable(false);
input_dialog.setComboBoxItems(std::move(input_ui_options));
input_dialog.setTextValue(current);
if (input_dialog.exec() == 0)
return;
// If a controller is unplugged, we won't have the setting string to save
// Skip saving if selected is an existing bind from an unplugged controller
const int selected = input_ui_options.indexOf(input_dialog.textValue());
if (selected >= 0 && selected < input_setting_options.size())
{
// Update config
const std::string new_setting_value(input_setting_options[selected].toStdString());
Host::SetBaseStringSettingValue(m_section_name.c_str(), m_key_name.c_str(), new_setting_value.c_str());
Host::CommitBaseSettingChanges();
// Update ui
const QString new_ui_value(input_dialog.textValue());
m_binding = new_ui_value.toStdString();
setText(new_ui_value);
}
}
void InputVibrationBindingWidget::mouseReleaseEvent(QMouseEvent* e)
{
if (e->button() == Qt::RightButton)
{
clearBinding();
return;
}
QPushButton::mouseReleaseEvent(e);
}

View File

@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "pcsx2/Config.h"
#include "pcsx2/Input/InputManager.h"
#include <QtWidgets/QPushButton>
#include <optional>
#include <utility>
#include <vector>
class QTimer;
class ControllerSettingsWindow;
class SettingsInterface;
class InputBindingWidget : public QPushButton
{
Q_OBJECT
public:
InputBindingWidget(QWidget* parent);
InputBindingWidget(
QWidget* parent, SettingsInterface* sif, InputBindingInfo::Type bind_type, std::string section_name, std::string key_name);
~InputBindingWidget();
static bool isMouseMappingEnabled(SettingsInterface* sif);
void initialize(SettingsInterface* sif, InputBindingInfo::Type bind_type, std::string section_name, std::string key_name);
public Q_SLOTS:
void clearBinding();
void reloadBinding();
protected Q_SLOTS:
void onClicked();
void onInputListenTimerTimeout();
void inputManagerHookCallback(InputBindingKey key, float value);
void onInputDeviceConnected(const QString& identifier, const QString& device_name);
void onInputDeviceDisconnected(const QString& identifier);
protected:
enum : u32
{
TIMEOUT_FOR_SINGLE_BINDING = 5,
TIMEOUT_FOR_ALL_BINDING = 10
};
virtual bool eventFilter(QObject* watched, QEvent* event) override;
virtual bool event(QEvent* event) override;
virtual void mouseReleaseEvent(QMouseEvent* e) override;
virtual void startListeningForInput(u32 timeout_in_seconds);
virtual void stopListeningForInput();
virtual void openDialog();
bool isListeningForInput() const { return m_input_listen_timer != nullptr; }
void setNewBinding();
void updateText();
void hookInputManager();
void unhookInputManager();
SettingsInterface* m_sif = nullptr;
InputBindingInfo::Type m_bind_type = InputBindingInfo::Type::Unknown;
std::string m_section_name;
std::string m_key_name;
std::vector<std::string> m_bindings_settings;
std::vector<std::string> m_bindings_ui;
std::vector<InputBindingKey> m_new_bindings;
std::vector<std::pair<InputBindingKey, std::pair<float, float>>> m_value_ranges;
QTimer* m_input_listen_timer = nullptr;
u32 m_input_listen_remaining_seconds = 0;
QPoint m_input_listen_start_position{};
bool m_mouse_mapping_enabled = false;
};
class InputVibrationBindingWidget : public QPushButton
{
Q_OBJECT
public:
InputVibrationBindingWidget(QWidget* parent);
InputVibrationBindingWidget(QWidget* parent, ControllerSettingsWindow* dialog, std::string section_name, std::string key_name);
~InputVibrationBindingWidget();
void setKey(ControllerSettingsWindow* dialog, std::string section_name, std::string key_name);
public Q_SLOTS:
void clearBinding();
protected Q_SLOTS:
void onClicked();
protected:
virtual void mouseReleaseEvent(QMouseEvent* e) override;
private:
std::string m_section_name;
std::string m_key_name;
std::string m_binding;
ControllerSettingsWindow* m_dialog;
};

View File

@@ -0,0 +1,199 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "InterfaceSettingsWidget.h"
#include "AutoUpdaterDialog.h"
#include "Common.h"
#include "MainWindow.h"
#include "SettingWidgetBinder.h"
#include "SettingsWindow.h"
#include "QtHost.h"
const char* InterfaceSettingsWidget::THEME_NAMES[] = {
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Native"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
#ifdef _WIN32
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Classic Windows"),
#endif
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Fusion [Light/Dark]"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Dark Fusion (Gray) [Dark]"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Dark Fusion (Blue) [Dark]"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Grey Matter (Gray) [Dark]"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Untouched Lagoon (Grayish Green/-Blue ) [Light]"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Baby Pastel (Pink) [Light]"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Pizza Time! (Brown-ish/Creamy White) [Light]"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "PCSX2 (White/Blue) [Light]"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Scarlet Devil (Red/Purple) [Dark]"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Violet Angel (Blue/Purple) [Dark]"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Cobalt Sky (Blue) [Dark]"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "AMOLED (Black) [Dark]"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Ruby (Black/Red) [Dark]"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Sapphire (Black/Blue) [Dark]"),
//: Ignore what Crowdin says in this string about "[Light]/[Dark]" being untouchable here, these are not variables in this case and must be translated.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Emerald (Black/Green) [Dark]"),
//: "Custom.qss" must be kept as-is.
QT_TRANSLATE_NOOP("InterfaceSettingsWidget", "Custom.qss [Drop in PCSX2 Folder]"),
nullptr};
const char* InterfaceSettingsWidget::THEME_VALUES[] = {
"",
#ifdef _WIN32
"windowsvista",
#endif
"fusion",
"darkfusion",
"darkfusionblue",
"GreyMatter",
"UntouchedLagoon",
"BabyPastel",
"PizzaBrown",
"PCSX2Blue",
"ScarletDevilRed",
"VioletAngelPurple",
"CobaltSky",
"AMOLED",
"Ruby",
"Sapphire",
"Emerald",
"Custom",
nullptr};
InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: QWidget(parent)
{
SettingsInterface* sif = dialog->getSettingsInterface();
m_ui.setupUi(this);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.inhibitScreensaver, "EmuCore", "InhibitScreensaver", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.confirmShutdown, "UI", "ConfirmShutdown", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pauseOnFocusLoss, "UI", "PauseOnFocusLoss", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pauseOnControllerDisconnection, "UI", "PauseOnControllerDisconnection", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.discordPresence, "EmuCore", "EnableDiscordPresence", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.mouseLock, "EmuCore", "EnableMouseLock", false);
connectCheckStateChanged(m_ui.mouseLock, nullptr, [](Qt::CheckState state) {
if (state == Qt::Checked)
Common::AttachMousePositionCb([](int x, int y) { g_main_window->checkMousePosition(x, y); });
else
Common::DetachMousePositionCb();
});
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.startFullscreen, "UI", "StartFullscreen", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.doubleClickTogglesFullscreen, "UI", "DoubleClickTogglesFullscreen",
true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.hideMouseCursor, "UI", "HideMouseCursor", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.renderToSeparateWindow, "UI", "RenderToSeparateWindow", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.hideMainWindow, "UI", "HideMainWindowWhenRunning", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.disableWindowResizing, "UI", "DisableWindowResize", false);
connectCheckStateChanged(m_ui.renderToSeparateWindow, this, &InterfaceSettingsWidget::onRenderToSeparateWindowChanged);
SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.theme, "UI", "Theme", THEME_NAMES, THEME_VALUES,
QtHost::GetDefaultThemeName(), "InterfaceSettingsWidget");
connect(m_ui.theme, QOverload<int>::of(&QComboBox::currentIndexChanged), [this]() { emit themeChanged(); });
populateLanguages();
SettingWidgetBinder::BindWidgetToStringSetting(sif, m_ui.language, "UI", "Language", QtHost::GetDefaultLanguage());
connect(m_ui.language, QOverload<int>::of(&QComboBox::currentIndexChanged), [this]() { emit languageChanged(); });
// Per-game settings is special, we don't want to bind it if we're editing per-game settings.
if (!dialog->isPerGameSettings())
{
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pauseOnStart, "UI", "StartPaused", false);
}
if (!dialog->isPerGameSettings() && AutoUpdaterDialog::isSupported())
{
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.autoUpdateEnabled, "AutoUpdater", "CheckAtStartup", true);
dialog->registerWidgetHelp(m_ui.autoUpdateEnabled, tr("Enable Automatic Update Check"), tr("Checked"),
tr("Automatically checks for updates to the program on startup. Updates can be deferred "
"until later or skipped entirely."));
m_ui.autoUpdateTag->addItems(AutoUpdaterDialog::getTagList());
SettingWidgetBinder::BindWidgetToStringSetting(sif, m_ui.autoUpdateTag, "AutoUpdater", "UpdateTag",
AutoUpdaterDialog::getDefaultTag());
//: Variable %1 shows the version number and variable %2 shows a timestamp.
m_ui.autoUpdateCurrentVersion->setText(tr("%1 (%2)").arg(AutoUpdaterDialog::getCurrentVersion()).arg(AutoUpdaterDialog::getCurrentVersionDate()));
connect(m_ui.checkForUpdates, &QPushButton::clicked, this, []() { g_main_window->checkForUpdates(true, true); });
}
else
{
m_ui.verticalLayout->removeWidget(m_ui.automaticUpdaterGroup);
m_ui.automaticUpdaterGroup->hide();
}
if (dialog->isPerGameSettings())
{
// language/theme doesn't make sense to have in per-game settings
m_ui.verticalLayout->removeWidget(m_ui.preferencesGroup);
m_ui.preferencesGroup->hide();
// start paused doesn't make sense, because settings are applied after ELF load.
m_ui.pauseOnStart->setEnabled(false);
}
dialog->registerWidgetHelp(
m_ui.inhibitScreensaver, tr("Inhibit Screensaver"), tr("Checked"),
tr("Prevents the screen saver from activating and the host from sleeping while emulation is running."));
dialog->registerWidgetHelp(
m_ui.confirmShutdown, tr("Confirm Shutdown"), tr("Checked"),
tr("Determines whether a prompt will be displayed to confirm shutting down the virtual machine "
"when the hotkey is pressed."));
dialog->registerWidgetHelp(m_ui.pauseOnStart, tr("Pause On Start"), tr("Unchecked"),
tr("Pauses the emulator when a game is started."));
dialog->registerWidgetHelp(m_ui.pauseOnFocusLoss, tr("Pause On Focus Loss"), tr("Unchecked"),
tr("Pauses the emulator when you minimize the window or switch to another application, "
"and unpauses when you switch back."));
dialog->registerWidgetHelp(m_ui.pauseOnControllerDisconnection, tr("Pause On Controller Disconnection"),
tr("Unchecked"), tr("Pauses the emulator when a controller with bindings is disconnected."));
dialog->registerWidgetHelp(m_ui.startFullscreen, tr("Start Fullscreen"), tr("Unchecked"),
tr("Automatically switches to fullscreen mode when a game is started."));
dialog->registerWidgetHelp(m_ui.hideMouseCursor, tr("Hide Cursor In Fullscreen"), tr("Unchecked"),
tr("Hides the mouse pointer/cursor when the emulator is in fullscreen mode."));
dialog->registerWidgetHelp(
m_ui.renderToSeparateWindow, tr("Render To Separate Window"), tr("Unchecked"),
tr("Renders the game to a separate window, instead of the main window. If unchecked, the game will display over the top of the game list."));
dialog->registerWidgetHelp(
m_ui.hideMainWindow, tr("Hide Main Window When Running"), tr("Unchecked"),
tr("Hides the main window (with the game list) when a game is running, requires Render To Separate Window to be enabled."));
dialog->registerWidgetHelp(
m_ui.discordPresence, tr("Enable Discord Presence"), tr("Unchecked"),
tr("Shows the game you are currently playing as part of your profile in Discord."));
dialog->registerWidgetHelp(
m_ui.mouseLock, tr("Enable Mouse Lock"), tr("Unchecked"),
tr("Locks the mouse cursor to the windows when PCSX2 is in focus and all other windows are closed.<br><b>Unavailable on Linux Wayland.</b><br><b>Requires accessibility permissions on macOS.</b>"));
dialog->registerWidgetHelp(
m_ui.doubleClickTogglesFullscreen, tr("Double-Click Toggles Fullscreen"), tr("Checked"),
tr("Allows switching in and out of fullscreen mode by double-clicking the game window."));
dialog->registerWidgetHelp(
m_ui.disableWindowResizing, tr("Disable Window Resizing"), tr("Unchecked"),
tr("Prevents the main window from being resized."));
onRenderToSeparateWindowChanged();
}
InterfaceSettingsWidget::~InterfaceSettingsWidget() = default;
void InterfaceSettingsWidget::onRenderToSeparateWindowChanged()
{
m_ui.hideMainWindow->setEnabled(m_ui.renderToSeparateWindow->isChecked());
}
void InterfaceSettingsWidget::populateLanguages()
{
for (const std::pair<QString, QString>& it : QtHost::GetAvailableLanguageList())
m_ui.language->addItem(it.first, it.second);
}

View File

@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QWidget>
#include "ui_InterfaceSettingsWidget.h"
class SettingsWindow;
class InterfaceSettingsWidget : public QWidget
{
Q_OBJECT
public:
InterfaceSettingsWidget(SettingsWindow* dialog, QWidget* parent);
~InterfaceSettingsWidget();
Q_SIGNALS:
void themeChanged();
void languageChanged();
private Q_SLOTS:
void onRenderToSeparateWindowChanged();
private:
void populateLanguages();
Ui::InterfaceSettingsWidget m_ui;
public:
static const char* THEME_NAMES[];
static const char* THEME_VALUES[];
};

View File

@@ -0,0 +1,234 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>InterfaceSettingsWidget</class>
<widget class="QWidget" name="InterfaceSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>698</width>
<height>512</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="behaviourGroup">
<property name="title">
<string>Behaviour</string>
</property>
<layout class="QGridLayout" name="formLayout_4">
<item row="1" column="0">
<widget class="QCheckBox" name="inhibitScreensaver">
<property name="text">
<string>Inhibit Screensaver</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="confirmShutdown">
<property name="text">
<string>Confirm Shutdown</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="discordPresence">
<property name="text">
<string>Enable Discord Presence</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="mouseLock">
<property name="text">
<string>Enable Mouse Lock</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="pauseOnStart">
<property name="text">
<string>Pause On Start</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="pauseOnControllerDisconnection">
<property name="text">
<string>Pause On Controller Disconnection</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="pauseOnFocusLoss">
<property name="text">
<string>Pause On Focus Loss</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gameDisplayGroup">
<property name="title">
<string>Game Display</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QCheckBox" name="startFullscreen">
<property name="text">
<string>Start Fullscreen</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="doubleClickTogglesFullscreen">
<property name="text">
<string>Double-Click Toggles Fullscreen</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="renderToSeparateWindow">
<property name="text">
<string>Render To Separate Window</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="hideMainWindow">
<property name="text">
<string>Hide Main Window When Running</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="disableWindowResizing">
<property name="text">
<string>Disable Window Resizing</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="hideMouseCursor">
<property name="text">
<string>Hide Cursor In Fullscreen</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="preferencesGroup">
<property name="title">
<string>Preferences</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="languageLabel">
<property name="text">
<string>Language:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="language"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="themeLabel">
<property name="text">
<string>Theme:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="theme"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="automaticUpdaterGroup">
<property name="title">
<string>Automatic Updater</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="1">
<widget class="QLabel" name="autoUpdateCurrentVersion">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="updaterChannelLabel">
<property name="text">
<string>Update Channel:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="currentVersionLabel">
<property name="text">
<string>Current Version:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="autoUpdateTag"/>
</item>
<item row="2" column="0" colspan="2">
<layout class="QHBoxLayout" name="automaticUpdaterCheckGroup" stretch="1,0">
<item>
<widget class="QCheckBox" name="autoUpdateEnabled">
<property name="text">
<string>Enable Automatic Update Check</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="checkForUpdates">
<property name="text">
<string>Check for Updates...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,326 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "MemoryCardConvertDialog.h"
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QProgressDialog>
#include "common/Console.h"
#include "common/Error.h"
#include "common/Path.h"
#include "common/StringUtil.h"
MemoryCardConvertDialog::MemoryCardConvertDialog(QWidget* parent, QString selectedCard)
: QDialog(parent)
{
m_ui.setupUi(this);
// For some reason, setting these in the .ui doesn't work..
m_ui.conversionTypeDescription->setFrameStyle(QFrame::Sunken);
m_ui.conversionTypeDescription->setFrameShape(QFrame::WinPanel);
m_ui.note->setFrameStyle(QFrame::Sunken);
m_ui.note->setFrameShape(QFrame::WinPanel);
m_selectedCard = selectedCard;
std::optional<AvailableMcdInfo> srcCardInfo = FileMcd_GetCardInfo(m_selectedCard.toStdString());
if (srcCardInfo.has_value())
{
m_srcCardInfo = srcCardInfo.value();
}
isSetup = SetupPicklist();
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
m_ui.progressBar->setRange(0, 100);
m_ui.progressBar->setValue(0);
connect(m_ui.conversionTypeSelect, &QComboBox::currentIndexChanged, this, [this]()
{
switch (m_srcCardInfo.type)
{
case MemoryCardType::File:
SetType(MemoryCardType::Folder, MemoryCardFileType::Unknown, tr("Uses a folder on your PC filesystem, instead of a file. Infinite capacity, while keeping the same compatibility as an 8 MB Memory Card."));
break;
case MemoryCardType::Folder:
switch (m_ui.conversionTypeSelect->currentData().toInt())
{
case 8:
SetType(MemoryCardType::File, MemoryCardFileType::PS2_8MB, tr("A standard, 8 MB Memory Card. Most compatible, but smallest capacity."));
break;
case 16:
SetType(MemoryCardType::File, MemoryCardFileType::PS2_16MB, tr("2x larger than a standard Memory Card. May have some compatibility issues."));
break;
case 32:
SetType(MemoryCardType::File, MemoryCardFileType::PS2_32MB, tr("4x larger than a standard Memory Card. Likely to have compatibility issues."));
break;
case 64:
SetType(MemoryCardType::File, MemoryCardFileType::PS2_64MB, tr("8x larger than a standard Memory Card. Likely to have compatibility issues."));
break;
default:
//: MemoryCardType should be left as-is.
QMessageBox::critical(this, tr("Convert Memory Card Failed"), tr("Invalid MemoryCardType"));
return;
}
break;
default:
//: MemoryCardType should be left as-is.
QMessageBox::critical(this, tr("Convert Memory Card Failed"), tr("Invalid MemoryCardType"));
return;
}
}
);
disconnect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, nullptr);
connect(m_ui.buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &MemoryCardConvertDialog::ConvertCard);
connect(m_ui.buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &MemoryCardConvertDialog::close);
}
MemoryCardConvertDialog::~MemoryCardConvertDialog() = default;
bool MemoryCardConvertDialog::IsSetup()
{
return isSetup;
}
void MemoryCardConvertDialog::onStatusUpdated()
{
}
void MemoryCardConvertDialog::onProgressUpdated(int value, int range)
{
m_ui.progressBar->setRange(0, range);
m_ui.progressBar->setValue(value);
}
void MemoryCardConvertDialog::onThreadFinished()
{
QMessageBox::information(this, tr("Conversion Complete"), tr("Memory Card \"%1\" converted to \"%2\"").arg(m_selectedCard).arg(m_destCardName));
accept();
}
void MemoryCardConvertDialog::StartThread()
{
m_thread = std::make_unique<MemoryCardConvertWorker>(this, m_srcCardInfo.type, m_fileType, m_selectedCard.toStdString(), m_destCardName.toStdString());
connect(m_thread.get(), &MemoryCardConvertWorker::statusUpdated, this, &MemoryCardConvertDialog::onStatusUpdated);
connect(m_thread.get(), &MemoryCardConvertWorker::progressUpdated, this, &MemoryCardConvertDialog::onProgressUpdated);
connect(m_thread.get(), &MemoryCardConvertWorker::threadFinished, this, &MemoryCardConvertDialog::onThreadFinished);
m_thread->start();
UpdateEnabled();
}
void MemoryCardConvertDialog::CancelThread()
{
if (!m_thread)
{
return;
}
m_thread->requestInterruption();
m_thread->join();
m_thread.reset();
}
void MemoryCardConvertDialog::UpdateEnabled()
{
if (m_thread)
{
m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
m_ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
}
}
bool MemoryCardConvertDialog::SetupPicklist()
{
FileSystem::FindResultsArray rootDir;
size_t sizeBytes = 0;
bool typeSet = false;
m_ui.conversionTypeSelect->clear();
switch (m_srcCardInfo.type)
{
case MemoryCardType::File:
m_ui.conversionTypeSelect->addItems({"Folder"});
SetType(MemoryCardType::Folder, MemoryCardFileType::Unknown, tr("Uses a folder on your PC filesystem, instead of a file. Infinite capacity, while keeping the same compatibility as an 8 MB Memory Card."));
break;
case MemoryCardType::Folder:
// Compute which file types should be allowed.
FileSystem::FindFiles(m_srcCardInfo.path.c_str(), "*", FLAGS, &rootDir);
for (auto dirEntry : rootDir)
{
const std::string_view fileName = Path::GetFileName(dirEntry.FileName);
if (fileName.size() >= 7 && fileName.substr(0, 7).compare("_pcsx2_") == 0)
{
continue;
}
else if (dirEntry.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY)
{
sizeBytes += 512;
}
else
{
size_t toAdd = static_cast<size_t>(dirEntry.Size + (1024 - (dirEntry.Size % 1024)));
sizeBytes += toAdd + 512; // The file content needs to be added, PLUS a directory entry
}
}
// Finally, round up to the nearest erase block.
sizeBytes += (512 * 16) - (sizeBytes % (512 * 16));
if (sizeBytes < CardCapacity::_8_MB)
{
m_ui.conversionTypeSelect->addItem(tr("8 MB File"), 8);
if (!typeSet)
{
SetType_8();
typeSet = true;
}
}
if (sizeBytes < CardCapacity::_16_MB)
{
m_ui.conversionTypeSelect->addItem(tr("16 MB File"), 16);
if (!typeSet)
{
SetType_16();
typeSet = true;
}
}
if (sizeBytes < CardCapacity::_32_MB)
{
m_ui.conversionTypeSelect->addItem(tr("32 MB File"), 32);
if (!typeSet)
{
SetType_32();
typeSet = true;
}
}
if (sizeBytes < CardCapacity::_64_MB)
{
m_ui.conversionTypeSelect->addItem(tr("64 MB File"), 64);
if (!typeSet)
{
SetType_64();
typeSet = true;
}
}
if (!typeSet)
{
QMessageBox::critical(this, tr("Cannot Convert Memory Card"), tr("Your folder Memory Card has too much data inside it to be converted to a file Memory Card. The largest supported file Memory Card has a capacity of 64 MB. To convert your folder Memory Card, you must remove game folders until its size is 64 MB or less."));
return false;
}
break;
default:
//: MemoryCardType should be left as-is.
QMessageBox::critical(this, tr("Convert Memory Card Failed"), tr("Invalid MemoryCardType"));
return false;
}
return true;
}
void MemoryCardConvertDialog::ConvertCard()
{
if (m_thread)
{
CancelThread();
}
else
{
QString baseName = m_selectedCard;
// Get our destination file name
size_t extensionPos = baseName.lastIndexOf(".ps2", -1);
// Strip the extension off of it
baseName.replace(extensionPos, 4, "");
// Add _converted to the end of it
baseName.append("_converted");
size_t num = 0;
QString destName = baseName;
destName.append(".ps2");
// If a match is found, revert back to the base name, add a number and the extension, and try again.
// Keep incrementing the number until we get a unique result.
while (m_srcCardInfo.type == MemoryCardType::File ? FileSystem::DirectoryExists(Path::Combine(EmuFolders::MemoryCards, destName.toStdString()).c_str()) : FileSystem::FileExists(Path::Combine(EmuFolders::MemoryCards, destName.toStdString()).c_str()))
{
destName = baseName;
destName.append(StringUtil::StdStringFromFormat("_%02zd.ps2", ++num).c_str());
}
// Check if we have write permission in the memory card directory
const std::string destPath = Path::Combine(EmuFolders::MemoryCards, destName.toStdString());
Error error;
FILE* tmpFile = FileSystem::OpenCFile(destPath.c_str(), "w", &error);
if (tmpFile == nullptr)
{
FileOpenError(error.GetDescription().c_str());
return;
}
else
{
fclose(tmpFile);
FileSystem::DeleteFilePath(destPath.c_str());
}
m_destCardName = destName;
StartThread();
}
}
void MemoryCardConvertDialog::ConvertCallback()
{
Console.WriteLn("%s() Finished", __FUNCTION__);
}
void MemoryCardConvertDialog::SetType(MemoryCardType type, MemoryCardFileType fileType, const QString& description)
{
m_type = type;
m_fileType = fileType;
m_ui.conversionTypeDescription->setText(QStringLiteral("<center>%1</center>").arg(description));
}
void MemoryCardConvertDialog::SetType_8()
{
SetType(MemoryCardType::File, MemoryCardFileType::PS2_8MB, tr("A standard, 8 MB Memory Card. Most compatible, but smallest capacity."));
}
void MemoryCardConvertDialog::SetType_16()
{
SetType(MemoryCardType::File, MemoryCardFileType::PS2_16MB, tr("2x larger than a standard Memory Card. May have some compatibility issues."));
}
void MemoryCardConvertDialog::SetType_32()
{
SetType(MemoryCardType::File, MemoryCardFileType::PS2_32MB, tr("4x larger than a standard Memory Card. Likely to have compatibility issues."));
}
void MemoryCardConvertDialog::SetType_64()
{
SetType(MemoryCardType::File, MemoryCardFileType::PS2_64MB, tr("8x larger than a standard Memory Card. Likely to have compatibility issues."));
}
void MemoryCardConvertDialog::SetType_Folder()
{
SetType(MemoryCardType::Folder, MemoryCardFileType::Unknown, tr("Uses a folder on your PC filesystem, instead of a file. Infinite capacity, while keeping the same compatibility as an 8 MB Memory Card."));
}
void MemoryCardConvertDialog::FileOpenError(const QString errmsg)
{
QMessageBox::critical(this, tr("Cannot Convert Memory Card"),tr("There was an error when accessing the memory card directory. Error message: %0").arg(errmsg));
}

View File

@@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QDialog>
#include "common/FileSystem.h"
#include "ui_MemoryCardConvertDialog.h"
#include "MemoryCardConvertWorker.h"
#include "pcsx2/SIO/Memcard/MemoryCardFile.h"
class MemoryCardConvertDialog final : public QDialog
{
Q_OBJECT
public:
explicit MemoryCardConvertDialog(QWidget* parent, QString selectedCard);
~MemoryCardConvertDialog();
bool IsSetup();
void onStatusUpdated();
void onProgressUpdated(int value, int range);
void onThreadFinished();
private Q_SLOTS:
void ConvertCard();
void ConvertCallback();
private:
void StartThread();
void CancelThread();
void UpdateEnabled();
bool SetupPicklist();
void SetType(MemoryCardType type, MemoryCardFileType fileType, const QString& description);
void SetType_8();
void SetType_16();
void SetType_32();
void SetType_64();
void SetType_Folder();
void FileOpenError(const QString errmsg);
Ui::MemoryCardConvertDialog m_ui;
bool isSetup = false;
AvailableMcdInfo m_srcCardInfo;
QString m_selectedCard;
QString m_destCardName;
MemoryCardType m_type = MemoryCardType::File;
MemoryCardFileType m_fileType = MemoryCardFileType::PS2_8MB;
std::unique_ptr<MemoryCardConvertWorker> m_thread;
static constexpr u32 FLAGS = FILESYSTEM_FIND_RECURSIVE | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_FILES;
};
// Card capacities computed from freshly formatted superblocks.
namespace CardCapacity
{
static constexpr size_t _8_MB = 0x1f40 * 512 * 2; //(0x1fc7 - 0x29) * 2 * 512;
static constexpr size_t _16_MB = 0x3e80 * 512 * 2; //(0x3fa7 - 0x49) * 2 * 512;
static constexpr size_t _32_MB = 0x7d00 * 512 * 2; //(0x7f67 - 0x89) * 2 * 512;
static constexpr size_t _64_MB = 0xfde8 * 512 * 2; //(0xfee7 - 0x0109) * 2 * 512;
}

View File

@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MemoryCardConvertDialog</class>
<widget class="QDialog" name="MemoryCardConvertDialog">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>440</width>
<height>282</height>
</rect>
</property>
<property name="windowTitle">
<string>Convert Memory Card</string>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="modal">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="conversionTypeGroup">
<property name="title">
<string>Conversion Type</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QComboBox" name="conversionTypeSelect">
<item>
<property name="text">
<string>8 MB File</string>
</property>
</item>
<item>
<property name="text">
<string>16 MB File</string>
</property>
</item>
<item>
<property name="text">
<string>32 MB File</string>
</property>
</item>
<item>
<property name="text">
<string>64 MB File</string>
</property>
</item>
<item>
<property name="text">
<string>Folder</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="conversionTypeDescription">
<property name="minimumSize">
<size>
<width>0</width>
<height>40</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="note">
<property name="minimumSize">
<size>
<width>0</width>
<height>35</height>
</size>
</property>
<property name="text">
<string>&lt;center&gt;&lt;strong&gt;Note:&lt;/strong&gt; Converting a Memory Card creates a &lt;strong&gt;COPY&lt;/strong&gt; of your existing Memory Card. It does &lt;strong&gt;NOT delete, modify, or replace&lt;/strong&gt; your existing Memory Card.&lt;/center&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="progressGroup">
<property name="title">
<string>Progress</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,167 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "MemoryCardConvertWorker.h"
#include "common/Console.h"
#include "common/Path.h"
#include "common/FileSystem.h"
MemoryCardConvertWorker::MemoryCardConvertWorker(QWidget* parent, MemoryCardType type, MemoryCardFileType fileType, const std::string& srcFileName, const std::string& destFileName)
: QtAsyncProgressThread(parent)
{
this->type = type;
this->fileType = fileType;
this->srcFileName = srcFileName;
this->destFileName = destFileName;
}
MemoryCardConvertWorker::~MemoryCardConvertWorker() = default;
void MemoryCardConvertWorker::runAsync()
{
switch (type)
{
case MemoryCardType::File:
ConvertToFolder(srcFileName, destFileName, fileType);
break;
case MemoryCardType::Folder:
ConvertToFile(srcFileName, destFileName, fileType);
break;
default:
break;
}
}
bool MemoryCardConvertWorker::ConvertToFile(const std::string& srcFolderName, const std::string& destFileName, const MemoryCardFileType type)
{
const std::string srcPath(Path::Combine(EmuFolders::MemoryCards, srcFolderName));
const std::string destPath(Path::Combine(EmuFolders::MemoryCards, destFileName));
size_t sizeInMB = 0;
switch (type)
{
case MemoryCardFileType::PS2_8MB:
sizeInMB = 8;
break;
case MemoryCardFileType::PS2_16MB:
sizeInMB = 16;
break;
case MemoryCardFileType::PS2_32MB:
sizeInMB = 32;
break;
case MemoryCardFileType::PS2_64MB:
sizeInMB = 64;
break;
default:
Console.Error("%s(%s, %s, %d) Received invalid MemoryCardFileType, aborting", __FUNCTION__, srcPath.c_str(), destPath.c_str(), type);
return false;
}
FolderMemoryCard sourceFolderMemoryCard;
Pcsx2Config::McdOptions config;
config.Enabled = true;
config.Type = MemoryCardType::Folder;
sourceFolderMemoryCard.Open(srcPath, config, (sizeInMB * 1024 * 1024) / FolderMemoryCard::ClusterSize, false, "");
const size_t capacity = sourceFolderMemoryCard.GetSizeInClusters() * FolderMemoryCard::ClusterSizeRaw;
std::vector<u8> sourceBuffer;
sourceBuffer.resize(capacity);
size_t address = 0;
this->SetProgressRange(capacity);
this->SetProgressValue(0);
while (address < capacity)
{
sourceFolderMemoryCard.Read(sourceBuffer.data() + address, address, FolderMemoryCard::PageSizeRaw);
address += FolderMemoryCard::PageSizeRaw;
// Only report progress every 16 pages. Substantially speeds up the conversion.
if (address % (FolderMemoryCard::PageSizeRaw * 16) == 0)
this->SetProgressValue(address);
}
bool writeResult = FileSystem::WriteBinaryFile(destPath.c_str(), sourceBuffer.data(), sourceBuffer.size());
if (!writeResult)
{
Console.Error("%s(%s, %s, %d) Failed to write Memory Card contents to file", __FUNCTION__, srcPath.c_str(), destPath.c_str(), type);
return false;
}
#ifdef _WIN32
else
{
FileSystem::SetPathCompression(destPath.c_str(), true);
}
#endif
sourceFolderMemoryCard.Close(false);
return true;
}
bool MemoryCardConvertWorker::ConvertToFolder(const std::string& srcFileName, const std::string& destFolderName, const MemoryCardFileType type)
{
const std::string srcPath(Path::Combine(EmuFolders::MemoryCards, srcFileName));
const std::string destPath(Path::Combine(EmuFolders::MemoryCards, destFolderName));
FolderMemoryCard targetFolderMemoryCard;
Pcsx2Config::McdOptions config;
config.Enabled = true;
config.Type = MemoryCardType::Folder;
std::optional<std::vector<u8>> sourceBufferOpt = FileSystem::ReadBinaryFile(srcPath.c_str());
if (!sourceBufferOpt.has_value())
{
Console.Error("%s(%s, %s, %d) Failed to open file Memory Card!", __FUNCTION__, srcFileName.c_str(), destFolderName.c_str(), type);
return false;
}
std::vector<u8> sourceBuffer = sourceBufferOpt.value();
// Set progress bar to the literal number of bytes in the memcard.
// Plus two because there is a lag period after the Save calls complete
// where the progress bar stalls out; this lets us stop the progress bar
// just shy of 50 and 100% so it seems like it's still doing some work.
this->SetProgressRange((sourceBuffer.size() * 2) + 2);
this->SetProgressValue(0);
// Attempt the write twice. Once with writes being simulated rather than truly committed.
// Again with actual writes. If a file memcard has a corrupted page or something which would
// cause the conversion to fail, it will fail on the simulated run, with no files committed
// to the filesystem yet.
for (int i = 0; i < 2; i++)
{
bool simulateWrites = (i == 0);
targetFolderMemoryCard.Open(destPath, config, 0, false, "", simulateWrites);
size_t address = 0;
while (address < sourceBuffer.size())
{
targetFolderMemoryCard.Save(sourceBuffer.data() + address, address, FolderMemoryCard::PageSizeRaw);
address += FolderMemoryCard::PageSizeRaw;
// Only report progress every 16 pages. Substantially speeds up the conversion.
if (address % (FolderMemoryCard::PageSizeRaw * 16) == 0)
this->SetProgressValue(address + (i * sourceBuffer.size()));
}
targetFolderMemoryCard.Close();
// If the source file Memory Card was larger than 8 MB, the raw copy will have also made the superblock of
// the destination folder Memory Card larger than 8 MB. For compatibility, we always want folder Memory Cards
// to report 8 MB, so we'll override that here. Don't do this on the simulated run, only the actual.
if (!simulateWrites && sourceBuffer.size() != FolderMemoryCard::TotalSizeRaw)
{
targetFolderMemoryCard.Open(destPath, config, 0, false, "", simulateWrites);
targetFolderMemoryCard.SetSizeInMB(8);
targetFolderMemoryCard.Close();
}
this->IncrementProgressValue();
}
this->IncrementProgressValue();
return true;
}

View File

@@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtCore/QtCore>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QProgressDialog>
#include "QtProgressCallback.h"
#include "pcsx2/SIO/Memcard/MemoryCardFile.h"
#include "pcsx2/SIO/Memcard/MemoryCardFolder.h"
class MemoryCardConvertWorker : public QtAsyncProgressThread
{
public:
MemoryCardConvertWorker(QWidget* parent, MemoryCardType type, MemoryCardFileType fileType, const std::string& srcFileName, const std::string& destFileName);
~MemoryCardConvertWorker();
protected:
void runAsync() override;
private:
MemoryCardType type;
MemoryCardFileType fileType;
std::string srcFileName;
std::string destFileName;
bool ConvertToFile(const std::string& srcFolderName, const std::string& destFileName, const MemoryCardFileType type);
bool ConvertToFolder(const std::string& srcFolderName, const std::string& destFileName, const MemoryCardFileType type);
};

View File

@@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/FileSystem.h"
#include "common/Path.h"
#include "common/StringUtil.h"
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QPushButton>
#include "Settings/MemoryCardCreateDialog.h"
#include "QtUtils.h"
#include "pcsx2/SIO/Memcard/MemoryCardFile.h"
MemoryCardCreateDialog::MemoryCardCreateDialog(QWidget* parent /* = nullptr */)
: QDialog(parent)
{
m_ui.setupUi(this);
QtUtils::SetScalableIcon(m_ui.icon, QIcon::fromTheme(QStringLiteral("memcard-line")), QSize(m_ui.icon->width(), m_ui.icon->width()));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
connect(m_ui.name, &QLineEdit::textChanged, this, &MemoryCardCreateDialog::nameTextChanged);
connect(m_ui.size8MB, &QRadioButton::clicked, this, [this]() { setType(MemoryCardType::File, MemoryCardFileType::PS2_8MB); });
connect(m_ui.size16MB, &QRadioButton::clicked, this, [this]() { setType(MemoryCardType::File, MemoryCardFileType::PS2_16MB); });
connect(m_ui.size32MB, &QRadioButton::clicked, this, [this]() { setType(MemoryCardType::File, MemoryCardFileType::PS2_32MB); });
connect(m_ui.size64MB, &QRadioButton::clicked, this, [this]() { setType(MemoryCardType::File, MemoryCardFileType::PS2_64MB); });
connect(m_ui.size128KB, &QRadioButton::clicked, this, [this]() { setType(MemoryCardType::File, MemoryCardFileType::PS1); });
connect(m_ui.sizeFolder, &QRadioButton::clicked, this, [this]() { setType(MemoryCardType::Folder, MemoryCardFileType::Unknown); });
disconnect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, nullptr);
connect(m_ui.buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &MemoryCardCreateDialog::createCard);
connect(m_ui.buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &MemoryCardCreateDialog::close);
connect(m_ui.buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, &MemoryCardCreateDialog::restoreDefaults);
#ifndef _WIN32
m_ui.ntfsCompressionLayout->removeWidget(m_ui.ntfsCompression);
safe_delete(m_ui.ntfsCompression);
m_ui.ntfsCompressionLayout->removeWidget(m_ui.ntfsCompressionLabel);
safe_delete(m_ui.ntfsCompressionLabel);
m_ui.mainLayout->removeItem(m_ui.ntfsCompressionLayout);
safe_delete(m_ui.ntfsCompressionLayout);
resize(600, 480);
#endif
updateState();
}
MemoryCardCreateDialog::~MemoryCardCreateDialog() = default;
void MemoryCardCreateDialog::nameTextChanged()
{
QString controlName(m_ui.name->text());
const int cursorPos = m_ui.name->cursorPosition();
controlName.replace(".", "");
QSignalBlocker sb(m_ui.name);
if (controlName.isEmpty())
m_ui.name->setText(QString());
else
m_ui.name->setText(controlName);
m_ui.name->setCursorPosition(cursorPos);
updateState();
}
void MemoryCardCreateDialog::setType(MemoryCardType type, MemoryCardFileType fileType)
{
m_type = type;
m_fileType = fileType;
updateState();
}
void MemoryCardCreateDialog::restoreDefaults()
{
setType(MemoryCardType::File, MemoryCardFileType::PS2_8MB);
m_ui.size8MB->setChecked(true);
m_ui.size16MB->setChecked(false);
m_ui.size32MB->setChecked(false);
m_ui.size64MB->setChecked(false);
m_ui.size128KB->setChecked(false);
m_ui.sizeFolder->setChecked(false);
}
void MemoryCardCreateDialog::updateState()
{
const bool okay = (m_ui.name->text().length() > 0);
m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(okay);
#ifdef _WIN32
m_ui.ntfsCompression->setEnabled(m_type == MemoryCardType::File);
#endif
}
void MemoryCardCreateDialog::createCard()
{
const QString name = m_ui.name->text();
const std::string name_str = QStringLiteral("%1.%2").arg(name)
.arg((m_fileType == MemoryCardFileType::PS1) ? QStringLiteral("mcr") : QStringLiteral("ps2"))
.toStdString();
if (!Path::IsValidFileName(name_str, false))
{
QMessageBox::critical(this, tr("Create Memory Card"),
tr("Failed to create the Memory Card, because the name '%1' contains one or more invalid characters.").arg(name));
return;
}
if (FileMcd_GetCardInfo(name_str).has_value())
{
QMessageBox::critical(this, tr("Create Memory Card"),
tr("Failed to create the Memory Card, because another card with the name '%1' already exists.").arg(name));
return;
}
if (!FileMcd_CreateNewCard(name_str, m_type, m_fileType))
{
QMessageBox::critical(this, tr("Create Memory Card"),
tr("Failed to create the Memory Card, the log may contain more information."));
return;
}
#ifdef _WIN32
if (m_type == MemoryCardType::File)
{
const std::string fullPath = Path::Combine(EmuFolders::MemoryCards, name_str);
FileSystem::SetPathCompression(fullPath.c_str(), m_ui.ntfsCompression->isChecked());
}
#endif
QMessageBox::information(this, tr("Create Memory Card"), tr("Memory Card '%1' created.").arg(name));
accept();
}

View File

@@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <QtWidgets/QDialog>
#include "ui_MemoryCardCreateDialog.h"
#include "pcsx2/Config.h"
class MemoryCardCreateDialog final : public QDialog
{
Q_OBJECT
public:
explicit MemoryCardCreateDialog(QWidget* parent = nullptr);
~MemoryCardCreateDialog();
private Q_SLOTS:
void nameTextChanged();
void createCard();
private:
void setType(MemoryCardType type, MemoryCardFileType fileType);
void restoreDefaults();
void updateState();
Ui::MemoryCardCreateDialog m_ui;
MemoryCardType m_type = MemoryCardType::File;
MemoryCardFileType m_fileType = MemoryCardFileType::PS2_8MB;
};

View File

@@ -0,0 +1,318 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MemoryCardCreateDialog</class>
<widget class="QDialog" name="MemoryCardCreateDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>535</height>
</rect>
</property>
<property name="windowTitle">
<string>Create Memory Card</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="mainLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
<property name="spacing">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="icon">
<property name="minimumSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
<property name="pixmap">
<pixmap resource="../resources/resources.qrc">:/icons/black/svg/memcard-line.svg</pixmap>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Create Memory Card&lt;/span&gt;&lt;br /&gt;Enter the name of the Memory Card you wish to create, and choose a size. We recommend either using 8MB Memory Cards, or folder Memory Cards for best compatibility.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Memory Card Name:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="name"/>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QRadioButton" name="size8MB">
<property name="text">
<string>8 MB [Most Compatible]</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>This is the standard Sony-provisioned size, and is supported by all games and BIOS versions.</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="size16MB">
<property name="text">
<string>16 MB</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>A typical size for third-party Memory Cards which should work with most games.</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QRadioButton" name="size32MB">
<property name="text">
<string>32 MB</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>A typical size for third-party Memory Cards which should work with most games.</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QRadioButton" name="size64MB">
<property name="text">
<string>64 MB</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Low compatibility warning: yes, it's very big, but may not work with many games.</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QRadioButton" name="sizeFolder">
<property name="text">
<string>Folder [Recommended]</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>Store Memory Card contents in the host filesystem instead of a file.</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QRadioButton" name="size128KB">
<property name="text">
<string>128 KB (PS1)</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>This is the standard Sony-provisioned size PS1 Memory Card, and only compatible with PS1 games.</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="ntfsCompressionLayout">
<item>
<widget class="QCheckBox" name="ntfsCompression">
<property name="text">
<string>Use NTFS Compression</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="ntfsCompressionLabel">
<property name="text">
<string>NTFS compression is built-in, fast, and completely reliable. Typically compresses Memory Cards (highly recommended).</string>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>MemoryCardCreateDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>MemoryCardCreateDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,532 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include <QtGui/QDrag>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QMenu>
#include <QtWidgets/QMessageBox>
#include <algorithm>
#include "common/StringUtil.h"
#include "MemoryCardConvertDialog.h"
#include "MemoryCardCreateDialog.h"
#include "MemoryCardSettingsWidget.h"
#include "QtHost.h"
#include "QtUtils.h"
#include "SettingWidgetBinder.h"
#include "SettingsWindow.h"
#include "pcsx2/SIO/Memcard/MemoryCardFile.h"
static constexpr const char* CONFIG_SECTION = "MemoryCards";
static std::string getSlotFilenameKey(u32 slot)
{
return StringUtil::StdStringFromFormat("Slot%u_Filename", slot + 1);
}
MemoryCardSettingsWidget::MemoryCardSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: QWidget(parent)
, m_dialog(dialog)
{
SettingsInterface* sif = m_dialog->getSettingsInterface();
m_ui.setupUi(this);
// this is a bit lame, but resizeEvent() isn't good enough to autosize our columns,
// since the group box hasn't been resized at that point.
m_ui.cardGroupBox->installEventFilter(this);
SettingWidgetBinder::BindWidgetToFolderSetting(sif, m_ui.directory, m_ui.browse, m_ui.open, m_ui.reset, "Folders",
"MemoryCards", Path::Combine(EmuFolders::DataRoot, "memcards"));
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.automaticManagement, "EmuCore", "McdFolderAutoManage", true);
setupAdditionalUi();
connect(m_ui.directory, &QLineEdit::textChanged, this, &MemoryCardSettingsWidget::refresh);
m_ui.cardList->setContextMenuPolicy(Qt::CustomContextMenu);
connect(
m_ui.cardList, &MemoryCardListWidget::itemSelectionChanged, this, &MemoryCardSettingsWidget::updateCardActions);
connect(m_ui.cardList, &MemoryCardListWidget::customContextMenuRequested, this,
&MemoryCardSettingsWidget::listContextMenuRequested);
connect(m_ui.refreshCard, &QPushButton::clicked, this, &MemoryCardSettingsWidget::refresh);
connect(m_ui.createCard, &QPushButton::clicked, this, &MemoryCardSettingsWidget::createCard);
connect(m_ui.renameCard, &QPushButton::clicked, this, &MemoryCardSettingsWidget::renameCard);
connect(m_ui.convertCard, &QPushButton::clicked, this, &MemoryCardSettingsWidget::convertCard);
connect(m_ui.deleteCard, &QPushButton::clicked, this, &MemoryCardSettingsWidget::deleteCard);
refresh();
dialog->registerWidgetHelp(m_ui.automaticManagement, tr("Automatically manage saves based on running game"),
tr("Checked"),
tr("(Folder type only / Card size: Auto) Loads only the relevant booted game saves, ignoring others. Avoids "
"running out of space for saves."));
}
MemoryCardSettingsWidget::~MemoryCardSettingsWidget() = default;
void MemoryCardSettingsWidget::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
autoSizeUI();
}
bool MemoryCardSettingsWidget::eventFilter(QObject* watched, QEvent* event)
{
if (watched == m_ui.cardGroupBox && event->type() == QEvent::Resize)
autoSizeUI();
return QWidget::eventFilter(watched, event);
}
void MemoryCardSettingsWidget::setupAdditionalUi()
{
for (u32 i = 0; i < static_cast<u32>(m_slots.size()); i++)
createSlotWidgets(&m_slots[i], i);
// button to swap Memory Cards
QToolButton* swap_button = new QToolButton(m_ui.slotGroupBox);
swap_button->setIcon(QIcon::fromTheme("arrow-left-right-line"));
swap_button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
swap_button->setToolTip(tr("Swap Memory Cards"));
connect(swap_button, &QToolButton::clicked, this, &MemoryCardSettingsWidget::swapCards);
static_cast<QGridLayout*>(m_ui.slotGroupBox->layout())->addWidget(swap_button, 0, 1);
}
void MemoryCardSettingsWidget::createSlotWidgets(SlotGroup* port, u32 slot)
{
const bool perGame = m_dialog->isPerGameSettings();
port->root = new QWidget(m_ui.slotGroupBox);
SettingsInterface* sif = m_dialog->getSettingsInterface();
port->enable = new QCheckBox(tr("Slot %1").arg(slot + 1), port->root);
SettingWidgetBinder::BindWidgetToBoolSetting(
sif, port->enable, CONFIG_SECTION, StringUtil::StdStringFromFormat("Slot%u_Enable", slot + 1), true);
connectCheckStateChanged(port->enable, this, &MemoryCardSettingsWidget::refresh);
port->eject = new QToolButton(port->root);
port->eject->setIcon(QIcon::fromTheme(perGame ? QStringLiteral("delete-back-2-line") : QStringLiteral("eject-line")));
port->eject->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
port->eject->setToolTip(perGame ? tr("Reset") : tr("Eject Memory Card"));
connect(port->eject, &QToolButton::clicked, this, [this, slot]() { ejectSlot(slot); });
port->slot = new MemoryCardSlotWidget(port->root);
connect(port->slot, &MemoryCardSlotWidget::cardDropped, this,
[this, slot](const QString& card) { tryInsertCard(slot, card); });
QHBoxLayout* bottom_layout = new QHBoxLayout();
bottom_layout->setContentsMargins(0, 0, 0, 0);
bottom_layout->addWidget(port->slot, 1);
bottom_layout->addWidget(port->eject, 0);
QVBoxLayout* vert_layout = new QVBoxLayout(port->root);
vert_layout->setContentsMargins(0, 0, 0, 0);
vert_layout->addWidget(port->enable, 0);
vert_layout->addLayout(bottom_layout, 1);
static_cast<QGridLayout*>(m_ui.slotGroupBox->layout())->addWidget(port->root, 0, (slot != 0) ? 2 : 0);
}
void MemoryCardSettingsWidget::autoSizeUI()
{
QtUtils::ResizeColumnsForTreeView(m_ui.cardList, {-1, 100, 80, 150});
}
void MemoryCardSettingsWidget::tryInsertCard(u32 slot, const QString& newCard)
{
// handle where the card is dragged in from explorer or something
const int lastSlashPos = std::max(newCard.lastIndexOf('/'), newCard.lastIndexOf('\\'));
const std::string newCardStr(
(lastSlashPos >= 0) ? newCard.mid(0, lastSlashPos).toStdString() : newCard.toStdString());
if (newCardStr.empty())
return;
// make sure it's a card in the directory
const std::vector<AvailableMcdInfo> mcds(FileMcd_GetAvailableCards(true));
if (std::none_of(
mcds.begin(), mcds.end(), [&newCardStr](const AvailableMcdInfo& mcd) { return mcd.name == newCardStr; }))
{
QMessageBox::critical(this, tr("Error"), tr("This Memory Card cannot be recognized or is not a valid file type."));
return;
}
m_dialog->setStringSettingValue(CONFIG_SECTION, getSlotFilenameKey(slot).c_str(), newCardStr.c_str());
refresh();
}
void MemoryCardSettingsWidget::ejectSlot(u32 slot)
{
m_dialog->setStringSettingValue(CONFIG_SECTION, getSlotFilenameKey(slot).c_str(),
m_dialog->isPerGameSettings() ? std::nullopt : std::optional<const char*>(""));
refresh();
}
void MemoryCardSettingsWidget::createCard()
{
MemoryCardCreateDialog dialog(QtUtils::GetRootWidget(this));
if (dialog.exec() == QDialog::Accepted)
refresh();
}
QString MemoryCardSettingsWidget::getSelectedCard() const
{
QString ret;
const QList<QTreeWidgetItem*> selection(m_ui.cardList->selectedItems());
if (!selection.empty())
ret = selection[0]->text(0);
return ret;
}
bool MemoryCardSettingsWidget::isSelectedCardFormatted() const
{
const QList<QTreeWidgetItem*> selection(m_ui.cardList->selectedItems());
if (!selection.empty())
return selection[0]->data(0, Qt::UserRole).toBool();
return false;
}
void MemoryCardSettingsWidget::updateCardActions()
{
QString selectedCard = getSelectedCard();
const bool hasSelection = !selectedCard.isEmpty();
std::optional<AvailableMcdInfo> cardInfo = FileMcd_GetCardInfo(selectedCard.toStdString());
bool isPS1 = (cardInfo.has_value() ? cardInfo.value().file_type == MemoryCardFileType::PS1 : false);
m_ui.convertCard->setEnabled(hasSelection && !isPS1);
m_ui.renameCard->setEnabled(hasSelection);
m_ui.deleteCard->setEnabled(hasSelection);
}
void MemoryCardSettingsWidget::deleteCard()
{
const QString selectedCard(getSelectedCard());
if (selectedCard.isEmpty())
return;
if (QMessageBox::question(QtUtils::GetRootWidget(this), tr("Delete Memory Card"),
tr("Are you sure you wish to delete the Memory Card '%1'?\n\n"
"This action cannot be reversed, and you will lose any saves on the card.")
.arg(selectedCard)) != QMessageBox::Yes)
{
return;
}
if (!FileMcd_DeleteCard(selectedCard.toStdString()))
{
QMessageBox::critical(QtUtils::GetRootWidget(this), tr("Delete Memory Card"),
tr("Failed to delete the Memory Card. The log may have more information."));
return;
}
refresh();
}
void MemoryCardSettingsWidget::renameCard()
{
const QString selectedCard(getSelectedCard());
if (selectedCard.isEmpty())
return;
const QString newName(QInputDialog::getText(
QtUtils::GetRootWidget(this), tr("Rename Memory Card"), tr("New Card Name"), QLineEdit::Normal, selectedCard));
if (newName.isEmpty() || newName == selectedCard)
return;
if (!newName.endsWith(QStringLiteral(".ps2")) || newName.length() <= 4)
{
QMessageBox::critical(
QtUtils::GetRootWidget(this), tr("Rename Memory Card"), tr("New name is invalid, it must end with .ps2"));
return;
}
const std::string newNameStr(newName.toStdString());
if (FileMcd_GetCardInfo(newNameStr).has_value())
{
QMessageBox::critical(QtUtils::GetRootWidget(this), tr("Rename Memory Card"),
tr("New name is invalid, a card with this name already exists."));
return;
}
if (!FileMcd_RenameCard(selectedCard.toStdString(), newNameStr))
{
QMessageBox::critical(QtUtils::GetRootWidget(this), tr("Rename Memory Card"),
tr("Failed to rename Memory Card. The log may contain more information."));
return;
}
refresh();
}
void MemoryCardSettingsWidget::convertCard()
{
const QString selectedCard(getSelectedCard());
if (selectedCard.isEmpty())
return;
if (!isSelectedCardFormatted())
{
QMessageBox::critical(this, tr("Error"), tr("Cannot convert an unformatted memory card."));
return;
}
MemoryCardConvertDialog dialog(QtUtils::GetRootWidget(this), selectedCard);
if (dialog.IsSetup() && dialog.exec() == QDialog::Accepted)
refresh();
}
void MemoryCardSettingsWidget::listContextMenuRequested(const QPoint& pos)
{
QMenu menu(this);
const QString selectedCard(getSelectedCard());
if (!selectedCard.isEmpty())
{
for (u32 slot = 0; slot < MAX_SLOTS; slot++)
{
connect(menu.addAction(tr("Use for Slot %1").arg(slot + 1)), &QAction::triggered, this,
[this, &selectedCard, slot]() { tryInsertCard(slot, selectedCard); });
}
menu.addSeparator();
connect(menu.addAction(tr("Rename")), &QAction::triggered, this, &MemoryCardSettingsWidget::renameCard);
connect(menu.addAction(tr("Convert")), &QAction::triggered, this, &MemoryCardSettingsWidget::convertCard);
connect(menu.addAction(tr("Delete")), &QAction::triggered, this, &MemoryCardSettingsWidget::deleteCard);
menu.addSeparator();
}
connect(menu.addAction(tr("Create")), &QAction::triggered, this, &MemoryCardSettingsWidget::createCard);
menu.exec(m_ui.cardList->mapToGlobal(pos));
}
void MemoryCardSettingsWidget::refresh()
{
const bool perGame = m_dialog->isPerGameSettings();
for (u32 slot = 0; slot < static_cast<u32>(m_slots.size()); slot++)
{
const bool enabled = m_slots[slot].enable->isChecked();
const std::string slotKey = getSlotFilenameKey(slot);
const std::optional<std::string> name(
m_dialog->getEffectiveStringValue(CONFIG_SECTION, slotKey.c_str(), FileMcd_GetDefaultName(slot).c_str()));
const bool inherited = perGame ? !m_dialog->containsSettingValue(CONFIG_SECTION, slotKey.c_str()) : false;
m_slots[slot].slot->setCard(name, inherited);
m_slots[slot].slot->setEnabled(enabled);
m_slots[slot].eject->setEnabled(enabled);
}
m_ui.cardList->refresh(m_dialog);
updateCardActions();
}
void MemoryCardSettingsWidget::swapCards()
{
const std::string card1Key = getSlotFilenameKey(0);
const std::string card2Key = getSlotFilenameKey(1);
std::optional<std::string> card1Name = m_dialog->getStringValue(CONFIG_SECTION, card1Key.c_str(), std::nullopt);
std::optional<std::string> card2Name = m_dialog->getStringValue(CONFIG_SECTION, card2Key.c_str(), std::nullopt);
if (!card1Name.has_value() || card1Name->empty() || !card2Name.has_value() || card2Name->empty())
{
QMessageBox::critical(
QtUtils::GetRootWidget(this), tr("Error"), tr("Both slots must have a card selected to swap."));
return;
}
m_dialog->setStringSettingValue(CONFIG_SECTION, card1Key.c_str(), card2Name->c_str());
m_dialog->setStringSettingValue(CONFIG_SECTION, card2Key.c_str(), card1Name->c_str());
refresh();
}
static QString getSizeSummary(const AvailableMcdInfo& mcd)
{
if (mcd.type == MemoryCardType::File)
{
switch (mcd.file_type)
{
case MemoryCardFileType::PS2_8MB:
return qApp->translate("MemoryCardSettingsWidget", "PS2 (8MB)");
case MemoryCardFileType::PS2_16MB:
return qApp->translate("MemoryCardSettingsWidget", "PS2 (16MB)");
case MemoryCardFileType::PS2_32MB:
return qApp->translate("MemoryCardSettingsWidget", "PS2 (32MB)");
case MemoryCardFileType::PS2_64MB:
return qApp->translate("MemoryCardSettingsWidget", "PS2 (64MB)");
case MemoryCardFileType::PS1:
return qApp->translate("MemoryCardSettingsWidget", "PS1 (128KB)");
case MemoryCardFileType::Unknown:
default:
return qApp->translate("MemoryCardSettingsWidget", "Unknown");
}
}
else if (mcd.type == MemoryCardType::Folder)
{
return qApp->translate("MemoryCardSettingsWidget", "PS2 (Folder)");
}
else
{
return qApp->translate("MemoryCardSettingsWidget", "Unknown");
}
}
static QIcon getCardIcon(const AvailableMcdInfo& mcd)
{
if (mcd.type == MemoryCardType::File)
return QIcon::fromTheme(QStringLiteral("memcard-line"));
else
return QIcon::fromTheme(QStringLiteral("folder-open-line"));
}
MemoryCardListWidget::MemoryCardListWidget(QWidget* parent)
: QTreeWidget(parent)
{
}
MemoryCardListWidget::~MemoryCardListWidget() = default;
void MemoryCardListWidget::mousePressEvent(QMouseEvent* event)
{
if (event->button() == Qt::LeftButton)
m_dragStartPos = event->pos();
QTreeWidget::mousePressEvent(event);
}
void MemoryCardListWidget::mouseMoveEvent(QMouseEvent* event)
{
if (!(event->buttons() & Qt::LeftButton) ||
(event->pos() - m_dragStartPos).manhattanLength() < QApplication::startDragDistance())
{
QTreeWidget::mouseMoveEvent(event);
return;
}
const QList<QTreeWidgetItem*> selection(selectedItems());
if (selection.empty())
return;
QDrag* drag = new QDrag(this);
QMimeData* mimeData = new QMimeData();
mimeData->setText(selection[0]->text(0));
drag->setMimeData(mimeData);
drag->exec(Qt::CopyAction);
}
void MemoryCardListWidget::refresh(SettingsWindow* dialog)
{
clear();
// we can't use the in use flag here anyway, because the config may not be in line with per game settings.
const std::vector<AvailableMcdInfo> mcds(FileMcd_GetAvailableCards(true));
if (mcds.empty())
return;
std::array<std::string, MemoryCardSettingsWidget::MAX_SLOTS> currentCards;
for (u32 i = 0; i < static_cast<u32>(currentCards.size()); i++)
{
const std::optional<std::string> filename = dialog->getEffectiveStringValue(
CONFIG_SECTION, getSlotFilenameKey(i).c_str(), FileMcd_GetDefaultName(i).c_str());
if (filename.has_value())
currentCards[i] = std::move(filename.value());
}
for (const AvailableMcdInfo& mcd : mcds)
{
QTreeWidgetItem* item = new QTreeWidgetItem();
const QDateTime mtime(QDateTime::fromSecsSinceEpoch(static_cast<qint64>(mcd.modified_time)));
const bool inUse = (std::find(currentCards.begin(), currentCards.end(), mcd.name) != currentCards.end());
item->setDisabled(inUse);
item->setIcon(0, getCardIcon(mcd));
item->setText(0, QString::fromStdString(mcd.name));
item->setText(1, getSizeSummary(mcd));
item->setText(2, mcd.formatted ? tr("Yes") : tr("No"));
item->setText(3, mtime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)));
// store formatted metadata
item->setData(0, Qt::UserRole, mcd.formatted);
addTopLevelItem(item);
}
}
MemoryCardSlotWidget::MemoryCardSlotWidget(QWidget* parent)
: QListWidget(parent)
{
setAcceptDrops(true);
setSelectionMode(NoSelection);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
}
MemoryCardSlotWidget::~MemoryCardSlotWidget() = default;
void MemoryCardSlotWidget::dragEnterEvent(QDragEnterEvent* event)
{
if (event->mimeData()->hasFormat("text/plain"))
event->acceptProposedAction();
}
void MemoryCardSlotWidget::dragMoveEvent(QDragMoveEvent* event)
{
}
void MemoryCardSlotWidget::dropEvent(QDropEvent* event)
{
const QMimeData* data = event->mimeData();
const QString text(data ? data->text() : QString());
if (text.isEmpty())
{
event->ignore();
return;
}
event->acceptProposedAction();
emit cardDropped(text);
}
void MemoryCardSlotWidget::setCard(const std::optional<std::string>& name, bool inherited)
{
clear();
if (!name.has_value() || name->empty())
return;
const std::optional<AvailableMcdInfo> mcd(FileMcd_GetCardInfo(name.value()));
QListWidgetItem* item = new QListWidgetItem(this);
if (mcd.has_value())
{
item->setIcon(getCardIcon(mcd.value()));
item->setText(tr("%1 [%2]").arg(QString::fromStdString(mcd->name)).arg(getSizeSummary(mcd.value())));
}
else
{
item->setIcon(QIcon::fromTheme("close-line"));
//: Ignore Crowdin's warning for [Missing], the text should be translated.
item->setText(tr("%1 [Missing]").arg(QString::fromStdString(name.value())));
}
if (inherited)
{
QFont font = item->font();
font.setItalic(true);
item->setFont(font);
item->setForeground(palette().brush(QPalette::Disabled, QPalette::Text));
}
item->setToolTip(item->text());
}

Some files were not shown because too many files have changed in this diff Show More