Qt: Add setting to show state load errors using a dialog or OSD message

This commit is contained in:
chaoticgd 2025-11-30 20:15:57 +00:00 committed by Ty
parent 92c7eaa383
commit 764875ddbf
13 changed files with 166 additions and 25 deletions

View File

@ -37,6 +37,7 @@
#include "pcsx2/PerformanceMetrics.h"
#include "pcsx2/Recording/InputRecording.h"
#include "pcsx2/Recording/InputRecordingControls.h"
#include "pcsx2/SaveState.h"
#include "pcsx2/SIO/Sio.h"
#include "pcsx2/GS/GSExtra.h"
@ -1213,6 +1214,58 @@ void MainWindow::onStatusMessage(const QString& message)
m_ui.statusBar->showMessage(message);
}
void MainWindow::reportStateLoadError(const QString& message, std::optional<s32> slot, bool backup)
{
const bool prompt_on_error = Host::GetBaseBoolSettingValue("UI", "PromptOnStateLoadSaveFailure", true);
if (!prompt_on_error)
{
SaveState_ReportLoadErrorOSD(message.toStdString(), slot, backup);
return;
}
QString title;
if (slot.has_value())
{
if (backup)
title = tr("Failed to Load State From Backup Slot %1").arg(*slot);
else
title = tr("Failed to Load State From Slot %1").arg(*slot);
}
else
{
title = tr("Failed to Load State");
}
VMLock lock(pauseAndLockVM());
QCheckBox* do_not_show_again = new QCheckBox(tr("Do not show again"));
QPointer<QMessageBox> message_box = new QMessageBox(this);
message_box->setWindowTitle(title);
message_box->setText(message);
message_box->setIcon(QMessageBox::Critical);
message_box->addButton(QMessageBox::Ok);
message_box->setDefaultButton(QMessageBox::Ok);
message_box->setCheckBox(do_not_show_again);
message_box->exec();
if (message_box.isNull())
return;
if (do_not_show_again->isChecked())
{
Host::SetBaseBoolSettingValue("UI", "PromptOnStateLoadSaveFailure", false);
Host::CommitBaseSettingChanges();
if (m_settings_window)
{
InterfaceSettingsWidget* interface_settings = m_settings_window->getInterfaceSettingsWidget();
interface_settings->updatePromptOnStateLoadSaveFailureCheckbox(Qt::Unchecked);
}
}
delete message_box;
}
void MainWindow::runOnUIThread(const std::function<void()>& func)
{
func();

View File

@ -120,6 +120,7 @@ public Q_SLOTS:
void reportError(const QString& title, const QString& message);
bool confirmMessage(const QString& title, const QString& message);
void onStatusMessage(const QString& message);
void reportStateLoadError(const QString& message, std::optional<s32> slot, bool backup);
void runOnUIThread(const std::function<void()>& func);
void requestReset();

View File

@ -299,7 +299,11 @@ void EmuThread::loadState(const QString& filename)
Error error;
if (!VMManager::LoadState(filename.toUtf8().constData(), &error))
Host::ReportErrorAsync(TRANSLATE_SV("QtHost", "Failed to Load State"), error.GetDescription());
{
QtHost::RunOnUIThread([message = QString::fromStdString(error.GetDescription())]() {
g_main_window->reportStateLoadError(message, std::nullopt, false);
});
}
}
void EmuThread::loadStateFromSlot(qint32 slot, bool load_backup)
@ -315,7 +319,11 @@ void EmuThread::loadStateFromSlot(qint32 slot, bool load_backup)
Error error;
if (!VMManager::LoadStateFromSlot(slot, load_backup, &error))
Host::ReportErrorAsync(TRANSLATE_SV("QtHost", "Failed to Load State"), error.GetDescription());
{
QtHost::RunOnUIThread([message = QString::fromStdString(error.GetDescription()), slot, load_backup]() {
g_main_window->reportStateLoadError(message, slot, load_backup);
});
}
}
void EmuThread::saveState(const QString& filename)

View File

