[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
This commit is contained in:
Herman S.
2026-01-19 10:34:41 +09:00
parent f1aa4ea244
commit f77c654e77
9 changed files with 140 additions and 79 deletions
+13 -4
View File
@@ -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);
});
}
+58 -44
View File
@@ -9,6 +9,7 @@
#include "xenia/ui/game_list_dialog_qt.h"
#include <QApplication>
#include <QCursor>
#include <QDesktopServices>
#include <QDialog>
@@ -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
+6
View File
@@ -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_;
+24 -6
View File
@@ -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();
}
}
+1
View File
@@ -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
+22 -16
View File
@@ -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());
+6
View File
@@ -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;
+5
View File
@@ -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_; }
+5 -9
View File
@@ -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<xe::app::GameListDialogQt*>(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();