diff --git a/src/common/progress_callback.cpp b/src/common/progress_callback.cpp index 51fa41b07..af452f5e3 100644 --- a/src/common/progress_callback.cpp +++ b/src/common/progress_callback.cpp @@ -101,3 +101,15 @@ void ProgressCallback::IncrementProgressValue() { SetProgressValue((m_progress_value - m_base_progress_value) + 1); } + +ProgressCallbackWithPrompt::~ProgressCallbackWithPrompt() = default; + +void ProgressCallbackWithPrompt::AlertPrompt(PromptIcon icon, std::string_view message) +{ +} + +bool ProgressCallbackWithPrompt::ConfirmPrompt(PromptIcon icon, std::string_view message, + std::string_view yes_text /*= {}*/, std::string_view no_text /*= {}*/) +{ + return false; +} diff --git a/src/common/progress_callback.h b/src/common/progress_callback.h index 60ffe54d5..a092e22c2 100644 --- a/src/common/progress_callback.h +++ b/src/common/progress_callback.h @@ -68,3 +68,21 @@ protected: public: static ProgressCallback* NullProgressCallback; }; + +class ProgressCallbackWithPrompt : public ProgressCallback +{ +public: + virtual ~ProgressCallbackWithPrompt() override; + + enum class PromptIcon + { + Error, + Warning, + Question, + Information, + }; + + virtual void AlertPrompt(PromptIcon icon, std::string_view message); + virtual bool ConfirmPrompt(PromptIcon icon, std::string_view message, std::string_view yes_text = {}, + std::string_view no_text = {}); +}; diff --git a/src/core/fullscreenui_widgets.cpp b/src/core/fullscreenui_widgets.cpp index cacbd019a..a0ea304e9 100644 --- a/src/core/fullscreenui_widgets.cpp +++ b/src/core/fullscreenui_widgets.cpp @@ -263,12 +263,12 @@ public: ProgressDialog(); ~ProgressDialog(); - std::unique_ptr GetProgressCallback(std::string title, float window_unscaled_width); + std::unique_ptr GetProgressCallback(std::string title, float window_unscaled_width); void Draw(); private: - class ProgressCallbackImpl : public ProgressCallback + class ProgressCallbackImpl : public ProgressCallbackWithPrompt { public: ProgressCallbackImpl(); @@ -279,6 +279,10 @@ private: void SetProgressValue(u32 value) override; void SetCancellable(bool cancellable) override; bool IsCancelled() const override; + + void AlertPrompt(PromptIcon icon, std::string_view message) override; + bool ConfirmPrompt(PromptIcon icon, std::string_view message, std::string_view yes_text = {}, + std::string_view no_text = {}) override; }; std::string m_status_text; @@ -287,6 +291,8 @@ private: u32 m_progress_value = 0; u32 m_progress_range = 0; std::atomic_bool m_cancelled{false}; + std::atomic_bool m_prompt_result{false}; + std::atomic_flag m_prompt_waiting = ATOMIC_FLAG_INIT; }; class FixedPopupDialog : public PopupDialog @@ -337,12 +343,12 @@ struct WidgetsState s32 enum_choice_button_value = 0; bool enum_choice_button_set = false; - MessageDialog message_dialog; ChoiceDialog choice_dialog; FileSelectorDialog file_selector_dialog; InputStringDialog input_string_dialog; FixedPopupDialog fixed_popup_dialog; ProgressDialog progress_dialog; + MessageDialog message_dialog; ImAnimatedVec2 menu_button_frame_min_animated; ImAnimatedVec2 menu_button_frame_max_animated; @@ -1000,11 +1006,11 @@ void FullscreenUI::BeginLayout() void FullscreenUI::EndLayout() { - s_state.message_dialog.Draw(); s_state.choice_dialog.Draw(); s_state.file_selector_dialog.Draw(); s_state.input_string_dialog.Draw(); s_state.progress_dialog.Draw(); + s_state.message_dialog.Draw(); DrawFullscreenFooter(); @@ -4106,14 +4112,14 @@ void FullscreenUI::ProgressDialog::Draw() EndRender(); } -std::unique_ptr FullscreenUI::ProgressDialog::GetProgressCallback(std::string title, - float window_unscaled_width) +std::unique_ptr +FullscreenUI::ProgressDialog::GetProgressCallback(std::string title, float window_unscaled_width) { if (m_state == PopupDialog::State::Open) { // return dummy callback so the op can still go through ERROR_LOG("Progress dialog is already open, cannot create dialog for '{}'.", std::move(title)); - return std::make_unique(); + return std::make_unique(); } SetTitleAndOpen(std::move(title)); @@ -4200,7 +4206,96 @@ bool FullscreenUI::ProgressDialog::ProgressCallbackImpl::IsCancelled() const return s_state.progress_dialog.m_cancelled.load(std::memory_order_acquire); } -std::unique_ptr FullscreenUI::OpenModalProgressDialog(std::string title, float window_unscaled_width) +void FullscreenUI::ProgressDialog::ProgressCallbackImpl::AlertPrompt(PromptIcon icon, std::string_view message) +{ + s_state.progress_dialog.m_prompt_waiting.test_and_set(std::memory_order_release); + + Host::RunOnCPUThread([message = std::string(message)]() mutable { + GPUThread::RunOnThread([message = std::move(message)]() mutable { + if (!s_state.progress_dialog.IsOpen()) + { + s_state.progress_dialog.m_prompt_waiting.clear(std::memory_order_release); + s_state.progress_dialog.m_prompt_waiting.notify_one(); + return; + } + + // need to save state, since opening the next dialog will close this one + std::string existing_title = s_state.progress_dialog.GetTitle(); + u32 progress_range = s_state.progress_dialog.m_progress_range; + u32 progress_value = s_state.progress_dialog.m_progress_value; + float last_frac = s_state.progress_dialog.m_last_frac; + float width = s_state.progress_dialog.m_width; + s_state.progress_dialog.CloseImmediately(); + + OpenInfoMessageDialog( + s_state.progress_dialog.GetTitle(), std::move(message), + [existing_title = std::move(existing_title), progress_range, progress_value, last_frac, width]() mutable { + s_state.progress_dialog.SetTitleAndOpen(std::move(existing_title)); + s_state.progress_dialog.m_progress_range = progress_range; + s_state.progress_dialog.m_progress_value = progress_value; + s_state.progress_dialog.m_last_frac = last_frac; + s_state.progress_dialog.m_width = width; + s_state.progress_dialog.m_prompt_waiting.clear(std::memory_order_release); + s_state.progress_dialog.m_prompt_waiting.notify_one(); + }); + }); + }); + + s_state.progress_dialog.m_prompt_waiting.wait(true, std::memory_order_acquire); +} + +bool FullscreenUI::ProgressDialog::ProgressCallbackImpl::ConfirmPrompt(PromptIcon icon, std::string_view message, + std::string_view yes_text /* = */, + std::string_view no_text /* = */) +{ + s_state.progress_dialog.m_prompt_result.store(false, std::memory_order_relaxed); + s_state.progress_dialog.m_prompt_waiting.test_and_set(std::memory_order_release); + + Host::RunOnCPUThread( + [message = std::string(message), yes_text = std::string(yes_text), no_text = std::string(no_text)]() mutable { + GPUThread::RunOnThread( + [message = std::move(message), yes_text = std::move(yes_text), no_text = std::move(no_text)]() mutable { + if (!s_state.progress_dialog.IsOpen()) + { + s_state.progress_dialog.m_prompt_waiting.clear(std::memory_order_release); + s_state.progress_dialog.m_prompt_waiting.notify_one(); + return; + } + + // need to save state, since opening the next dialog will close this one + std::string existing_title = s_state.progress_dialog.GetTitle(); + u32 progress_range = s_state.progress_dialog.m_progress_range; + u32 progress_value = s_state.progress_dialog.m_progress_value; + float last_frac = s_state.progress_dialog.m_last_frac; + float width = s_state.progress_dialog.m_width; + s_state.progress_dialog.CloseImmediately(); + + if (yes_text.empty()) + yes_text = FSUI_ICONSTR(ICON_FA_CHECK, "Yes"); + if (no_text.empty()) + no_text = FSUI_ICONSTR(ICON_FA_XMARK, "No"); + + OpenConfirmMessageDialog(s_state.progress_dialog.GetTitle(), std::move(message), + [existing_title = std::move(existing_title), progress_range, progress_value, + last_frac, width](bool result) mutable { + s_state.progress_dialog.SetTitleAndOpen(std::move(existing_title)); + s_state.progress_dialog.m_progress_range = progress_range; + s_state.progress_dialog.m_progress_value = progress_value; + s_state.progress_dialog.m_last_frac = last_frac; + s_state.progress_dialog.m_width = width; + s_state.progress_dialog.m_prompt_result.store(result, std::memory_order_relaxed); + s_state.progress_dialog.m_prompt_waiting.clear(std::memory_order_release); + s_state.progress_dialog.m_prompt_waiting.notify_one(); + }); + }); + }); + + s_state.progress_dialog.m_prompt_waiting.wait(true, std::memory_order_acquire); + return s_state.progress_dialog.m_prompt_result.load(std::memory_order_relaxed); +} + +std::unique_ptr FullscreenUI::OpenModalProgressDialog(std::string title, + float window_unscaled_width) { return s_state.progress_dialog.GetProgressCallback(std::move(title), window_unscaled_width); } diff --git a/src/core/fullscreenui_widgets.h b/src/core/fullscreenui_widgets.h index 1eaee5a39..777d8d074 100644 --- a/src/core/fullscreenui_widgets.h +++ b/src/core/fullscreenui_widgets.h @@ -27,7 +27,7 @@ class Error; class Image; class GPUTexture; class GPUSwapChain; -class ProgressCallback; +class ProgressCallbackWithPrompt; enum class OSDMessageType : u8; @@ -484,7 +484,8 @@ void OpenMessageDialog(std::string_view title, std::string message, MessageDialo std::string first_button_text, std::string second_button_text, std::string third_button_text); void CloseMessageDialog(); -std::unique_ptr OpenModalProgressDialog(std::string title, float window_unscaled_width = 500.0f); +std::unique_ptr OpenModalProgressDialog(std::string title, + float window_unscaled_width = 500.0f); float GetNotificationVerticalPosition(); float GetNotificationVerticalDirection(); diff --git a/src/duckstation-qt/qtprogresscallback.cpp b/src/duckstation-qt/qtprogresscallback.cpp index a5397451b..c70cbd2e0 100644 --- a/src/duckstation-qt/qtprogresscallback.cpp +++ b/src/duckstation-qt/qtprogresscallback.cpp @@ -191,6 +191,9 @@ QtAsyncTaskWithProgressDialog::QtAsyncTaskWithProgressDialog(const QString& init : m_callback(std::move(callback)), m_show_delay(show_delay) { m_dialog = new ProgressDialog(initial_title, initial_status_text, cancellable, range, value, *this, dialog_parent); + m_cancellable = cancellable; + m_progress_range = range; + m_progress_value = value; if (show_delay <= 0.0f) { @@ -397,11 +400,94 @@ void QtAsyncTaskWithProgressDialog::SetProgressValue(u32 value) } } -void QtAsyncTaskWithProgressDialog::CheckForDelayedShow() +static QMessageBox::Icon ConvertPromptIcon(ProgressCallbackWithPrompt::PromptIcon icon) { - DebugAssert(!m_shown); + switch (icon) + { + case ProgressCallbackWithPrompt::PromptIcon::Error: + return QMessageBox::Critical; + case ProgressCallbackWithPrompt::PromptIcon::Warning: + return QMessageBox::Warning; + case ProgressCallbackWithPrompt::PromptIcon::Question: + return QMessageBox::Question; + case ProgressCallbackWithPrompt::PromptIcon::Information: + default: + return QMessageBox::Information; + } +} - if (m_show_timer.GetTimeSeconds() < m_show_delay) +void QtAsyncTaskWithProgressDialog::AlertPrompt(PromptIcon icon, std::string_view message) +{ + m_prompt_waiting.test_and_set(std::memory_order_release); + + Host::RunOnUIThread([this, icon, message = QtUtils::StringViewToQString(message)]() { + if (!m_dialog) + { + // dialog closed :( + m_prompt_waiting.clear(std::memory_order_release); + m_prompt_waiting.notify_one(); + return; + } + + EnsureShown(); + + QMessageBox* msgbox = + QtUtils::NewMessageBox(m_dialog, ConvertPromptIcon(icon), m_dialog->windowTitle(), message, QMessageBox::Ok); + connect(msgbox, &QMessageBox::finished, [this]() { + m_prompt_waiting.clear(std::memory_order_release); + m_prompt_waiting.notify_one(); + }); + msgbox->open(); + }); + + m_prompt_waiting.wait(true, std::memory_order_acquire); +} + +bool QtAsyncTaskWithProgressDialog::ConfirmPrompt(PromptIcon icon, std::string_view message, + std::string_view yes_text /*= {}*/, std::string_view no_text /*= {}*/) +{ + m_prompt_result.store(false, std::memory_order_relaxed); + m_prompt_waiting.test_and_set(std::memory_order_release); + + Host::RunOnUIThread([this, icon, message = QtUtils::StringViewToQString(message), + yes_text = QtUtils::StringViewToQString(yes_text), + no_text = QtUtils::StringViewToQString(no_text)]() { + if (!m_dialog) + { + // dialog closed :( + m_prompt_waiting.clear(std::memory_order_release); + m_prompt_waiting.notify_one(); + return; + } + + EnsureShown(); + + QMessageBox* msgbox = QtUtils::NewMessageBox(m_dialog, ConvertPromptIcon(icon), m_dialog->windowTitle(), message, + QMessageBox::NoButton); + QAbstractButton* yes_button; + if (!yes_text.isEmpty()) + yes_button = msgbox->addButton(yes_text, QMessageBox::YesRole); + else + yes_button = msgbox->addButton(QMessageBox::Yes); + if (!no_text.isEmpty()) + msgbox->addButton(no_text, QMessageBox::NoRole); + else + msgbox->addButton(QMessageBox::No); + connect(msgbox, &QMessageBox::finished, [this, msgbox, yes_button]() { + m_prompt_result.store((msgbox->clickedButton() == yes_button), std::memory_order_relaxed); + m_prompt_waiting.clear(std::memory_order_release); + m_prompt_waiting.notify_one(); + }); + msgbox->open(); + }); + + m_prompt_waiting.wait(true, std::memory_order_acquire); + return m_prompt_result.load(std::memory_order_relaxed); +} + +void QtAsyncTaskWithProgressDialog::EnsureShown() +{ + if (!m_shown) return; m_shown = true; @@ -420,3 +506,13 @@ void QtAsyncTaskWithProgressDialog::CheckForDelayedShow() m_dialog->open(); }); } + +void QtAsyncTaskWithProgressDialog::CheckForDelayedShow() +{ + DebugAssert(!m_shown); + + if (m_show_timer.GetTimeSeconds() < m_show_delay) + return; + + EnsureShown(); +} diff --git a/src/duckstation-qt/qtprogresscallback.h b/src/duckstation-qt/qtprogresscallback.h index a8df39925..5f592f375 100644 --- a/src/duckstation-qt/qtprogresscallback.h +++ b/src/duckstation-qt/qtprogresscallback.h @@ -88,13 +88,13 @@ private: std::atomic_bool m_ts_cancelled{false}; }; -class QtAsyncTaskWithProgressDialog final : public QObject, private ProgressCallback +class QtAsyncTaskWithProgressDialog final : public QObject, private ProgressCallbackWithPrompt { Q_OBJECT public: using CompletionCallback = std::function; - using WorkCallback = std::function; + using WorkCallback = std::function; static QtAsyncTaskWithProgressDialog* create(QWidget* parent, std::string_view initial_title, std::string_view initial_status_text, bool cancellable, int range, @@ -152,13 +152,20 @@ private: void SetProgressRange(u32 range) override; void SetProgressValue(u32 value) override; + void AlertPrompt(PromptIcon icon, std::string_view message) override; + bool ConfirmPrompt(PromptIcon icon, std::string_view message, std::string_view yes_text = {}, + std::string_view no_text = {}) override; + void CheckForDelayedShow(); + void EnsureShown(); std::variant m_callback; ProgressDialog* m_dialog = nullptr; Timer m_show_timer; float m_show_delay; - std::atomic_bool m_ts_cancelled{false}; bool m_shown = false; + std::atomic_bool m_ts_cancelled{false}; + std::atomic_bool m_prompt_result{false}; + std::atomic_flag m_prompt_waiting = ATOMIC_FLAG_INIT; };