@ -97,15 +97,16 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* settings_dialog
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.promptOnStateLoadSaveFailure, "UI", "PromptOnStateLoadSaveFailure", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.discordPresence, "EmuCore", "EnableDiscordPresence", false);
#ifdef __linux__ // Mouse locking is only supported on X11
#ifdef __linux__ // Mouse locking is only supported on X11
const bool mouse_lock_supported = QGuiApplication::platformName().toLower() == "xcb";
#else
const bool mouse_lock_supported = true;
#endif
if(mouse_lock_supported)
if (mouse_lock_supported)
{
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.mouseLock, "EmuCore", "EnableMouseLock", false);
connect(m_ui.mouseLock, &QCheckBox::checkStateChanged, [](Qt::CheckState state) {
@ -196,6 +197,8 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* settings_dialog
"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.promptOnStateLoadSaveFailure, tr("Pause On State Load/Save Failure"),
tr("Checked"), tr("Display a modal dialog when a save state load/save operation fails."));
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"),
@ -224,7 +227,7 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* settings_dialog
dialog()->registerWidgetHelp(
m_ui.backgroundBrowse, tr("Game List Background"), tr("None"),
tr("Enable an animated/static background on the game list (where you launch your games).<br>"
"This background is only visible in the library and will be hidden once a game is launched. It will also be paused when it's not in focus."));
"This background is only visible in the library and will be hidden once a game is launched. It will also be paused when it's not in focus."));
dialog()->registerWidgetHelp(
m_ui.backgroundReset, tr("Disable/Reset Game List Background"), tr("None"),
tr("Disable and reset the currently applied game list background."));
@ -241,6 +244,12 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* settings_dialog
InterfaceSettingsWidget::~InterfaceSettingsWidget() = default;
void InterfaceSettingsWidget::updatePromptOnStateLoadSaveFailureCheckbox(Qt::CheckState state)
{
QSignalBlocker blocker(m_ui.promptOnStateLoadSaveFailure);
m_ui.promptOnStateLoadSaveFailure->setCheckState(state);
}
void InterfaceSettingsWidget::onRenderToSeparateWindowChanged()
{
m_ui.hideMainWindow->setEnabled(m_ui.renderToSeparateWindow->isChecked());

View File

@ -15,6 +15,8 @@ public:
InterfaceSettingsWidget(SettingsWindow* settings_dialog, QWidget* parent);
~InterfaceSettingsWidget();
void updatePromptOnStateLoadSaveFailureCheckbox(Qt::CheckState state);
Q_SIGNALS:
void themeChanged();
void languageChanged();

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>725</width>
<height>625</height>
<height>637</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@ -78,6 +78,13 @@
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="promptOnStateLoadSaveFailure">
<property name="text">
<string>Prompt On State Load/Save Failure</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -380,6 +387,7 @@
<tabstop>discordPresence</tabstop>
<tabstop>pauseOnControllerDisconnection</tabstop>
<tabstop>mouseLock</tabstop>
<tabstop>promptOnStateLoadSaveFailure</tabstop>
<tabstop>startFullscreen</tabstop>
<tabstop>doubleClickTogglesFullscreen</tabstop>
<tabstop>renderToSeparateWindow</tabstop>

View File

@ -106,8 +106,7 @@ static void HotkeyLoadStateSlot(s32 slot)
Error error;
if (!VMManager::LoadStateFromSlot(slot, false, &error))
Host::AddIconOSDMessage("LoadStateFromSlot", ICON_FA_TRIANGLE_EXCLAMATION,
error.GetDescription(), Host::OSD_INFO_DURATION);
FullscreenUI::ReportStateLoadError(error.GetDescription(), slot, false);
});
}

View File

