From 996139fff451564671da509682e8ca0e8741c42f Mon Sep 17 00:00:00 2001 From: collecting Date: Wed, 8 Apr 2026 23:10:32 -0400 Subject: [PATCH] feat: New .svg's / Better Dark/Light Accountability / Various UI fixes --- dist/grid.svg | 6 + dist/list.svg | 5 + dist/sort_ascending.svg | 4 + dist/sort_descending.svg | 4 + src/citron/citron.qrc | 4 + src/citron/game_list.cpp | 252 ++++++++++++++++++++--------- src/citron/game_list.h | 2 + src/citron/game_list_delegate.cpp | 245 +++++++++++++++-------------- src/citron/game_list_delegate.h | 2 + src/citron/game_list_worker.cpp | 53 +++++-- src/citron/main.cpp | 253 +++++++++++++++++++++--------- 11 files changed, 549 insertions(+), 281 deletions(-) create mode 100644 dist/grid.svg create mode 100644 dist/list.svg create mode 100644 dist/sort_ascending.svg create mode 100644 dist/sort_descending.svg diff --git a/dist/grid.svg b/dist/grid.svg new file mode 100644 index 0000000000..c4b8b52ead --- /dev/null +++ b/dist/grid.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/dist/list.svg b/dist/list.svg new file mode 100644 index 0000000000..8ab80b2a2e --- /dev/null +++ b/dist/list.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/dist/sort_ascending.svg b/dist/sort_ascending.svg new file mode 100644 index 0000000000..32d2b0c16d --- /dev/null +++ b/dist/sort_ascending.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dist/sort_descending.svg b/dist/sort_descending.svg new file mode 100644 index 0000000000..5e2ca0fa9f --- /dev/null +++ b/dist/sort_descending.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/citron/citron.qrc b/src/citron/citron.qrc index 1ee13909fe..538ce21119 100644 --- a/src/citron/citron.qrc +++ b/src/citron/citron.qrc @@ -11,6 +11,10 @@ SPDX-License-Identifier: GPL-2.0-or-later ../../dist/dice.svg ../../dist/carousel.svg + ../../dist/list.svg + ../../dist/grid.svg + ../../dist/sort_ascending.svg + ../../dist/sort_descending.svg ../../dist/controller_navigation.svg ../../dist/citron.svg diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp index 7b1b14f99f..523c24e1b2 100644 --- a/src/citron/game_list.cpp +++ b/src/citron/game_list.cpp @@ -926,10 +926,14 @@ void GameList::OnUpdateThemedIcons() { .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole); break; - default: + case GameListItemType::Game: break; } } + + // Refresh all theme-aware styles and icons + UpdateProgressBarColor(); + UpdateAccentColorStyles(); } void GameList::OnFilterCloseClicked() { @@ -1064,12 +1068,13 @@ GameList::GameList(std::shared_ptr vfs_, // List view button - icon-only with rounded corners btn_list_view = new QToolButton(toolbar); - QIcon list_icon = QIcon::fromTheme(QStringLiteral("view-list-details")); + QIcon list_icon(QStringLiteral(":/dist/list.svg")); if (list_icon.isNull()) { - list_icon = QIcon::fromTheme(QStringLiteral("view-list")); - } - if (list_icon.isNull()) { - list_icon = style()->standardIcon(QStyle::SP_FileDialogListView); + list_icon = QIcon::fromTheme(QStringLiteral("view-list-details")); + if (list_icon.isNull()) + list_icon = QIcon::fromTheme(QStringLiteral("view-list")); + if (list_icon.isNull()) + list_icon = style()->standardIcon(QStyle::SP_FileDialogListView); } btn_list_view->setIcon(list_icon); btn_list_view->setToolTip(tr("List View")); @@ -1079,28 +1084,30 @@ GameList::GameList(std::shared_ptr vfs_, btn_list_view->setIconSize(QSize(16, 16)); btn_list_view->setFixedSize(26, 26); btn_list_view->setStyleSheet(QStringLiteral("QToolButton {" - " border: 1px solid palette(mid);" + " border: 1px solid #3e3e42;" " border-radius: 4px;" - " background: palette(button);" + " background: #2b2b2f;" + " color: #ffffff;" "}" "QToolButton:hover {" - " background: palette(light);" + " background: #3e3e42;" "}" "QToolButton:checked {" - " background: palette(highlight);" - " border-color: palette(highlight);" + " background: #0078d4;" + " border-color: #0078d4;" "}")); connect(btn_list_view, &QToolButton::clicked, [this]() { SetViewMode(GameList::ViewMode::List); }); // Grid view button - icon-only with rounded corners btn_grid_view = new QToolButton(toolbar); - QIcon grid_icon = QIcon::fromTheme(QStringLiteral("view-grid")); + QIcon grid_icon(QStringLiteral(":/dist/grid.svg")); if (grid_icon.isNull()) { - grid_icon = QIcon::fromTheme(QStringLiteral("view-grid-details")); - } - if (grid_icon.isNull()) { - grid_icon = style()->standardIcon(QStyle::SP_FileDialogDetailedView); + grid_icon = QIcon::fromTheme(QStringLiteral("view-grid")); + if (grid_icon.isNull()) + grid_icon = QIcon::fromTheme(QStringLiteral("view-grid-details")); + if (grid_icon.isNull()) + grid_icon = style()->standardIcon(QStyle::SP_FileDialogDetailedView); } btn_grid_view->setIcon(grid_icon); btn_grid_view->setToolTip(tr("Grid View")); @@ -1110,16 +1117,17 @@ GameList::GameList(std::shared_ptr vfs_, btn_grid_view->setIconSize(QSize(16, 16)); btn_grid_view->setFixedSize(26, 26); btn_grid_view->setStyleSheet(QStringLiteral("QToolButton {" - " border: 1px solid palette(mid);" + " border: 1px solid #3e3e42;" " border-radius: 4px;" - " background: palette(button);" + " background: #2b2b2f;" + " color: #ffffff;" "}" "QToolButton:hover {" - " background: palette(light);" + " background: #3e3e42;" "}" "QToolButton:checked {" - " background: palette(highlight);" - " border-color: palette(highlight);" + " background: #0078d4;" + " border-color: #0078d4;" "}")); connect(btn_grid_view, &QToolButton::clicked, [this]() { SetViewMode(GameList::ViewMode::Grid); }); @@ -1140,16 +1148,17 @@ GameList::GameList(std::shared_ptr vfs_, btn_carousel_view->setIconSize(QSize(16, 16)); btn_carousel_view->setFixedSize(26, 26); btn_carousel_view->setStyleSheet(QStringLiteral("QToolButton {" - " border: 1px solid palette(mid);" + " border: 1px solid #3e3e42;" " border-radius: 4px;" - " background: palette(button);" + " background: #2b2b2f;" + " color: #ffffff;" "}" "QToolButton:hover {" - " background: palette(light);" + " background: #3e3e42;" "}" "QToolButton:checked {" - " background: palette(highlight);" - " border-color: palette(highlight);" + " background: #0078d4;" + " border-color: #0078d4;" "}")); connect(btn_carousel_view, &QToolButton::clicked, [this]() { SetViewMode(GameList::ViewMode::Carousel); }); @@ -1267,12 +1276,13 @@ GameList::GameList(std::shared_ptr vfs_, btn_sort_az->setIconSize(QSize(16, 16)); btn_sort_az->setFixedSize(26, 26); btn_sort_az->setStyleSheet(QStringLiteral("QToolButton {" - " border: 1px solid palette(mid);" + " border: 1px solid #3e3e42;" " border-radius: 4px;" - " background: palette(button);" + " background: #2b2b2f;" + " color: #ffffff;" "}" "QToolButton:hover {" - " background: palette(light);" + " background: #3e3e42;" "}")); connect(btn_sort_az, &QToolButton::clicked, this, &GameList::ToggleSortOrder); @@ -1295,12 +1305,13 @@ GameList::GameList(std::shared_ptr vfs_, btn_surprise_me->setIconSize(QSize(16, 16)); btn_surprise_me->setFixedSize(26, 26); btn_surprise_me->setStyleSheet(QStringLiteral("QToolButton {" - " border: 1px solid palette(mid);" + " border: 1px solid #3e3e42;" " border-radius: 4px;" - " background: palette(button);" + " background: #2b2b2f;" + " color: #ffffff;" "}" "QToolButton:hover {" - " background: palette(light);" + " background: #3e3e42;" "}")); connect(btn_surprise_me, &QToolButton::clicked, this, &GameList::onSurpriseMeClicked); @@ -1322,8 +1333,13 @@ GameList::GameList(std::shared_ptr vfs_, toolbar_layout->addWidget(btn_sort_az); toolbar_layout->addWidget(btn_surprise_me); + // Install event filter on toolbar for tooltip dismissal + if (auto* delegate = qobject_cast(tree_view->itemDelegate())) { + toolbar->installEventFilter(delegate); + } + // Controller Settings Button - auto* btn_controller_settings = new QToolButton(toolbar); + btn_controller_settings = new QToolButton(toolbar); QIcon ctrl_icon(QStringLiteral(":/dist/controller_navigation.svg")); if (ctrl_icon.isNull()) { ctrl_icon = QIcon::fromTheme(QStringLiteral("input-gaming")); @@ -1370,7 +1386,7 @@ GameList::GameList(std::shared_ptr vfs_, layout->addWidget(toolbar); } - setStyleSheet(QStringLiteral("background: #161618;")); + UpdateProgressBarColor(); tree_view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); main_stack->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); tree_view->setFocusPolicy(Qt::StrongFocus); @@ -3227,6 +3243,7 @@ void GameList::ToggleSortOrder() { current_sort_order = (current_sort_order == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder; SortAlphabetically(); + UpdateSortButtonIcon(); // Force update immediately } void GameList::UpdateSortButtonIcon() { @@ -3234,25 +3251,21 @@ void GameList::UpdateSortButtonIcon() { return; QIcon sort_icon; - if (current_sort_order == Qt::AscendingOrder) { - // Ascending (A-Z) - arrow up - sort_icon = QIcon::fromTheme(QStringLiteral("view-sort-ascending")); - if (sort_icon.isNull()) { - sort_icon = QIcon::fromTheme(QStringLiteral("sort-ascending")); - } - if (sort_icon.isNull()) { - sort_icon = style()->standardIcon(QStyle::SP_ArrowUp); - } - } else { + if (current_sort_order == Qt::DescendingOrder) { // Descending (Z-A) - arrow down - sort_icon = QIcon::fromTheme(QStringLiteral("view-sort-descending")); - if (sort_icon.isNull()) { - sort_icon = QIcon::fromTheme(QStringLiteral("sort-descending")); - } - if (sort_icon.isNull()) { - sort_icon = style()->standardIcon(QStyle::SP_ArrowDown); - } + sort_icon = GetThemedIcon(QStringLiteral(":/dist/sort_descending.svg"), true); + } else { + // Ascending (A-Z) - arrow up + sort_icon = GetThemedIcon(QStringLiteral(":/dist/sort_ascending.svg"), true); } + + if (sort_icon.isNull()) { + // Fallback to theme if resources fail + sort_icon = QIcon::fromTheme(current_sort_order == Qt::AscendingOrder + ? QStringLiteral("view-sort-ascending") + : QStringLiteral("view-sort-descending")); + } + btn_sort_az->setIcon(sort_icon); } @@ -3427,11 +3440,15 @@ void GameList::UpdateAccentColorStyles() { .arg(hover_background_color.alpha()); const bool dark = UISettings::IsDarkTheme(); - const QString header_bg = dark ? QStringLiteral("#24242a") : QStringLiteral("#dddde2"); - const QString header_fg = dark ? QStringLiteral("#9898a4") : QStringLiteral("#444450"); - const QString header_border = dark ? QStringLiteral("#32323a") : QStringLiteral("#c8c8d0"); - const QString header_bg_hov = dark ? QStringLiteral("#2e2e34") : QStringLiteral("#cdcdd4"); - const QString header_fg_hov = dark ? QStringLiteral("#d0d0e0") : QStringLiteral("#222230"); + + // Header & Search Colors: Onyx (Dark) vs Silver (Light) + const QString header_bg = dark ? QStringLiteral("#1c1c1e") : QStringLiteral("#ececf0"); + const QString header_fg = dark ? QStringLiteral("#d0d0e0") : QStringLiteral("#1a1a1e"); + const QString header_border = dark ? QStringLiteral("#2e2e34") : QStringLiteral("#d0d0d5"); + const QString header_bg_hov = dark ? QStringLiteral("#2e2e34") : QStringLiteral("#e0e0e5"); + const QString header_fg_hov = dark ? QStringLiteral("#ffffff") : QStringLiteral("#000000"); + const QString search_bg = dark ? QStringLiteral("#1c1c1e") : QStringLiteral("#fdfdfd"); + const QString search_fg = dark ? QStringLiteral("#f0f0f5") : QStringLiteral("#1a1a1e"); QString accent_style = QStringLiteral( /* Tree View (List View) Selection & Hover Style */ @@ -3516,37 +3533,128 @@ void GameList::UpdateAccentColorStyles() { grid_view->setStyleSheet(accent_style); carousel_view->ApplyTheme(); - // Update the toolbar buttons as before - QString button_base_style = QStringLiteral("QToolButton {" - " border: 1px solid palette(mid);" + // Explicitly style the header with a clean, high-fidelity theme + tree_view->header()->setStyleSheet(QString::fromLatin1( + "QHeaderView::section { background-color: %1; color: %2; border: none; border-bottom: 1px solid %3; padding: 6px 10px; font-weight: bold; }" + "QHeaderView::section:hover { background-color: %4; }") + .arg(header_bg, header_fg, header_border, header_bg_hov)); + + // Restore theme-aware button styling + const QString button_bg = dark ? QStringLiteral("#2b2b2f") : QStringLiteral("#fdfdfd"); + const QString button_border = dark ? QStringLiteral("#3e3e42") : QStringLiteral("#d0d0d5"); + const QString button_hover = dark ? QStringLiteral("#3e3e42") : QStringLiteral("#eeeeef"); + const QString button_fg = dark ? QStringLiteral("#ffffff") : QStringLiteral("#1a1a1e"); + + // Locked Dark Style for top-right buttons: consistently dark grey (#2b2b2f) + const QString icon_btn_bg = QStringLiteral("#2b2b2f"); + const QString icon_btn_border = QStringLiteral("#3e3e42"); + const QString icon_btn_hover = QStringLiteral("#3e3e42"); + const QString icon_btn_fg = QStringLiteral("#ffffff"); + + QString button_base_style = QString::fromLatin1("QToolButton {" + " border: 1px solid %1;" " border-radius: 4px;" - " background: palette(button);" + " background: %2;" + " color: %3;" "}" "QToolButton:hover {" - " background: palette(light);" - "}"); - QString button_checked_style = QStringLiteral("QToolButton:checked {" - " background: %1;" - " border-color: %1;" - "}") + " background: %4;" + "}") + .arg(button_border, button_bg, button_fg, button_hover); + + QString icon_button_style = QString::fromLatin1("QToolButton {" + " border: 1px solid %1;" + " border-radius: 4px;" + " background: %2;" + " color: %3;" + "}" + "QToolButton:hover {" + " background: %4;" + "}") + .arg(icon_btn_border, icon_btn_bg, icon_btn_fg, icon_btn_hover); + + QString button_checked_style = QString::fromLatin1("QToolButton:checked {" + " background: %1;" + " border-color: %1;" + " color: #ffffff;" + "}") .arg(color_name); - btn_list_view->setStyleSheet(button_base_style + button_checked_style); - btn_grid_view->setStyleSheet(button_base_style + button_checked_style); + btn_list_view->setStyleSheet(icon_button_style + button_checked_style); + btn_grid_view->setStyleSheet(icon_button_style + button_checked_style); if (btn_carousel_view) - btn_carousel_view->setStyleSheet(button_base_style + button_checked_style); + btn_carousel_view->setStyleSheet(icon_button_style + button_checked_style); + + // Also update Sort, Surprise Me and Controller buttons with the locked dark style + btn_sort_az->setStyleSheet(icon_button_style); + btn_surprise_me->setStyleSheet(icon_button_style); + btn_controller_settings->setStyleSheet(icon_button_style); + + // Update toolbar background: Onyx Grey in Dark Mode, Pure White in Light Mode + toolbar->setStyleSheet(dark ? QStringLiteral("background: #24242a; border: none;") + : QStringLiteral("background: #ffffff; border: none;")); + + // Update main GameList background + setStyleSheet(dark ? QStringLiteral("background: #161618;") : QStringLiteral("background: #ffffff;")); search_field->setStyleSheet(QStringLiteral("QLineEdit {" " border: 1px solid palette(mid);" " border-radius: 6px;" " padding: 4px 8px;" - " background: palette(base);" + " background: %2;" + " color: %3;" "}" "QLineEdit:focus {" " border: 1px solid %1;" - " background: palette(base);" + " background: %2;" "}") - .arg(color_name)); + .arg(color_name, search_bg, search_fg)); + + // Force light icons for the locked-dark buttons even in Light Mode + const bool force_light = true; + btn_list_view->setIcon(GetThemedIcon(QStringLiteral(":/dist/list.svg"), force_light)); + btn_grid_view->setIcon(GetThemedIcon(QStringLiteral(":/dist/grid.svg"), force_light)); + if (btn_carousel_view) + btn_carousel_view->setIcon(GetThemedIcon(QStringLiteral(":/dist/carousel.svg"), force_light)); + + UpdateSortButtonIcon(); + btn_surprise_me->setIcon(GetThemedIcon(QStringLiteral(":/dist/dice.svg"), force_light)); + btn_controller_settings->setIcon(GetThemedIcon(QStringLiteral(":/dist/controller_navigation.svg"), force_light)); + + // Standarize icon size for consistent aesthetic accountability + const QSize icon_size(20, 20); + btn_list_view->setIconSize(icon_size); + btn_grid_view->setIconSize(icon_size); + if (btn_carousel_view) + btn_carousel_view->setIconSize(icon_size); + btn_sort_az->setIconSize(icon_size); + btn_surprise_me->setIconSize(icon_size); + btn_controller_settings->setIconSize(icon_size); +} + +QIcon GameList::GetThemedIcon(const QString& path, bool force_light) { + const bool dark = UISettings::IsDarkTheme(); + const QColor icon_color = (dark || force_light) ? QColor(224, 224, 228) : QColor(32, 32, 36); + + // Load at 2x resolution for high-fidelity rendering on all displays + const QSize base_size(24, 24); + QPixmap pixmap = QIcon(path).pixmap(base_size * 2); + if (pixmap.isNull()) return QIcon(path); + + QPainter painter(&pixmap); + + // Special handling for Surprise Me (Dice) to preserve internal details (dots) + if (path.contains(QStringLiteral("dice.svg"))) { + // Use Multiply mode to tint the body while keeping black dots sharp + painter.setCompositionMode(QPainter::CompositionMode_Multiply); + } else { + // Standard tinting for flat icons + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + } + + painter.fillRect(pixmap.rect(), icon_color); + painter.end(); + return QIcon(pixmap); } void GameList::SaveGameListIndex() { diff --git a/src/citron/game_list.h b/src/citron/game_list.h index 628c228913..16505d4bd1 100644 --- a/src/citron/game_list.h +++ b/src/citron/game_list.h @@ -234,6 +234,7 @@ private: void changeEvent(QEvent*) override; void RetranslateUI(); void UpdateSortButtonIcon(); + QIcon GetThemedIcon(const QString& path, bool force_light = false); std::shared_ptr vfs; FileSys::ManualContentProvider* provider; @@ -249,6 +250,7 @@ private: QSlider* slider_title_size = nullptr; QToolButton* btn_sort_az = nullptr; QToolButton* btn_surprise_me = nullptr; + QToolButton* btn_controller_settings = nullptr; Qt::SortOrder current_sort_order = Qt::AscendingOrder; QStandardItemModel* item_model = nullptr; GameDetailsPanel* details_panel = nullptr; diff --git a/src/citron/game_list_delegate.cpp b/src/citron/game_list_delegate.cpp index 7667415783..29ace7bd65 100644 --- a/src/citron/game_list_delegate.cpp +++ b/src/citron/game_list_delegate.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include @@ -19,9 +21,15 @@ #include #include #include -#include #include #include +#include +#include +#include + +#include +#include +#include #include "citron/game_list.h" #include "citron/game_list_delegate.h" @@ -49,48 +57,27 @@ public: m_label->setAlignment(Qt::AlignLeft | Qt::AlignTop); layout->addWidget(m_label); - setStyleSheet(QStringLiteral( - "QWidget { background-color: #24242a; border: 1px solid #32323a; border-radius: 8px; }" - "QLabel { color: #ffffff; background: transparent; border: none; font-family: 'Outfit', 'Inter', sans-serif; }" - )); + const bool is_dark = UISettings::IsDarkTheme(); + const QString bg_color = is_dark ? QStringLiteral("#24242a") : QStringLiteral("#f5f5fa"); + const QString text_color = is_dark ? QStringLiteral("#ffffff") : QStringLiteral("#1a1a1e"); + const QString border_color = is_dark ? QStringLiteral("#32323a") : QStringLiteral("#dcdce2"); + + setStyleSheet(QString::fromLatin1( + "QWidget { background-color: %1; border: 1px solid %2; border-radius: 8px; }" + "QLabel { color: %3; background: transparent; border: none; font-family: 'Outfit', 'Inter', sans-serif; }" + ).arg(bg_color, border_color, text_color)); } - static void showText(const QPoint& pos, const QString& text, QWidget* w) { - static OnyxTooltip* instance = nullptr; - if (!instance) { - instance = new OnyxTooltip(); - } - - instance->m_label->setText(text); - instance->adjustSize(); - - QScreen* screen = QGuiApplication::screenAt(pos); - if (!screen) screen = QGuiApplication::primaryScreen(); - QRect screenGeom = screen->availableGeometry(); - - QPoint showPos = pos + QPoint(10, 10); - if (showPos.x() + instance->width() > screenGeom.right()) { - showPos.setX(pos.x() - instance->width() - 10); - } - if (showPos.y() + instance->height() > screenGeom.bottom()) { - showPos.setY(pos.y() - instance->height() - 10); - } - - instance->move(showPos); - instance->show(); - QTimer::singleShot(5000, instance, &QWidget::hide); - } - - static void hideTooltip() { - // Implementation simplified as static instance persists - } + static void showText(const QPoint& pos, const QString& text, QWidget* w); + static void hideTooltip(); protected: void paintEvent(QPaintEvent* event) override { + const bool is_dark = UISettings::IsDarkTheme(); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); - painter.setBrush(QColor(0x24, 0x24, 0x2a)); - painter.setPen(QColor(0x32, 0x32, 0x3a)); + painter.setBrush(is_dark ? QColor(0x24, 0x24, 0x2a) : QColor(0xf5, 0xf5, 0xfa)); + painter.setPen(is_dark ? QColor(0x32, 0x32, 0x3a) : QColor(0xdc, 0xdc, 0xe2)); painter.drawRoundedRect(rect().adjusted(0, 0, -1, -1), 8, 8); } @@ -98,11 +85,52 @@ private: QLabel* m_label; }; +static OnyxTooltip* s_onyx_tooltip_instance = nullptr; + +void OnyxTooltip::showText(const QPoint& pos, const QString& text, QWidget* w) { + if (!s_onyx_tooltip_instance) { + s_onyx_tooltip_instance = new OnyxTooltip(); + } + + s_onyx_tooltip_instance->m_label->setText(text); + s_onyx_tooltip_instance->adjustSize(); + + QScreen* screen = QGuiApplication::screenAt(pos); + if (!screen) screen = QGuiApplication::primaryScreen(); + QRect screenGeom = screen->availableGeometry(); + + QPoint showPos = pos + QPoint(10, 10); + if (showPos.x() + s_onyx_tooltip_instance->width() > screenGeom.right()) { + showPos.setX(pos.x() - s_onyx_tooltip_instance->width() - 10); + } + if (showPos.y() + s_onyx_tooltip_instance->height() > screenGeom.bottom()) { + showPos.setY(pos.y() - s_onyx_tooltip_instance->height() - 10); + } + + s_onyx_tooltip_instance->move(showPos); + s_onyx_tooltip_instance->show(); +} + +void OnyxTooltip::hideTooltip() { + if (s_onyx_tooltip_instance) { + s_onyx_tooltip_instance->hide(); + } +} + GameListDelegate::GameListDelegate(QTreeView* view, QObject* parent) : QStyledItemDelegate(parent), tree_view(view) { animation_timer = new QTimer(this); connect(animation_timer, &QTimer::timeout, this, &GameListDelegate::AdvanceAnimations); animation_timer->start(40); // ~25 FPS + + if (tree_view) { + if (tree_view->viewport()) { + tree_view->viewport()->installEventFilter(this); + } + if (tree_view->header()) { + tree_view->header()->installEventFilter(this); + } + } } GameListDelegate::~GameListDelegate() = default; @@ -377,6 +405,32 @@ bool GameListDelegate::helpEvent(QHelpEvent* event, QAbstractItemView* view, return QStyledItemDelegate::helpEvent(event, view, option, index); } +bool GameListDelegate::eventFilter(QObject* obj, QEvent* event) { + if (obj == tree_view->viewport()) { + if (event->type() == QEvent::MouseMove) { + auto* mouseEvent = static_cast(event); + int column = tree_view->header()->logicalIndexAt(mouseEvent->pos().x()); + + // If the mouse is NOT horizontally within the add-ons column, hide the tooltip. + // Also hide if moving too far up (towards the header/toolbar boundaries). + if (column != GameList::COLUMN_ADD_ONS || mouseEvent->pos().y() < 0) { + OnyxTooltip::hideTooltip(); + } + } else if (event->type() == QEvent::Leave) { + // Hide tooltip when leaving the main viewport area + OnyxTooltip::hideTooltip(); + } else if (event->type() == QEvent::Wheel) { + // Hide tooltip immediately on any scroll action + OnyxTooltip::hideTooltip(); + } + } else if (event->type() == QEvent::MouseMove || event->type() == QEvent::Enter || + event->type() == QEvent::HoverMove || event->type() == QEvent::HoverEnter) { + // Fallback dismissal for other monitored widgets (like the header or toolbar) + OnyxTooltip::hideTooltip(); + } + return QStyledItemDelegate::eventFilter(obj, event); +} + void GameListDelegate::PaintBackground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { const bool is_selected = option.state & QStyle::State_Selected; @@ -647,101 +701,64 @@ void GameListDelegate::PaintDefault(QPainter* painter, const QRect& rect, painter->setPen(TextColor()); } - painter->setFont(option.font); - const int margin = 10; QRect content_rect = rect.adjusted(margin, 4, -margin, -4); - // Check if we need scrolling (only for Add-ons or columns with likely multi-line text) - const bool is_addons = index.column() == GameList::COLUMN_ADD_ONS; - - if (is_addons) { - // Optimization: Use cache for string processing + // [MARQUEE RESTORATION] Restore full vertical list for Add-ons column + if (index.column() == GameList::COLUMN_ADD_ONS) { if (!addons_item_cache.contains(text)) { QStringList lines = text.split(QLatin1Char('\n'), Qt::SkipEmptyParts); addons_item_cache.insert(text, lines); - - // Limit cache size - if (addons_item_cache.size() > 500) - addons_item_cache.clear(); + if (addons_item_cache.size() > 500) addons_item_cache.clear(); } const QStringList& lines = addons_item_cache[text]; - - // Increase line height for breathing room - const int line_v_padding = 2; - const int line_h = painter->fontMetrics().height() + line_v_padding; - const int max_lines = content_rect.height() / line_h; + const int line_h = painter->fontMetrics().height() + 2; + const int total_h = lines.size() * line_h; const bool is_hovered = option.state & QStyle::State_MouseOver; - const int total_h = lines.size() * line_h; - - if (lines.size() > 1 || total_h > content_rect.height()) { + if (total_h > content_rect.height() && is_hovered) { const QPersistentModelIndex key(index); - - if (is_hovered && total_h > content_rect.height()) { - if (!vertical_scroll_offsets.contains(key)) { - vertical_scroll_offsets[key] = 0; - vertical_scroll_pause[key] = 30; - } - - int& offset = vertical_scroll_offsets[key]; - int& pause = vertical_scroll_pause[key]; - const int full_max_offset = total_h - content_rect.height() + 10; - - if (pause > 0) { - pause--; - } else if (offset < full_max_offset) { - offset++; - } else { - offset = 0; - pause = 60; - } - - painter->save(); - painter->setClipRect(content_rect); - painter->translate(0, -offset); - for (int i = 0; i < lines.size(); ++i) { - painter->drawText(QRect(content_rect.left(), content_rect.top() + (i * line_h), - content_rect.width(), line_h), - Qt::AlignVCenter | Qt::AlignLeft, - painter->fontMetrics().elidedText(lines[i], Qt::ElideRight, - content_rect.width())); - } - painter->restore(); - } else { - // Decay scroll offset when not hovered - if (vertical_scroll_offsets.contains(key)) { - vertical_scroll_offsets.remove(key); - vertical_scroll_pause.remove(key); - } - - painter->save(); - painter->setClipRect(content_rect); - const int block_h = std::min((int)lines.size(), max_lines) * line_h; - const int block_top = - content_rect.top() + std::max(0, (content_rect.height() - block_h) / 2); - - for (int i = 0; i < std::min((int)lines.size(), max_lines); ++i) { - painter->drawText(QRect(content_rect.left(), block_top + (i * line_h), - content_rect.width(), line_h), - Qt::AlignVCenter | Qt::AlignLeft, - painter->fontMetrics().elidedText(lines[i], Qt::ElideRight, - content_rect.width())); - } - - if (lines.size() > max_lines) { - painter->setPen(DimColor()); - painter->drawText(content_rect, Qt::AlignBottom | Qt::AlignRight, - QStringLiteral("...")); - } - painter->restore(); + if (!vertical_scroll_offsets.contains(key)) { + vertical_scroll_offsets[key] = 0; + vertical_scroll_pause[key] = 30; } + int& offset = vertical_scroll_offsets[key]; + int& pause = vertical_scroll_pause[key]; + const int max_offset = total_h - content_rect.height() + 10; + if (pause > 0) pause--; + else if (offset < max_offset) offset++; + else { offset = 0; pause = 60; } + + painter->save(); + painter->setClipRect(content_rect); + painter->translate(0, -offset); + for (int i = 0; i < lines.size(); ++i) { + painter->drawText(QRect(content_rect.left(), content_rect.top() + (i * line_h), + content_rect.width(), line_h), + Qt::AlignVCenter | Qt::AlignLeft, + painter->fontMetrics().elidedText(lines[i], Qt::ElideRight, content_rect.width())); + } + painter->restore(); + return; + } else { + // Static centered vertical list + painter->save(); + painter->setClipRect(content_rect); + const int block_h = std::min((int)total_h, content_rect.height()); + const int block_top = content_rect.top() + std::max(0, (content_rect.height() - block_h) / 2); + for (int i = 0; i < lines.size() && (i * line_h) < content_rect.height(); ++i) { + painter->drawText(QRect(content_rect.left(), block_top + (i * line_h), + content_rect.width(), line_h), + Qt::AlignVCenter | Qt::AlignLeft, + painter->fontMetrics().elidedText(lines[i], Qt::ElideRight, content_rect.width())); + } + painter->restore(); return; } } - // Default static rendering + // Default static rendering for other columns painter->drawText( content_rect, Qt::AlignVCenter | Qt::AlignLeft, painter->fontMetrics().elidedText(text, Qt::ElideRight, content_rect.width())); diff --git a/src/citron/game_list_delegate.h b/src/citron/game_list_delegate.h index 9833364fbb..a5f3fc77f6 100644 --- a/src/citron/game_list_delegate.h +++ b/src/citron/game_list_delegate.h @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -37,6 +38,7 @@ public: const QModelIndex& index) const override; bool helpEvent(QHelpEvent* event, QAbstractItemView* view, const QStyleOptionViewItem& option, const QModelIndex& index) override; + bool eventFilter(QObject* obj, QEvent* event) override; // Constants for layout static constexpr int kCardMarginV = 3; diff --git a/src/citron/game_list_worker.cpp b/src/citron/game_list_worker.cpp index 6881a5298a..4bd7345110 100644 --- a/src/citron/game_list_worker.cpp +++ b/src/citron/game_list_worker.cpp @@ -414,7 +414,7 @@ QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, loader.ReadUpdateRaw(update_raw); for (const auto& patch : patch_manager.GetPatches(update_raw)) { const QString patch_name = QString::fromStdString(patch.name); - const bool is_update = patch_name == QLatin1String("Update"); + const bool is_update = patch_name.startsWith(QLatin1String("Update")); const bool is_dlc = patch_name.startsWith(QLatin1String("DLC")) || patch_name.contains(QLatin1String("Add-On"), Qt::CaseInsensitive); @@ -422,29 +422,37 @@ QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, continue; } - QString type = patch.enabled ? patch_name : QLatin1String("[D] ") + patch_name; + QString clean_name = patch_name; + // Surgically strip redundant prefixes like "DLC: " or "Add-on: " + if (clean_name.startsWith(QLatin1String("DLC: "), Qt::CaseInsensitive)) { + clean_name.remove(0, 5); + } else if (clean_name.startsWith(QLatin1String("Add-on: "), Qt::CaseInsensitive)) { + clean_name.remove(0, 8); + } + + QString type = patch.enabled ? clean_name : QLatin1String("[D] ") + clean_name; QString version = QString::fromStdString(patch.version); if (is_update && version == QLatin1String("PACKED")) { version = QString::fromStdString(Loader::GetFileTypeString(loader.GetFileType())); } - QString entry = version.isEmpty() ? type : QStringLiteral("%1 (%2)").arg(type, version); + // Avoid doubling version if it's already in the name (e.g. "Update v1.0.1 (1.0.1)") + QString entry; + if (!version.isEmpty() && (type.contains(version) || type.contains(version.mid(1)))) { + entry = type; + } else { + entry = version.isEmpty() ? type : QStringLiteral("%1 (%2)").arg(type, version); + } if (is_update) { updates.append(entry + QStringLiteral("\n")); } else if (is_dlc) { - dlcs.append(entry + QStringLiteral(", ")); + dlcs.append(entry + QStringLiteral("\n")); } else { mods.append(entry + QStringLiteral("\n")); } } - if (!dlcs.isEmpty()) { - dlcs.chop(2); // Remove trailing comma and space - dlcs.prepend(QStringLiteral("DLC: ")); - dlcs.append(QStringLiteral("\n")); - } - QString out = updates + dlcs + mods; if (out.endsWith(QLatin1Char('\n'))) { out.chop(1); @@ -490,7 +498,7 @@ QList MakeGameListEntry(const std::string& path, const std::stri new GameListItemOnline(online_text)}; const auto patch_versions = GetGameListCachedObject( - fmt::format("{:016X}", patch.GetTitleID()), "pv.txt", [&patch, &loader] { + fmt::format("{:016X}", patch.GetTitleID()), "pv_v610_marquee.txt", [&patch, &loader] { return FormatPatchNameVersions(patch, loader, loader.IsRomFSUpdatable()); }); @@ -498,17 +506,28 @@ QList MakeGameListEntry(const std::string& path, const std::stri // Create a high-end HTML tooltip for the Add-ons column if (!patch_versions.isEmpty()) { - QString tooltip = QStringLiteral( - "" - "
ACTIVE ADD-ONS & MODS
" - "
" - ); + const bool is_dark = UISettings::IsDarkTheme(); + const QString bg_color = is_dark ? QStringLiteral("#24242a") : QStringLiteral("#f5f5fa"); + const QString divider_color = is_dark ? QStringLiteral("#303035") : QStringLiteral("#dcdce2"); + const QString text_color = is_dark ? QStringLiteral("#ffffff") : QStringLiteral("#000000"); + const QString bullet_color = is_dark ? QStringLiteral("#e0e0e4") : QStringLiteral("#222228"); + + QString tooltip = QString::fromLatin1( + "" + "
ACTIVE ADD-ONS & MODS
" + "
"); + + tooltip = tooltip.arg(bg_color, text_color, + QString::fromStdString(UISettings::values.accent_color.GetValue()), + divider_color); QStringList categories = patch_versions.split(QLatin1Char('\n'), Qt::SkipEmptyParts); for (const auto& line : categories) { - tooltip.append(QStringLiteral("
%1
").arg(line.toHtmlEscaped())); + tooltip.append(QString::fromLatin1("
%2
") + .arg(is_dark ? QStringLiteral("#e0e0e4") : QStringLiteral("#444444"), line.toHtmlEscaped())); } + tooltip.append(QStringLiteral("
")); addon_item->setData(tooltip, Qt::ToolTipRole); } diff --git a/src/citron/main.cpp b/src/citron/main.cpp index 3db96234f8..89acb149dd 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -98,13 +98,14 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include #include #include -#include #include #include #include +#include #include #include + #ifdef HAVE_SDL2 #include // For SDL ScreenSaver functions #endif @@ -116,10 +117,10 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "common/fs/path_util.h" #include "common/literals.h" #include "common/logging.h" -#include "common/logging.h" #include "common/memory_detect.h" #include "common/scm_rev.h" #include "common/scope_exit.h" + #ifdef _WIN32 #include #include "common/windows/timer_resolution.h" @@ -140,8 +141,8 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "citron/controller_overlay.h" #include "citron/debugger/console.h" #include "citron/debugger/controller.h" -#include "citron/debugger/wait_tree.h" #include "citron/debugger/memory_tools.h" +#include "citron/debugger/wait_tree.h" #include "citron/discord.h" #include "citron/game_list.h" #include "citron/game_list_p.h" @@ -186,6 +187,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "video_core/renderer_vulkan/vk_rasterizer.h" #include "video_core/shader_notify.h" + #ifdef CITRON_USE_AUTO_UPDATER #include "citron/updater/updater_dialog.h" #include "citron/updater/updater_service.h" @@ -538,7 +540,6 @@ GMainWindow::GMainWindow(std::unique_ptr config_, bool has_broken_vulk } */ - if (has_broken_vulkan) { UISettings::values.has_broken_vulkan = true; @@ -1084,32 +1085,46 @@ void GMainWindow::WebBrowserRequestExit() { #endif } -#include #include +#include + class MenuAnimationFilter : public QObject { public: explicit MenuAnimationFilter(QMenu* menu) : QObject(menu), target_menu(menu) {} bool eventFilter(QObject* obj, QEvent* event) override { + // [HOVER-TO-SWITCH] Restore fluid navigation logic + if (event->type() == QEvent::Enter) { + // Check if any other menu is already open + for (auto* topLevel : QApplication::topLevelWidgets()) { + if (topLevel->inherits("QMenu") && topLevel->isVisible() && + topLevel != target_menu) { + // Instantly switch to this menu for a premium console-grade feel + target_menu->show(); + break; + } + } + } + if (event->type() == QEvent::Show && obj == target_menu && !is_animating) { is_animating = true; QPropertyAnimation* geom = new QPropertyAnimation(target_menu, "geometry", target_menu); geom->setDuration(150); QRect final_geom = target_menu->geometry(); // Ensure we keep the SAME x and width to prevent horizontal shifting - QRect start_geom = QRect(final_geom.x(), final_geom.y() - 5, final_geom.width(), final_geom.height()); + QRect start_geom = + QRect(final_geom.x(), final_geom.y() - 5, final_geom.width(), final_geom.height()); geom->setStartValue(start_geom); geom->setEndValue(final_geom); geom->setEasingCurve(QEasingCurve::OutCubic); - connect(geom, &QAbstractAnimation::finished, [this]() { - is_animating = false; - }); + connect(geom, &QAbstractAnimation::finished, [this]() { is_animating = false; }); geom->start(QAbstractAnimation::DeleteWhenStopped); } return QObject::eventFilter(obj, event); } + private: QMenu* target_menu; bool is_animating = false; @@ -1117,6 +1132,48 @@ private: #include +class GlobalOnyxUIFilter : public QObject { +public: + explicit GlobalOnyxUIFilter(QObject* parent = nullptr) : QObject(parent) {} + bool eventFilter(QObject* obj, QEvent* event) override { + // [RIBBON STYLING] Enforce style on Paint to prevent 'clear' tooltips during recycling + if ((event->type() == QEvent::Show || event->type() == QEvent::Paint) && + obj->inherits("QTipLabel")) { + auto* widget = qobject_cast(obj); + if (widget) { + widget->setStyleSheet( + QStringLiteral("QToolTip, QTipLabel { " + " background-color: #24242a; " + " color: #e0e0e4; " + " border: 1px solid #32323a; " + " border-radius: 3px; " + " padding: 1px 8px; font-size: 10pt; " + " font-family: 'Outfit', 'Inter', sans-serif; " + "}")); + } + } + + // [FLUID NAVIGATION] Global Grab-Aware hand-off logic + // This MUST be MouseMove to catch transitions while a menu has the mouse grab + if (event->type() == QEvent::MouseMove) { + QWidget* active_popup = QApplication::activePopupWidget(); + if (active_popup && active_popup->inherits("QMenu")) { + QWidget* hovered = QApplication::widgetAt(QCursor::pos()); + if (hovered && hovered->inherits("QPushButton") && hovered->parent() && + hovered->parent()->objectName() == QStringLiteral("UnifiedTopBar")) { + auto* btn = static_cast(hovered); + if (btn->menu() && btn->menu() != active_popup) { + active_popup->close(); + btn->showMenu(); + return true; + } + } + } + } + return QObject::eventFilter(obj, event); + } +}; + void GMainWindow::InitializeWidgets() { #ifdef CITRON_ENABLE_COMPATIBILITY_REPORTING ui->action_Report_Compatibility->setVisible(true); @@ -1128,7 +1185,7 @@ void GMainWindow::InitializeWidgets() { game_list = new GameList(vfs, provider.get(), *play_time_manager, *system, this); game_list->SetToolbarInMain(true); ui->horizontalLayout->addWidget(game_list); - + // Create a new master layout for centralwidget // We create it first without a parent to avoid warnings QVBoxLayout* master_layout = new QVBoxLayout(); @@ -1139,11 +1196,13 @@ void GMainWindow::InitializeWidgets() { unified_top_bar = new QWidget(this); unified_top_bar->setObjectName(QStringLiteral("UnifiedTopBar")); unified_top_bar->setAutoFillBackground(true); - unified_top_bar->setStyleSheet(QStringLiteral("QWidget#UnifiedTopBar { background-color: #24242a; border-bottom: 1px solid #32323a; }")); - + unified_top_bar->setStyleSheet(QStringLiteral( + "QWidget#UnifiedTopBar { background-color: #24242a; border-bottom: 1px solid #32323a; }")); + // Retrieve dynamic accent color const QString accent_hex = QString::fromStdString(UISettings::values.accent_color.GetValue()); - const QColor accent_color = QColor(accent_hex).isValid() ? QColor(accent_hex) : QColor(60, 120, 216); + const QColor accent_color = + QColor(accent_hex).isValid() ? QColor(accent_hex) : QColor(60, 120, 216); const QString accent_str = accent_color.name(); const QString accent_dim = accent_color.darker(120).name(); @@ -1152,20 +1211,25 @@ void GMainWindow::InitializeWidgets() { unified_top_bar_layout->setSpacing(0); auto add_menu = [this, accent_str](QMenu* menu) { - if (!menu) return; + if (!menu) + return; menu->installEventFilter(new MenuAnimationFilter(menu)); - menu->setWindowFlags(menu->windowFlags() | Qt::NoDropShadowWindowHint | Qt::FramelessWindowHint); + menu->setWindowFlags(menu->windowFlags() | Qt::NoDropShadowWindowHint | + Qt::FramelessWindowHint); menu->setAttribute(Qt::WA_TranslucentBackground, false); menu->setStyleSheet(QString::asprintf( - "QMenu { background: #24242a; border: 1px solid #32323a; border-radius: 8px; padding: 6px; color: #ffffff; }" - "QMenu::item { padding: 4px 28px 4px 32px; border-radius: 4px; margin: 1px; font-size: 8.5pt; min-width: 140px; color: #e0e0e4; }" + "QMenu { background: #24242a; border: 1px solid #32323a; border-radius: 8px; padding: " + "6px; color: #ffffff; }" + "QMenu::item { padding: 4px 28px 4px 32px; border-radius: 4px; margin: 1px; font-size: " + "8.5pt; min-width: 140px; color: #e0e0e4; }" "QMenu::item:selected { background-color: %s; color: #ffffff; }" "QMenu::item:disabled { color: #555558; }" "QMenu::separator { height: 1px; background: #303035; margin: 4px 10px; }" - "QMenu::indicator { width: 14px; height: 14px; left: 10px; border-radius: 3px; border: 1px solid #4a4a50; background: #121214; }" + "QMenu::indicator { width: 14px; height: 14px; left: 10px; border-radius: 3px; border: " + "1px solid #4a4a50; background: #121214; }" "QMenu::indicator:checked { background: %s; border: 1px solid %s; }", - accent_str.toUtf8().constData(), accent_str.toUtf8().constData(), accent_str.toUtf8().constData() - )); + accent_str.toUtf8().constData(), accent_str.toUtf8().constData(), + accent_str.toUtf8().constData())); QPushButton* btn = new QPushButton(menu->title().remove(QLatin1Char('&')), unified_top_bar); btn->setFlat(true); btn->setFocusPolicy(Qt::NoFocus); @@ -1173,13 +1237,13 @@ void GMainWindow::InitializeWidgets() { btn->setFixedHeight(42); btn->setStyleSheet(QString::asprintf( "QPushButton { border: none; padding: 0 14px; font-weight: normal; font-size: 9pt; " - "background: transparent; color: #e0e0e4; text-align: center; margin: 0; outline: none; }" + "background: transparent; color: #e0e0e4; text-align: center; margin: 0; outline: " + "none; }" "QPushButton:hover { background: rgba(255, 255, 255, 0.05); color: #ffffff; " "border-bottom: 2px solid %s; }" "QPushButton:pressed { background: rgba(255, 255, 255, 0.10); }" "QPushButton::menu-indicator { image: none; width: 0; }", - accent_str.toUtf8().constData() - )); + accent_str.toUtf8().constData())); btn->setMenu(menu); unified_top_bar_layout->addWidget(btn); }; @@ -1190,7 +1254,7 @@ void GMainWindow::InitializeWidgets() { add_menu(ui->menu_Tools); add_menu(ui->menu_Multiplayer); add_menu(ui->menu_Help); - + // Set first/last button specific styling if needed, but the new flat look is preferred // Padding logic handled in QPushButton style above @@ -1198,19 +1262,21 @@ void GMainWindow::InitializeWidgets() { unified_top_bar_layout->addStretch(); if (game_list && game_list->GetToolbarWidget()) { - unified_top_bar_layout->addWidget(game_list->GetToolbarWidget(), 0, Qt::AlignRight | Qt::AlignVCenter); + unified_top_bar_layout->addWidget(game_list->GetToolbarWidget(), 0, + Qt::AlignRight | Qt::AlignVCenter); } - + ui->action_Show_Filter_Bar->setChecked(true); ui->action_Show_Status_Bar->setChecked(true); ui->action_Fullscreen->setChecked(windowState().testFlag(Qt::WindowFullScreen)); game_list->SetFilterVisible(true); - // Re-parent the existing horizontal layout (which contains game list etc) into our master layout + // Re-parent the existing horizontal layout (which contains game list etc) into our master + // layout QWidget* content_container = new QWidget(this); // Taking the layout from centralwidget (this removes the old layout from centralwidget) - content_container->setLayout(ui->centralwidget->layout()); - + content_container->setLayout(ui->centralwidget->layout()); + master_layout->addWidget(unified_top_bar, 0); master_layout->addWidget(content_container, 1); @@ -1241,8 +1307,9 @@ void GMainWindow::InitializeWidgets() { multiplayer_state->setVisible(false); // Create status bar - statusBar()->setStyleSheet(QStringLiteral("QStatusBar { background-color: #24242a; border-top: 1px solid #32323a; color: #aaa; }" - "QStatusBar::item { border: none; }")); + statusBar()->setStyleSheet(QStringLiteral( + "QStatusBar { background-color: #24242a; border-top: 1px solid #32323a; color: #aaa; }" + "QStatusBar::item { border: none; }")); message_label = new QLabel(); message_label->setFrameStyle(QFrame::NoFrame); message_label->setContentsMargins(4, 0, 4, 0); @@ -1488,10 +1555,12 @@ void GMainWindow::InitializeWidgets() { QString gamescope_style = qApp->styleSheet(); gamescope_style.append(QStringLiteral( - "QMenu { background: #24242a !important; border: 1px solid #32323a; border-radius: 8px; padding: 6px; color: #ffffff; }" + "QMenu { background: #24242a !important; border: 1px solid #32323a; border-radius: " + "8px; padding: 6px; color: #ffffff; }" "QMenu::item { padding: 6px 30px; border-radius: 4px; margin: 2px; color: #ffffff; }" "QMenu::item:selected { background-color: #32323a; border: 1px solid #42424a; }" - "QToolTip { background: #24242a !important; color: #ffffff; border: 1px solid #32323a; border-radius: 6px; padding: 8px; }")); + "QToolTip { background: #24242a !important; color: #ffffff; border: 1px solid #32323a; " + "border-radius: 6px; padding: 8px; }")); qApp->setStyleSheet(gamescope_style); multiplayer_room_overlay->resize(360, 240); @@ -1795,13 +1864,13 @@ void GMainWindow::ConnectWidgetEvents() { if (QWidget* toolbar = game_list->GetToolbarWidget()) { QGraphicsOpacityEffect* effect = new QGraphicsOpacityEffect(toolbar); toolbar->setGraphicsEffect(effect); - + QPropertyAnimation* anim = new QPropertyAnimation(effect, "opacity", this); anim->setDuration(250); anim->setStartValue(1.0); anim->setEndValue(0.0); anim->setEasingCurve(QEasingCurve::OutCubic); - + connect(anim, &QPropertyAnimation::finished, toolbar, &QWidget::hide); anim->start(QAbstractAnimation::DeleteWhenStopped); } @@ -1812,19 +1881,18 @@ void GMainWindow::ConnectWidgetEvents() { if (game_list) { if (QWidget* toolbar = game_list->GetToolbarWidget()) { toolbar->show(); - + QGraphicsOpacityEffect* effect = new QGraphicsOpacityEffect(toolbar); toolbar->setGraphicsEffect(effect); - + QPropertyAnimation* anim = new QPropertyAnimation(effect, "opacity", this); anim->setDuration(250); anim->setStartValue(0.0); anim->setEndValue(1.0); anim->setEasingCurve(QEasingCurve::OutCubic); - - connect(anim, &QPropertyAnimation::finished, [toolbar]() { - toolbar->setGraphicsEffect(nullptr); - }); + + connect(anim, &QPropertyAnimation::finished, + [toolbar]() { toolbar->setGraphicsEffect(nullptr); }); anim->start(QAbstractAnimation::DeleteWhenStopped); } } @@ -3351,7 +3419,9 @@ bool GMainWindow::CreateShortcutLink(const std::filesystem::path& shortcut_path, LOG_ERROR(Frontend, "Failed to get IPersistFile interface"); return false; } - hres = persist_file->Save(std::filesystem::path{shortcut_path / (Common::UTF8ToUTF16W(name) + L".lnk")}.c_str(), TRUE); + hres = persist_file->Save( + std::filesystem::path{shortcut_path / (Common::UTF8ToUTF16W(name) + L".lnk")}.c_str(), + TRUE); if (FAILED(hres)) { LOG_ERROR(Frontend, "Failed to save shortcut"); return false; @@ -4180,9 +4250,7 @@ void GMainWindow::ErrorDisplayRequestExit() { } } -void GMainWindow::OnMenuReportCompatibility() { - -} +void GMainWindow::OnMenuReportCompatibility() {} void GMainWindow::OpenURL(const QUrl& url) { const bool open = QDesktopServices::openUrl(url); @@ -6263,35 +6331,68 @@ void GMainWindow::UpdateUITheme() { if (!f.open(QFile::ReadOnly | QFile::Text)) { LOG_ERROR(Frontend, "Unable to open style \"{}\", fallback to the default theme", UISettings::values.theme); - current_theme = default_theme_name; + } else { + qApp->setStyleSheet(QString::fromUtf8(f.readAll())); + } + } else { + qApp->setStyleSheet(QStringLiteral("")); + } + + // Refresh status bar style to follow the theme (Silver for Light, Onyx for Dark) + // We STRICTLY follow the text-only aesthetic from Screenshot 1 (No boxes/borders) + const bool is_dark = UISettings::IsDarkTheme(); + const QString status_bg = is_dark ? QStringLiteral("#24242a") : QStringLiteral("#f0f0f5"); + const QString status_fg = is_dark ? QStringLiteral("#aaa") : QStringLiteral("#1a1a1e"); + const QString status_border = is_dark ? QStringLiteral("#32323a") : QStringLiteral("#d0d0d5"); + + // Unified Top Bar styling (Reverting hardcoded dark segments) + const QString toolbar_bg = is_dark ? QStringLiteral("#24242a") : QStringLiteral("#ffffff"); + const QString toolbar_border = is_dark ? QStringLiteral("#32323a") : QStringLiteral("#d0d0d5"); + const QString toolbar_fg = is_dark ? QStringLiteral("#e0e0e4") : QStringLiteral("#1a1a1e"); + + if (unified_top_bar) { + unified_top_bar->setStyleSheet( + QStringLiteral("QWidget#UnifiedTopBar { background-color: %1; border-bottom: 1px solid %2; }") + .arg(toolbar_bg, toolbar_border)); + + // Update top bar buttons to adapt text color + const QString accent_hex = QString::fromStdString(UISettings::values.accent_color.GetValue()); + const QColor accent_color = QColor(accent_hex).isValid() ? QColor(accent_hex) : QColor(60, 120, 216); + const QString accent_str = accent_color.name(); + + QString top_btn_style = QString::fromLatin1( + "QPushButton { border: none; padding: 0 14px; font-weight: normal; font-size: 9pt; " + "background: transparent; color: %1; text-align: center; margin: 0; outline: none; }" + "QPushButton:hover { background: %2; color: %3; border-bottom: 2px solid %4; }" + "QPushButton:pressed { background: %5; }" + "QPushButton::menu-indicator { image: none; width: 0; }") + .arg(toolbar_fg, (is_dark ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.05)"), + (is_dark ? "#ffffff" : "#000000"), accent_str, + (is_dark ? "rgba(255, 255, 255, 0.10)" : "rgba(0, 0, 0, 0.10)")); + + for (auto* btn : unified_top_bar->findChildren()) { + btn->setStyleSheet(top_btn_style); } } - QString theme_uri{QStringLiteral(":%1/style.qss").arg(current_theme)}; - QFile f(theme_uri); - if (f.open(QFile::ReadOnly | QFile::Text)) { - QString style = QString::fromUtf8(f.readAll()); - - // Append Grey Onyx overrides for popups (Menus) and the Top Unified Toolbar - const QString onyx_overrides = QStringLiteral( - "QMenuBar { background-color: #08080a; border-bottom: 1px solid #1a1a1e; min-height: 38px; }" - "QMenuBar::item { padding: 0px 14px; background: transparent; border-radius: 4px; color: #ffffff; margin: 4px 2px; height: 30px; }" - "QMenuBar::item:selected { background-color: #24242a; }" - "QMenu { background-color: #1a1a1e !important; border: 1px solid #32323a; border-radius: 8px; padding: 4px; color: #ffffff; }" - "QMenu::item { padding: 6px 25px; border-radius: 4px; margin: 1px; color: #ffffff; }" - "QMenu::item:selected { background-color: #32323a; border: 1px solid #42424a; }" - "QMenu::separator { height: 1px; background: #32323a; margin: 4px 10px; }" - ); - qApp->setStyleSheet(style + onyx_overrides); - } else { - LOG_ERROR(Frontend, "Unable to set style \"{}\", stylesheet file not found", - UISettings::values.theme); - qApp->setStyleSheet({}); - } + // Force aggressive transparency for status bar children to kill redundant 'boxes' + // We explicitly target :checked and :hover states to ensure native styles don't apply backgrounds + statusBar()->setStyleSheet( + QStringLiteral( + "QStatusBar { background-color: %1; border-top: 1px solid %2; color: %3; }" + "QStatusBar QLabel { color: %3 !important; background: transparent !important; border: none !important; }" + "QStatusBar QPushButton, QStatusBar QToolButton, " + "QStatusBar QPushButton:checked, QStatusBar QToolButton:checked, " + "QStatusBar QPushButton:hover, QStatusBar QToolButton:hover { " + "background: transparent !important; background-color: transparent !important; " + "border: none !important; color: %3 !important; padding: 0px 6px !important; outline: none !important; }" + "QStatusBar QPushButton#RendererStatusBarButton { color: #ff8c00 !important; font-weight: bold !important; background: transparent !important; border: none !important; }" + "QStatusBar QPushButton#GPUStatusBarButton { color: #32cd32 !important; font-weight: bold !important; background: transparent !important; border: none !important; }" + "QStatusBar::item { border: none !important; }") + .arg(status_bg, status_border, status_fg)); - emit themeChanged(); + emit UpdateThemedIcons(); - // Once everything is done, reset the flag to false. m_is_updating_theme = false; } @@ -6359,7 +6460,7 @@ void GMainWindow::changeEvent(QEvent* event) { const QColor window_color = test_palette.color(QPalette::Active, QPalette::Window); if (last_window_color != window_color && (current_theme == QStringLiteral("default") || current_theme == QStringLiteral("colorful"))) { - UpdateUITheme(); + QTimer::singleShot(0, this, &GMainWindow::UpdateUITheme); } last_window_color = window_color; } @@ -6445,16 +6546,13 @@ static void SetHighDPIAttributes() { HMODULE shcore = LoadLibrary(L"shcore.dll"); if (shcore) { - using SetProcessDpiAwarenessFunc = - HRESULT(WINAPI*)(PROCESS_DPI_AWARENESS); + using SetProcessDpiAwarenessFunc = HRESULT(WINAPI*)(PROCESS_DPI_AWARENESS); - auto* shcoreProcAddress { - reinterpret_cast(GetProcAddress(shcore, - "SetProcessDpiAwareness")) - }; + auto* shcoreProcAddress{ + reinterpret_cast(GetProcAddress(shcore, "SetProcessDpiAwareness"))}; auto setProcessDpiAwareness = - reinterpret_cast(shcoreProcAddress); + reinterpret_cast(shcoreProcAddress); if (setProcessDpiAwareness) { setProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); @@ -6556,7 +6654,6 @@ int main(int argc, char* argv[]) { QCoreApplication::setAttribute(Qt::AA_DisableWindowContextHelpButton); #endif - QApplication app(argc, argv); #ifdef _WIN32