feat: New .svg's / Better Dark/Light Accountability / Various UI fixes

This commit is contained in:
collecting
2026-04-08 23:10:32 -04:00
parent bd78f8ddac
commit 996139fff4
11 changed files with 549 additions and 281 deletions
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<rect x="3" y="3" width="7" height="7" rx="1.5" fill="#FEFEFE"/>
<rect x="14" y="3" width="7" height="7" rx="1.5" fill="#848484"/>
<rect x="3" y="14" width="7" height="7" rx="1.5" fill="#848484"/>
<rect x="14" y="14" width="7" height="7" rx="1.5" fill="#848484"/>
</svg>

After

Width:  |  Height:  |  Size: 371 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<rect x="2" y="5" width="20" height="2" rx="1" fill="#FEFEFE"/>
<rect x="2" y="11" width="20" height="2" rx="1" fill="#848484"/>
<rect x="2" y="17" width="20" height="2" rx="1" fill="#848484"/>
</svg>

After

Width:  |  Height:  |  Size: 297 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<path d="M3 18h6v-2H3v2zM3 6v2h18V6H3zm0 7h12v-2H3v2z" fill="#FEFEFE"/>
<path d="M18 16V8h-2l3-3 3 3h-2v8h-2z" fill="#848484"/>
</svg>

After

Width:  |  Height:  |  Size: 227 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<path d="M3 18h6v-2H3v2zM3 6v2h18V6H3zm0 7h12v-2H3v2z" fill="#FEFEFE"/>
<path d="M18 8v8h2l-3 3-3-3h2V8h2z" fill="#848484"/>
</svg>

After

Width:  |  Height:  |  Size: 224 B

+4
View File
@@ -11,6 +11,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<qresource prefix="/">
<file alias="dist/dice.svg">../../dist/dice.svg</file>
<file alias="dist/carousel.svg">../../dist/carousel.svg</file>
<file alias="dist/list.svg">../../dist/list.svg</file>
<file alias="dist/grid.svg">../../dist/grid.svg</file>
<file alias="dist/sort_ascending.svg">../../dist/sort_ascending.svg</file>
<file alias="dist/sort_descending.svg">../../dist/sort_descending.svg</file>
<file alias="dist/controller_navigation.svg">../../dist/controller_navigation.svg</file>
<file alias="citron.svg">../../dist/citron.svg</file>
</qresource>
+180 -72
View File
@@ -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<FileSys::VfsFilesystem> 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<FileSys::VfsFilesystem> 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<FileSys::VfsFilesystem> 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<FileSys::VfsFilesystem> 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<FileSys::VfsFilesystem> 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<FileSys::VfsFilesystem> 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<FileSys::VfsFilesystem> 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<GameListDelegate*>(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<FileSys::VfsFilesystem> 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() {
+2
View File
@@ -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<FileSys::VfsFilesystem> 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;
+131 -114
View File
@@ -12,6 +12,8 @@
#include <QPainter>
#include <QPainterPath>
#include <QPixmap>
#include <QPoint>
#include <QRect>
#include <QStyle>
#include <QStyleOptionViewItem>
#include <QTimer>
@@ -19,9 +21,15 @@
#include <QHelpEvent>
#include <QLabel>
#include <QVBoxLayout>
#include <QPainter>
#include <QScreen>
#include <QGuiApplication>
#include <QWidget>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QWidget>
#include <QObject>
#include <QEvent>
#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<QMouseEvent*>(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()));
+2
View File
@@ -5,6 +5,7 @@
#include <QColor>
#include <QMap>
#include <QEvent>
#include <QObject>
#include <QPersistentModelIndex>
#include <QSize>
@@ -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;
+36 -17
View File
@@ -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<QStandardItem*> 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<QStandardItem*> 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(
"<html><body style='background-color: #24242a; color: #ffffff; padding: 15px; border-radius: 10px; font-family: \"Outfit\", \"Inter\", sans-serif;'>"
"<div style='margin-bottom: 8px; color: #0096ff; font-size: 13px; font-weight: bold; border-bottom: 1px solid #303035; padding-bottom: 4px;'>ACTIVE ADD-ONS & MODS</div>"
"<div style='line-height: 1.5;'>"
);
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(
"<html><body style='background-color: %1; color: %2; padding: 15px; border-radius: 10px; font-family: \"Outfit\", \"Inter\", sans-serif;'>"
"<div style='margin-bottom: 8px; color: %3; font-size: 13px; font-weight: bold; border-bottom: 1px solid %4; padding-bottom: 4px;'>ACTIVE ADD-ONS & MODS</div>"
"<div style='line-height: 1.5;'>");
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("<div style='margin: 3px 0; color: #e0e0e4;'><b>•</b> %1</div>").arg(line.toHtmlEscaped()));
tooltip.append(QString::fromLatin1("<div style='margin: 3px 0; color: %1;'><b>•</b> %2</div>")
.arg(is_dark ? QStringLiteral("#e0e0e4") : QStringLiteral("#444444"), line.toHtmlEscaped()));
}
tooltip.append(QStringLiteral("</div></body></html>"));
addon_item->setData(tooltip, Qt::ToolTipRole);
}
+175 -78
View File
@@ -98,13 +98,14 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include <QShortcut>
#include <QStandardPaths>
#include <QStatusBar>
#include <QToolTip>
#include <QString>
#include <QStyleFactory>
#include <QSysInfo>
#include <QToolTip>
#include <QUrl>
#include <QtConcurrent/QtConcurrent>
#ifdef HAVE_SDL2
#include <SDL.h> // 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 <shlobj.h>
#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<QtConfig> config_, bool has_broken_vulk
}
*/
if (has_broken_vulkan) {
UISettings::values.has_broken_vulkan = true;
@@ -1084,32 +1085,46 @@ void GMainWindow::WebBrowserRequestExit() {
#endif
}
#include <QPropertyAnimation>
#include <QParallelAnimationGroup>
#include <QPropertyAnimation>
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 <QGraphicsOpacityEffect>
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<QWidget*>(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<QPushButton*>(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<QPushButton*>()) {
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<void*>(GetProcAddress(shcore,
"SetProcessDpiAwareness"))
};
auto* shcoreProcAddress{
reinterpret_cast<void*>(GetProcAddress(shcore, "SetProcessDpiAwareness"))};
auto setProcessDpiAwareness =
reinterpret_cast<SetProcessDpiAwarenessFunc>(shcoreProcAddress);
reinterpret_cast<SetProcessDpiAwarenessFunc>(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