diff --git a/pcsx2-qt/EmuThread.cpp b/pcsx2-qt/EmuThread.cpp index 232c6cbe99..1395265f13 100644 --- a/pcsx2-qt/EmuThread.cpp +++ b/pcsx2-qt/EmuThread.cpp @@ -91,6 +91,41 @@ void EmuThread::stopInThread() m_shutdown_flag.store(true); } +bool EmuThread::confirmMessage(const QString& title, const QString& message) +{ + if (!isOnEmuThread()) + { + // This is definitely deadlock risky, but unlikely to happen (why would GS be confirming?). + bool result = false; + QMetaObject::invokeMethod(g_emu_thread, "confirmMessage", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, result), Q_ARG(const QString&, title), Q_ARG(const QString&, message)); + return result; + } + + // Easy if there's no VM. + if (!VMManager::HasValidVM()) + return emit messageConfirmed(title, message); + + // Preemptively pause/set surfaceless on the emu thread, because it can't run while the popup is open. + const bool was_paused = (VMManager::GetState() == VMState::Paused); + const bool was_fullscreen = isFullscreen(); + if (!was_paused) + VMManager::SetPaused(true); + if (was_fullscreen) + setSurfaceless(true); + + // This won't return until the user confirms one way or another. + const bool result = emit messageConfirmed(title, message); + + // Resume VM after confirming. + if (was_fullscreen) + setSurfaceless(false); + if (!was_paused) + VMManager::SetPaused(false); + + return result; +} + void EmuThread::startVM(std::shared_ptr boot_params) { if (!isOnEmuThread()) diff --git a/pcsx2-qt/EmuThread.h b/pcsx2-qt/EmuThread.h index 93e4e0d4cd..bb43889f7b 100644 --- a/pcsx2-qt/EmuThread.h +++ b/pcsx2-qt/EmuThread.h @@ -61,6 +61,7 @@ public: void updatePerformanceMetrics(bool force); public Q_SLOTS: + bool confirmMessage(const QString& title, const QString& message); void startVM(std::shared_ptr boot_params); void resetVM(); void setVMPaused(bool paused); @@ -88,6 +89,8 @@ public Q_SLOTS: void queueSnapshot(quint32 gsdump_frames); Q_SIGNALS: + bool messageConfirmed(const QString& title, const QString& message); + DisplayWidget* onCreateDisplayRequested(bool fullscreen, bool render_to_main); DisplayWidget* onUpdateDisplayRequested(bool fullscreen, bool render_to_main, bool surfaceless); void onResizeDisplayRequested(qint32 width, qint32 height); diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index 7f8937a7a2..5582777abe 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -354,6 +354,7 @@ void MainWindow::connectSignals() void MainWindow::connectVMThreadSignals(EmuThread* thread) { + connect(thread, &EmuThread::messageConfirmed, this, &MainWindow::confirmMessage, Qt::BlockingQueuedConnection); connect(thread, &EmuThread::onCreateDisplayRequested, this, &MainWindow::createDisplay, Qt::BlockingQueuedConnection); connect(thread, &EmuThread::onUpdateDisplayRequested, this, &MainWindow::updateDisplay, Qt::BlockingQueuedConnection); connect(thread, &EmuThread::onDestroyDisplayRequested, this, &MainWindow::destroyDisplay, Qt::BlockingQueuedConnection); @@ -1002,6 +1003,12 @@ void MainWindow::reportError(const QString& title, const QString& message) QMessageBox::critical(this, title, message); } +bool MainWindow::confirmMessage(const QString& title, const QString& message) +{ + VMLock lock(pauseAndLockVM()); + return (QMessageBox::question(this, title, message) == QMessageBox::Yes); +} + void MainWindow::runOnUIThread(const std::function& func) { func(); diff --git a/pcsx2-qt/MainWindow.h b/pcsx2-qt/MainWindow.h index b0bd38db80..5060a44573 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -102,6 +102,7 @@ public Q_SLOTS: void cancelGameListRefresh(); void invalidateSaveStateCache(); void reportError(const QString& title, const QString& message); + bool confirmMessage(const QString& title, const QString& message); void runOnUIThread(const std::function& func); bool requestShutdown(bool allow_confirm = true, bool allow_save_to_state = true, bool block_until_done = false); void requestExit(); diff --git a/pcsx2-qt/QtHost.cpp b/pcsx2-qt/QtHost.cpp index 670ef0b088..b17c6051ea 100644 --- a/pcsx2-qt/QtHost.cpp +++ b/pcsx2-qt/QtHost.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #ifdef _WIN32 #include "common/RedtapeWindows.h" @@ -48,6 +49,7 @@ #include "GameList/GameListWidget.h" #include "MainWindow.h" #include "QtHost.h" +#include "QtUtils.h" #include "svnrev.h" static constexpr u32 SETTINGS_VERSION = 1; @@ -432,6 +434,30 @@ void Host::ReportErrorAsync(const std::string_view& title, const std::string_vie Q_ARG(const QString&, message.empty() ? QString() : QString::fromUtf8(message.data(), message.size()))); } +bool Host::ConfirmMessage(const std::string_view& title, const std::string_view& message) +{ + const QString qtitle(QString::fromUtf8(title.data(), title.size())); + const QString qmessage(QString::fromUtf8(message.data(), message.size())); + return g_emu_thread->confirmMessage(qtitle, qmessage); +} + +void Host::OpenURL(const std::string_view& url) +{ + QtHost::RunOnUIThread([url = QtUtils::StringViewToQString(url)]() { + QtUtils::OpenURL(g_main_window, QUrl(url)); + }); +} + +bool Host::CopyTextToClipboard(const std::string_view& text) +{ + QtHost::RunOnUIThread([text = QtUtils::StringViewToQString(text)]() { + QClipboard* clipboard = QGuiApplication::clipboard(); + if (clipboard) + clipboard->setText(text); + }); + return true; +} + void Host::OnInputDeviceConnected(const std::string_view& identifier, const std::string_view& device_name) { emit g_emu_thread->onInputDeviceConnected( diff --git a/pcsx2/Host.cpp b/pcsx2/Host.cpp index dadf64576c..6e7759c9a0 100644 --- a/pcsx2/Host.cpp +++ b/pcsx2/Host.cpp @@ -25,4 +25,14 @@ void Host::ReportFormattedErrorAsync(const std::string_view& title, const char* std::string message(StringUtil::StdStringFromFormatV(format, ap)); va_end(ap); ReportErrorAsync(title, message); -} \ No newline at end of file +} + +bool Host::ConfirmFormattedMessage(const std::string_view& title, const char* format, ...) +{ + std::va_list ap; + va_start(ap, format); + std::string message = StringUtil::StdStringFromFormatV(format, ap); + va_end(ap); + + return ConfirmMessage(title, message); +} diff --git a/pcsx2/Host.h b/pcsx2/Host.h index fea1cf7e54..6d0d1541b0 100644 --- a/pcsx2/Host.h +++ b/pcsx2/Host.h @@ -62,4 +62,14 @@ namespace Host /// Displays an asynchronous error on the UI thread, i.e. doesn't block the caller. void ReportErrorAsync(const std::string_view& title, const std::string_view& message); void ReportFormattedErrorAsync(const std::string_view& title, const char* format, ...); + + /// Displays a synchronous confirmation on the UI thread, i.e. blocks the caller. + bool ConfirmMessage(const std::string_view& title, const std::string_view& message); + bool ConfirmFormattedMessage(const std::string_view& title, const char* format, ...); + + /// Opens a URL, using the default application. + void OpenURL(const std::string_view& url); + + /// Copies the provided text to the host's clipboard, if present. + bool CopyTextToClipboard(const std::string_view& text); } // namespace Host diff --git a/pcsx2/gui/AppHost.cpp b/pcsx2/gui/AppHost.cpp index 294356e6c1..90063bccf9 100644 --- a/pcsx2/gui/AppHost.cpp +++ b/pcsx2/gui/AppHost.cpp @@ -102,6 +102,11 @@ void Host::ReportErrorAsync(const std::string_view& title, const std::string_vie MsgButtons().OK())); } +bool Host::ConfirmMessage(const std::string_view& title, const std::string_view& message) +{ + return true; +} + static std::unique_ptr s_host_display; HostDisplay* Host::AcquireHostDisplay(HostDisplay::RenderAPI api)