+
+#include "common/types.h"
+#include "input/controller.h"
+#include "input_mouse.h"
+
+#include "SDL3/SDL.h"
+
+namespace Input {
+
+int mouse_joystick_binding = 0;
+float mouse_deadzone_offset = 0.5, mouse_speed = 1, mouse_speed_offset = 0.1250;
+Uint32 mouse_polling_id = 0;
+bool mouse_enabled = false;
+
+// We had to go through 3 files of indirection just to update a flag
+void ToggleMouseEnabled() {
+ mouse_enabled = !mouse_enabled;
+}
+
+void SetMouseToJoystick(int joystick) {
+ mouse_joystick_binding = joystick;
+}
+
+void SetMouseParams(float mdo, float ms, float mso) {
+ mouse_deadzone_offset = mdo;
+ mouse_speed = ms;
+ mouse_speed_offset = mso;
+}
+
+Uint32 MousePolling(void* param, Uint32 id, Uint32 interval) {
+ auto* controller = (GameController*)param;
+ if (!mouse_enabled)
+ return interval;
+
+ Axis axis_x, axis_y;
+ switch (mouse_joystick_binding) {
+ case 1:
+ axis_x = Axis::LeftX;
+ axis_y = Axis::LeftY;
+ break;
+ case 2:
+ axis_x = Axis::RightX;
+ axis_y = Axis::RightY;
+ break;
+ default:
+ return interval; // no update needed
+ }
+
+ float d_x = 0, d_y = 0;
+ SDL_GetRelativeMouseState(&d_x, &d_y);
+
+ float output_speed =
+ SDL_clamp((sqrt(d_x * d_x + d_y * d_y) + mouse_speed_offset * 128) * mouse_speed,
+ mouse_deadzone_offset * 128, 128.0);
+
+ float angle = atan2(d_y, d_x);
+ float a_x = cos(angle) * output_speed, a_y = sin(angle) * output_speed;
+
+ if (d_x != 0 && d_y != 0) {
+ controller->Axis(0, axis_x, GetAxis(-0x80, 0x80, a_x));
+ controller->Axis(0, axis_y, GetAxis(-0x80, 0x80, a_y));
+ } else {
+ controller->Axis(0, axis_x, GetAxis(-0x80, 0x80, 0));
+ controller->Axis(0, axis_y, GetAxis(-0x80, 0x80, 0));
+ }
+
+ return interval;
+}
+
+} // namespace Input
diff --git a/src/input/input_mouse.h b/src/input/input_mouse.h
new file mode 100644
index 000000000..da18ee04e
--- /dev/null
+++ b/src/input/input_mouse.h
@@ -0,0 +1,18 @@
+// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "SDL3/SDL.h"
+#include "common/types.h"
+
+namespace Input {
+
+void ToggleMouseEnabled();
+void SetMouseToJoystick(int joystick);
+void SetMouseParams(float mouse_deadzone_offset, float mouse_speed, float mouse_speed_offset);
+
+// Polls the mouse for changes, and simulates joystick movement from it.
+Uint32 MousePolling(void* param, Uint32 id, Uint32 interval);
+
+} // namespace Input
diff --git a/src/main.cpp b/src/main.cpp
index fad3b1f53..6b334e446 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -167,12 +167,12 @@ int main(int argc, char* argv[]) {
// Check if the provided path is a valid file
if (!std::filesystem::exists(eboot_path)) {
- // If not a file, treat it as a game ID and search in install directories
+ // If not a file, treat it as a game ID and search in install directories recursively
bool game_found = false;
+ const int max_depth = 5;
for (const auto& install_dir : Config::getGameInstallDirs()) {
- const auto candidate_path = install_dir / game_path / "eboot.bin";
- if (std::filesystem::exists(candidate_path)) {
- eboot_path = candidate_path;
+ if (auto found_path = Common::FS::FindGameByID(install_dir, game_path, max_depth)) {
+ eboot_path = *found_path;
game_found = true;
break;
}
diff --git a/src/qt_gui/check_update.cpp b/src/qt_gui/check_update.cpp
index e3e019144..0c1cce5da 100644
--- a/src/qt_gui/check_update.cpp
+++ b/src/qt_gui/check_update.cpp
@@ -146,14 +146,14 @@ void CheckUpdate::CheckForUpdates(const bool showMessage) {
}
QString currentRev = (updateChannel == "Nightly")
- ? QString::fromStdString(Common::g_scm_rev).left(7)
+ ? QString::fromStdString(Common::g_scm_rev)
: "v." + QString::fromStdString(Common::VERSION);
QString currentDate = Common::g_scm_date;
QDateTime dateTime = QDateTime::fromString(latestDate, Qt::ISODate);
latestDate = dateTime.isValid() ? dateTime.toString("yyyy-MM-dd HH:mm:ss") : "Unknown date";
- if (latestRev == currentRev) {
+ if (latestRev == currentRev.left(7)) {
if (showMessage) {
QMessageBox::information(this, tr("Auto Updater"),
tr("Your version is already up to date!"));
@@ -190,7 +190,7 @@ void CheckUpdate::setupUI(const QString& downloadUrl, const QString& latestDate,
QString("
" + tr("Update Channel") + ": " + updateChannel + "
" +
tr("Current Version") + ": %1 (%2)
" + tr("Latest Version") +
": %3 (%4)
" + tr("Do you want to update?") + "
")
- .arg(currentRev, currentDate, latestRev, latestDate);
+ .arg(currentRev.left(7), currentDate, latestRev, latestDate);
QLabel* updateLabel = new QLabel(updateText, this);
layout->addWidget(updateLabel);
diff --git a/src/qt_gui/compatibility_info.cpp b/src/qt_gui/compatibility_info.cpp
index 69fb3e377..884387061 100644
--- a/src/qt_gui/compatibility_info.cpp
+++ b/src/qt_gui/compatibility_info.cpp
@@ -260,3 +260,22 @@ void CompatibilityInfoClass::ExtractCompatibilityInfo(QByteArray response) {
return;
}
+
+const QString CompatibilityInfoClass::GetCompatStatusString(const CompatibilityStatus status) {
+ switch (status) {
+ case CompatibilityStatus::Unknown:
+ return tr("Unknown");
+ case CompatibilityStatus::Nothing:
+ return tr("Nothing");
+ case CompatibilityStatus::Boots:
+ return tr("Boots");
+ case CompatibilityStatus::Menus:
+ return tr("Menus");
+ case CompatibilityStatus::Ingame:
+ return tr("Ingame");
+ case CompatibilityStatus::Playable:
+ return tr("Playable");
+ default:
+ return tr("Unknown");
+ }
+}
\ No newline at end of file
diff --git a/src/qt_gui/compatibility_info.h b/src/qt_gui/compatibility_info.h
index 0c47c27ff..511c106ce 100644
--- a/src/qt_gui/compatibility_info.h
+++ b/src/qt_gui/compatibility_info.h
@@ -69,13 +69,6 @@ public:
{QStringLiteral("os-windows"), OSType::Win32},
};
- inline static const std::unordered_map CompatStatusToString = {
- {CompatibilityStatus::Unknown, QStringLiteral("Unknown")},
- {CompatibilityStatus::Nothing, QStringLiteral("Nothing")},
- {CompatibilityStatus::Boots, QStringLiteral("Boots")},
- {CompatibilityStatus::Menus, QStringLiteral("Menus")},
- {CompatibilityStatus::Ingame, QStringLiteral("Ingame")},
- {CompatibilityStatus::Playable, QStringLiteral("Playable")}};
inline static const std::unordered_map OSTypeToString = {
{OSType::Linux, QStringLiteral("os-linux")},
{OSType::macOS, QStringLiteral("os-macOS")},
@@ -87,6 +80,7 @@ public:
void UpdateCompatibilityDatabase(QWidget* parent = nullptr, bool forced = false);
bool LoadCompatibilityFile();
CompatibilityEntry GetCompatibilityInfo(const std::string& serial);
+ const QString GetCompatStatusString(const CompatibilityStatus status);
void ExtractCompatibilityInfo(QByteArray response);
static bool WaitForReply(QNetworkReply* reply);
QNetworkReply* FetchPage(int page_num);
diff --git a/src/qt_gui/game_grid_frame.cpp b/src/qt_gui/game_grid_frame.cpp
index 2ebb09e5d..d719ac878 100644
--- a/src/qt_gui/game_grid_frame.cpp
+++ b/src/qt_gui/game_grid_frame.cpp
@@ -38,17 +38,18 @@ GameGridFrame::GameGridFrame(std::shared_ptr game_info_get,
void GameGridFrame::onCurrentCellChanged(int currentRow, int currentColumn, int previousRow,
int previousColumn) {
- cellClicked = true;
crtRow = currentRow;
crtColumn = currentColumn;
columnCnt = this->columnCount();
auto itemID = (crtRow * columnCnt) + currentColumn;
if (itemID > m_game_info->m_games.count() - 1) {
+ cellClicked = false;
validCellSelected = false;
BackgroundMusicPlayer::getInstance().stopMusic();
return;
}
+ cellClicked = true;
validCellSelected = true;
SetGridBackgroundImage(crtRow, crtColumn);
auto snd0Path = QString::fromStdString(m_game_info->m_games[itemID].snd0_path.string());
diff --git a/src/qt_gui/game_info.cpp b/src/qt_gui/game_info.cpp
index e4750fa1d..adbf392ed 100644
--- a/src/qt_gui/game_info.cpp
+++ b/src/qt_gui/game_info.cpp
@@ -7,6 +7,33 @@
#include "compatibility_info.h"
#include "game_info.h"
+// Maximum depth to search for games in subdirectories
+const int max_recursion_depth = 5;
+
+void ScanDirectoryRecursively(const QString& dir, QStringList& filePaths, int current_depth = 0) {
+ // Stop recursion if we've reached the maximum depth
+ if (current_depth >= max_recursion_depth) {
+ return;
+ }
+
+ QDir directory(dir);
+ QFileInfoList entries = directory.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
+
+ for (const auto& entry : entries) {
+ if (entry.fileName().endsWith("-UPDATE")) {
+ continue;
+ }
+
+ // Check if this directory contains a PS4 game (has sce_sys/param.sfo)
+ if (QFile::exists(entry.filePath() + "/sce_sys/param.sfo")) {
+ filePaths.append(entry.absoluteFilePath());
+ } else {
+ // If not a game directory, recursively scan it with increased depth
+ ScanDirectoryRecursively(entry.absoluteFilePath(), filePaths, current_depth + 1);
+ }
+ }
+}
+
GameInfoClass::GameInfoClass() = default;
GameInfoClass::~GameInfoClass() = default;
@@ -15,13 +42,7 @@ void GameInfoClass::GetGameInfo(QWidget* parent) {
for (const auto& installLoc : Config::getGameInstallDirs()) {
QString installDir;
Common::FS::PathToQString(installDir, installLoc);
- QDir parentFolder(installDir);
- QFileInfoList fileList = parentFolder.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
- for (const auto& fileInfo : fileList) {
- if (fileInfo.isDir() && !fileInfo.filePath().endsWith("-UPDATE")) {
- filePaths.append(fileInfo.absoluteFilePath());
- }
- }
+ ScanDirectoryRecursively(installDir, filePaths, 0);
}
m_games = QtConcurrent::mapped(filePaths, [&](const QString& path) {
diff --git a/src/qt_gui/game_list_frame.cpp b/src/qt_gui/game_list_frame.cpp
index 9753f511b..f2d08f578 100644
--- a/src/qt_gui/game_list_frame.cpp
+++ b/src/qt_gui/game_list_frame.cpp
@@ -69,7 +69,7 @@ GameListFrame::GameListFrame(std::shared_ptr game_info_get,
ListSortedAsc = true;
}
this->clearContents();
- PopulateGameList();
+ PopulateGameList(false);
});
connect(this, &QTableWidget::customContextMenuRequested, this, [=, this](const QPoint& pos) {
@@ -103,7 +103,7 @@ void GameListFrame::PlayBackgroundMusic(QTableWidgetItem* item) {
BackgroundMusicPlayer::getInstance().playMusic(snd0path);
}
-void GameListFrame::PopulateGameList() {
+void GameListFrame::PopulateGameList(bool isInitialPopulation) {
// Do not show status column if it is not enabled
this->setColumnHidden(2, !Config::getCompatibilityEnabled());
this->setColumnHidden(6, !Config::GetLoadGameSizeEnabled());
@@ -111,6 +111,11 @@ void GameListFrame::PopulateGameList() {
this->setRowCount(m_game_info->m_games.size());
ResizeIcons(icon_size);
+ if (isInitialPopulation) {
+ SortNameAscending(1); // Column 1 = Name
+ ResizeIcons(icon_size);
+ }
+
for (int i = 0; i < m_game_info->m_games.size(); i++) {
SetTableItem(i, 1, QString::fromStdString(m_game_info->m_games[i].name));
SetTableItem(i, 3, QString::fromStdString(m_game_info->m_games[i].serial));
@@ -284,7 +289,7 @@ void GameListFrame::SetCompatibilityItem(int row, int column, CompatibilityEntry
QLabel* dotLabel = new QLabel("", widget);
dotLabel->setPixmap(circle_pixmap);
- QLabel* label = new QLabel(m_compat_info->CompatStatusToString.at(entry.status), widget);
+ QLabel* label = new QLabel(m_compat_info->GetCompatStatusString(entry.status), widget);
label->setStyleSheet("color: white; font-size: 16px; font-weight: bold;");
diff --git a/src/qt_gui/game_list_frame.h b/src/qt_gui/game_list_frame.h
index 8c6fcb1e2..7e37c4ea7 100644
--- a/src/qt_gui/game_list_frame.h
+++ b/src/qt_gui/game_list_frame.h
@@ -3,6 +3,9 @@
#pragma once
+#include // std::transform
+#include // std::tolower
+
#include
#include
#include
@@ -43,7 +46,7 @@ private:
bool ListSortedAsc = true;
public:
- void PopulateGameList();
+ void PopulateGameList(bool isInitialPopulation = true);
void ResizeIcons(int iconSize);
QImage backgroundImage;
@@ -65,8 +68,12 @@ public:
static bool CompareStringsAscending(GameInfo a, GameInfo b, int columnIndex) {
switch (columnIndex) {
- case 1:
- return a.name < b.name;
+ case 1: {
+ std::string name_a = a.name, name_b = b.name;
+ std::transform(name_a.begin(), name_a.end(), name_a.begin(), ::tolower);
+ std::transform(name_b.begin(), name_b.end(), name_b.begin(), ::tolower);
+ return name_a < name_b;
+ }
case 2:
return a.compatibility.status < b.compatibility.status;
case 3:
@@ -90,8 +97,12 @@ public:
static bool CompareStringsDescending(GameInfo a, GameInfo b, int columnIndex) {
switch (columnIndex) {
- case 1:
- return a.name > b.name;
+ case 1: {
+ std::string name_a = a.name, name_b = b.name;
+ std::transform(name_a.begin(), name_a.end(), name_a.begin(), ::tolower);
+ std::transform(name_b.begin(), name_b.end(), name_b.begin(), ::tolower);
+ return name_a > name_b;
+ }
case 2:
return a.compatibility.status > b.compatibility.status;
case 3:
diff --git a/src/qt_gui/gui_context_menus.h b/src/qt_gui/gui_context_menus.h
index 0e8675c0c..bdc2aec0c 100644
--- a/src/qt_gui/gui_context_menus.h
+++ b/src/qt_gui/gui_context_menus.h
@@ -41,8 +41,8 @@ public:
itemID = widget->currentRow() * widget->columnCount() + widget->currentColumn();
}
- // Do not show the menu if an item is selected
- if (itemID == -1) {
+ // Do not show the menu if no item is selected
+ if (itemID < 0 || itemID >= m_games.size()) {
return;
}
@@ -52,10 +52,12 @@ public:
// "Open Folder..." submenu
QMenu* openFolderMenu = new QMenu(tr("Open Folder..."), widget);
QAction* openGameFolder = new QAction(tr("Open Game Folder"), widget);
+ QAction* openUpdateFolder = new QAction(tr("Open Update Folder"), widget);
QAction* openSaveDataFolder = new QAction(tr("Open Save Data Folder"), widget);
QAction* openLogFolder = new QAction(tr("Open Log Folder"), widget);
openFolderMenu->addAction(openGameFolder);
+ openFolderMenu->addAction(openUpdateFolder);
openFolderMenu->addAction(openSaveDataFolder);
openFolderMenu->addAction(openLogFolder);
@@ -87,10 +89,12 @@ public:
QMenu* deleteMenu = new QMenu(tr("Delete..."), widget);
QAction* deleteGame = new QAction(tr("Delete Game"), widget);
QAction* deleteUpdate = new QAction(tr("Delete Update"), widget);
+ QAction* deleteSaveData = new QAction(tr("Delete Save Data"), widget);
QAction* deleteDLC = new QAction(tr("Delete DLC"), widget);
deleteMenu->addAction(deleteGame);
deleteMenu->addAction(deleteUpdate);
+ deleteMenu->addAction(deleteSaveData);
deleteMenu->addAction(deleteDLC);
menu.addMenu(deleteMenu);
@@ -122,6 +126,18 @@ public:
QDesktopServices::openUrl(QUrl::fromLocalFile(folderPath));
}
+ if (selected == openUpdateFolder) {
+ QString open_update_path;
+ Common::FS::PathToQString(open_update_path, m_games[itemID].path);
+ open_update_path += "-UPDATE";
+ if (!std::filesystem::exists(Common::FS::PathFromQString(open_update_path))) {
+ QMessageBox::critical(nullptr, tr("Error"),
+ QString(tr("This game has no update folder to open!")));
+ } else {
+ QDesktopServices::openUrl(QUrl::fromLocalFile(open_update_path));
+ }
+ }
+
if (selected == openSaveDataFolder) {
QString userPath;
Common::FS::PathToQString(userPath,
@@ -143,7 +159,7 @@ public:
PSF psf;
std::filesystem::path game_folder_path = m_games[itemID].path;
std::filesystem::path game_update_path = game_folder_path;
- game_update_path += "UPDATE";
+ game_update_path += "-UPDATE";
if (std::filesystem::exists(game_update_path)) {
game_folder_path = game_update_path;
}
@@ -238,6 +254,11 @@ public:
QString trophyPath, gameTrpPath;
Common::FS::PathToQString(trophyPath, m_games[itemID].serial);
Common::FS::PathToQString(gameTrpPath, m_games[itemID].path);
+ auto game_update_path = Common::FS::PathFromQString(gameTrpPath);
+ game_update_path += "-UPDATE";
+ if (std::filesystem::exists(game_update_path)) {
+ Common::FS::PathToQString(gameTrpPath, game_update_path);
+ }
TrophyViewer* trophyViewer = new TrophyViewer(trophyPath, gameTrpPath);
trophyViewer->show();
connect(widget->parent(), &QWidget::destroyed, trophyViewer,
@@ -335,14 +356,18 @@ public:
clipboard->setText(combinedText);
}
- if (selected == deleteGame || selected == deleteUpdate || selected == deleteDLC) {
+ if (selected == deleteGame || selected == deleteUpdate || selected == deleteDLC ||
+ selected == deleteSaveData) {
bool error = false;
- QString folder_path, game_update_path, dlc_path;
+ QString folder_path, game_update_path, dlc_path, save_data_path;
Common::FS::PathToQString(folder_path, m_games[itemID].path);
game_update_path = folder_path + "-UPDATE";
Common::FS::PathToQString(
dlc_path, Config::getAddonInstallDir() /
Common::FS::PathFromQString(folder_path).parent_path().filename());
+ Common::FS::PathToQString(save_data_path,
+ Common::FS::GetUserPath(Common::FS::PathType::UserDir) /
+ "savedata/1" / m_games[itemID].serial);
QString message_type = tr("Game");
if (selected == deleteUpdate) {
@@ -363,6 +388,15 @@ public:
folder_path = dlc_path;
message_type = tr("DLC");
}
+ } else if (selected == deleteSaveData) {
+ if (!std::filesystem::exists(Common::FS::PathFromQString(save_data_path))) {
+ QMessageBox::critical(nullptr, tr("Error"),
+ QString(tr("This game has no save data to delete!")));
+ error = true;
+ } else {
+ folder_path = save_data_path;
+ message_type = tr("Save Data");
+ }
}
if (!error) {
QString gameName = QString::fromStdString(m_games[itemID].name);
@@ -374,7 +408,10 @@ public:
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
dir.removeRecursively();
- widget->removeRow(itemID);
+ if (selected == deleteGame) {
+ widget->removeRow(itemID);
+ m_games.removeAt(itemID);
+ }
}
}
}
diff --git a/src/qt_gui/install_dir_select.cpp b/src/qt_gui/install_dir_select.cpp
index e0951b123..e90a10ee6 100644
--- a/src/qt_gui/install_dir_select.cpp
+++ b/src/qt_gui/install_dir_select.cpp
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include
#include
#include
#include
@@ -15,10 +16,11 @@
#include "install_dir_select.h"
InstallDirSelect::InstallDirSelect() : selected_dir() {
- selected_dir = Config::getGameInstallDirs().empty() ? "" : Config::getGameInstallDirs().front();
+ auto install_dirs = Config::getGameInstallDirs();
+ selected_dir = install_dirs.empty() ? "" : install_dirs.front();
- if (!Config::getGameInstallDirs().empty() && Config::getGameInstallDirs().size() == 1) {
- reject();
+ if (!install_dirs.empty() && install_dirs.size() == 1) {
+ accept();
}
auto layout = new QVBoxLayout(this);
@@ -53,6 +55,14 @@ QWidget* InstallDirSelect::SetupInstallDirList() {
vlayout->addWidget(m_path_list);
+ auto checkbox = new QCheckBox(tr("Install All Queued to Selected Folder"));
+ connect(checkbox, &QCheckBox::toggled, this, &InstallDirSelect::setUseForAllQueued);
+ vlayout->addWidget(checkbox);
+
+ auto checkbox2 = new QCheckBox(tr("Delete PKG File on Install"));
+ connect(checkbox2, &QCheckBox::toggled, this, &InstallDirSelect::setDeleteFileOnInstall);
+ vlayout->addWidget(checkbox2);
+
group->setLayout(vlayout);
return group;
}
@@ -66,6 +76,14 @@ void InstallDirSelect::setSelectedDirectory(QListWidgetItem* item) {
}
}
+void InstallDirSelect::setUseForAllQueued(bool enabled) {
+ use_for_all_queued = enabled;
+}
+
+void InstallDirSelect::setDeleteFileOnInstall(bool enabled) {
+ delete_file_on_install = enabled;
+}
+
QWidget* InstallDirSelect::SetupDialogActions() {
auto actions = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
diff --git a/src/qt_gui/install_dir_select.h b/src/qt_gui/install_dir_select.h
index e3e81575a..e11cbf381 100644
--- a/src/qt_gui/install_dir_select.h
+++ b/src/qt_gui/install_dir_select.h
@@ -22,9 +22,21 @@ public:
return selected_dir;
}
+ bool useForAllQueued() {
+ return use_for_all_queued;
+ }
+
+ bool deleteFileOnInstall() {
+ return delete_file_on_install;
+ }
+
private:
QWidget* SetupInstallDirList();
QWidget* SetupDialogActions();
void setSelectedDirectory(QListWidgetItem* item);
+ void setDeleteFileOnInstall(bool enabled);
+ void setUseForAllQueued(bool enabled);
std::filesystem::path selected_dir;
+ bool delete_file_on_install = false;
+ bool use_for_all_queued = false;
};
diff --git a/src/qt_gui/kbm_config_dialog.cpp b/src/qt_gui/kbm_config_dialog.cpp
new file mode 100644
index 000000000..74a49034b
--- /dev/null
+++ b/src/qt_gui/kbm_config_dialog.cpp
@@ -0,0 +1,248 @@
+// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "kbm_config_dialog.h"
+#include "kbm_help_dialog.h"
+
+#include
+#include
+#include
+#include "common/config.h"
+#include "common/path_util.h"
+#include "game_info.h"
+#include "src/sdl_window.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+QString previous_game = "default";
+bool isHelpOpen = false;
+HelpDialog* helpDialog;
+
+EditorDialog::EditorDialog(QWidget* parent) : QDialog(parent) {
+
+ setWindowTitle("Edit Keyboard + Mouse and Controller input bindings");
+ resize(600, 400);
+
+ // Create the editor widget
+ editor = new QPlainTextEdit(this);
+ editorFont.setPointSize(10); // Set default text size
+ editor->setFont(editorFont); // Apply font to the editor
+
+ // Create the game selection combo box
+ gameComboBox = new QComboBox(this);
+ gameComboBox->addItem("default"); // Add default option
+ /*
+ gameComboBox = new QComboBox(this);
+ layout->addWidget(gameComboBox); // Add the combobox for selecting game configurations
+
+ // Populate the combo box with game configurations
+ QStringList gameConfigs = GameInfoClass::GetGameInfo(this);
+ gameComboBox->addItems(gameConfigs);
+ gameComboBox->setCurrentText("default.ini"); // Set the default selection
+ */
+ // Load all installed games
+ loadInstalledGames();
+
+ QCheckBox* unifiedInputCheckBox = new QCheckBox("Use Per-Game configs", this);
+ unifiedInputCheckBox->setChecked(!Config::GetUseUnifiedInputConfig());
+
+ // Connect checkbox signal
+ connect(unifiedInputCheckBox, &QCheckBox::toggled, this, [](bool checked) {
+ Config::SetUseUnifiedInputConfig(!checked);
+ Config::save(Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.toml");
+ });
+ // Create Save, Cancel, and Help buttons
+ Config::SetUseUnifiedInputConfig(!Config::GetUseUnifiedInputConfig());
+ QPushButton* saveButton = new QPushButton("Save", this);
+ QPushButton* cancelButton = new QPushButton("Cancel", this);
+ QPushButton* helpButton = new QPushButton("Help", this);
+ QPushButton* defaultButton = new QPushButton("Default", this);
+
+ // Layout for the game selection and buttons
+ QHBoxLayout* topLayout = new QHBoxLayout();
+ topLayout->addWidget(unifiedInputCheckBox);
+ topLayout->addWidget(gameComboBox);
+ topLayout->addStretch();
+ topLayout->addWidget(saveButton);
+ topLayout->addWidget(cancelButton);
+ topLayout->addWidget(defaultButton);
+ topLayout->addWidget(helpButton);
+
+ // Main layout with editor and buttons
+ QVBoxLayout* layout = new QVBoxLayout(this);
+ layout->addLayout(topLayout);
+ layout->addWidget(editor);
+
+ // Load the default config file content into the editor
+ loadFile(gameComboBox->currentText());
+
+ // Connect button and combo box signals
+ connect(saveButton, &QPushButton::clicked, this, &EditorDialog::onSaveClicked);
+ connect(cancelButton, &QPushButton::clicked, this, &EditorDialog::onCancelClicked);
+ connect(helpButton, &QPushButton::clicked, this, &EditorDialog::onHelpClicked);
+ connect(defaultButton, &QPushButton::clicked, this, &EditorDialog::onResetToDefaultClicked);
+ connect(gameComboBox, &QComboBox::currentTextChanged, this,
+ &EditorDialog::onGameSelectionChanged);
+}
+
+void EditorDialog::loadFile(QString game) {
+
+ const auto config_file = Config::GetFoolproofKbmConfigFile(game.toStdString());
+ QFile file(config_file);
+
+ if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ QTextStream in(&file);
+ editor->setPlainText(in.readAll());
+ originalConfig = editor->toPlainText();
+ file.close();
+ } else {
+ QMessageBox::warning(this, "Error", "Could not open the file for reading");
+ }
+}
+
+void EditorDialog::saveFile(QString game) {
+
+ const auto config_file = Config::GetFoolproofKbmConfigFile(game.toStdString());
+ QFile file(config_file);
+
+ if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
+ QTextStream out(&file);
+ out << editor->toPlainText();
+ file.close();
+ } else {
+ QMessageBox::warning(this, "Error", "Could not open the file for writing");
+ }
+}
+
+// Override the close event to show the save confirmation dialog only if changes were made
+void EditorDialog::closeEvent(QCloseEvent* event) {
+ if (isHelpOpen) {
+ helpDialog->close();
+ isHelpOpen = false;
+ // at this point I might have to add this flag and the help dialog to the class itself
+ }
+ if (hasUnsavedChanges()) {
+ QMessageBox::StandardButton reply;
+ reply = QMessageBox::question(this, "Save Changes", "Do you want to save changes?",
+ QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
+
+ if (reply == QMessageBox::Yes) {
+ saveFile(gameComboBox->currentText());
+ event->accept(); // Close the dialog
+ } else if (reply == QMessageBox::No) {
+ event->accept(); // Close the dialog without saving
+ } else {
+ event->ignore(); // Cancel the close event
+ }
+ } else {
+ event->accept(); // No changes, close the dialog without prompting
+ }
+}
+void EditorDialog::keyPressEvent(QKeyEvent* event) {
+ if (event->key() == Qt::Key_Escape) {
+ if (isHelpOpen) {
+ helpDialog->close();
+ isHelpOpen = false;
+ }
+ close(); // Trigger the close action, same as pressing the close button
+ } else {
+ QDialog::keyPressEvent(event); // Call the base class implementation for other keys
+ }
+}
+
+void EditorDialog::onSaveClicked() {
+ if (isHelpOpen) {
+ helpDialog->close();
+ isHelpOpen = false;
+ }
+ saveFile(gameComboBox->currentText());
+ reject(); // Close the dialog
+}
+
+void EditorDialog::onCancelClicked() {
+ if (isHelpOpen) {
+ helpDialog->close();
+ isHelpOpen = false;
+ }
+ reject(); // Close the dialog
+}
+
+void EditorDialog::onHelpClicked() {
+ if (!isHelpOpen) {
+ helpDialog = new HelpDialog(&isHelpOpen, this);
+ helpDialog->setWindowTitle("Help");
+ helpDialog->setAttribute(Qt::WA_DeleteOnClose); // Clean up on close
+ // Get the position and size of the Config window
+ QRect configGeometry = this->geometry();
+ int helpX = configGeometry.x() + configGeometry.width() + 10; // 10 pixels offset
+ int helpY = configGeometry.y();
+ // Move the Help dialog to the right side of the Config window
+ helpDialog->move(helpX, helpY);
+ helpDialog->show();
+ isHelpOpen = true;
+ } else {
+ helpDialog->close();
+ isHelpOpen = false;
+ }
+}
+
+void EditorDialog::onResetToDefaultClicked() {
+ bool default_default = gameComboBox->currentText() == "default";
+ QString prompt =
+ default_default
+ ? "Do you want to reset your custom default config to the original default config?"
+ : "Do you want to reset this config to your custom default config?";
+ QMessageBox::StandardButton reply =
+ QMessageBox::question(this, "Reset to Default", prompt, QMessageBox::Yes | QMessageBox::No);
+
+ if (reply == QMessageBox::Yes) {
+ if (default_default) {
+ const auto default_file = Config::GetFoolproofKbmConfigFile("default");
+ std::filesystem::remove(default_file);
+ }
+ const auto config_file = Config::GetFoolproofKbmConfigFile("default");
+ QFile file(config_file);
+
+ if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ QTextStream in(&file);
+ editor->setPlainText(in.readAll());
+ file.close();
+ } else {
+ QMessageBox::warning(this, "Error", "Could not open the file for reading");
+ }
+ // saveFile(gameComboBox->currentText());
+ }
+}
+
+bool EditorDialog::hasUnsavedChanges() {
+ // Compare the current content with the original content to check if there are unsaved changes
+ return editor->toPlainText() != originalConfig;
+}
+void EditorDialog::loadInstalledGames() {
+ previous_game = "default";
+ QStringList filePaths;
+ for (const auto& installLoc : Config::getGameInstallDirs()) {
+ QString installDir;
+ Common::FS::PathToQString(installDir, installLoc);
+ QDir parentFolder(installDir);
+ QFileInfoList fileList = parentFolder.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
+ for (const auto& fileInfo : fileList) {
+ if (fileInfo.isDir() && !fileInfo.filePath().endsWith("-UPDATE")) {
+ gameComboBox->addItem(fileInfo.fileName()); // Add game name to combo box
+ }
+ }
+ }
+}
+void EditorDialog::onGameSelectionChanged(const QString& game) {
+ saveFile(previous_game);
+ loadFile(gameComboBox->currentText()); // Reload file based on the selected game
+ previous_game = gameComboBox->currentText();
+}
diff --git a/src/qt_gui/kbm_config_dialog.h b/src/qt_gui/kbm_config_dialog.h
new file mode 100644
index 000000000..f436b4a71
--- /dev/null
+++ b/src/qt_gui/kbm_config_dialog.h
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+#include
+#include
+#include "string"
+
+class EditorDialog : public QDialog {
+ Q_OBJECT // Necessary for using Qt's meta-object system (signals/slots)
+ public : explicit EditorDialog(QWidget* parent = nullptr); // Constructor
+
+protected:
+ void closeEvent(QCloseEvent* event) override; // Override close event
+ void keyPressEvent(QKeyEvent* event) override;
+
+private:
+ QPlainTextEdit* editor; // Editor widget for the config file
+ QFont editorFont; // To handle the text size
+ QString originalConfig; // Starting config string
+ std::string gameId;
+
+ QComboBox* gameComboBox; // Combo box for selecting game configurations
+
+ void loadFile(QString game); // Function to load the config file
+ void saveFile(QString game); // Function to save the config file
+ void loadInstalledGames(); // Helper to populate gameComboBox
+ bool hasUnsavedChanges(); // Checks for unsaved changes
+
+private slots:
+ void onSaveClicked(); // Save button slot
+ void onCancelClicked(); // Slot for handling cancel button
+ void onHelpClicked(); // Slot for handling help button
+ void onResetToDefaultClicked();
+ void onGameSelectionChanged(const QString& game); // Slot for game selection changes
+};
diff --git a/src/qt_gui/kbm_help_dialog.cpp b/src/qt_gui/kbm_help_dialog.cpp
new file mode 100644
index 000000000..44f75f6f8
--- /dev/null
+++ b/src/qt_gui/kbm_help_dialog.cpp
@@ -0,0 +1,112 @@
+// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "kbm_help_dialog.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+ExpandableSection::ExpandableSection(const QString& title, const QString& content,
+ QWidget* parent = nullptr)
+ : QWidget(parent) {
+ QVBoxLayout* layout = new QVBoxLayout(this);
+
+ // Button to toggle visibility of content
+ toggleButton = new QPushButton(title);
+ layout->addWidget(toggleButton);
+
+ // QTextBrowser for content (initially hidden)
+ contentBrowser = new QTextBrowser();
+ contentBrowser->setPlainText(content);
+ contentBrowser->setVisible(false);
+
+ // Remove scrollbars from QTextBrowser
+ contentBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ contentBrowser->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+
+ // Set size policy to allow vertical stretching only
+ contentBrowser->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
+
+ // Calculate and set initial height based on content
+ updateContentHeight();
+
+ layout->addWidget(contentBrowser);
+
+ // Connect button click to toggle visibility
+ connect(toggleButton, &QPushButton::clicked, [this]() {
+ contentBrowser->setVisible(!contentBrowser->isVisible());
+ if (contentBrowser->isVisible()) {
+ updateContentHeight(); // Update height when expanding
+ }
+ emit expandedChanged(); // Notify for layout adjustments
+ });
+
+ // Connect to update height if content changes
+ connect(contentBrowser->document(), &QTextDocument::contentsChanged, this,
+ &ExpandableSection::updateContentHeight);
+
+ // Minimal layout settings for spacing
+ layout->setSpacing(2);
+ layout->setContentsMargins(0, 0, 0, 0);
+}
+
+void HelpDialog::closeEvent(QCloseEvent* event) {
+ *help_open_ptr = false;
+ close();
+}
+void HelpDialog::reject() {
+ *help_open_ptr = false;
+ close();
+}
+
+HelpDialog::HelpDialog(bool* open_flag, QWidget* parent) : QDialog(parent) {
+ help_open_ptr = open_flag;
+ // Main layout for the help dialog
+ QVBoxLayout* mainLayout = new QVBoxLayout(this);
+
+ // Container widget for the scroll area
+ QWidget* containerWidget = new QWidget;
+ QVBoxLayout* containerLayout = new QVBoxLayout(containerWidget);
+
+ // Add expandable sections to container layout
+ auto* quickstartSection = new ExpandableSection("Quickstart", quickstart());
+ auto* faqSection = new ExpandableSection("FAQ", faq());
+ auto* syntaxSection = new ExpandableSection("Syntax", syntax());
+ auto* specialSection = new ExpandableSection("Special Bindings", special());
+ auto* bindingsSection = new ExpandableSection("Keybindings", bindings());
+
+ containerLayout->addWidget(quickstartSection);
+ containerLayout->addWidget(faqSection);
+ containerLayout->addWidget(syntaxSection);
+ containerLayout->addWidget(specialSection);
+ containerLayout->addWidget(bindingsSection);
+ containerLayout->addStretch(1);
+
+ // Scroll area wrapping the container
+ QScrollArea* scrollArea = new QScrollArea;
+ scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+ scrollArea->setWidgetResizable(true);
+ scrollArea->setWidget(containerWidget);
+
+ // Add the scroll area to the main dialog layout
+ mainLayout->addWidget(scrollArea);
+ setLayout(mainLayout);
+
+ // Minimum size for the dialog
+ setMinimumSize(500, 400);
+
+ // Re-adjust dialog layout when any section expands/collapses
+ connect(quickstartSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
+ connect(faqSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
+ connect(syntaxSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
+ connect(specialSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
+ connect(bindingsSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
+}
\ No newline at end of file
diff --git a/src/qt_gui/kbm_help_dialog.h b/src/qt_gui/kbm_help_dialog.h
new file mode 100644
index 000000000..c482d2b5c
--- /dev/null
+++ b/src/qt_gui/kbm_help_dialog.h
@@ -0,0 +1,175 @@
+// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class ExpandableSection : public QWidget {
+ Q_OBJECT
+public:
+ explicit ExpandableSection(const QString& title, const QString& content, QWidget* parent);
+
+signals:
+ void expandedChanged(); // Signal to indicate layout size change
+
+private:
+ QPushButton* toggleButton;
+ QTextBrowser* contentBrowser; // Changed from QLabel to QTextBrowser
+ QPropertyAnimation* animation;
+ int contentHeight;
+ void updateContentHeight() {
+ int contentHeight = contentBrowser->document()->size().height();
+ contentBrowser->setMinimumHeight(contentHeight + 5);
+ contentBrowser->setMaximumHeight(contentHeight + 50);
+ }
+};
+
+class HelpDialog : public QDialog {
+ Q_OBJECT
+public:
+ explicit HelpDialog(bool* open_flag = nullptr, QWidget* parent = nullptr);
+
+protected:
+ void closeEvent(QCloseEvent* event) override;
+ void reject() override;
+
+private:
+ bool* help_open_ptr;
+
+ QString quickstart() {
+ return
+ R"(The keyboard and controller remapping backend, GUI and documentation have been written by kalaposfos
+
+In this section, you will find information about the project, its features and help on setting up your ideal setup.
+To view the config file's syntax, check out the Syntax tab, for keybind names, visit Normal Keybinds and Special Bindings, and if you are here to view emulator-wide keybinds, you can find it in the FAQ section.
+This project started out because I didn't like the original unchangeable keybinds, but rather than waiting for someone else to do it, I implemented this myself. From the default keybinds, you can clearly tell this was a project built for Bloodborne, but ovbiously you can make adjustments however you like.
+)";
+ }
+ QString faq() {
+ return
+ R"(Q: What are the emulator-wide keybinds?
+A: -F12: Triggers Renderdoc capture
+-F11: Toggles fullscreen
+-F10: Toggles FPS counter
+-Ctrl F10: Open the debug menu
+-F9: Pauses emultor, if the debug menu is open
+-F8: Reparses the config file while in-game
+-F7: Toggles mouse capture and mouse input
+
+Q: How do I change between mouse and controller joystick input, and why is it even required?
+A: You can switch between them with F7, and it is required, because mouse input is done with polling, which means mouse movement is checked every frame, and if it didn't move, the code manually sets the emulator's virtual controller to 0 (back to the center), even if other input devices would update it.
+
+Q: What happens if I accidentally make a typo in the config?
+A: The code recognises the line as wrong, and skip it, so the rest of the file will get parsed, but that line in question will be treated like a comment line. You can find these lines in the log, if you search for 'input_handler'.
+
+Q: I want to bind to