@ -670,7 +670,7 @@ namespace FullscreenUI
static void DrawSaveStateSelector(bool is_loading);
static bool OpenLoadStateSelectorForGameResume(const GameList::Entry* entry);
static void DrawResumeStateSelector();
static void DoLoadState(std::string path);
static void DoLoadState(std::string path, std::optional<s32> slot, bool backup);
static std::vector<SaveStateListEntry> s_save_state_selector_slots;
static std::string s_save_state_selector_game_path;
@ -4129,6 +4129,8 @@ void FullscreenUI::DrawInterfaceSettingsPage()
FSUI_CSTR("Pauses the emulator when a controller with bindings is disconnected."), "UI", "PauseOnControllerDisconnection", false);
DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_RECTANGLE_LIST, "Pause On Menu"),
FSUI_CSTR("Pauses the emulator when you open the quick menu, and unpauses when you close it."), "UI", "PauseOnMenu", true);
DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_FLOPPY_DISK, "Prompt On State Load/Save Failure"),
FSUI_CSTR("Display a modal dialog when a save state load/save operation fails."), "UI", "PromptOnStateLoadSaveFailure", true);
DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_POWER_OFF, "Confirm Shutdown"),
FSUI_CSTR("Determines whether a prompt will be displayed to confirm shutting down the emulator/game when the hotkey is pressed."),
"UI", "ConfirmShutdown", true);
@ -7345,7 +7347,7 @@ void FullscreenUI::DrawSaveStateSelector(bool is_loading)
false, is_loading ? !Achievements::IsHardcoreModeActive() : true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
{
if (is_loading)
DoLoadState(std::move(entry.path));
DoLoadState(std::move(entry.path), entry.slot, false);
else
Host::RunOnCPUThread([slot = entry.slot]() { VMManager::SaveStateToSlot(slot); });
@ -7483,7 +7485,7 @@ void FullscreenUI::DrawSaveStateSelector(bool is_loading)
if (pressed)
{
if (is_loading)
DoLoadState(entry.path);
DoLoadState(entry.path, entry.slot, false);
else
Host::RunOnCPUThread([slot = entry.slot]() { VMManager::SaveStateToSlot(slot); });
@ -7639,19 +7641,16 @@ void FullscreenUI::DrawResumeStateSelector()
}
}
void FullscreenUI::DoLoadState(std::string path)
void FullscreenUI::DoLoadState(std::string path, std::optional<s32> slot, bool backup)
{
Host::RunOnCPUThread([boot_path = s_save_state_selector_game_path, path = std::move(path)]() {
std::string boot_path = s_save_state_selector_game_path;
Host::RunOnCPUThread([boot_path = std::move(boot_path), path = std::move(path), slot, backup]() {
if (VMManager::HasValidVM())
{
Error error;
if (!VMManager::LoadState(path.c_str(), &error))
{
MTGS::RunOnGSThread([error = std::move(error)]() {
ImGuiFullscreen::OpenInfoMessageDialog(
FSUI_ICONSTR(ICON_FA_TRIANGLE_EXCLAMATION, "Failed to Load State"),
error.GetDescription());
});
ReportStateLoadError(error.GetDescription(), slot, backup);
return;
}
@ -9169,6 +9168,44 @@ void FullscreenUI::DrawAchievementsSettingsPage(std::unique_lock<std::mutex>& se
EndMenuButtons();
}
void FullscreenUI::ReportStateLoadError(std::string message, std::optional<s32> slot, bool backup)
{
MTGS::RunOnGSThread([message = std::move(message), slot, backup]() {
const bool prompt_on_error = Host::GetBaseBoolSettingValue("UI", "PromptOnStateLoadSaveFailure", true);
if (!prompt_on_error || !ImGuiManager::InitializeFullscreenUI())
{
SaveState_ReportLoadErrorOSD(message, slot, backup);
return;
}
std::string title;
if (slot.has_value())
{
if (backup)
title = fmt::format(FSUI_FSTR("Failed to Load State From Backup Slot {}"), *slot);
else
title = fmt::format(FSUI_FSTR("Failed to Load State From Slot {}"), *slot);
}
else
{
title = FSUI_STR("Failed to Load State");
}
ImGuiFullscreen::InfoMessageDialogCallback callback;
if (VMManager::GetState() == VMState::Running)
{
Host::RunOnCPUThread([]() { VMManager::SetPaused(true); });
callback = []() {
Host::RunOnCPUThread([]() { VMManager::SetPaused(false); });
};
}
ImGuiFullscreen::OpenInfoMessageDialog(
fmt::format("{} {}", ICON_FA_TRIANGLE_EXCLAMATION, title),
std::move(message), std::move(callback));
});
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Translation String Area
// To avoid having to type T_RANSLATE("FullscreenUI", ...) everywhere, we use the shorter macros at the top

View File

@ -10,6 +10,7 @@
#include <ctime>
#include <string>
#include <memory>
#include <optional>
struct Pcsx2Config;
@ -26,6 +27,7 @@ namespace FullscreenUI
void OpenPauseMenu();
bool OpenAchievementsWindow();
bool OpenLeaderboardsWindow();
void ReportStateLoadError(std::string message, std::optional<s32> slot, bool backup);
// NOTE: Only call from GS thread.
bool IsAchievementsWindowOpen();

View File

@ -1382,8 +1382,7 @@ void SaveStateSelectorUI::LoadCurrentSlot()
Host::RunOnCPUThread([slot = GetCurrentSlot()]() {
Error error;
if (!VMManager::LoadStateFromSlot(slot, false, &error))
Host::AddIconOSDMessage("LoadStateFromSlot", ICON_FA_TRIANGLE_EXCLAMATION,
error.GetDescription(), Host::OSD_INFO_DURATION);
FullscreenUI::ReportStateLoadError(error.GetDescription(), slot, false);
});
Close();
}
@ -1393,8 +1392,7 @@ void SaveStateSelectorUI::LoadCurrentBackupSlot()
Host::RunOnCPUThread([slot = GetCurrentSlot()]() {
Error error;
if (!VMManager::LoadStateFromSlot(slot, true, &error))
Host::AddIconOSDMessage("LoadStateFromSlot", ICON_FA_TRIANGLE_EXCLAMATION,
error.GetDescription(), Host::OSD_INFO_DURATION);
FullscreenUI::ReportStateLoadError(error.GetDescription(), slot, true);
});
Close();
}

View File

@ -6,6 +6,7 @@
#include "Host.h"
#include "Memory.h"
#include "Elfheader.h"
#include "SaveState.h"
#include "PINE.h"
#include "VMManager.h"
#include "common/Error.h"
@ -19,7 +20,6 @@
#include <thread>
#include "fmt/format.h"
#include "IconsFontAwesome6.h"
#if defined(_WIN32)
#define read_portable(a, b, c) (recv(a, (char*)b, c, 0))
@ -659,8 +659,7 @@ PINEServer::IPCBuffer PINEServer::ParseCommand(std::span<u8> buf, std::vector<u8
Host::RunOnCPUThread([slot = FromSpan<u8>(buf, buf_cnt)] {
Error state_error;
if (!VMManager::LoadStateFromSlot(slot, false, &state_error))
Host::AddIconOSDMessage("LoadStateFromSlot", ICON_FA_TRIANGLE_EXCLAMATION,
state_error.GetDescription(), Host::OSD_INFO_DURATION);
SaveState_ReportLoadErrorOSD(state_error.GetDescription(), slot, false);
});
buf_cnt += 1;
break;

View File

@ -38,6 +38,7 @@
#include "common/StringUtil.h"
#include "common/ZipHelpers.h"
#include "IconsFontAwesome6.h"
#include "fmt/format.h"
#include <csetjmp>
@ -1231,3 +1232,24 @@ bool SaveState_UnzipFromDisk(const std::string& filename, Error* error)
PostLoadPrep();
return true;
}
void SaveState_ReportLoadErrorOSD(const std::string& message, std::optional<s32> slot, bool backup)
{
std::string full_message;
if (slot.has_value())
{
if (backup)
full_message = fmt::format(
TRANSLATE_FS("SaveState", "Failed to load state from slot {}: {}"), *slot, message);
else
full_message = fmt::format(
TRANSLATE_FS("SaveState", "Failed to load state from backup slot {}: {}"), *slot, message);
}
else
{
full_message = fmt::format(TRANSLATE_FS("SaveState", "Failed to load state: {}"), message);
}
Host::AddIconOSDMessage("LoadState", ICON_FA_TRIANGLE_EXCLAMATION,
full_message, Host::OSD_WARNING_DURATION);
}

View File

@ -5,6 +5,7 @@
#include <deque>
#include <memory>
#include <optional>
#include <string>
#include <vector>
@ -353,3 +354,5 @@ public:
void FreezeMem(void* data, int size) override;
bool IsSaving() const override { return false; }
};
void SaveState_ReportLoadErrorOSD(const std::string& message, std::optional<s32> slot, bool backup);