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();