From f77c654e77496d697da3521485b6ed11deb08cb3 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:34:41 +0900 Subject: [PATCH] [UI] Achievement notification and font handling fixes Ensure fonts applied properly to game list dialog, fix achievement notification double scaling fonts and not respecting window scaling properly, as well as add cvar to control whether achiement notification respects the game's requested position --- src/xenia/kernel/xam/achievement_manager.cc | 17 +++- src/xenia/ui/game_list_dialog_qt.cc | 102 +++++++++++--------- src/xenia/ui/game_list_dialog_qt.h | 6 ++ src/xenia/ui/imgui_drawer.cc | 30 ++++-- src/xenia/ui/imgui_drawer.h | 1 + src/xenia/ui/imgui_guest_notification.cc | 38 +++++--- src/xenia/ui/imgui_guest_notification.h | 6 ++ src/xenia/ui/imgui_notification.h | 5 + src/xenia/ui/windowed_app_main_qt.cc | 14 +-- 9 files changed, 140 insertions(+), 79 deletions(-) diff --git a/src/xenia/kernel/xam/achievement_manager.cc b/src/xenia/kernel/xam/achievement_manager.cc index fb3246dc8..2834a8813 100644 --- a/src/xenia/kernel/xam/achievement_manager.cc +++ b/src/xenia/kernel/xam/achievement_manager.cc @@ -20,6 +20,11 @@ DEFINE_bool(show_achievement_notification, true, "Show achievement notification on screen.", "UI"); +DEFINE_bool(achievement_notification_position_by_game, false, + "Use game-specified notification position for achievements. " + "When disabled, achievements always appear at center-bottom.", + "UI"); + DEFINE_string( default_achievements_backend, "GPD", "Defines which achievements backend should be used as an default. " @@ -151,14 +156,18 @@ void AchievementManager::ShowAchievementEarnedNotification( emulator->display_window()->app_context(); ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); - app_context.CallInUIThread([imgui_drawer, description]() { + // Use game-specified position if enabled, otherwise default to center-bottom + const uint8_t position = cvars::achievement_notification_position_by_game + ? kernel_state()->notification_position_ + : 2; + + app_context.CallInUIThread([imgui_drawer, description, position]() { // Play achievement sound ui::AudioHelper::Instance().PlayAchievementSound(); // Show notification - new ui::AchievementNotificationWindow( - imgui_drawer, "Achievement unlocked", description, 0, - kernel_state()->notification_position_); + new ui::AchievementNotificationWindow(imgui_drawer, "Achievement unlocked", + description, 0, position); }); } diff --git a/src/xenia/ui/game_list_dialog_qt.cc b/src/xenia/ui/game_list_dialog_qt.cc index b11a55c52..359c8f3c0 100644 --- a/src/xenia/ui/game_list_dialog_qt.cc +++ b/src/xenia/ui/game_list_dialog_qt.cc @@ -9,6 +9,7 @@ #include "xenia/ui/game_list_dialog_qt.h" +#include #include #include #include @@ -68,8 +69,6 @@ GameListDialogQt::GameListDialogQt(QWidget* parent, EmulatorWindow* emulator_window) : QWidget(parent), emulator_window_(emulator_window) { SetupUI(); - LoadGameList(); - TryLoadIcons(); // Start timer to monitor game state and profile state game_state_timer_ = new QTimer(this); @@ -87,6 +86,39 @@ GameListDialogQt::~GameListDialogQt() { // Cleanup is handled automatically by Qt } +void GameListDialogQt::RefreshFonts() { + int toolbar_font_size = cvars::font_size > 0 ? cvars::font_size : 10; + int search_font_size = cvars::font_size > 0 ? cvars::font_size * 1.5 : 16; + + QFont toolbar_font = QApplication::font(); + toolbar_font.setPointSize(toolbar_font_size); + + QFont search_font = QApplication::font(); + search_font.setPointSize(search_font_size); + + QFont question_font = QApplication::font(); + question_font.setPointSize(32); + question_font.setBold(true); + + // Apply fonts to toolbar labels + if (open_label_) open_label_->setFont(toolbar_font); + if (play_label_) play_label_->setFont(toolbar_font); + if (settings_label_) settings_label_->setFont(toolbar_font); + if (profile_label_) profile_label_->setFont(toolbar_font); + if (search_box_) search_box_->setFont(search_font); + if (profile_question_label_) profile_question_label_->setFont(question_font); + + // Apply fonts to table header labels + QFont header_font = QApplication::font(); + header_font.setBold(true); + if (icon_header_) icon_header_->setFont(header_font); + if (title_header_) title_header_->setFont(header_font); + if (last_played_header_) last_played_header_->setFont(header_font); + + // Reload game list to refresh game item fonts + LoadGameList(); +} + void GameListDialogQt::SetupUI() { // No window title, modal, or size settings - widget fills parent setAttribute(Qt::WA_DeleteOnClose); @@ -120,11 +152,11 @@ void GameListDialogQt::SetupUI() { } }); - auto* open_label = new QLabel("Open", this); - open_label->setAlignment(Qt::AlignCenter); + open_label_ = new QLabel("Open", this); + open_label_->setAlignment(Qt::AlignCenter); open_layout->addWidget(open_button); - open_layout->addWidget(open_label); + open_layout->addWidget(open_label_); toolbar_layout->addWidget(open_container); // Play button @@ -151,11 +183,6 @@ void GameListDialogQt::SetupUI() { play_label_ = new QLabel("Play", this); play_label_->setAlignment(Qt::AlignCenter); - QFont text_font = play_label_->font(); - int toolbar_font_size = cvars::font_size > 0 ? cvars::font_size : 10; - text_font.setPointSize(toolbar_font_size); - play_label_->setFont(text_font); - open_label->setFont(text_font); // Apply to open label too // Create opacity effect for label play_label_opacity_ = new QGraphicsOpacityEffect(play_label_); @@ -180,12 +207,11 @@ void GameListDialogQt::SetupUI() { connect(settings_button_, &QToolButton::clicked, this, &GameListDialogQt::OnSettingsClicked); - auto* settings_label = new QLabel("Config", this); - settings_label->setAlignment(Qt::AlignCenter); - settings_label->setFont(text_font); + settings_label_ = new QLabel("Config", this); + settings_label_->setAlignment(Qt::AlignCenter); settings_layout->addWidget(settings_button_); - settings_layout->addWidget(settings_label); + settings_layout->addWidget(settings_label_); toolbar_layout->addWidget(settings_container); // Spacer to push profile button to the right @@ -211,10 +237,6 @@ void GameListDialogQt::SetupUI() { // Question mark overlay for logged out state profile_question_label_ = new QLabel("?", profile_button_); profile_question_label_->setAlignment(Qt::AlignCenter); - QFont question_font = profile_question_label_->font(); - question_font.setPointSize(32); - question_font.setBold(true); - profile_question_label_->setFont(question_font); profile_question_label_->setGeometry(0, 0, 100, 80); profile_question_label_->setAttribute(Qt::WA_TransparentForMouseEvents); profile_question_label_->setStyleSheet( @@ -224,7 +246,6 @@ void GameListDialogQt::SetupUI() { profile_label_ = new QLabel("Logged Out", this); profile_label_->setAlignment(Qt::AlignCenter); - profile_label_->setFont(text_font); profile_layout->addWidget(profile_button_); profile_layout->addWidget(profile_label_); @@ -237,10 +258,6 @@ void GameListDialogQt::SetupUI() { search_box_->setPlaceholderText("Search games..."); search_box_->setClearButtonEnabled(true); search_box_->setStyleSheet("padding-left: 8px;"); - QFont search_font = search_box_->font(); - int search_font_size = cvars::font_size > 0 ? cvars::font_size * 1.5 : 16; - search_font.setPointSize(search_font_size); - search_box_->setFont(search_font); connect(search_box_, &QLineEdit::textChanged, this, &GameListDialogQt::OnFilterTextChanged); @@ -255,23 +272,18 @@ void GameListDialogQt::SetupUI() { header_layout->setContentsMargins(10, 5, 10, 5); header_layout->setSpacing(15); - auto* icon_header = new QLabel("Icon", this); - icon_header->setMinimumWidth(80); - icon_header->setMaximumWidth(80); - QFont header_font = icon_header->font(); - header_font.setBold(true); - icon_header->setFont(header_font); - header_layout->addWidget(icon_header); + icon_header_ = new QLabel("Icon", this); + icon_header_->setMinimumWidth(80); + icon_header_->setMaximumWidth(80); + header_layout->addWidget(icon_header_); - auto* title_header = new QLabel("Title", this); - title_header->setFont(header_font); - header_layout->addWidget(title_header, 1); + title_header_ = new QLabel("Title", this); + header_layout->addWidget(title_header_, 1); - auto* last_played_header = new QLabel("Last Played", this); - last_played_header->setFont(header_font); - last_played_header->setMinimumWidth(200); - last_played_header->setAlignment(Qt::AlignRight | Qt::AlignVCenter); - header_layout->addWidget(last_played_header); + last_played_header_ = new QLabel("Last Played", this); + last_played_header_->setMinimumWidth(200); + last_played_header_->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + header_layout->addWidget(last_played_header_); main_layout->addWidget(header_widget); @@ -319,6 +331,9 @@ void GameListDialogQt::SetupUI() { &GameListDialogQt::OnSelectionChanged); main_layout->addWidget(table_widget_); + + // Apply initial fonts to all widgets + RefreshFonts(); } void GameListDialogQt::LoadGameList() { @@ -705,7 +720,7 @@ void GameListDialogQt::PopulateTable() { auto* title_label = new QLabel(display_title); title_label->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); title_label->setAttribute(Qt::WA_TransparentForMouseEvents); - QFont title_font = title_label->font(); + QFont title_font = QApplication::font(); title_font.setBold(true); int base_font_size = cvars::font_size > 0 ? cvars::font_size : 13; title_font.setPointSize(base_font_size * 1.5); // 1.5x larger @@ -721,8 +736,8 @@ void GameListDialogQt::PopulateTable() { new QLabel(SafeQString(xe::path_to_utf8(entry.path_to_file))); path_label->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); path_label->setAttribute(Qt::WA_TransparentForMouseEvents); - QFont path_font = path_label->font(); - path_font.setPointSize(path_font.pointSize() * 0.8); // Smaller font + QFont path_font = QApplication::font(); + path_font.setPointSize(base_font_size * 0.8); // Smaller font path_label->setFont(path_font); path_label->setStyleSheet("color: gray;"); title_layout->addWidget(path_label); @@ -739,9 +754,8 @@ void GameListDialogQt::PopulateTable() { auto* achievement_label = new QLabel(achievement_text); achievement_label->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); achievement_label->setAttribute(Qt::WA_TransparentForMouseEvents); - QFont achievement_font = achievement_label->font(); - achievement_font.setPointSize(achievement_font.pointSize() * - 0.8); // Smaller font + QFont achievement_font = QApplication::font(); + achievement_font.setPointSize(base_font_size * 0.8); // Smaller font achievement_label->setFont(achievement_font); // Color based on completion diff --git a/src/xenia/ui/game_list_dialog_qt.h b/src/xenia/ui/game_list_dialog_qt.h index a6d4cda49..8811c23fb 100644 --- a/src/xenia/ui/game_list_dialog_qt.h +++ b/src/xenia/ui/game_list_dialog_qt.h @@ -63,6 +63,7 @@ class GameListDialogQt : public QWidget { ~GameListDialogQt() override; void LoadGameList(); + void RefreshFonts(); void RefreshIcons(); void UpdateProfileButtonState(); @@ -109,8 +110,13 @@ class GameListDialogQt : public QWidget { QToolButton* settings_button_; QToolButton* profile_button_; QLabel* play_label_; + QLabel* open_label_; + QLabel* settings_label_; QLabel* profile_label_; QLabel* profile_question_label_; + QLabel* icon_header_; + QLabel* title_header_; + QLabel* last_played_header_; QTimer* game_state_timer_; QGraphicsOpacityEffect* play_opacity_; QGraphicsOpacityEffect* play_label_opacity_; diff --git a/src/xenia/ui/imgui_drawer.cc b/src/xenia/ui/imgui_drawer.cc index 2b2e3c394..afb685f9f 100644 --- a/src/xenia/ui/imgui_drawer.cc +++ b/src/xenia/ui/imgui_drawer.cc @@ -674,15 +674,19 @@ void ImGuiDrawer::Draw(UIDrawContext& ui_draw_context) { io.FontGlobalScale = combined_scale; // Apply style scaling (padding, spacing, etc.) - // Store base style on first frame, then apply scaling each frame + // Cache base style on first frame, then only recalculate scaled style when + // scale changes to avoid expensive ScaleAllSizes call every frame auto& style = ImGui::GetStyle(); if (!base_style_initialized_) { base_style_ = style; base_style_initialized_ = true; + last_combined_scale_ = 0.f; // Force initial scaling + } + if (last_combined_scale_ != combined_scale) { + last_combined_scale_ = combined_scale; + style = base_style_; + style.ScaleAllSizes(combined_scale); } - // Reset to base style and apply current scale - style = base_style_; - style.ScaleAllSizes(combined_scale); if (!dialogs_.empty()) { UpdateGamepads(); @@ -736,8 +740,22 @@ void ImGuiDrawer::Draw(UIDrawContext& ui_draw_context) { // it now if needed. DetachIfLastWindowRemoved(); - if (!dialogs_.empty() || !notifications_.empty()) { - // Repaint (and handle input) continuously if still active. + // Request continuous repaints only when necessary: + // - Dialogs always need continuous repaints for input handling + // - Notifications only need continuous repaints while animating + // (FazeIn/FazeOut) During static display (Present stage), game frames will + // trigger repaints via RefreshGuestOutput -> + // RequestPaintOrConnectionRecoveryViaWindow + bool needs_continuous_repaint = !dialogs_.empty(); + if (!needs_continuous_repaint) { + for (const auto* notification : notifications_) { + if (notification->IsAnimating()) { + needs_continuous_repaint = true; + break; + } + } + } + if (needs_continuous_repaint) { presenter_->RequestUIPaintFromUIThread(); } } diff --git a/src/xenia/ui/imgui_drawer.h b/src/xenia/ui/imgui_drawer.h index 1bf54eeb7..81bfc2c65 100644 --- a/src/xenia/ui/imgui_drawer.h +++ b/src/xenia/ui/imgui_drawer.h @@ -178,6 +178,7 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer { // Store original style for proper scaling (ScaleAllSizes is cumulative) ImGuiStyle base_style_; bool base_style_initialized_ = false; + float last_combined_scale_ = 0.f; }; } // namespace ui diff --git a/src/xenia/ui/imgui_guest_notification.cc b/src/xenia/ui/imgui_guest_notification.cc index 33bebbcc7..c5a9f82c2 100644 --- a/src/xenia/ui/imgui_guest_notification.cc +++ b/src/xenia/ui/imgui_guest_notification.cc @@ -99,10 +99,14 @@ void AchievementNotificationWindow::OnDraw(ImGuiIO& io) { const float window_scale = std::fminf(screen_size.x / default_drawing_resolution.x, screen_size.y / default_drawing_resolution.y); - const float font_scale = default_font_size / io.Fonts->Fonts[0]->FontSize; - const ImVec2 text_size = io.Fonts->Fonts[0]->CalcTextSizeA( - default_font_size * default_notification_text_scale * window_scale, - FLT_MAX, -1.0f, longest_notification_text_line.c_str()); + // Calculate effective font size accounting for global scale (resolution + + // font_size cvar) and notification's own text scale multiplier + const float effective_font_size = io.Fonts->Fonts[0]->FontSize * + io.FontGlobalScale * + default_notification_text_scale; + const ImVec2 text_size = + io.Fonts->Fonts[0]->CalcTextSizeA(effective_font_size, FLT_MAX, -1.0f, + longest_notification_text_line.c_str()); const ImVec2 final_notification_size = CalculateNotificationSize(text_size, window_scale); @@ -134,8 +138,9 @@ void AchievementNotificationWindow::OnDraw(ImGuiIO& io) { ImGui::Begin("Notification Window", NULL, NOTIFY_TOAST_FLAGS); { - ImGui::SetWindowFontScale(default_notification_text_scale * font_scale * - window_scale); + // Only apply notification's own text scale - global scale already handles + // resolution and font_size cvar scaling + ImGui::SetWindowFontScale(default_notification_text_scale); // Set offset to image to prevent it from being right on border. ImGui::SetCursorPos(ImVec2(final_notification_size.x * 0.005f, final_notification_size.y * 0.05f)); @@ -173,10 +178,14 @@ void XNotifyWindow::OnDraw(ImGuiIO& io) { const float window_scale = std::fminf(screen_size.x / default_drawing_resolution.x, screen_size.y / default_drawing_resolution.y); - const float font_scale = default_font_size / io.Fonts->Fonts[0]->FontSize; - const ImVec2 text_size = io.Fonts->Fonts[0]->CalcTextSizeA( - default_font_size * default_notification_text_scale * window_scale, - FLT_MAX, -1.0f, longest_notification_text_line.c_str()); + // Calculate effective font size accounting for global scale (resolution + + // font_size cvar) and notification's own text scale multiplier + const float effective_font_size = io.Fonts->Fonts[0]->FontSize * + io.FontGlobalScale * + default_notification_text_scale; + const ImVec2 text_size = + io.Fonts->Fonts[0]->CalcTextSizeA(effective_font_size, FLT_MAX, -1.0f, + longest_notification_text_line.c_str()); const ImVec2 final_notification_size = CalculateNotificationSize(text_size, window_scale); @@ -208,8 +217,9 @@ void XNotifyWindow::OnDraw(ImGuiIO& io) { ImGui::Begin("Notification Window", NULL, NOTIFY_TOAST_FLAGS); { - ImGui::SetWindowFontScale(default_notification_text_scale * font_scale * - window_scale); + // Only apply notification's own text scale - global scale already handles + // resolution and font_size cvar scaling + ImGui::SetWindowFontScale(default_notification_text_scale); // Set offset to image to prevent it from being right on border. ImGui::SetCursorPos(ImVec2(final_notification_size.x * 0.005f, final_notification_size.y * 0.05f)); @@ -219,10 +229,6 @@ void XNotifyWindow::OnDraw(ImGuiIO& io) { ImVec2(default_notification_icon_size.x * window_scale, default_notification_icon_size.y * window_scale)); - // Set offset to image to prevent it from being right on border. - // ImGui::SetCursorPos(ImVec2(final_notification_size.x * 0.1f, - // final_notification_size.y * 0.2f)); - ImGui::SameLine(); if (notification_draw_progress_ > 0.5f) { ImGui::TextColored(white_color, "%s", GetDescription().c_str()); diff --git a/src/xenia/ui/imgui_guest_notification.h b/src/xenia/ui/imgui_guest_notification.h index 1e6ae57a2..23febc472 100644 --- a/src/xenia/ui/imgui_guest_notification.h +++ b/src/xenia/ui/imgui_guest_notification.h @@ -53,6 +53,12 @@ class ImGuiGuestNotification : public ImGuiNotification { return current_stage_ == NotificationStage::Finished; } + // Returns true if the notification is animating and needs continuous repaints + bool IsAnimating() const override { + return current_stage_ == NotificationStage::FazeIn || + current_stage_ == NotificationStage::FazeOut; + } + void UpdateNotificationState(); const ImVec2 CalculateNotificationSize(ImVec2 text_size, float scale) override; diff --git a/src/xenia/ui/imgui_notification.h b/src/xenia/ui/imgui_notification.h index 746684970..a746b77b4 100644 --- a/src/xenia/ui/imgui_notification.h +++ b/src/xenia/ui/imgui_notification.h @@ -65,6 +65,11 @@ class ImGuiNotification { void SetDeletionPending() { marked_for_deletion_ = true; } + // Returns true if the notification is animating and needs continuous + // repaints. Default returns true for safety; subclasses can override to + // optimize. + virtual bool IsAnimating() const { return true; } + protected: ImGuiDrawer* GetDrawer() { return imgui_drawer_; } diff --git a/src/xenia/ui/windowed_app_main_qt.cc b/src/xenia/ui/windowed_app_main_qt.cc index 3d5435f02..b2aa5ba5a 100644 --- a/src/xenia/ui/windowed_app_main_qt.cc +++ b/src/xenia/ui/windowed_app_main_qt.cc @@ -311,24 +311,20 @@ int main(int argc, char** argv) { qt_app.setFont(app_font); - // Force all existing widgets to update their fonts - for (QWidget* widget : qt_app.allWidgets()) { - widget->setFont(app_font); - } - - // Refresh game lists to apply custom font sizes (title +2) + // Refresh game lists to re-apply font settings for (QWidget* widget : qt_app.allWidgets()) { if (auto* game_list = qobject_cast(widget)) { - game_list->LoadGameList(); + game_list->RefreshFonts(); } } }; - // Apply font settings initially + // Set application font immediately so new widgets inherit it apply_font_settings(); - // Re-apply font settings when config is saved + // Re-apply font settings when config is saved (also used by + // OnEmulatorInitialized to apply fonts after GameListDialogQt is created) config::SetConfigSavedCallback(apply_font_settings); app_context.RunMainQtLoop();