mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-25 12:26:51 +01:00
launcher: draw the game grid as cards over the selected game's background
This commit is contained in:
@@ -37,6 +37,16 @@ endif()
|
||||
target_include_directories(launcher PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
set_target_properties(launcher PROPERTIES AUTOUIC ON AUTOMOC ON AUTORCC ON)
|
||||
|
||||
add_executable(launcher_game_views_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/LauncherGameViewsTests.cpp"
|
||||
src/gameContent.cpp
|
||||
src/launcherTheme.cpp
|
||||
resources.qrc
|
||||
)
|
||||
target_link_libraries(launcher_game_views_tests Qt6::Widgets common)
|
||||
target_include_directories(launcher_game_views_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||
set_target_properties(launcher_game_views_tests PROPERTIES AUTOUIC ON AUTORCC ON)
|
||||
|
||||
if(WIN32 AND NOT KYTY_CLANG_CL)
|
||||
set_target_properties(launcher PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS}")
|
||||
elseif(LINUX)
|
||||
|
||||
@@ -304,8 +304,11 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="game_views">
|
||||
<widget class="GameListTreeWidget" name="cfgs_list">
|
||||
<widget class="GameViews" name="game_views">
|
||||
<widget class="QTreeWidget" name="cfgs_list">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#cfgs_list { background: transparent; }</string>
|
||||
</property>
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
@@ -350,7 +353,11 @@
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
<widget class="QListView" name="cfgs_grid"/>
|
||||
<widget class="QListView" name="cfgs_grid">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#cfgs_grid { background: transparent; }</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
@@ -369,9 +376,10 @@
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>GameListTreeWidget</class>
|
||||
<extends>QTreeWidget</extends>
|
||||
<header>gameListTreeWidget.h</header>
|
||||
<class>GameViews</class>
|
||||
<extends>QStackedWidget</extends>
|
||||
<header>gameViews.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
#ifndef GAME_LIST_TREE_WIDGET_H
|
||||
#define GAME_LIST_TREE_WIDGET_H
|
||||
#ifndef GAME_VIEWS_H
|
||||
#define GAME_VIEWS_H
|
||||
|
||||
#include "gameContent.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QIcon>
|
||||
#include <QListView>
|
||||
#include <QPaintEvent>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QPixmap>
|
||||
#include <QPoint>
|
||||
#include <QResizeEvent>
|
||||
#include <QStackedWidget>
|
||||
#include <QString>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QTreeWidget>
|
||||
|
||||
class GameListTreeWidget: public QTreeWidget {
|
||||
inline constexpr int GAME_NAME_COLUMN = 0;
|
||||
inline constexpr int GAME_SERIAL_COLUMN = 1;
|
||||
inline constexpr int GAME_VERSION_COLUMN = 2;
|
||||
inline constexpr int GAME_FIRMWARE_VERSION_COLUMN = 3;
|
||||
inline constexpr int GAME_SIZE_COLUMN = 4;
|
||||
inline constexpr int GAME_PATH_COLUMN = 5;
|
||||
inline constexpr int GAME_STATUS_COLUMN = 6;
|
||||
inline constexpr int GAME_COMMENT_COLUMN = 7;
|
||||
|
||||
// Holds the transparent list and grid views and paints the selected game's pic0 behind both.
|
||||
class GameViews: public QStackedWidget {
|
||||
public:
|
||||
explicit GameListTreeWidget(QWidget* parent = nullptr): QTreeWidget(parent) {
|
||||
setAutoFillBackground(false);
|
||||
viewport()->setAutoFillBackground(false);
|
||||
}
|
||||
using QStackedWidget::QStackedWidget;
|
||||
|
||||
void SetBackgroundImage(const QString& key) {
|
||||
if (m_path == key) {
|
||||
@@ -30,48 +41,108 @@ public:
|
||||
GameContent::MaxImageSize));
|
||||
}
|
||||
UpdateScaledBackground();
|
||||
viewport()->update();
|
||||
update();
|
||||
}
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* event) override {
|
||||
QTreeWidget::resizeEvent(event);
|
||||
QStackedWidget::resizeEvent(event);
|
||||
UpdateScaledBackground();
|
||||
}
|
||||
|
||||
void paintEvent(QPaintEvent* event) override {
|
||||
{
|
||||
QPainter painter(viewport());
|
||||
if (m_scaled.isNull()) {
|
||||
painter.fillRect(event->rect(), palette().brush(QPalette::Base));
|
||||
} else {
|
||||
painter.drawPixmap(m_scaled_pos, m_scaled);
|
||||
auto overlay = palette().color(QPalette::Base);
|
||||
overlay.setAlpha(185);
|
||||
painter.fillRect(event->rect(), overlay);
|
||||
}
|
||||
QPainter painter(this);
|
||||
auto overlay = palette().color(QPalette::Base);
|
||||
if (!m_scaled.isNull()) {
|
||||
painter.drawPixmap((width() - m_scaled.width()) / 2, (height() - m_scaled.height()) / 2,
|
||||
m_scaled);
|
||||
overlay.setAlpha(160);
|
||||
}
|
||||
|
||||
QTreeWidget::paintEvent(event);
|
||||
painter.fillRect(event->rect(), overlay);
|
||||
}
|
||||
|
||||
private:
|
||||
void UpdateScaledBackground() {
|
||||
m_scaled = QPixmap();
|
||||
if (m_source.isNull() || viewport()->size().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_scaled = m_source.scaled(viewport()->size(), Qt::KeepAspectRatioByExpanding,
|
||||
Qt::FastTransformation);
|
||||
m_scaled_pos = QPoint((viewport()->width() - m_scaled.width()) / 2,
|
||||
(viewport()->height() - m_scaled.height()) / 2);
|
||||
m_scaled = m_source.isNull() ? QPixmap()
|
||||
: m_source.scaled(size(), Qt::KeepAspectRatioByExpanding,
|
||||
Qt::FastTransformation);
|
||||
}
|
||||
|
||||
QString m_path;
|
||||
QPixmap m_source;
|
||||
QPixmap m_scaled;
|
||||
QPoint m_scaled_pos;
|
||||
};
|
||||
|
||||
#endif // GAME_LIST_TREE_WIDGET_H
|
||||
// A card on a translucent panel, so the text keeps its contrast over the game background.
|
||||
class GameGridDelegate: public QStyledItemDelegate {
|
||||
public:
|
||||
using QStyledItemDelegate::QStyledItemDelegate;
|
||||
|
||||
QSize sizeHint(const QStyleOptionViewItem& option,
|
||||
const QModelIndex& /*index*/) const override {
|
||||
return option.decorationSize +
|
||||
QSize(2 * CARD_PADDING, 3 * CARD_PADDING + 2 * option.fontMetrics.lineSpacing());
|
||||
}
|
||||
|
||||
void paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const override {
|
||||
// The stylesheet made the view's own palette transparent, so take the panel from the app's.
|
||||
auto panel = QGuiApplication::palette().color(QPalette::Window);
|
||||
auto text_color = option.palette.color(QPalette::Text);
|
||||
panel.setAlpha(210);
|
||||
painter->save();
|
||||
painter->setRenderHint(QPainter::Antialiasing);
|
||||
painter->setPen(option.state.testFlag(QStyle::State_Selected) ? QPen(text_color)
|
||||
: Qt::NoPen);
|
||||
painter->setBrush(panel);
|
||||
painter->drawRoundedRect(QRectF(option.rect).adjusted(0.5, 0.5, -0.5, -0.5), 4, 4);
|
||||
|
||||
const QRect cover(option.rect.topLeft() + QPoint(CARD_PADDING, CARD_PADDING),
|
||||
option.decorationSize);
|
||||
index.data(Qt::DecorationRole).value<QIcon>().paint(painter, cover);
|
||||
|
||||
QRect line(cover.left(), cover.bottom() + 1 + CARD_PADDING, cover.width(),
|
||||
option.fontMetrics.lineSpacing());
|
||||
auto title_font = option.font;
|
||||
title_font.setBold(true);
|
||||
painter->setFont(title_font);
|
||||
painter->setPen(text_color);
|
||||
painter->drawText(line, Qt::AlignLeft | Qt::AlignVCenter,
|
||||
painter->fontMetrics().elidedText(index.data().toString(), Qt::ElideRight,
|
||||
line.width()));
|
||||
|
||||
line.translate(0, line.height());
|
||||
const auto size = index.siblingAtColumn(GAME_SIZE_COLUMN).data().toString();
|
||||
const auto details = QStringLiteral("%1 %2").arg(
|
||||
index.siblingAtColumn(GAME_SERIAL_COLUMN).data().toString(),
|
||||
index.siblingAtColumn(GAME_VERSION_COLUMN).data().toString());
|
||||
text_color.setAlpha(200);
|
||||
painter->setFont(option.font);
|
||||
painter->setPen(text_color);
|
||||
painter->drawText(line, Qt::AlignRight | Qt::AlignVCenter, size);
|
||||
line.setRight(line.right() - option.fontMetrics.horizontalAdvance(size) - CARD_PADDING);
|
||||
painter->drawText(line, Qt::AlignLeft | Qt::AlignVCenter,
|
||||
option.fontMetrics.elidedText(details, Qt::ElideRight, line.width()));
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr int CARD_PADDING = 8;
|
||||
};
|
||||
|
||||
// Shows the list's games as cards in the grid: both views share the items and the current game.
|
||||
inline void SetupGameGrid(QTreeWidget* list, QListView* grid) {
|
||||
grid->setModel(list->model());
|
||||
grid->setSelectionModel(list->selectionModel());
|
||||
grid->setViewMode(QListView::IconMode);
|
||||
grid->setMovement(QListView::Static);
|
||||
grid->setResizeMode(QListView::Adjust);
|
||||
grid->setSpacing(8);
|
||||
grid->setUniformItemSizes(true);
|
||||
grid->setItemDelegate(new GameGridDelegate(grid));
|
||||
// A card also shows other columns (the size arrives late), which the grid does not repaint on.
|
||||
QObject::connect(list->model(), &QAbstractItemModel::dataChanged, grid->viewport(),
|
||||
qOverload<>(&QWidget::update));
|
||||
}
|
||||
|
||||
#endif // GAME_VIEWS_H
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include "configurationEditDialog.h"
|
||||
#include "configurationItem.h"
|
||||
#include "gameContent.h"
|
||||
#include "gameListTreeWidget.h"
|
||||
#include "gameViews.h"
|
||||
#include "inputMappingDialog.h"
|
||||
#include "mainDialog.h"
|
||||
#include "patchesDialog.h"
|
||||
@@ -53,7 +53,6 @@
|
||||
#include <QSignalBlocker>
|
||||
#include <QSlider>
|
||||
#include <QStyle>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QToolButton>
|
||||
#include <QTreeWidget>
|
||||
#include <QUrl>
|
||||
@@ -83,15 +82,6 @@ constexpr char CONF_ADDONS_DIR[] = "addons_dir";
|
||||
constexpr char CONF_GLOBAL[] = "GlobalConfiguration";
|
||||
constexpr char SAVE_DATA_DIR[] = "_SaveData";
|
||||
|
||||
constexpr int GAME_NAME_COLUMN = 0;
|
||||
constexpr int GAME_SERIAL_COLUMN = 1;
|
||||
constexpr int GAME_VERSION_COLUMN = 2;
|
||||
constexpr int GAME_FIRMWARE_VERSION_COLUMN = 3;
|
||||
constexpr int GAME_SIZE_COLUMN = 4;
|
||||
constexpr int GAME_PATH_COLUMN = 5;
|
||||
constexpr int GAME_STATUS_COLUMN = 6;
|
||||
constexpr int GAME_COMMENT_COLUMN = 7;
|
||||
|
||||
static QString NormalizeGameDirectory(const QString& dir) {
|
||||
const auto trimmed = dir.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
@@ -151,54 +141,6 @@ static QStringList SettingsStringList(const QVariant& value) {
|
||||
return text.isEmpty() ? QStringList() : QStringList({text});
|
||||
}
|
||||
|
||||
class GameGridDelegate: public QStyledItemDelegate {
|
||||
public:
|
||||
using QStyledItemDelegate::QStyledItemDelegate;
|
||||
|
||||
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex&) const override {
|
||||
return option.decorationSize + QSize(16, 24 + option.fontMetrics.lineSpacing() * 2);
|
||||
}
|
||||
|
||||
protected:
|
||||
void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override {
|
||||
const auto image_size = option->decorationSize;
|
||||
QStyledItemDelegate::initStyleOption(option, index);
|
||||
option->decorationSize = image_size;
|
||||
option->decorationPosition = QStyleOptionViewItem::Top;
|
||||
option->decorationAlignment = Qt::AlignCenter;
|
||||
option->displayAlignment = Qt::AlignHCenter | Qt::AlignTop;
|
||||
option->features |= QStyleOptionViewItem::WrapText;
|
||||
}
|
||||
};
|
||||
|
||||
static void ConfigureGameList(Ui::ConfigurationListWidget* ui) {
|
||||
ui->cfgs_list->setAlternatingRowColors(false);
|
||||
ui->cfgs_list->setAllColumnsShowFocus(true);
|
||||
ui->cfgs_list->setIndentation(0);
|
||||
ui->cfgs_list->setMouseTracking(false);
|
||||
ui->cfgs_list->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
ui->cfgs_list->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
ui->cfgs_list->setTextElideMode(Qt::ElideMiddle);
|
||||
ui->cfgs_list->header()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
ui->cfgs_list->header()->setHighlightSections(false);
|
||||
ui->cfgs_list->header()->setStretchLastSection(true);
|
||||
|
||||
// Both presentations use the same items, icons and current game.
|
||||
ui->cfgs_grid->setModel(ui->cfgs_list->model());
|
||||
ui->cfgs_grid->setSelectionModel(ui->cfgs_list->selectionModel());
|
||||
ui->cfgs_grid->setViewMode(QListView::IconMode);
|
||||
ui->cfgs_grid->setMovement(QListView::Static);
|
||||
ui->cfgs_grid->setResizeMode(QListView::Adjust);
|
||||
ui->cfgs_grid->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
ui->cfgs_grid->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
ui->cfgs_grid->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
ui->cfgs_grid->setSpacing(8);
|
||||
ui->cfgs_grid->setUniformItemSizes(true);
|
||||
ui->cfgs_grid->setWordWrap(true);
|
||||
ui->cfgs_grid->setItemDelegate(new GameGridDelegate(ui->cfgs_grid));
|
||||
ui->cfgs_grid->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
}
|
||||
|
||||
static void AddSaveDataDir(QStringList* dirs, QSet<QString>* seen, const QString& root,
|
||||
const QString& title_id) {
|
||||
const auto path = QDir(root).filePath(
|
||||
@@ -252,7 +194,6 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
|
||||
m_compatibility = new CompatibilityDatabase(
|
||||
QCoreApplication::arguments().contains(QStringLiteral("--local")), this);
|
||||
m_ui->setupUi(this);
|
||||
ConfigureGameList(m_ui);
|
||||
|
||||
m_ui->refresh_action->setShortcuts(QKeySequence::Refresh);
|
||||
m_ui->refresh_button->setDefaultAction(m_ui->refresh_action);
|
||||
@@ -266,8 +207,9 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
|
||||
m_ui->edit_button->setEnabled(false);
|
||||
|
||||
m_ui->cfgs_list->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
m_ui->cfgs_list->setAllColumnsShowFocus(true);
|
||||
m_ui->cfgs_list->setTextElideMode(Qt::ElideMiddle);
|
||||
m_ui->cfgs_list->setIconSize(QSize(48, 48));
|
||||
m_ui->cfgs_list->setRootIsDecorated(false);
|
||||
m_ui->cfgs_list->setUniformRowHeights(true);
|
||||
m_ui->cfgs_list->setSortingEnabled(true);
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_NAME_COLUMN, 320);
|
||||
@@ -279,6 +221,8 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_STATUS_COLUMN, 150);
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_COMMENT_COLUMN, 240);
|
||||
m_ui->cfgs_list->sortItems(GAME_NAME_COLUMN, Qt::AscendingOrder);
|
||||
SetupGameGrid(m_ui->cfgs_list, m_ui->cfgs_grid);
|
||||
m_ui->cfgs_grid->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
|
||||
connect(m_ui->refresh_action, &QAction::triggered, this,
|
||||
&ConfigurationListWidget::ScanGameDirectory);
|
||||
@@ -323,16 +267,12 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
|
||||
connect(m_compatibility, &CompatibilityDatabase::Updated, this,
|
||||
&ConfigurationListWidget::ApplyCompatibility);
|
||||
|
||||
m_ui->cfgs_list->setDragDropMode(QAbstractItemView::NoDragDrop);
|
||||
|
||||
ReadSettings();
|
||||
m_ui->grid_size_slider->setVisible(m_ui->grid_view_button->isChecked());
|
||||
const auto resize_grid = [this](int width) {
|
||||
m_ui->cfgs_grid->setIconSize(QSize(width, width * 9 / 16));
|
||||
};
|
||||
resize_grid(m_ui->grid_size_slider->value());
|
||||
connect(m_ui->grid_size_slider, &QSlider::valueChanged, this, [this, resize_grid](int width) {
|
||||
resize_grid(width);
|
||||
const int grid_size = m_ui->grid_size_slider->value();
|
||||
m_ui->cfgs_grid->setIconSize(QSize(grid_size, grid_size));
|
||||
connect(m_ui->grid_size_slider, &QSlider::valueChanged, this, [this](int width) {
|
||||
m_ui->cfgs_grid->setIconSize(QSize(width, width));
|
||||
if (!m_ui->grid_size_slider->isSliderDown()) {
|
||||
WriteSettings();
|
||||
}
|
||||
@@ -1274,7 +1214,7 @@ void ConfigurationListWidget::SelectItem(QTreeWidgetItem* witem) {
|
||||
auto* item = static_cast<ConfigurationItem*>(witem);
|
||||
if (item == nullptr) {
|
||||
m_selected_item = nullptr;
|
||||
m_ui->cfgs_list->SetBackgroundImage({});
|
||||
m_ui->game_views->SetBackgroundImage({});
|
||||
m_ui->edit_button->setEnabled(false);
|
||||
m_ui->delete_button->setEnabled(false);
|
||||
emit Select();
|
||||
@@ -1285,7 +1225,7 @@ void ConfigurationListWidget::SelectItem(QTreeWidgetItem* witem) {
|
||||
m_ui->edit_button->setEnabled(!item->IsRunning());
|
||||
|
||||
m_selected_item = item;
|
||||
m_ui->cfgs_list->SetBackgroundImage(item->GetInfo().basedir);
|
||||
m_ui->game_views->SetBackgroundImage(item->GetInfo().basedir);
|
||||
|
||||
emit Select();
|
||||
}
|
||||
|
||||
@@ -1,32 +1,24 @@
|
||||
/* The views get their transparent background from their own styleSheet in
|
||||
configuration_list_widget.ui: Breeze paints black behind the scrollbars of views without one. */
|
||||
QTreeWidget#cfgs_list,
|
||||
QListView#cfgs_grid {
|
||||
background: palette(base);
|
||||
border: 1px solid palette(mid);
|
||||
color: palette(text);
|
||||
}
|
||||
|
||||
QListView#cfgs_grid::item {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
GameListTreeWidget#cfgs_list {
|
||||
background: transparent;
|
||||
border: 1px solid palette(mid);
|
||||
color: palette(text);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
GameListTreeWidget#cfgs_list::item {
|
||||
QTreeWidget#cfgs_list::item {
|
||||
background: transparent;
|
||||
border-bottom: 1px solid palette(mid);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
GameListTreeWidget#cfgs_list::item:selected {
|
||||
QTreeWidget#cfgs_list::item:selected {
|
||||
background: palette(highlight);
|
||||
color: palette(highlighted-text);
|
||||
}
|
||||
|
||||
GameListTreeWidget#cfgs_list QHeaderView::section {
|
||||
QTreeWidget#cfgs_list QHeaderView::section {
|
||||
background: palette(button);
|
||||
color: palette(button-text);
|
||||
border: 0;
|
||||
|
||||
Reference in New Issue
Block a user