Host: Add icon to ConfirmMessageAsync()

And use it for achievements.
This commit is contained in:
Stenzek
2026-05-27 18:00:57 +10:00
parent ef7637413b
commit 5a21a6c8df
9 changed files with 127 additions and 36 deletions
+1 -1
View File
@@ -2396,7 +2396,7 @@ void Achievements::Logout()
void Achievements::ConfirmHardcoreModeDisableAsync(std::string_view trigger, std::function<void(bool)> callback)
{
Host::ConfirmMessageAsync(
TRANSLATE_STR("Achievements", "Confirm Hardcore Mode Disable"),
RA_LOGO_SVG_ICON_NAME, TRANSLATE_STR("Achievements", "Confirm Hardcore Mode Disable"),
fmt::format(TRANSLATE_FS("Achievements", "{0} cannot be performed while hardcore mode is active. Do you want to "
"disable hardcore mode? {0} will be cancelled if you select No."),
trigger),
+23 -15
View File
@@ -207,7 +207,7 @@ public:
MessageDialog();
~MessageDialog();
void Open(std::string_view icon, std::string_view title, std::string message, CallbackVariant callback,
void Open(std::string icon, std::string_view title, std::string message, CallbackVariant callback,
std::string first_button_text, std::string second_button_text, std::string third_button_text);
void ClearState();
@@ -350,7 +350,7 @@ private:
std::string_view no_text = {}) override;
protected:
static std::string_view GetIconString(PromptIcon icon);
static std::string GetIconString(PromptIcon icon);
void StateChanged(StateChange changed) override;
};
@@ -5438,12 +5438,12 @@ FullscreenUI::MessageDialog::MessageDialog() = default;
FullscreenUI::MessageDialog::~MessageDialog() = default;
void FullscreenUI::MessageDialog::Open(std::string_view icon, std::string_view title, std::string message,
void FullscreenUI::MessageDialog::Open(std::string icon, std::string_view title, std::string message,
CallbackVariant callback, std::string first_button_text,
std::string second_button_text, std::string third_button_text)
{
SetTitleAndOpen(fmt::format("{}##message_dialog", title));
m_icon = icon;
m_icon = std::move(icon);
m_message = std::move(message);
m_callback = std::move(callback);
m_buttons[0] = std::move(first_button_text);
@@ -5476,9 +5476,17 @@ void FullscreenUI::MessageDialog::Draw()
if (!m_icon.empty())
{
ImGui::PushFont(nullptr, LayoutScale(50.0f), 0.0f);
ImGui::TextUnformatted(IMSTR_START_END(m_icon));
ImGui::PopFont();
const ImVec2 icon_size = LayoutScale(52.0f, 52.0f);
if (Path::IsAbsolute(m_icon))
{
ImGui::Image(GetCachedTexture(m_icon, icon_size), icon_size);
}
else
{
ImGui::PushFont(nullptr, LayoutScale(52.0f), 0.0f);
ImGui::TextUnformatted(IMSTR_START_END(m_icon));
ImGui::PopFont();
}
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + LayoutScale(10.0f));
}
@@ -5553,26 +5561,26 @@ bool FullscreenUI::IsMessageBoxDialogOpen()
return s_state.message_dialog.IsOpen();
}
void FullscreenUI::OpenConfirmMessageDialog(std::string_view icon, std::string_view title, std::string message,
void FullscreenUI::OpenConfirmMessageDialog(std::string icon, std::string_view title, std::string message,
ConfirmMessageDialogCallback callback, std::string yes_button_text,
std::string no_button_text)
{
s_state.message_dialog.Open(icon, std::move(title), std::move(message), std::move(callback),
s_state.message_dialog.Open(std::move(icon), std::move(title), std::move(message), std::move(callback),
std::move(yes_button_text), std::move(no_button_text), std::string());
}
void FullscreenUI::OpenInfoMessageDialog(std::string_view icon, std::string_view title, std::string message,
void FullscreenUI::OpenInfoMessageDialog(std::string icon, std::string_view title, std::string message,
InfoMessageDialogCallback callback, std::string button_text)
{
s_state.message_dialog.Open(icon, std::move(title), std::move(message), std::move(callback), std::move(button_text),
std::string(), std::string());
s_state.message_dialog.Open(std::move(icon), std::move(title), std::move(message), std::move(callback),
std::move(button_text), std::string(), std::string());
}
void FullscreenUI::OpenMessageDialog(std::string_view icon, std::string_view title, std::string message,
void FullscreenUI::OpenMessageDialog(std::string icon, std::string_view title, std::string message,
MessageDialogCallback callback, std::string first_button_text,
std::string second_button_text, std::string third_button_text)
{
s_state.message_dialog.Open(icon, std::move(title), std::move(message), std::move(callback),
s_state.message_dialog.Open(std::move(icon), std::move(title), std::move(message), std::move(callback),
std::move(first_button_text), std::move(second_button_text),
std::move(third_button_text));
}
@@ -5744,7 +5752,7 @@ bool FullscreenUI::ProgressDialog::ProgressCallbackImpl::IsCancelled() const
return s_state.progress_dialog.m_cancelled.load(std::memory_order_acquire);
}
std::string_view FullscreenUI::ProgressDialog::ProgressCallbackImpl::GetIconString(PromptIcon icon)
std::string FullscreenUI::ProgressDialog::ProgressCallbackImpl::GetIconString(PromptIcon icon)
{
switch (icon)
{
+4 -5
View File
@@ -566,16 +566,15 @@ using ConfirmMessageDialogCallback = std::function<void(bool)>;
using InfoMessageDialogCallback = std::function<void()>;
using MessageDialogCallback = std::function<void(s32)>;
bool IsMessageBoxDialogOpen();
void OpenConfirmMessageDialog(std::string_view icon, std::string_view title, std::string message,
void OpenConfirmMessageDialog(std::string icon, std::string_view title, std::string message,
ConfirmMessageDialogCallback callback,
std::string yes_button_text = FSUI_ICONSTR(ICON_FA_CHECK, "Yes"),
std::string no_button_text = FSUI_ICONSTR(ICON_FA_XMARK, "No"));
void OpenInfoMessageDialog(std::string_view icon, std::string_view title, std::string message,
void OpenInfoMessageDialog(std::string icon, std::string_view title, std::string message,
InfoMessageDialogCallback callback = {},
std::string button_text = FSUI_ICONSTR(ICON_FA_SQUARE_XMARK, "Close"));
void OpenMessageDialog(std::string_view icon, std::string_view title, std::string message,
MessageDialogCallback callback, std::string first_button_text, std::string second_button_text,
std::string third_button_text);
void OpenMessageDialog(std::string icon, std::string_view title, std::string message, MessageDialogCallback callback,
std::string first_button_text, std::string second_button_text, std::string third_button_text);
void CloseMessageDialog();
std::unique_ptr<ProgressCallbackWithPrompt> OpenModalProgressDialog(std::string title,
+3 -2
View File
@@ -49,8 +49,9 @@ void ReportStatusMessage(std::string_view message);
/// Displays an asynchronous confirmation on the UI thread, but does not block the caller.
/// The callback may be executed on a different thread. Use RunOnCoreThread() in the callback to ensure safety.
using ConfirmMessageAsyncCallback = std::function<void(bool)>;
void ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback,
std::string_view yes_text = std::string_view(), std::string_view no_text = std::string_view());
void ConfirmMessageAsync(std::string_view icon, std::string_view title, std::string_view message,
ConfirmMessageAsyncCallback callback, std::string_view yes_text = std::string_view(),
std::string_view no_text = std::string_view());
/// Opens a URL, using the default application.
void OpenURL(std::string_view url);
+1 -1
View File
@@ -1684,7 +1684,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
if (Core::GetBoolSettingValue("CDROM", "AllowBootingWithoutSBIFile", false))
{
Host::ConfirmMessageAsync(
"Confirm Unsupported Configuration",
"images/warning.svg", "Confirm Unsupported Configuration",
fmt::format(TRANSLATE_FS("System",
"You are attempting to run a libcrypt protected game without an SBI file:\n\n{0}: "
"{1}\n\nThe game will likely not run properly.\n\nPlease check the README for "
+24 -9
View File
@@ -11,6 +11,7 @@
#include "qtwindowinfo.h"
#include "settingswindow.h"
#include "setupwizarddialog.h"
#include "svgwidget.h"
#include "core/achievements.h"
#include "core/bus.h"
@@ -2351,8 +2352,9 @@ void Host::ReportStatusMessage(std::string_view message)
emit g_core_thread->statusMessage(QtUtils::StringViewToQString(message));
}
void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback,
std::string_view yes_text, std::string_view no_text)
void Host::ConfirmMessageAsync(std::string_view icon, std::string_view title, std::string_view message,
ConfirmMessageAsyncCallback callback, std::string_view yes_text,
std::string_view no_text)
{
INFO_LOG("ConfirmMessageAsync({}, {})", title, message);
@@ -2365,9 +2367,10 @@ void Host::ConfirmMessageAsync(std::string_view title, std::string_view message,
// Ensure it always comes from the CPU thread.
if (!g_core_thread->isCurrentThread())
{
Host::RunOnCoreThread([title = std::string(title), message = std::string(message), callback = std::move(callback),
yes_text = std::string(yes_text), no_text = std::string(no_text)]() mutable {
ConfirmMessageAsync(title, message, std::move(callback));
Host::RunOnCoreThread([title = std::string(title), icon = std::string(icon), message = std::string(message),
callback = std::move(callback), yes_text = std::string(yes_text),
no_text = std::string(no_text)]() mutable {
ConfirmMessageAsync(icon, title, message, std::move(callback), yes_text, no_text);
});
return;
}
@@ -2380,7 +2383,7 @@ void Host::ConfirmMessageAsync(std::string_view title, std::string_view message,
// Use FSUI if we're ingame.
if (System::IsValid() || g_core_thread->isFullscreenUIStarted())
{
VideoThread::RunOnThread([title = std::string(title), message = std::string(message),
VideoThread::RunOnThread([title = std::string(title), icon = std::string(icon), message = std::string(message),
callback = std::move(callback), yes_text = std::string(yes_text),
no_text = std::string(no_text), needs_pause]() mutable {
// Need to reset run idle state _again_ after displaying.
@@ -2396,8 +2399,13 @@ void Host::ConfirmMessageAsync(std::string_view title, std::string_view message,
callback(result);
};
if (icon.empty())
icon = ICON_EMOJI_QUESTION_MARK;
else if (StringUtil::GetUTF8CharacterCount(icon) > 1 && !Path::IsAbsolute(icon))
icon = QtHost::GetResourcePath(icon, true);
FullscreenUI::Initialize();
FullscreenUI::OpenConfirmMessageDialog(ICON_EMOJI_QUESTION_MARK, std::move(title), std::move(message),
FullscreenUI::OpenConfirmMessageDialog(std::move(icon), std::move(title), std::move(message),
std::move(final_callback), fmt::format(ICON_FA_CHECK " {}", yes_text),
fmt::format(ICON_FA_XMARK " {}", no_text));
FullscreenUI::UpdateRunIdleState();
@@ -2405,15 +2413,22 @@ void Host::ConfirmMessageAsync(std::string_view title, std::string_view message,
}
else
{
QString qicon;
if (!icon.empty())
qicon = Path::IsAbsolute(icon) ? QtUtils::StringViewToQString(icon) : QtHost::GetResourceQPath(icon, true);
// Otherwise, use the desktop UI.
Host::RunOnUIThread([title = QtUtils::StringViewToQString(title), message = QtUtils::StringViewToQString(message),
callback = std::move(callback), yes_text = QtUtils::StringViewToQString(yes_text),
Host::RunOnUIThread([qicon = std::move(qicon), title = QtUtils::StringViewToQString(title),
message = QtUtils::StringViewToQString(message), callback = std::move(callback),
yes_text = QtUtils::StringViewToQString(yes_text),
no_text = QtUtils::StringViewToQString(no_text), needs_pause]() mutable {
auto lock = g_main_window->pauseAndLockSystem();
QWidget* const dialog_parent = lock.getDialogParent();
QMessageBox* const msgbox =
QtUtils::NewMessageBox(dialog_parent, QMessageBox::Question, title, message, QMessageBox::NoButton);
if (!qicon.isEmpty())
msgbox->setIconPixmap(SVGWidget::renderSVGToPixmap(qicon, QSize(64, 64), msgbox->devicePixelRatio(), QColor()));
QPushButton* const yes_button = msgbox->addButton(yes_text, QMessageBox::AcceptRole);
msgbox->addButton(no_text, QMessageBox::RejectRole);
+64 -1
View File
@@ -179,4 +179,67 @@ void SVGWidget::changeEvent(QEvent* event)
rasterize();
update();
}
}
}
QPixmap SVGWidget::renderSVGToPixmap(const QString& resource_path, const QSize& size, qreal device_pixel_ratio,
const QColor& color)
{
DynamicHeapArray<u8> svg_data;
QFile file(resource_path);
if (!file.open(QFile::ReadOnly) || !QtUtils::ReadFileToByteArray(&file, svg_data))
{
qCritical() << "Failed to open SVG file: " << resource_path;
return {};
}
// plutosvg borrows the raw pointer; svg_data must outlive m_document.
plutosvg_document* const document = plutosvg_document_load_from_data(
reinterpret_cast<const char*>(svg_data.data()), static_cast<int>(svg_data.size()), -1.0f, -1.0f, nullptr, nullptr);
if (!document)
{
qCritical() << "PlutoSVGWidget: failed to parse SVG" << resource_path;
return {};
}
// Determine SVG intrinsic size and compute a uniform scale that fits inside physical,
// preserving aspect ratio.
const float svg_w = plutosvg_document_get_width(document);
const float svg_h = plutosvg_document_get_height(document);
const QSize physical = QtUtils::ApplyDevicePixelRatioToSize(size, device_pixel_ratio);
int render_w = physical.width();
int render_h = physical.height();
if (svg_w > 0.0f && svg_h > 0.0f)
{
// Scale to fit, preserving aspect ratio (letterbox / pillarbox).
const float scale_x = static_cast<float>(physical.width()) / svg_w;
const float scale_y = static_cast<float>(physical.height()) / svg_h;
const float scale = std::min(scale_x, scale_y);
render_w = std::max(1, static_cast<int>(svg_w * scale));
render_h = std::max(1, static_cast<int>(svg_h * scale));
}
// Use white as currentColor so the SVG's own colours are preserved.
// Swap in a palette colour here if tinting is ever desired.
const plutovg_color_t current_color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = 1.0f};
plutovg_surface_t* const surface =
plutosvg_document_render_to_surface(document, nullptr, render_w, render_h, &current_color, nullptr, nullptr);
if (!surface)
{
qCritical() << "PlutoSVGWidget: render failed for" << resource_path;
plutosvg_document_destroy(document);
return {};
}
// plutovg surfaces are premultiplied ARGB (native-endian) == QImage::Format_ARGB32_Premultiplied.
const QImage img(plutovg_surface_get_data(surface), plutovg_surface_get_width(surface),
plutovg_surface_get_height(surface), plutovg_surface_get_stride(surface),
QImage::Format_ARGB32_Premultiplied, CleanupSurface, surface);
QPixmap pm = QPixmap::fromImage(img);
pm.setDevicePixelRatio(device_pixel_ratio);
plutosvg_document_destroy(document);
return pm;
}
+4
View File
@@ -40,6 +40,10 @@ public:
/// Returns the path that was passed to setSource().
const QString& source() const { return m_resource_path; }
/// Renders a SVG for the specified widget and returns it.
static QPixmap renderSVGToPixmap(const QString& resource_path, const QSize& size, qreal device_pixel_ratio,
const QColor& color = QColor());
protected:
void paintEvent(QPaintEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
+3 -2
View File
@@ -131,8 +131,9 @@ void Host::ReportStatusMessage(std::string_view message)
INFO_LOG("ReportStatusMessage: {}", message);
}
void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback,
std::string_view yes_text, std::string_view no_text)
void Host::ConfirmMessageAsync(std::string_view icon, std::string_view title, std::string_view message,
ConfirmMessageAsyncCallback callback, std::string_view yes_text,
std::string_view no_text)
{
if (!title.empty() && !message.empty())
ERROR_LOG("ConfirmMessage: {}: {}", title, message);