From 27ad7529adb6883c945ccf402bf11b85b2b04703 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Wed, 28 Feb 2024 20:34:04 +0200 Subject: [PATCH] initial add of qt gui , not yet usable --- src/qt_gui/custom_dock_widget.h | 62 ++ src/qt_gui/custom_table_widget_item.cpp | 67 ++ src/qt_gui/custom_table_widget_item.h | 26 + src/qt_gui/game_info.h | 20 + src/qt_gui/game_install_dialog.cpp | 83 +++ src/qt_gui/game_install_dialog.h | 27 + src/qt_gui/game_list_frame.cpp | 886 ++++++++++++++++++++++++ src/qt_gui/game_list_frame.h | 146 ++++ src/qt_gui/game_list_grid.cpp | 164 +++++ src/qt_gui/game_list_grid.h | 62 ++ src/qt_gui/game_list_grid_delegate.cpp | 67 ++ src/qt_gui/game_list_grid_delegate.h | 24 + src/qt_gui/game_list_item.h | 35 + src/qt_gui/game_list_table.cpp | 18 + src/qt_gui/game_list_table.h | 28 + src/qt_gui/game_list_utils.h | 109 +++ src/qt_gui/gui_save.h | 29 + src/qt_gui/gui_settings.cpp | 29 + src/qt_gui/gui_settings.h | 106 +++ src/qt_gui/main_window.cpp | 364 ++++++++++ src/qt_gui/main_window.h | 75 ++ src/qt_gui/main_window_themes.cpp | 120 ++++ src/qt_gui/main_window_themes.h | 21 + src/qt_gui/main_window_ui.h | 279 ++++++++ src/qt_gui/qt_utils.h | 20 + src/qt_gui/settings.cpp | 77 ++ src/qt_gui/settings.h | 50 ++ 27 files changed, 2994 insertions(+) create mode 100644 src/qt_gui/custom_dock_widget.h create mode 100644 src/qt_gui/custom_table_widget_item.cpp create mode 100644 src/qt_gui/custom_table_widget_item.h create mode 100644 src/qt_gui/game_info.h create mode 100644 src/qt_gui/game_install_dialog.cpp create mode 100644 src/qt_gui/game_install_dialog.h create mode 100644 src/qt_gui/game_list_frame.cpp create mode 100644 src/qt_gui/game_list_frame.h create mode 100644 src/qt_gui/game_list_grid.cpp create mode 100644 src/qt_gui/game_list_grid.h create mode 100644 src/qt_gui/game_list_grid_delegate.cpp create mode 100644 src/qt_gui/game_list_grid_delegate.h create mode 100644 src/qt_gui/game_list_item.h create mode 100644 src/qt_gui/game_list_table.cpp create mode 100644 src/qt_gui/game_list_table.h create mode 100644 src/qt_gui/game_list_utils.h create mode 100644 src/qt_gui/gui_save.h create mode 100644 src/qt_gui/gui_settings.cpp create mode 100644 src/qt_gui/gui_settings.h create mode 100644 src/qt_gui/main_window.cpp create mode 100644 src/qt_gui/main_window.h create mode 100644 src/qt_gui/main_window_themes.cpp create mode 100644 src/qt_gui/main_window_themes.h create mode 100644 src/qt_gui/main_window_ui.h create mode 100644 src/qt_gui/qt_utils.h create mode 100644 src/qt_gui/settings.cpp create mode 100644 src/qt_gui/settings.h diff --git a/src/qt_gui/custom_dock_widget.h b/src/qt_gui/custom_dock_widget.h new file mode 100644 index 00000000..9d40bb2f --- /dev/null +++ b/src/qt_gui/custom_dock_widget.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +class CustomDockWidget : public QDockWidget { +private: + std::shared_ptr m_title_bar_widget; + bool m_is_title_bar_visible = true; + +public: + explicit CustomDockWidget(const QString& title, QWidget* parent = Q_NULLPTR, + Qt::WindowFlags flags = Qt::WindowFlags()) + : QDockWidget(title, parent, flags) { + m_title_bar_widget.reset(titleBarWidget()); + + connect(this, &QDockWidget::topLevelChanged, [this](bool /* topLevel*/) { + SetTitleBarVisible(m_is_title_bar_visible); + style()->unpolish(this); + style()->polish(this); + }); + } + + void SetTitleBarVisible(bool visible) { + if (visible || isFloating()) { + if (m_title_bar_widget.get() != titleBarWidget()) { + setTitleBarWidget(m_title_bar_widget.get()); + QMargins margins = widget()->contentsMargins(); + margins.setTop(0); + widget()->setContentsMargins(margins); + } + } else { + setTitleBarWidget(new QWidget()); + QMargins margins = widget()->contentsMargins(); + margins.setTop(1); + widget()->setContentsMargins(margins); + } + + m_is_title_bar_visible = visible; + } + +protected: + void paintEvent(QPaintEvent* event) override { + // We need to repaint the dock widgets as plain widgets in floating mode. + // Source: + // https://stackoverflow.com/questions/10272091/cannot-add-a-background-image-to-a-qdockwidget + if (isFloating()) { + QStyleOption opt; + opt.initFrom(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + return; + } + + // Use inherited method for docked mode because otherwise the dock would lose the title etc. + QDockWidget::paintEvent(event); + } +}; diff --git a/src/qt_gui/custom_table_widget_item.cpp b/src/qt_gui/custom_table_widget_item.cpp new file mode 100644 index 00000000..321f22dc --- /dev/null +++ b/src/qt_gui/custom_table_widget_item.cpp @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "custom_table_widget_item.h" + +CustomTableWidgetItem::CustomTableWidgetItem(const std::string& text, int sort_role, + const QVariant& sort_value) + : GameListItem( + QString::fromStdString(text).simplified()) // simplified() forces single line text +{ + if (sort_role != Qt::DisplayRole) { + setData(sort_role, sort_value, true); + } +} + +CustomTableWidgetItem::CustomTableWidgetItem(const QString& text, int sort_role, + const QVariant& sort_value) + : GameListItem(text.simplified()) // simplified() forces single line text +{ + if (sort_role != Qt::DisplayRole) { + setData(sort_role, sort_value, true); + } +} + +bool CustomTableWidgetItem::operator<(const QTableWidgetItem& other) const { + if (m_sort_role == Qt::DisplayRole) { + return QTableWidgetItem::operator<(other); + } + + const QVariant data_l = data(m_sort_role); + const QVariant data_r = other.data(m_sort_role); + const QVariant::Type type_l = data_l.type(); + const QVariant::Type type_r = data_r.type(); + + switch (type_l) { + case QVariant::Type::Bool: + case QVariant::Type::Int: + return data_l.toInt() < data_r.toInt(); + case QVariant::Type::UInt: + return data_l.toUInt() < data_r.toUInt(); + case QVariant::Type::LongLong: + return data_l.toLongLong() < data_r.toLongLong(); + case QVariant::Type::ULongLong: + return data_l.toULongLong() < data_r.toULongLong(); + case QVariant::Type::Double: + return data_l.toDouble() < data_r.toDouble(); + case QVariant::Type::Date: + return data_l.toDate() < data_r.toDate(); + case QVariant::Type::Time: + return data_l.toTime() < data_r.toTime(); + case QVariant::Type::DateTime: + return data_l.toDateTime() < data_r.toDateTime(); + case QVariant::Type::Char: + case QVariant::Type::String: + return data_l.toString() < data_r.toString(); + default: + throw std::runtime_error("unsupported type"); + } +} + +void CustomTableWidgetItem::setData(int role, const QVariant& value, bool assign_sort_role) { + if (assign_sort_role) { + m_sort_role = role; + } + QTableWidgetItem::setData(role, value); +} diff --git a/src/qt_gui/custom_table_widget_item.h b/src/qt_gui/custom_table_widget_item.h new file mode 100644 index 00000000..e2c497f1 --- /dev/null +++ b/src/qt_gui/custom_table_widget_item.h @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "game_list_item.h" + +class CustomTableWidgetItem : public GameListItem { +private: + int m_sort_role = Qt::DisplayRole; + +public: + using QTableWidgetItem::setData; + + CustomTableWidgetItem() = default; + CustomTableWidgetItem(const std::string& text, int sort_role = Qt::DisplayRole, + const QVariant& sort_value = 0); + CustomTableWidgetItem(const QString& text, int sort_role = Qt::DisplayRole, + const QVariant& sort_value = 0); + + bool operator<(const QTableWidgetItem& other) const override; + + void setData(int role, const QVariant& value, bool assign_sort_role); +}; diff --git a/src/qt_gui/game_info.h b/src/qt_gui/game_info.h new file mode 100644 index 00000000..fb29948a --- /dev/null +++ b/src/qt_gui/game_info.h @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +struct GameInfo { + std::string path; // root path of game directory (normaly directory that contains eboot.bin) + std::string icon_path; // path of icon0.png + std::string pic_path; // path of pic1.png + + // variables extracted from param.sfo + std::string name = "Unknown"; + std::string serial = "Unknown"; + std::string app_ver = "Unknown"; + std::string version = "Unknown"; + std::string category = "Unknown"; + std::string fw = "Unknown"; +}; \ No newline at end of file diff --git a/src/qt_gui/game_install_dialog.cpp b/src/qt_gui/game_install_dialog.cpp new file mode 100644 index 00000000..1fa6880b --- /dev/null +++ b/src/qt_gui/game_install_dialog.cpp @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "game_install_dialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "gui_settings.h" + +GameInstallDialog::GameInstallDialog(std::shared_ptr gui_settings) + : m_gamesDirectory(nullptr), m_gui_settings(std::move(gui_settings)) { + auto layout = new QVBoxLayout(this); + + layout->addWidget(SetupGamesDirectory()); + layout->addStretch(); + layout->addWidget(SetupDialogActions()); + + setWindowTitle("Shadps4 - Choose directory"); +} + +GameInstallDialog::~GameInstallDialog() {} + +void GameInstallDialog::Browse() { + auto path = QFileDialog::getExistingDirectory(this, "Directory to install games"); + + if (!path.isEmpty()) { + m_gamesDirectory->setText(QDir::toNativeSeparators(path)); + } +} + +QWidget* GameInstallDialog::SetupGamesDirectory() { + auto group = new QGroupBox("Directory to install games"); + auto layout = new QHBoxLayout(group); + + // Input. + m_gamesDirectory = new QLineEdit(); + m_gamesDirectory->setText(m_gui_settings->GetValue(gui::settings_install_dir).toString()); + m_gamesDirectory->setMinimumWidth(400); + + layout->addWidget(m_gamesDirectory); + + // Browse button. + auto browse = new QPushButton("..."); + + connect(browse, &QPushButton::clicked, this, &GameInstallDialog::Browse); + + layout->addWidget(browse); + + return group; +} + +QWidget* GameInstallDialog::SetupDialogActions() { + auto actions = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + connect(actions, &QDialogButtonBox::accepted, this, &GameInstallDialog::Save); + connect(actions, &QDialogButtonBox::rejected, this, &GameInstallDialog::reject); + + return actions; +} + +void GameInstallDialog::Save() { + // Check games directory. + auto gamesDirectory = m_gamesDirectory->text(); + + if (gamesDirectory.isEmpty() || !QDir(gamesDirectory).exists() || + !QDir::isAbsolutePath(gamesDirectory)) { + QMessageBox::critical(this, "Error", + "The value for location to install games is not valid."); + return; + } + + m_gui_settings->SetValue(gui::settings_install_dir, QDir::toNativeSeparators(gamesDirectory)); + + accept(); +} diff --git a/src/qt_gui/game_install_dialog.h b/src/qt_gui/game_install_dialog.h new file mode 100644 index 00000000..b75aaaf6 --- /dev/null +++ b/src/qt_gui/game_install_dialog.h @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "gui_settings.h" + +class QLineEdit; + +class GameInstallDialog final : public QDialog { +public: + GameInstallDialog(std::shared_ptr gui_settings); + ~GameInstallDialog(); + +private slots: + void Browse(); + +private: + QWidget* SetupGamesDirectory(); + QWidget* SetupDialogActions(); + void Save(); + +private: + QLineEdit* m_gamesDirectory; + std::shared_ptr m_gui_settings; +}; \ No newline at end of file diff --git a/src/qt_gui/game_list_frame.cpp b/src/qt_gui/game_list_frame.cpp new file mode 100644 index 00000000..2b3cfcb0 --- /dev/null +++ b/src/qt_gui/game_list_frame.cpp @@ -0,0 +1,886 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include + +#include "custom_table_widget_item.h" +#include "emulator/file_format/psf.h" +#include "game_list_frame.h" +#include "gui_settings.h" +#include "qt_utils.h" + +GameListFrame::GameListFrame(std::shared_ptr gui_settings, QWidget* parent) + : CustomDockWidget(tr("Game List"), parent), m_gui_settings(std::move(gui_settings)) { + m_icon_size = gui::game_list_icon_size_min; // ensure a valid size + m_is_list_layout = m_gui_settings->GetValue(gui::game_list_listMode).toBool(); + m_margin_factor = m_gui_settings->GetValue(gui::game_list_marginFactor).toReal(); + m_text_factor = m_gui_settings->GetValue(gui::game_list_textFactor).toReal(); + m_icon_color = m_gui_settings->GetValue(gui::game_list_iconColor).value(); + m_col_sort_order = m_gui_settings->GetValue(gui::game_list_sortAsc).toBool() + ? Qt::AscendingOrder + : Qt::DescendingOrder; + m_sort_column = m_gui_settings->GetValue(gui::game_list_sortCol).toInt(); + + m_old_layout_is_list = m_is_list_layout; + + // Save factors for first setup + m_gui_settings->SetValue(gui::game_list_iconColor, m_icon_color); + m_gui_settings->SetValue(gui::game_list_marginFactor, m_margin_factor); + m_gui_settings->SetValue(gui::game_list_textFactor, m_text_factor); + + m_game_dock = new QMainWindow(this); + m_game_dock->setWindowFlags(Qt::Widget); + setWidget(m_game_dock); + + m_game_grid = new GameListGrid(QSize(), m_icon_color, m_margin_factor, m_text_factor, false); + + m_game_list = new GameListTable(); + m_game_list->setShowGrid(false); + m_game_list->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_game_list->setSelectionBehavior(QAbstractItemView::SelectRows); + m_game_list->setSelectionMode(QAbstractItemView::SingleSelection); + m_game_list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + m_game_list->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); + m_game_list->verticalScrollBar()->installEventFilter(this); + m_game_list->verticalScrollBar()->setSingleStep(20); + m_game_list->horizontalScrollBar()->setSingleStep(20); + m_game_list->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); + m_game_list->verticalHeader()->setVisible(false); + m_game_list->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + m_game_list->horizontalHeader()->setHighlightSections(false); + m_game_list->horizontalHeader()->setSortIndicatorShown(true); + m_game_list->horizontalHeader()->setStretchLastSection(true); + m_game_list->setContextMenuPolicy(Qt::CustomContextMenu); + m_game_list->installEventFilter(this); + m_game_list->setColumnCount(gui::column_count); + m_game_list->setColumnWidth(1, 250); + m_game_list->setColumnWidth(2, 110); + m_game_list->setColumnWidth(3, 80); + m_game_list->setColumnWidth(4, 90); + m_game_list->setColumnWidth(5, 80); + m_game_list->setColumnWidth(6, 80); + QPalette palette; + palette.setColor(QPalette::Base, QColor(230, 230, 230, 80)); + QColor transparentColor = QColor(135, 206, 235, 40); + palette.setColor(QPalette::Highlight, transparentColor); + m_game_list->setPalette(palette); + m_central_widget = new QStackedWidget(this); + m_central_widget->addWidget(m_game_list); + m_central_widget->addWidget(m_game_grid); + m_central_widget->setCurrentWidget(m_is_list_layout ? m_game_list : m_game_grid); + + m_game_dock->setCentralWidget(m_central_widget); + + // Actions regarding showing/hiding columns + auto add_column = [this](gui::game_list_columns col, const QString& header_text, + const QString& action_text) { + QTableWidgetItem* item_ = new QTableWidgetItem(header_text); + item_->setTextAlignment(Qt::AlignCenter); // Center-align text + m_game_list->setHorizontalHeaderItem(col, item_); + m_columnActs.append(new QAction(action_text, this)); + }; + + add_column(gui::column_icon, tr("Icon"), tr("Show Icons")); + add_column(gui::column_name, tr("Name"), tr("Show Names")); + add_column(gui::column_serial, tr("Serial"), tr("Show Serials")); + add_column(gui::column_firmware, tr("Firmware"), tr("Show Firmwares")); + add_column(gui::column_size, tr("Size"), tr("Show Size")); + add_column(gui::column_version, tr("Version"), tr("Show Versions")); + add_column(gui::column_category, tr("Category"), tr("Show Categories")); + add_column(gui::column_path, tr("Path"), tr("Show Paths")); + + for (int col = 0; col < m_columnActs.count(); ++col) { + m_columnActs[col]->setCheckable(true); + + connect(m_columnActs[col], &QAction::triggered, this, [this, col](bool checked) { + if (!checked) // be sure to have at least one column left so you can call the context + // menu at all time + { + int c = 0; + for (int i = 0; i < m_columnActs.count(); ++i) { + if (m_gui_settings->GetGamelistColVisibility(i) && ++c > 1) + break; + } + if (c < 2) { + m_columnActs[col]->setChecked( + true); // re-enable the checkbox if we don't change the actual state + return; + } + } + m_game_list->setColumnHidden( + col, !checked); // Negate because it's a set col hidden and we have menu say show. + m_gui_settings->SetGamelistColVisibility(col, checked); + + if (checked) // handle hidden columns that have zero width after showing them (stuck + // between others) + { + FixNarrowColumns(); + } + }); + } + + // events + connect(m_game_list->horizontalHeader(), &QHeaderView::customContextMenuRequested, this, + [this](const QPoint& pos) { + QMenu* configure = new QMenu(this); + configure->addActions(m_columnActs); + configure->exec(m_game_list->horizontalHeader()->viewport()->mapToGlobal(pos)); + }); + connect(m_game_list->horizontalHeader(), &QHeaderView::sectionClicked, this, + &GameListFrame::OnHeaderColumnClicked); + connect(&m_repaint_watcher, &QFutureWatcher::resultReadyAt, this, + [this](int index) { + if (!m_is_list_layout) + return; + if (GameListItem* item = m_repaint_watcher.resultAt(index)) { + item->call_icon_func(); + } + }); + connect(&m_repaint_watcher, &QFutureWatcher::finished, this, + &GameListFrame::OnRepaintFinished); + + connect(&m_refresh_watcher, &QFutureWatcher::finished, this, + &GameListFrame::OnRefreshFinished); + connect(&m_refresh_watcher, &QFutureWatcher::canceled, this, [this]() { + gui::utils::stop_future_watcher(m_repaint_watcher, true); + + m_path_list.clear(); + m_game_data.clear(); + m_games.clear(); + }); + connect(m_game_list, &QTableWidget::customContextMenuRequested, this, + &GameListFrame::RequestGameMenu); + connect(m_game_grid, &QTableWidget::customContextMenuRequested, this, + &GameListFrame::RequestGameMenu); + + connect(m_game_list, &QTableWidget::itemClicked, this, &GameListFrame::SetListBackgroundImage); + connect(this, &GameListFrame::ResizedWindow, this, &GameListFrame::SetListBackgroundImage); + connect(m_game_list->verticalScrollBar(), &QScrollBar::valueChanged, this, + &GameListFrame::RefreshListBackgroundImage); + connect(m_game_list->horizontalScrollBar(), &QScrollBar::valueChanged, this, + &GameListFrame::RefreshListBackgroundImage); +} + +GameListFrame::~GameListFrame() { + gui::utils::stop_future_watcher(m_repaint_watcher, true); + gui::utils::stop_future_watcher(m_refresh_watcher, true); + SaveSettings(); +} + +void GameListFrame::OnRefreshFinished() { + gui::utils::stop_future_watcher(m_repaint_watcher, true); + for (auto&& g : m_games) { + m_game_data.push_back(g); + } + m_games.clear(); + // Sort by name at the very least. + std::sort(m_game_data.begin(), m_game_data.end(), + [&](const game_info& game1, const game_info& game2) { + const QString title1 = m_titles.value(QString::fromStdString(game1->info.serial), + QString::fromStdString(game1->info.name)); + const QString title2 = m_titles.value(QString::fromStdString(game2->info.serial), + QString::fromStdString(game2->info.name)); + return title1.toLower() < title2.toLower(); + }); + + m_path_list.clear(); + + Refresh(); +} + +void GameListFrame::RequestGameMenu(const QPoint& pos) { + + QPoint global_pos; + game_info gameinfo; + + if (m_is_list_layout) { + QTableWidgetItem* item = m_game_list->item( + m_game_list->indexAt(pos).row(), static_cast(gui::game_list_columns::column_icon)); + global_pos = m_game_list->viewport()->mapToGlobal(pos); + gameinfo = GetGameInfoFromItem(item); + } else { + const QModelIndex mi = m_game_grid->indexAt(pos); + QTableWidgetItem* item = m_game_grid->item(mi.row(), mi.column()); + global_pos = m_game_grid->viewport()->mapToGlobal(pos); + gameinfo = GetGameInfoFromItem(item); + } + + if (!gameinfo) { + return; + } + + // Setup menu. + QMenu menu(this); + QAction openFolder("Open Game Folder", this); + QAction openSfoViewer("SFO Viewer", this); + + menu.addAction(&openFolder); + menu.addAction(&openSfoViewer); + // Show menu. + auto selected = menu.exec(global_pos); + if (!selected) { + return; + } + + if (selected == &openFolder) { + QString folderPath = QString::fromStdString(gameinfo->info.path); + QDesktopServices::openUrl(QUrl::fromLocalFile(folderPath)); + } + + if (selected == &openSfoViewer) { + PSF psf; + if (psf.open(gameinfo->info.path + "/sce_sys/param.sfo")) { + int rows = psf.map_strings.size() + psf.map_integers.size(); + QTableWidget* tableWidget = new QTableWidget(rows, 2); + tableWidget->verticalHeader()->setVisible(false); // Hide vertical header + int row = 0; + + for (const auto& pair : psf.map_strings) { + QTableWidgetItem* keyItem = + new QTableWidgetItem(QString::fromStdString(pair.first)); + QTableWidgetItem* valueItem = + new QTableWidgetItem(QString::fromStdString(pair.second)); + + tableWidget->setItem(row, 0, keyItem); + tableWidget->setItem(row, 1, valueItem); + keyItem->setFlags(keyItem->flags() & ~Qt::ItemIsEditable); + valueItem->setFlags(valueItem->flags() & ~Qt::ItemIsEditable); + row++; + } + for (const auto& pair : psf.map_integers) { + QTableWidgetItem* keyItem = + new QTableWidgetItem(QString::fromStdString(pair.first)); + QTableWidgetItem* valueItem = new QTableWidgetItem(QString::number(pair.second)); + + tableWidget->setItem(row, 0, keyItem); + tableWidget->setItem(row, 1, valueItem); + keyItem->setFlags(keyItem->flags() & ~Qt::ItemIsEditable); + valueItem->setFlags(valueItem->flags() & ~Qt::ItemIsEditable); + row++; + } + tableWidget->resizeColumnsToContents(); + tableWidget->resizeRowsToContents(); + + int width = tableWidget->horizontalHeader()->sectionSize(0) + + tableWidget->horizontalHeader()->sectionSize(1) + 2; + int height = (rows + 1) * (tableWidget->rowHeight(0)); + tableWidget->setFixedSize(width, height); + tableWidget->sortItems(0, Qt::AscendingOrder); + tableWidget->horizontalHeader()->setVisible(false); + + tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed); + tableWidget->setWindowTitle("SFO Viewer"); + tableWidget->show(); + } + } +} + +void GameListFrame::RefreshListBackgroundImage() { + QPixmap blurredPixmap = QPixmap::fromImage(backgroundImage); + QPalette palette; + palette.setBrush(QPalette::Base, QBrush(blurredPixmap.scaled(size(), Qt::IgnoreAspectRatio))); + QColor transparentColor = QColor(135, 206, 235, 40); + palette.setColor(QPalette::Highlight, transparentColor); + m_game_list->setPalette(palette); +} + +void GameListFrame::SetListBackgroundImage(QTableWidgetItem* item) { + if (!item) { + // handle case where no item was clicked + return; + } + QTableWidgetItem* iconItem = + m_game_list->item(item->row(), static_cast(gui::game_list_columns::column_icon)); + + if (!iconItem) { + // handle case where icon item does not exist + return; + } + game_info gameinfo = GetGameInfoFromItem(iconItem); + QString pic1Path = QString::fromStdString(gameinfo->info.pic_path); + QString blurredPic1Path = + qApp->applicationDirPath() + + QString::fromStdString("/game_data/" + gameinfo->info.serial + "/pic1.png"); + + backgroundImage = QImage(blurredPic1Path); + if (backgroundImage.isNull()) { + QImage image(pic1Path); + backgroundImage = m_game_list_utils.BlurImage(image, image.rect(), 18); + + std::filesystem::path img_path = + std::filesystem::path("game_data/") / gameinfo->info.serial; + std::filesystem::create_directories(img_path); + if (!backgroundImage.save(blurredPic1Path, "PNG")) { + // qDebug() << "Error: Unable to save image."; + } + } + QPixmap blurredPixmap = QPixmap::fromImage(backgroundImage); + QPalette palette; + palette.setBrush(QPalette::Base, QBrush(blurredPixmap.scaled(size(), Qt::IgnoreAspectRatio))); + QColor transparentColor = QColor(135, 206, 235, 40); + palette.setColor(QPalette::Highlight, transparentColor); + m_game_list->setPalette(palette); +} + +void GameListFrame::OnRepaintFinished() { + if (m_is_list_layout) { + // Fixate vertical header and row height + m_game_list->verticalHeader()->setMinimumSectionSize(m_icon_size.height()); + m_game_list->verticalHeader()->setMaximumSectionSize(m_icon_size.height()); + + // Resize the icon column + m_game_list->resizeColumnToContents(gui::column_icon); + + // Shorten the last section to remove horizontal scrollbar if possible + m_game_list->resizeColumnToContents(gui::column_count - 1); + } else { + // The game grid needs to be recreated from scratch + int games_per_row = 0; + + if (m_icon_size.width() > 0 && m_icon_size.height() > 0) { + games_per_row = width() / (m_icon_size.width() + + m_icon_size.width() * m_game_grid->getMarginFactor() * 2); + } + + const int scroll_position = m_game_grid->verticalScrollBar()->value(); + // TODO add connections + PopulateGameGrid(games_per_row, m_icon_size, m_icon_color); + m_central_widget->addWidget(m_game_grid); + m_central_widget->setCurrentWidget(m_game_grid); + m_game_grid->verticalScrollBar()->setValue(scroll_position); + + connect(m_game_grid, &QTableWidget::customContextMenuRequested, this, + &GameListFrame::RequestGameMenu); + } +} + +bool GameListFrame::IsEntryVisible(const game_info& game) { + const QString serial = QString::fromStdString(game->info.serial); + return SearchMatchesApp(QString::fromStdString(game->info.name), serial); +} + +game_info GameListFrame::GetGameInfoFromItem(const QTableWidgetItem* item) { + if (!item) { + return nullptr; + } + + const QVariant var = item->data(gui::game_role); + if (!var.canConvert()) { + return nullptr; + } + + return var.value(); +} + +void GameListFrame::PopulateGameGrid(int maxCols, const QSize& image_size, + const QColor& image_color) { + int r = 0; + int c = 0; + + const std::string selected_item = CurrentSelectionPath(); + + // Release old data + m_game_list->clear_list(); + m_game_grid->deleteLater(); + + const bool show_text = m_icon_size_index > gui::game_list_max_slider_pos * 2 / 5; + + if (m_icon_size_index < gui::game_list_max_slider_pos * 2 / 3) { + m_game_grid = new GameListGrid(image_size, image_color, m_margin_factor, m_text_factor * 2, + show_text); + } else { + m_game_grid = + new GameListGrid(image_size, image_color, m_margin_factor, m_text_factor, show_text); + } + + // Get list of matching apps + QList matching_apps; + + for (const auto& app : m_game_data) { + if (IsEntryVisible(app)) { + matching_apps.push_back(app); + } + } + + const int entries = matching_apps.count(); + + // Edge cases! + if (entries == 0) { // For whatever reason, 0%x is division by zero. Absolute nonsense by + // definition of modulus. But, I'll acquiesce. + return; + } + + maxCols = std::clamp(maxCols, 1, entries); + + const int needs_extra_row = (entries % maxCols) != 0; + const int max_rows = needs_extra_row + entries / maxCols; + m_game_grid->setRowCount(max_rows); + m_game_grid->setColumnCount(maxCols); + + for (const auto& app : matching_apps) { + const QString serial = QString::fromStdString(app->info.serial); + const QString title = m_titles.value(serial, QString::fromStdString(app->info.name)); + + GameListItem* item = m_game_grid->addItem(app, title, r, c); + app->item = item; + item->setData(gui::game_role, QVariant::fromValue(app)); + + item->setToolTip(tr("%0 [%1]").arg(title).arg(serial)); + + if (selected_item == app->info.path + app->info.icon_path) { + m_game_grid->setCurrentItem(item); + } + + if (++c >= maxCols) { + c = 0; + r++; + } + } + + if (c != 0) { // if left over games exist -- if empty entries exist + for (int col = c; col < maxCols; ++col) { + GameListItem* empty_item = new GameListItem(); + empty_item->setFlags(Qt::NoItemFlags); + m_game_grid->setItem(r, col, empty_item); + } + } + + m_game_grid->resizeColumnsToContents(); + m_game_grid->resizeRowsToContents(); + m_game_grid->installEventFilter(this); + m_game_grid->verticalScrollBar()->installEventFilter(this); +} +void GameListFrame::Refresh(const bool from_drive, const bool scroll_after) { + gui::utils::stop_future_watcher(m_repaint_watcher, true); + gui::utils::stop_future_watcher(m_refresh_watcher, from_drive); + + if (from_drive) { + m_path_list.clear(); + m_game_data.clear(); + m_games.clear(); + + // TODO better ATM manually add path from 1 dir to m_paths_list + QDir parent_folder(m_gui_settings->GetValue(gui::settings_install_dir).toString() + '/'); + QFileInfoList fList = + parent_folder.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::DirsFirst); + foreach (QFileInfo item, fList) { + m_path_list.emplace_back(item.absoluteFilePath().toStdString()); + } + + m_refresh_watcher.setFuture(QtConcurrent::map(m_path_list, [this](const std::string& dir) { + GameInfo game{}; + game.path = dir; + PSF psf; + if (psf.open(game.path + "/sce_sys/param.sfo")) { + QString iconpath(QString::fromStdString(game.path) + "/sce_sys/icon0.png"); + QString picpath(QString::fromStdString(game.path) + "/sce_sys/pic1.png"); + game.icon_path = iconpath.toStdString(); + game.pic_path = picpath.toStdString(); + game.name = psf.GetString("TITLE"); + game.serial = psf.GetString("TITLE_ID"); + game.fw = (QString("%1").arg(psf.GetInteger("SYSTEM_VER"), 8, 16, QLatin1Char('0'))) + .mid(1, 3) + .insert(1, '.') + .toStdString(); + game.version = psf.GetString("APP_VER"); + game.category = psf.GetString("CATEGORY"); + + m_titles.insert(QString::fromStdString(game.serial), + QString::fromStdString(game.name)); + + GuiGameInfo info{}; + info.info = game; + + m_games.push_back(std::make_shared(std::move(info))); + } + })); + return; + } + // Fill Game List / Game Grid + + if (m_is_list_layout) { + const int scroll_position = m_game_list->verticalScrollBar()->value(); + PopulateGameList(); + SortGameList(); + RepaintIcons(); + + if (scroll_after) { + m_game_list->scrollTo(m_game_list->currentIndex(), QAbstractItemView::PositionAtCenter); + } else { + m_game_list->verticalScrollBar()->setValue(scroll_position); + } + } else { + RepaintIcons(); + } +} +/** + Cleans and readds entries to table widget in UI. +*/ +void GameListFrame::PopulateGameList() { + int selected_row = -1; + + const std::string selected_item = CurrentSelectionPath(); + + // Release old data + m_game_grid->clear_list(); + m_game_list->clear_list(); + + m_game_list->setRowCount(m_game_data.size()); + + int row = 0; + int index = -1; + for (const auto& game : m_game_data) { + index++; + + if (!IsEntryVisible(game)) { + game->item = nullptr; + continue; + } + + // Icon + CustomTableWidgetItem* icon_item = new CustomTableWidgetItem; + game->item = icon_item; + icon_item->set_icon_func([this, icon_item, game](int) { + icon_item->setData(Qt::DecorationRole, game->pxmap); + game->pxmap = {}; + }); + + icon_item->setData(Qt::UserRole, index, true); + icon_item->setData(gui::custom_roles::game_role, QVariant::fromValue(game)); + + m_game_list->setItem(row, gui::column_icon, icon_item); + SetTableItem(m_game_list, row, gui::column_name, QString::fromStdString(game->info.name)); + SetTableItem(m_game_list, row, gui::column_serial, + QString::fromStdString(game->info.serial)); + SetTableItem(m_game_list, row, gui::column_firmware, QString::fromStdString(game->info.fw)); + SetTableItem( + m_game_list, row, gui::column_size, + m_game_list_utils.GetFolderSize(QDir(QString::fromStdString(game->info.path)))); + SetTableItem(m_game_list, row, gui::column_version, + QString::fromStdString(game->info.version)); + SetTableItem(m_game_list, row, gui::column_category, + QString::fromStdString(game->info.category)); + SetTableItem(m_game_list, row, gui::column_path, QString::fromStdString(game->info.path)); + + if (selected_item == game->info.path + game->info.icon_path) { + selected_row = row; + } + + row++; + } + m_game_list->setRowCount(row); + m_game_list->selectRow(selected_row); +} + +std::string GameListFrame::CurrentSelectionPath() { + std::string selection; + + QTableWidgetItem* item = nullptr; + + if (m_old_layout_is_list) { + if (!m_game_list->selectedItems().isEmpty()) { + item = m_game_list->item(m_game_list->currentRow(), 0); + } + } else if (m_game_grid) { + if (!m_game_grid->selectedItems().isEmpty()) { + item = m_game_grid->currentItem(); + } + } + + if (item) { + if (const QVariant var = item->data(gui::game_role); var.canConvert()) { + if (const game_info game = var.value()) { + selection = game->info.path + game->info.icon_path; + } + } + } + + m_old_layout_is_list = m_is_list_layout; + + return selection; +} + +void GameListFrame::RepaintIcons(const bool& from_settings) { + gui::utils::stop_future_watcher(m_repaint_watcher, true); + + if (from_settings) { + // TODO m_icon_color = gui::utils::get_label_color("gamelist_icon_background_color"); + } + + if (m_is_list_layout) { + QPixmap placeholder(m_icon_size); + placeholder.fill(Qt::transparent); + + for (auto& game : m_game_data) { + game->pxmap = placeholder; + } + + // Fixate vertical header and row height + m_game_list->verticalHeader()->setMinimumSectionSize(m_icon_size.height()); + m_game_list->verticalHeader()->setMaximumSectionSize(m_icon_size.height()); + + // Resize the icon column + m_game_list->resizeColumnToContents(gui::column_icon); + + // Shorten the last section to remove horizontal scrollbar if possible + m_game_list->resizeColumnToContents(gui::column_count - 1); + } + + const std::function func = [this](const game_info& game) -> GameListItem* { + if (game->icon.isNull() && + (game->info.icon_path.empty() || + !game->icon.load(QString::fromStdString(game->info.icon_path)))) { + // TODO added warning message if no found + } + game->pxmap = PaintedPixmap(game->icon); + return game->item; + }; + m_repaint_watcher.setFuture(QtConcurrent::mapped(m_game_data, func)); +} + +void GameListFrame::FixNarrowColumns() const { + qApp->processEvents(); + + // handle columns (other than the icon column) that have zero width after showing them (stuck + // between others) + for (int col = 1; col < m_columnActs.count(); ++col) { + if (m_game_list->isColumnHidden(col)) { + continue; + } + + if (m_game_list->columnWidth(col) <= + m_game_list->horizontalHeader()->minimumSectionSize()) { + m_game_list->setColumnWidth(col, m_game_list->horizontalHeader()->minimumSectionSize()); + } + } +} + +void GameListFrame::ResizeColumnsToContents(int spacing) const { + if (!m_game_list) { + return; + } + + m_game_list->verticalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents); + m_game_list->horizontalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents); + + // Make non-icon columns slighty bigger for better visuals + for (int i = 1; i < m_game_list->columnCount(); i++) { + if (m_game_list->isColumnHidden(i)) { + continue; + } + + const int size = m_game_list->horizontalHeader()->sectionSize(i) + spacing; + m_game_list->horizontalHeader()->resizeSection(i, size); + } +} + +void GameListFrame::OnHeaderColumnClicked(int col) { + if (col == 0) + return; // Don't "sort" icons. + + if (col == m_sort_column) { + m_col_sort_order = + (m_col_sort_order == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder; + } else { + m_col_sort_order = Qt::AscendingOrder; + } + m_sort_column = col; + + m_gui_settings->SetValue(gui::game_list_sortAsc, m_col_sort_order == Qt::AscendingOrder); + m_gui_settings->SetValue(gui::game_list_sortCol, col); + + SortGameList(); +} + +void GameListFrame::SortGameList() const { + // Back-up old header sizes to handle unwanted column resize in case of zero search results + QList column_widths; + const int old_row_count = m_game_list->rowCount(); + const int old_game_count = m_game_data.count(); + + for (int i = 0; i < m_game_list->columnCount(); i++) { + column_widths.append(m_game_list->columnWidth(i)); + } + + // Sorting resizes hidden columns, so unhide them as a workaround + QList columns_to_hide; + + for (int i = 0; i < m_game_list->columnCount(); i++) { + if (m_game_list->isColumnHidden(i)) { + m_game_list->setColumnHidden(i, false); + columns_to_hide << i; + } + } + + // Sort the list by column and sort order + m_game_list->sortByColumn(m_sort_column, m_col_sort_order); + + // Hide columns again + for (auto i : columns_to_hide) { + m_game_list->setColumnHidden(i, true); + } + + // Don't resize the columns if no game is shown to preserve the header settings + if (!m_game_list->rowCount()) { + for (int i = 0; i < m_game_list->columnCount(); i++) { + m_game_list->setColumnWidth(i, column_widths[i]); + } + + m_game_list->horizontalHeader()->setSectionResizeMode(gui::column_icon, QHeaderView::Fixed); + return; + } + + // Fixate vertical header and row height + m_game_list->verticalHeader()->setMinimumSectionSize(m_icon_size.height()); + m_game_list->verticalHeader()->setMaximumSectionSize(m_icon_size.height()); + m_game_list->resizeRowsToContents(); + + // Resize columns if the game list was empty before + if (!old_row_count && !old_game_count) { + ResizeColumnsToContents(); + } else { + m_game_list->resizeColumnToContents(gui::column_icon); + } + + // Fixate icon column + m_game_list->horizontalHeader()->setSectionResizeMode(gui::column_icon, QHeaderView::Fixed); + + // Shorten the last section to remove horizontal scrollbar if possible + m_game_list->resizeColumnToContents(gui::column_count - 1); +} + +QPixmap GameListFrame::PaintedPixmap(const QPixmap& icon) const { + const qreal device_pixel_ratio = devicePixelRatioF(); + QSize canvas_size(512, 512); + QSize icon_size(icon.size()); + QPoint target_pos; + + if (!icon.isNull()) { + // Let's upscale the original icon to at least fit into the outer rect of the size of PS4's + // ICON0.PNG + if (icon_size.width() < 512 || icon_size.height() < 512) { + icon_size.scale(512, 512, Qt::KeepAspectRatio); + } + + canvas_size = icon_size; + + // Calculate the centered size and position of the icon on our canvas. not needed I believe. + if (icon_size.width() != 512 || icon_size.height() != 512) { + constexpr double target_ratio = 1.0; // aspect ratio 20:11 + + if ((icon_size.width() / static_cast(icon_size.height())) > target_ratio) { + canvas_size.setHeight(std::ceil(icon_size.width() / target_ratio)); + } else { + canvas_size.setWidth(std::ceil(icon_size.height() * target_ratio)); + } + + target_pos.setX(std::max(0, (canvas_size.width() - icon_size.width()) / 2.0)); + target_pos.setY(std::max(0, (canvas_size.height() - icon_size.height()) / 2.0)); + } + } + + // Create a canvas large enough to fit our entire scaled icon + QPixmap canvas(canvas_size * device_pixel_ratio); + canvas.setDevicePixelRatio(device_pixel_ratio); + canvas.fill(m_icon_color); + + // Create a painter for our canvas + QPainter painter(&canvas); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + + // Draw the icon onto our canvas + if (!icon.isNull()) { + painter.drawPixmap(target_pos.x(), target_pos.y(), icon_size.width(), icon_size.height(), + icon); + } + + // Finish the painting + painter.end(); + + // Scale and return our final image + return canvas.scaled(m_icon_size * device_pixel_ratio, Qt::KeepAspectRatio, + Qt::TransformationMode::SmoothTransformation); +} +void GameListFrame::SetListMode(const bool& is_list) { + m_old_layout_is_list = m_is_list_layout; + m_is_list_layout = is_list; + + m_gui_settings->SetValue(gui::game_list_listMode, is_list); + + Refresh(true); + + m_central_widget->setCurrentWidget(m_is_list_layout ? m_game_list : m_game_grid); +} +void GameListFrame::SetSearchText(const QString& text) { + m_search_text = text; + Refresh(); +} +void GameListFrame::closeEvent(QCloseEvent* event) { + QDockWidget::closeEvent(event); + Q_EMIT GameListFrameClosed(); +} + +void GameListFrame::resizeEvent(QResizeEvent* event) { + if (!m_is_list_layout) { + Refresh(false, m_game_grid->selectedItems().count()); + } + Q_EMIT ResizedWindow(m_game_list->currentItem()); + QDockWidget::resizeEvent(event); +} +void GameListFrame::ResizeIcons(const int& slider_pos) { + m_icon_size_index = slider_pos; + m_icon_size = GuiSettings::SizeFromSlider(slider_pos); + + RepaintIcons(); +} + +void GameListFrame::LoadSettings() { + m_col_sort_order = m_gui_settings->GetValue(gui::game_list_sortAsc).toBool() + ? Qt::AscendingOrder + : Qt::DescendingOrder; + m_sort_column = m_gui_settings->GetValue(gui::game_list_sortCol).toInt(); + + Refresh(true); + + const QByteArray state = m_gui_settings->GetValue(gui::game_list_state).toByteArray(); + if (!m_game_list->horizontalHeader()->restoreState(state) && m_game_list->rowCount()) { + // If no settings exist, resize to contents. + ResizeColumnsToContents(); + } + + for (int col = 0; col < m_columnActs.count(); ++col) { + const bool vis = m_gui_settings->GetGamelistColVisibility(col); + m_columnActs[col]->setChecked(vis); + m_game_list->setColumnHidden(col, !vis); + } + SortGameList(); + FixNarrowColumns(); + + m_game_list->horizontalHeader()->restoreState(m_game_list->horizontalHeader()->saveState()); +} + +void GameListFrame::SaveSettings() { + for (int col = 0; col < m_columnActs.count(); ++col) { + m_gui_settings->SetGamelistColVisibility(col, m_columnActs[col]->isChecked()); + } + m_gui_settings->SetValue(gui::game_list_sortCol, m_sort_column); + m_gui_settings->SetValue(gui::game_list_sortAsc, m_col_sort_order == Qt::AscendingOrder); + m_gui_settings->SetValue(gui::game_list_state, m_game_list->horizontalHeader()->saveState()); +} + +/** + * Returns false if the game should be hidden because it doesn't match search term in toolbar. + */ +bool GameListFrame::SearchMatchesApp(const QString& name, const QString& serial) const { + if (!m_search_text.isEmpty()) { + const QString search_text = m_search_text.toLower(); + return m_titles.value(serial, name).toLower().contains(search_text) || + serial.toLower().contains(search_text); + } + return true; +} diff --git a/src/qt_gui/game_list_frame.h b/src/qt_gui/game_list_frame.h new file mode 100644 index 00000000..826015e5 --- /dev/null +++ b/src/qt_gui/game_list_frame.h @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "custom_dock_widget.h" +#include "game_list_grid.h" +#include "game_list_item.h" +#include "game_list_table.h" +#include "game_list_utils.h" +#include "gui_settings.h" + +class GameListFrame : public CustomDockWidget { + Q_OBJECT +public: + explicit GameListFrame(std::shared_ptr gui_settings, QWidget* parent = nullptr); + ~GameListFrame(); + /** Fix columns with width smaller than the minimal section size */ + void FixNarrowColumns() const; + + /** Loads from settings. Public so that main frame can easily reset these settings if needed. */ + void LoadSettings(); + + /** Saves settings. Public so that main frame can save this when a caching of column widths is + * needed for settings backup */ + void SaveSettings(); + + /** Resizes the columns to their contents and adds a small spacing */ + void ResizeColumnsToContents(int spacing = 20) const; + + /** Refresh the gamelist with/without loading game data from files. Public so that main frame + * can refresh after vfs or install */ + void Refresh(const bool from_drive = false, const bool scroll_after = true); + + /** Repaint Gamelist Icons with new background color */ + void RepaintIcons(const bool& from_settings = false); + + /** Resize Gamelist Icons to size given by slider position */ + void ResizeIcons(const int& slider_pos); + +public Q_SLOTS: + void SetSearchText(const QString& text); + void SetListMode(const bool& is_list); +private Q_SLOTS: + void OnHeaderColumnClicked(int col); + void OnRepaintFinished(); + void OnRefreshFinished(); + void RequestGameMenu(const QPoint& pos); + void SetListBackgroundImage(QTableWidgetItem* item); + void RefreshListBackgroundImage(); + +Q_SIGNALS: + void GameListFrameClosed(); + void RequestIconSizeChange(const int& val); + void ResizedWindow(QTableWidgetItem* item); + +protected: + void closeEvent(QCloseEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + +private: + QPixmap PaintedPixmap(const QPixmap& icon) const; + void SortGameList() const; + std::string CurrentSelectionPath(); + void PopulateGameList(); + void PopulateGameGrid(int maxCols, const QSize& image_size, const QColor& image_color); + bool SearchMatchesApp(const QString& name, const QString& serial) const; + bool IsEntryVisible(const game_info& game); + static game_info GetGameInfoFromItem(const QTableWidgetItem* item); + + // Which widget we are displaying depends on if we are in grid or list mode. + QMainWindow* m_game_dock = nullptr; + QStackedWidget* m_central_widget = nullptr; + + // Game Grid + GameListGrid* m_game_grid = nullptr; + + // Game List + GameListTable* m_game_list = nullptr; + QList m_columnActs; + Qt::SortOrder m_col_sort_order; + int m_sort_column; + QMap m_titles; + + // Game List Utils + GameListUtils m_game_list_utils; + + // List Mode + bool m_is_list_layout = true; + bool m_old_layout_is_list = true; + + // data + std::shared_ptr m_gui_settings; + QList m_game_data; + std::vector m_path_list; + std::vector m_games; + QFutureWatcher m_repaint_watcher; + QFutureWatcher m_refresh_watcher; + + // Search + QString m_search_text; + + // Icon Size + int m_icon_size_index = 0; + + // Icons + QSize m_icon_size; + QColor m_icon_color; + qreal m_margin_factor; + qreal m_text_factor; + + // Background Image + QImage backgroundImage; + + void SetTableItem(GameListTable* game_list, int row, int column, QString itemStr) { + QWidget* widget = new QWidget(); + QVBoxLayout* layout = new QVBoxLayout(); + QLabel* label = new QLabel(itemStr); + QTableWidgetItem* item = new QTableWidgetItem(); + + label->setStyleSheet("color: white; font-size: 15px; font-weight: bold;"); + + // Create shadow effect + QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect(); + shadowEffect->setBlurRadius(5); // Set the blur radius of the shadow + shadowEffect->setColor(QColor(0, 0, 0, 160)); // Set the color and opacity of the shadow + shadowEffect->setOffset(2, 2); // Set the offset of the shadow + + label->setGraphicsEffect(shadowEffect); // Apply shadow effect to the QLabel + + layout->addWidget(label); + if (column != 7 && column != 1) + layout->setAlignment(Qt::AlignCenter); + widget->setLayout(layout); + game_list->setItem(row, column, item); + game_list->setCellWidget(row, column, widget); + } +}; diff --git a/src/qt_gui/game_list_grid.cpp b/src/qt_gui/game_list_grid.cpp new file mode 100644 index 00000000..2239126b --- /dev/null +++ b/src/qt_gui/game_list_grid.cpp @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include "game_list_grid.h" +#include "game_list_grid_delegate.h" +#include "game_list_item.h" + +GameListGrid::GameListGrid(const QSize& icon_size, QColor icon_color, const qreal& margin_factor, + const qreal& text_factor, const bool& showText) + : m_icon_size(icon_size), m_icon_color(std::move(icon_color)), m_margin_factor(margin_factor), + m_text_factor(text_factor), m_text_enabled(showText) { + setObjectName("game_grid"); + + QSize item_size; + if (m_text_enabled) { + item_size = + m_icon_size + QSize(m_icon_size.width() * m_margin_factor * 2, + m_icon_size.height() * m_margin_factor * (m_text_factor + 1)); + } else { + item_size = m_icon_size + m_icon_size * m_margin_factor * 2; + } + + grid_item_delegate = new GameListGridDelegate(item_size, m_margin_factor, m_text_factor, this); + setItemDelegate(grid_item_delegate); + setEditTriggers(QAbstractItemView::NoEditTriggers); + setSelectionBehavior(QAbstractItemView::SelectItems); + setSelectionMode(QAbstractItemView::SingleSelection); + setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); + verticalScrollBar()->setSingleStep(20); + horizontalScrollBar()->setSingleStep(20); + setContextMenuPolicy(Qt::CustomContextMenu); + verticalHeader()->setVisible(false); + horizontalHeader()->setVisible(false); + setShowGrid(false); + QPalette palette; + palette.setColor(QPalette::Base, QColor(230, 230, 230, 80)); + setPalette(palette); + + connect(this, &GameListTable::itemClicked, this, &GameListGrid::SetGridBackgroundImage); + connect(this, &GameListGrid::ResizedWindowGrid, this, &GameListGrid::SetGridBackgroundImage); + connect(this->verticalScrollBar(), &QScrollBar::valueChanged, this, + &GameListGrid::RefreshBackgroundImage); + connect(this->horizontalScrollBar(), &QScrollBar::valueChanged, this, + &GameListGrid::RefreshBackgroundImage); +} + +void GameListGrid::enableText(const bool& enabled) { + m_text_enabled = enabled; +} + +void GameListGrid::setIconSize(const QSize& size) const { + if (m_text_enabled) { + grid_item_delegate->setItemSize( + size + QSize(size.width() * m_margin_factor * 2, + size.height() * m_margin_factor * (m_text_factor + 1))); + } else { + grid_item_delegate->setItemSize(size + size * m_margin_factor * 2); + } +} + +GameListItem* GameListGrid::addItem(const game_info& app, const QString& name, const int& row, + const int& col) { + GameListItem* item = new GameListItem; + item->set_icon_func([this, app, item](int) { + const qreal device_pixel_ratio = devicePixelRatioF(); + + // define size of expanded image, which is raw image size + margins + QSizeF exp_size_f; + if (m_text_enabled) { + exp_size_f = + m_icon_size + QSizeF(m_icon_size.width() * m_margin_factor * 2, + m_icon_size.height() * m_margin_factor * (m_text_factor + 1)); + } else { + exp_size_f = m_icon_size + m_icon_size * m_margin_factor * 2; + } + + // define offset for raw image placement + QPoint offset(m_icon_size.width() * m_margin_factor, + m_icon_size.height() * m_margin_factor); + const QSize exp_size = (exp_size_f * device_pixel_ratio).toSize(); + + // create empty canvas for expanded image + QImage exp_img(exp_size, QImage::Format_ARGB32); + exp_img.setDevicePixelRatio(device_pixel_ratio); + exp_img.fill(Qt::transparent); + + // create background for image + QImage bg_img(app->pxmap.size(), QImage::Format_ARGB32); + bg_img.setDevicePixelRatio(device_pixel_ratio); + bg_img.fill(m_icon_color); + + // place raw image inside expanded image + QPainter painter(&exp_img); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + painter.drawImage(offset, bg_img); + painter.drawPixmap(offset, app->pxmap); + app->pxmap = {}; + painter.end(); + + // create item with expanded image, title and position + item->setData(Qt::ItemDataRole::DecorationRole, QPixmap::fromImage(exp_img)); + }); + if (m_text_enabled) { + item->setData(Qt::ItemDataRole::DisplayRole, name); + } + + setItem(row, col, item); + return item; +} + +qreal GameListGrid::getMarginFactor() const { + return m_margin_factor; +} +void GameListGrid::RefreshBackgroundImage() { + QPixmap blurredPixmap = QPixmap::fromImage(backgroundImage); + QPalette palette; + palette.setBrush(QPalette::Base, QBrush(blurredPixmap.scaled(size(), Qt::IgnoreAspectRatio))); + QColor transparentColor = QColor(135, 206, 235, 40); + palette.setColor(QPalette::Highlight, transparentColor); + this->setPalette(palette); +} +void GameListGrid::SetGridBackgroundImage(QTableWidgetItem* item) { + if (!item) { + // handle case where icon item does not exist + return; + } + QTableWidgetItem* iconItem = this->item(item->row(), item->column()); + + if (!iconItem) { + // handle case where icon item does not exist + return; + } + game_info gameinfo = GetGameInfoFromItem(iconItem); + QString pic1Path = QString::fromStdString(gameinfo->info.pic_path); + QString blurredPic1Path = + qApp->applicationDirPath() + + QString::fromStdString("/game_data/" + gameinfo->info.serial + "/pic1.png"); + + backgroundImage = QImage(blurredPic1Path); + if (backgroundImage.isNull()) { + QImage image(pic1Path); + backgroundImage = m_game_list_utils.BlurImage(image, image.rect(), 18); + + std::filesystem::path img_path = + std::filesystem::path("game_data/") / gameinfo->info.serial; + std::filesystem::create_directories(img_path); + if (!backgroundImage.save(blurredPic1Path, "PNG")) { + // qDebug() << "Error: Unable to save image."; + } + } + QPixmap blurredPixmap = QPixmap::fromImage(backgroundImage); + QPalette palette; + palette.setBrush(QPalette::Base, QBrush(blurredPixmap.scaled(size(), Qt::IgnoreAspectRatio))); + QColor transparentColor = QColor(135, 206, 235, 40); + palette.setColor(QPalette::Highlight, transparentColor); + this->setPalette(palette); +} + +void GameListGrid::resizeEvent(QResizeEvent* event) { + Q_EMIT ResizedWindowGrid(this->currentItem()); +} \ No newline at end of file diff --git a/src/qt_gui/game_list_grid.h b/src/qt_gui/game_list_grid.h new file mode 100644 index 00000000..02a1c664 --- /dev/null +++ b/src/qt_gui/game_list_grid.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "custom_dock_widget.h" +#include "game_list_table.h" +#include "game_list_utils.h" +#include "gui_settings.h" + +class GameListGridDelegate; + +class GameListGrid : public GameListTable { + Q_OBJECT + + QSize m_icon_size; + QColor m_icon_color; + qreal m_margin_factor; + qreal m_text_factor; + bool m_text_enabled = true; + +Q_SIGNALS: + void ResizedWindowGrid(QTableWidgetItem* item); + +protected: + void resizeEvent(QResizeEvent* event) override; + +public: + explicit GameListGrid(const QSize& icon_size, QColor icon_color, const qreal& margin_factor, + const qreal& text_factor, const bool& showText); + + void enableText(const bool& enabled); + void setIconSize(const QSize& size) const; + GameListItem* addItem(const game_info& app, const QString& name, const int& row, + const int& col); + + [[nodiscard]] qreal getMarginFactor() const; + + game_info GetGameInfoFromItem(const QTableWidgetItem* item) { + if (!item) { + return nullptr; + } + + const QVariant var = item->data(gui::game_role); + if (!var.canConvert()) { + return nullptr; + } + + return var.value(); + } + +private: + void SetGridBackgroundImage(QTableWidgetItem* item); + void RefreshBackgroundImage(); + + GameListGridDelegate* grid_item_delegate; + GameListUtils m_game_list_utils; + + // Background Image + QImage backgroundImage; +}; diff --git a/src/qt_gui/game_list_grid_delegate.cpp b/src/qt_gui/game_list_grid_delegate.cpp new file mode 100644 index 00000000..4b7ffea0 --- /dev/null +++ b/src/qt_gui/game_list_grid_delegate.cpp @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "game_list_grid_delegate.h" + +GameListGridDelegate::GameListGridDelegate(const QSize& size, const qreal& margin_factor, + const qreal& text_factor, QObject* parent) + : QStyledItemDelegate(parent), m_size(size), m_margin_factor(margin_factor), + m_text_factor(text_factor) {} + +void GameListGridDelegate::initStyleOption(QStyleOptionViewItem* option, + const QModelIndex& index) const { + Q_UNUSED(index) + + // Remove the focus frame around selected items + option->state &= ~QStyle::State_HasFocus; + + // Call initStyleOption without a model index, since we want to paint the relevant data + // ourselves + QStyledItemDelegate::initStyleOption(option, QModelIndex()); +} + +void GameListGridDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const { + const QRect r = option.rect; + + painter->setRenderHints(QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); + painter->eraseRect(r); + + // Get title and image + const QPixmap image = qvariant_cast(index.data(Qt::DecorationRole)); + const QString title = index.data(Qt::DisplayRole).toString(); + + // Paint from our stylesheet + QStyledItemDelegate::paint(painter, option, index); + + // image + if (image.isNull() == false) { + painter->drawPixmap(option.rect, image); + } + + const int h = r.height() / (1 + m_margin_factor + m_margin_factor * m_text_factor); + const int height = r.height() - h - h * m_margin_factor; + const int top = r.bottom() - height; + + // title + if (option.state & QStyle::State_Selected) { + painter->setPen(QPen(option.palette.color(QPalette::HighlightedText), 1, Qt::SolidLine)); + } else { + painter->setPen(QPen(option.palette.color(QPalette::WindowText), 1, Qt::SolidLine)); + } + + painter->setFont(option.font); + painter->drawText(QRect(r.left(), top, r.width(), height), +Qt::TextWordWrap | +Qt::AlignCenter, + title); +} + +QSize GameListGridDelegate::sizeHint(const QStyleOptionViewItem& option, + const QModelIndex& index) const { + Q_UNUSED(option) + Q_UNUSED(index) + return m_size; +} + +void GameListGridDelegate::setItemSize(const QSize& size) { + m_size = size; +} diff --git a/src/qt_gui/game_list_grid_delegate.h b/src/qt_gui/game_list_grid_delegate.h new file mode 100644 index 00000000..b37e6bc6 --- /dev/null +++ b/src/qt_gui/game_list_grid_delegate.h @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +class GameListGridDelegate : public QStyledItemDelegate { +public: + GameListGridDelegate(const QSize& imageSize, const qreal& margin_factor, + const qreal& margin_ratio, QObject* parent = nullptr); + + void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override; + void paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; + void setItemSize(const QSize& size); + +private: + QSize m_size; + qreal m_margin_factor; + qreal m_text_factor; +}; diff --git a/src/qt_gui/game_list_item.h b/src/qt_gui/game_list_item.h new file mode 100644 index 00000000..7c625ff4 --- /dev/null +++ b/src/qt_gui/game_list_item.h @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include + +using icon_callback_t = std::function; + +class GameListItem : public QTableWidgetItem { +public: + GameListItem() : QTableWidgetItem() {} + GameListItem(const QString& text, int type = Type) : QTableWidgetItem(text, type) {} + GameListItem(const QIcon& icon, const QString& text, int type = Type) + : QTableWidgetItem(icon, text, type) {} + + ~GameListItem() {} + + void call_icon_func() const { + if (m_icon_callback) { + m_icon_callback(0); + } + } + + void set_icon_func(const icon_callback_t& func) { + m_icon_callback = func; + call_icon_func(); + } + +private: + icon_callback_t m_icon_callback = nullptr; +}; diff --git a/src/qt_gui/game_list_table.cpp b/src/qt_gui/game_list_table.cpp new file mode 100644 index 00000000..30600c7e --- /dev/null +++ b/src/qt_gui/game_list_table.cpp @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "game_list_table.h" + +void GameListTable::clear_list() { + clearSelection(); + clearContents(); +} + +void GameListTable::mousePressEvent(QMouseEvent* event) { + if (QTableWidgetItem* item = itemAt(event->pos()); + !item || !item->data(Qt::UserRole).isValid()) { + clearSelection(); + setCurrentItem(nullptr); // Needed for currentItemChanged + } + QTableWidget::mousePressEvent(event); +} \ No newline at end of file diff --git a/src/qt_gui/game_list_table.h b/src/qt_gui/game_list_table.h new file mode 100644 index 00000000..aec2a01a --- /dev/null +++ b/src/qt_gui/game_list_table.h @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "game_info.h" +#include "game_list_item.h" + +struct GuiGameInfo { + GameInfo info{}; + QPixmap icon; + QPixmap pxmap; + GameListItem* item = nullptr; +}; + +typedef std::shared_ptr game_info; +Q_DECLARE_METATYPE(game_info) + +class GameListTable : public QTableWidget { +public: + void clear_list(); + +protected: + void mousePressEvent(QMouseEvent* event) override; +}; \ No newline at end of file diff --git a/src/qt_gui/game_list_utils.h b/src/qt_gui/game_list_utils.h new file mode 100644 index 00000000..5c54ebeb --- /dev/null +++ b/src/qt_gui/game_list_utils.h @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +class GameListUtils { +public: + static QString FormatSize(qint64 size) { + static const QStringList suffixes = {"B", "KB", "MB", "GB", "TB"}; + int suffixIndex = 0; + + while (size >= 1024 && suffixIndex < suffixes.size() - 1) { + size /= 1024; + ++suffixIndex; + } + + return QString("%1 %2").arg(size).arg(suffixes[suffixIndex]); + } + + static QString GetFolderSize(const QDir& dir) { + + QDirIterator it(dir.absolutePath(), QDirIterator::Subdirectories); + qint64 total = 0; + + while (it.hasNext()) { + // check if entry is file + if (it.fileInfo().isFile()) { + total += it.fileInfo().size(); + } + it.next(); + } + + // if there is a file left "at the end" get it's size + if (it.fileInfo().isFile()) { + total += it.fileInfo().size(); + } + + return FormatSize(total); + } + + QImage BlurImage(const QImage& image, const QRect& rect, int radius) { + int tab[] = {14, 10, 8, 6, 5, 5, 4, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2}; + int alpha = (radius < 1) ? 16 : (radius > 17) ? 1 : tab[radius - 1]; + + QImage result = image.convertToFormat(QImage::Format_ARGB32); + int r1 = rect.top(); + int r2 = rect.bottom(); + int c1 = rect.left(); + int c2 = rect.right(); + + int bpl = result.bytesPerLine(); + int rgba[4]; + unsigned char* p; + + int i1 = 0; + int i2 = 3; + + for (int col = c1; col <= c2; col++) { + p = result.scanLine(r1) + col * 4; + for (int i = i1; i <= i2; i++) + rgba[i] = p[i] << 4; + + p += bpl; + for (int j = r1; j < r2; j++, p += bpl) + for (int i = i1; i <= i2; i++) + p[i] = (rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4; + } + + for (int row = r1; row <= r2; row++) { + p = result.scanLine(row) + c1 * 4; + for (int i = i1; i <= i2; i++) + rgba[i] = p[i] << 4; + + p += 4; + for (int j = c1; j < c2; j++, p += 4) + for (int i = i1; i <= i2; i++) + p[i] = (rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4; + } + + for (int col = c1; col <= c2; col++) { + p = result.scanLine(r2) + col * 4; + for (int i = i1; i <= i2; i++) + rgba[i] = p[i] << 4; + + p -= bpl; + for (int j = r1; j < r2; j++, p -= bpl) + for (int i = i1; i <= i2; i++) + p[i] = (rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4; + } + + for (int row = r1; row <= r2; row++) { + p = result.scanLine(row) + c2 * 4; + for (int i = i1; i <= i2; i++) + rgba[i] = p[i] << 4; + + p -= 4; + for (int j = c1; j < c2; j++, p -= 4) + for (int i = i1; i <= i2; i++) + p[i] = (rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4; + } + + return result; + } +}; diff --git a/src/qt_gui/gui_save.h b/src/qt_gui/gui_save.h new file mode 100644 index 00000000..e2434f75 --- /dev/null +++ b/src/qt_gui/gui_save.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +struct GuiSave { + QString key; + QString name; + QVariant def; + + GuiSave() { + key = ""; + name = ""; + def = QVariant(); + } + + GuiSave(const QString& k, const QString& n, const QVariant& d) { + key = k; + name = n; + def = d; + } + + bool operator==(const GuiSave& rhs) const noexcept { + return key == rhs.key && name == rhs.name && def == rhs.def; + } +}; diff --git a/src/qt_gui/gui_settings.cpp b/src/qt_gui/gui_settings.cpp new file mode 100644 index 00000000..962b0455 --- /dev/null +++ b/src/qt_gui/gui_settings.cpp @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "gui_settings.h" + +GuiSettings::GuiSettings(QObject* parent) { + m_settings.reset(new QSettings("shadps4.ini", QSettings::Format::IniFormat, + parent)); // TODO make the path configurable +} + +void GuiSettings::SetGamelistColVisibility(int col, bool val) const { + SetValue(GetGuiSaveForColumn(col), val); +} + +bool GuiSettings::GetGamelistColVisibility(int col) const { + return GetValue(GetGuiSaveForColumn(col)).toBool(); +} + +GuiSave GuiSettings::GetGuiSaveForColumn(int col) { + return GuiSave{gui::game_list, + "visibility_" + + gui::get_game_list_column_name(static_cast(col)), + true}; +} +QSize GuiSettings::SizeFromSlider(int pos) { + return gui::game_list_icon_size_min + + (gui::game_list_icon_size_max - gui::game_list_icon_size_min) * + (1.f * pos / gui::game_list_max_slider_pos); +} \ No newline at end of file diff --git a/src/qt_gui/gui_settings.h b/src/qt_gui/gui_settings.h new file mode 100644 index 00000000..9c780ec6 --- /dev/null +++ b/src/qt_gui/gui_settings.h @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "settings.h" + +namespace gui { +enum custom_roles { + game_role = Qt::UserRole + 1337, +}; + +enum game_list_columns { + column_icon, + column_name, + column_serial, + column_firmware, + column_size, + column_version, + column_category, + column_path, + column_count +}; + +inline QString get_game_list_column_name(game_list_columns col) { + switch (col) { + case column_icon: + return "column_icon"; + case column_name: + return "column_name"; + case column_serial: + return "column_serial"; + case column_firmware: + return "column_firmware"; + case column_size: + return "column_size"; + case column_version: + return "column_version"; + case column_category: + return "column_category"; + case column_path: + return "column_path"; + case column_count: + return ""; + } + + throw std::runtime_error("get_game_list_column_name: Invalid column"); +} + +const QSize game_list_icon_size_min = QSize(28, 28); +const QSize game_list_icon_size_small = QSize(56, 56); +const QSize game_list_icon_size_medium = QSize(128, 128); +const QSize game_list_icon_size_max = + QSize(256, 256); // let's do 256, 512 is too big (that's what she said) + +const int game_list_max_slider_pos = 100; + +inline int get_Index(const QSize& current) { + const int size_delta = game_list_icon_size_max.width() - game_list_icon_size_min.width(); + const int current_delta = current.width() - game_list_icon_size_min.width(); + return game_list_max_slider_pos * current_delta / size_delta; +} + +const QString main_window = "main_window"; +const QString game_list = "GameList"; +const QString settings = "Settings"; +const QString themes = "Themes"; + +const QColor game_list_icon_color = QColor(240, 240, 240, 255); + +const GuiSave main_window_gamelist_visible = GuiSave(main_window, "gamelistVisible", true); +const GuiSave main_window_geometry = GuiSave(main_window, "geometry", QByteArray()); +const GuiSave main_window_windowState = GuiSave(main_window, "windowState", QByteArray()); +const GuiSave main_window_mwState = GuiSave(main_window, "mwState", QByteArray()); + +const GuiSave game_list_sortAsc = GuiSave(game_list, "sortAsc", true); +const GuiSave game_list_sortCol = GuiSave(game_list, "sortCol", 1); +const GuiSave game_list_state = GuiSave(game_list, "state", QByteArray()); +const GuiSave game_list_iconSize = + GuiSave(game_list, "iconSize", get_Index(game_list_icon_size_small)); +const GuiSave game_list_iconSizeGrid = + GuiSave(game_list, "iconSizeGrid", get_Index(game_list_icon_size_small)); +const GuiSave game_list_iconColor = GuiSave(game_list, "iconColor", game_list_icon_color); +const GuiSave game_list_listMode = GuiSave(game_list, "listMode", true); +const GuiSave game_list_textFactor = GuiSave(game_list, "textFactor", qreal{2.0}); +const GuiSave game_list_marginFactor = GuiSave(game_list, "marginFactor", qreal{0.09}); +const GuiSave settings_install_dir = GuiSave(settings, "installDirectory", ""); +const GuiSave mw_themes = GuiSave(themes, "Themes", 0); + +} // namespace gui + +class GuiSettings : public Settings { + Q_OBJECT + +public: + explicit GuiSettings(QObject* parent = nullptr); + + bool GetGamelistColVisibility(int col) const; + +public Q_SLOTS: + void SetGamelistColVisibility(int col, bool val) const; + static GuiSave GetGuiSaveForColumn(int col); + static QSize SizeFromSlider(int pos); +}; diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp new file mode 100644 index 00000000..3ccaec1b --- /dev/null +++ b/src/qt_gui/main_window.cpp @@ -0,0 +1,364 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include + +#include "common/io_file.h" +#include "emulator/file_format/pkg.h" +#include "emulator/loader.h" +#include "game_install_dialog.h" +#include "game_list_frame.h" +#include "gui_settings.h" +#include "main_window.h" + +MainWindow::MainWindow(std::shared_ptr gui_settings, QWidget* parent) + : QMainWindow(parent), ui(new Ui::MainWindow), m_gui_settings(std::move(gui_settings)) { + + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose); +} + +MainWindow::~MainWindow() { + SaveWindowState(); +} + +bool MainWindow::Init() { + // add toolbar widgets + QApplication::setStyle("Fusion"); + ui->toolBar->setObjectName("mw_toolbar"); + ui->sizeSlider->setRange(0, gui::game_list_max_slider_pos); + ui->toolBar->addWidget(ui->sizeSliderContainer); + ui->toolBar->addWidget(ui->mw_searchbar); + + CreateActions(); + CreateDockWindows(); + CreateConnects(); + SetLastUsedTheme(); + + setMinimumSize(350, minimumSizeHint().height()); + setWindowTitle(QString::fromStdString("ShadPS4 v0.0.2")); + + ConfigureGuiFromSettings(); + + show(); + + // Fix possible hidden game list columns. The game list has to be visible already. Use this + // after show() + m_game_list_frame->FixNarrowColumns(); + + return true; +} + +void MainWindow::CreateActions() { + // create action group for icon size + m_icon_size_act_group = new QActionGroup(this); + m_icon_size_act_group->addAction(ui->setIconSizeTinyAct); + m_icon_size_act_group->addAction(ui->setIconSizeSmallAct); + m_icon_size_act_group->addAction(ui->setIconSizeMediumAct); + m_icon_size_act_group->addAction(ui->setIconSizeLargeAct); + + // create action group for list mode + m_list_mode_act_group = new QActionGroup(this); + m_list_mode_act_group->addAction(ui->setlistModeListAct); + m_list_mode_act_group->addAction(ui->setlistModeGridAct); + + // create action group for themes + m_theme_act_group = new QActionGroup(this); + m_theme_act_group->addAction(ui->setThemeLight); + m_theme_act_group->addAction(ui->setThemeDark); + m_theme_act_group->addAction(ui->setThemeGreen); + m_theme_act_group->addAction(ui->setThemeBlue); + m_theme_act_group->addAction(ui->setThemeViolet); +} + +void MainWindow::CreateDockWindows() { + m_main_window = new QMainWindow(); + m_main_window->setContextMenuPolicy(Qt::PreventContextMenu); + + m_game_list_frame = new GameListFrame(m_gui_settings, m_main_window); + m_game_list_frame->setObjectName("gamelist"); + + m_main_window->addDockWidget(Qt::LeftDockWidgetArea, m_game_list_frame); + + m_main_window->setDockNestingEnabled(true); + + setCentralWidget(m_main_window); + + connect(m_game_list_frame, &GameListFrame::GameListFrameClosed, this, [this]() { + if (ui->showGameListAct->isChecked()) { + ui->showGameListAct->setChecked(false); + m_gui_settings->SetValue(gui::main_window_gamelist_visible, false); + } + }); +} +void MainWindow::CreateConnects() { + connect(ui->exitAct, &QAction::triggered, this, &QWidget::close); + + connect(ui->showGameListAct, &QAction::triggered, this, [this](bool checked) { + checked ? m_game_list_frame->show() : m_game_list_frame->hide(); + m_gui_settings->SetValue(gui::main_window_gamelist_visible, checked); + }); + connect(ui->refreshGameListAct, &QAction::triggered, this, + [this] { m_game_list_frame->Refresh(true); }); + + connect(m_icon_size_act_group, &QActionGroup::triggered, this, [this](QAction* act) { + static const int index_small = gui::get_Index(gui::game_list_icon_size_small); + static const int index_medium = gui::get_Index(gui::game_list_icon_size_medium); + + int index; + + if (act == ui->setIconSizeTinyAct) + index = 0; + else if (act == ui->setIconSizeSmallAct) + index = index_small; + else if (act == ui->setIconSizeMediumAct) + index = index_medium; + else + index = gui::game_list_max_slider_pos; + + m_save_slider_pos = true; + ResizeIcons(index); + }); + connect(m_game_list_frame, &GameListFrame::RequestIconSizeChange, this, [this](const int& val) { + const int idx = ui->sizeSlider->value() + val; + m_save_slider_pos = true; + ResizeIcons(idx); + }); + + connect(m_list_mode_act_group, &QActionGroup::triggered, this, [this](QAction* act) { + const bool is_list_act = act == ui->setlistModeListAct; + if (is_list_act == m_is_list_mode) + return; + + const int slider_pos = ui->sizeSlider->sliderPosition(); + ui->sizeSlider->setSliderPosition(m_other_slider_pos); + SetIconSizeActions(m_other_slider_pos); + m_other_slider_pos = slider_pos; + + m_is_list_mode = is_list_act; + m_game_list_frame->SetListMode(m_is_list_mode); + }); + connect(ui->sizeSlider, &QSlider::valueChanged, this, &MainWindow::ResizeIcons); + connect(ui->sizeSlider, &QSlider::sliderReleased, this, [this] { + const int index = ui->sizeSlider->value(); + m_gui_settings->SetValue( + m_is_list_mode ? gui::game_list_iconSize : gui::game_list_iconSizeGrid, index); + SetIconSizeActions(index); + }); + connect(ui->sizeSlider, &QSlider::actionTriggered, this, [this](int action) { + if (action != QAbstractSlider::SliderNoAction && + action != + QAbstractSlider::SliderMove) { // we only want to save on mouseclicks or slider + // release (the other connect handles this) + m_save_slider_pos = true; // actionTriggered happens before the value was changed + } + }); + + connect(ui->mw_searchbar, &QLineEdit::textChanged, m_game_list_frame, + &GameListFrame::SetSearchText); + connect(ui->bootInstallPkgAct, &QAction::triggered, this, [this] { InstallPkg(); }); + connect(ui->gameInstallPathAct, &QAction::triggered, this, [this] { InstallDirectory(); }); + + // Themes + connect(ui->setThemeLight, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Light, ui->mw_searchbar); + m_gui_settings->SetValue(gui::mw_themes, static_cast(Theme::Light)); + }); + connect(ui->setThemeDark, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Dark, ui->mw_searchbar); + m_gui_settings->SetValue(gui::mw_themes, static_cast(Theme::Dark)); + }); + connect(ui->setThemeGreen, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Green, ui->mw_searchbar); + m_gui_settings->SetValue(gui::mw_themes, static_cast(Theme::Green)); + }); + connect(ui->setThemeBlue, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Blue, ui->mw_searchbar); + m_gui_settings->SetValue(gui::mw_themes, static_cast(Theme::Blue)); + }); + connect(ui->setThemeViolet, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Violet, ui->mw_searchbar); + m_gui_settings->SetValue(gui::mw_themes, static_cast(Theme::Violet)); + }); +} + +void MainWindow::SetIconSizeActions(int idx) const { + static const int threshold_tiny = + gui::get_Index((gui::game_list_icon_size_small + gui::game_list_icon_size_min) / 2); + static const int threshold_small = + gui::get_Index((gui::game_list_icon_size_medium + gui::game_list_icon_size_small) / 2); + static const int threshold_medium = + gui::get_Index((gui::game_list_icon_size_max + gui::game_list_icon_size_medium) / 2); + + if (idx < threshold_tiny) + ui->setIconSizeTinyAct->setChecked(true); + else if (idx < threshold_small) + ui->setIconSizeSmallAct->setChecked(true); + else if (idx < threshold_medium) + ui->setIconSizeMediumAct->setChecked(true); + else + ui->setIconSizeLargeAct->setChecked(true); +} +void MainWindow::ResizeIcons(int index) { + if (ui->sizeSlider->value() != index) { + ui->sizeSlider->setSliderPosition(index); + return; // ResizeIcons will be triggered again by setSliderPosition, so return here + } + + if (m_save_slider_pos) { + m_save_slider_pos = false; + m_gui_settings->SetValue( + m_is_list_mode ? gui::game_list_iconSize : gui::game_list_iconSizeGrid, index); + + // this will also fire when we used the actions, but i didn't want to add another boolean + // member + SetIconSizeActions(index); + } + + m_game_list_frame->ResizeIcons(index); +} +void MainWindow::ConfigureGuiFromSettings() { + // Restore GUI state if needed. We need to if they exist. + if (!restoreGeometry(m_gui_settings->GetValue(gui::main_window_geometry).toByteArray())) { + resize(QGuiApplication::primaryScreen()->availableSize() * 0.7); + } + + restoreState(m_gui_settings->GetValue(gui::main_window_windowState).toByteArray()); + m_main_window->restoreState(m_gui_settings->GetValue(gui::main_window_mwState).toByteArray()); + + ui->showGameListAct->setChecked( + m_gui_settings->GetValue(gui::main_window_gamelist_visible).toBool()); + + m_game_list_frame->setVisible(ui->showGameListAct->isChecked()); + + // handle icon size options + m_is_list_mode = m_gui_settings->GetValue(gui::game_list_listMode).toBool(); + if (m_is_list_mode) + ui->setlistModeListAct->setChecked(true); + else + ui->setlistModeGridAct->setChecked(true); + + const int icon_size_index = + m_gui_settings + ->GetValue(m_is_list_mode ? gui::game_list_iconSize : gui::game_list_iconSizeGrid) + .toInt(); + m_other_slider_pos = + m_gui_settings + ->GetValue(!m_is_list_mode ? gui::game_list_iconSize : gui::game_list_iconSizeGrid) + .toInt(); + ui->sizeSlider->setSliderPosition(icon_size_index); + SetIconSizeActions(icon_size_index); + + // Gamelist + m_game_list_frame->LoadSettings(); +} + +void MainWindow::SaveWindowState() const { + // Save gui settings + m_gui_settings->SetValue(gui::main_window_geometry, saveGeometry()); + m_gui_settings->SetValue(gui::main_window_windowState, saveState()); + m_gui_settings->SetValue(gui::main_window_mwState, m_main_window->saveState()); + + // Save column settings + m_game_list_frame->SaveSettings(); +} + +void MainWindow::InstallPkg() { + QStringList fileNames = QFileDialog::getOpenFileNames( + this, tr("Install PKG Files"), QDir::currentPath(), tr("PKG File (*.PKG)")); + int nPkg = fileNames.size(); + int pkgNum = 0; + for (const QString& file : fileNames) { + pkgNum++; + MainWindow::InstallDragDropPkg(file.toStdString(), pkgNum, nPkg); + } +} + +void MainWindow::InstallDragDropPkg(std::string file, int pkgNum, int nPkg) { + + if (detectFileType(file) == FILETYPE_PKG) { + PKG pkg; + pkg.Open(file); + std::string failreason; + const auto extract_path = + std::filesystem::path( + m_gui_settings->GetValue(gui::settings_install_dir).toString().toStdString()) / + pkg.GetTitleID(); + if (!pkg.Extract(file, extract_path, failreason)) { + QMessageBox::critical(this, "PKG ERROR", QString::fromStdString(failreason), + QMessageBox::Ok, 0); + } else { + int nfiles = pkg.GetNumberOfFiles(); + + QList indices; + for (int i = 0; i < nfiles; i++) { + indices.append(i); + } + + QProgressDialog dialog; + dialog.setWindowTitle("PKG Extraction"); + QString extractmsg = QString("Extracting PKG %1/%2").arg(pkgNum).arg(nPkg); + dialog.setLabelText(extractmsg); + + // Create a QFutureWatcher and connect signals and slots. + QFutureWatcher futureWatcher; + QObject::connect(&futureWatcher, SIGNAL(finished()), &dialog, SLOT(reset())); + QObject::connect(&dialog, SIGNAL(canceled()), &futureWatcher, SLOT(cancel())); + QObject::connect(&futureWatcher, SIGNAL(progressRangeChanged(int, int)), &dialog, + SLOT(setRange(int, int))); + QObject::connect(&futureWatcher, SIGNAL(progressValueChanged(int)), &dialog, + SLOT(setValue(int))); + + futureWatcher.setFuture(QtConcurrent::map( + indices, std::bind(&PKG::ExtractFiles, pkg, std::placeholders::_1))); + + // Display the dialog and start the event loop. + dialog.exec(); + futureWatcher.waitForFinished(); + + auto path = m_gui_settings->GetValue(gui::settings_install_dir).toString(); + if (pkgNum == nPkg) { + QMessageBox::information(this, "Extraction Finished", + "Game successfully installed at " + path, QMessageBox::Ok, + 0); + m_game_list_frame->Refresh(true); + } + } + } else { + QMessageBox::critical(this, "PKG ERROR", "File doesn't appear to be a valid PKG file", + QMessageBox::Ok, 0); + } +} + +void MainWindow::InstallDirectory() { + GameInstallDialog dlg(m_gui_settings); + dlg.exec(); +} + +void MainWindow::SetLastUsedTheme() { + + Theme lastTheme = static_cast(m_gui_settings->GetValue(gui::mw_themes).toInt()); + m_window_themes.SetWindowTheme(lastTheme, ui->mw_searchbar); + + switch (lastTheme) { + case Theme::Light: + ui->setThemeLight->setChecked(true); + break; + case Theme::Dark: + ui->setThemeDark->setChecked(true); + break; + case Theme::Green: + ui->setThemeGreen->setChecked(true); + break; + case Theme::Blue: + ui->setThemeBlue->setChecked(true); + break; + case Theme::Violet: + ui->setThemeViolet->setChecked(true); + break; + } +} \ No newline at end of file diff --git a/src/qt_gui/main_window.h b/src/qt_gui/main_window.h new file mode 100644 index 00000000..62474f05 --- /dev/null +++ b/src/qt_gui/main_window.h @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include "main_window_themes.h" +#include "main_window_ui.h" + +class GuiSettings; +class GameListFrame; + +class MainWindow : public QMainWindow { + Q_OBJECT + + std::unique_ptr ui; + + bool m_is_list_mode = true; + bool m_save_slider_pos = false; + int m_other_slider_pos = 0; + +public: + explicit MainWindow(std::shared_ptr gui_settings, QWidget* parent = nullptr); + ~MainWindow(); + bool Init(); + void InstallPkg(); + void InstallDragDropPkg(std::string file, int pkgNum, int nPkg); + void InstallDirectory(); + +private Q_SLOTS: + void ConfigureGuiFromSettings(); + void SetIconSizeActions(int idx) const; + void ResizeIcons(int index); + void SaveWindowState() const; + +private: + void CreateActions(); + void CreateDockWindows(); + void CreateConnects(); + void SetLastUsedTheme(); + + QActionGroup* m_icon_size_act_group = nullptr; + QActionGroup* m_list_mode_act_group = nullptr; + QActionGroup* m_theme_act_group = nullptr; + + // Dockable widget frames + QMainWindow* m_main_window = nullptr; + GameListFrame* m_game_list_frame = nullptr; + WindowThemes m_window_themes; + + std::shared_ptr m_gui_settings; + +protected: + void dragEnterEvent(QDragEnterEvent* event1) override { + if (event1->mimeData()->hasUrls()) { + event1->acceptProposedAction(); + } + } + + void dropEvent(QDropEvent* event1) override { + const QMimeData* mimeData = event1->mimeData(); + if (mimeData->hasUrls()) { + QList urlList = mimeData->urls(); + int pkgNum = 0; + int nPkg = urlList.size(); + for (const QUrl& url : urlList) { + pkgNum++; + InstallDragDropPkg(url.toLocalFile().toStdString(), pkgNum, nPkg); + } + } + } +}; diff --git a/src/qt_gui/main_window_themes.cpp b/src/qt_gui/main_window_themes.cpp new file mode 100644 index 00000000..858bbb07 --- /dev/null +++ b/src/qt_gui/main_window_themes.cpp @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "main_window_themes.h" + +void WindowThemes::SetWindowTheme(Theme theme, QLineEdit* mw_searchbar) { + QPalette themePalette; + + switch (theme) { + case Theme::Light: + mw_searchbar->setStyleSheet("background-color: #ffffff; /* Light gray background */" + "color: #000000; /* Black text */" + "padding: 5px;"); + themePalette.setColor(QPalette::Window, QColor(240, 240, 240)); // Light gray + themePalette.setColor(QPalette::WindowText, Qt::black); // Black + themePalette.setColor(QPalette::Base, QColor(230, 230, 230, 80)); // Grayish + themePalette.setColor(QPalette::ToolTipBase, Qt::black); // Black + themePalette.setColor(QPalette::ToolTipText, Qt::black); // Black + themePalette.setColor(QPalette::Text, Qt::black); // Black + themePalette.setColor(QPalette::Button, QColor(240, 240, 240)); // Light gray + themePalette.setColor(QPalette::ButtonText, Qt::black); // Black + themePalette.setColor(QPalette::BrightText, Qt::red); // Red + themePalette.setColor(QPalette::Link, QColor(42, 130, 218)); // Blue + themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); // Blue + themePalette.setColor(QPalette::HighlightedText, Qt::white); // White + qApp->setPalette(themePalette); + break; + + case Theme::Dark: + mw_searchbar->setStyleSheet("background-color: #1e1e1e; /* Dark background */" + "color: #ffffff; /* White text */" + "border: 1px solid #ffffff; /* White border */" + "padding: 5px;"); + themePalette.setColor(QPalette::Window, QColor(53, 53, 53)); + themePalette.setColor(QPalette::WindowText, Qt::white); + themePalette.setColor(QPalette::Base, QColor(25, 25, 25)); + themePalette.setColor(QPalette::AlternateBase, QColor(25, 25, 25)); + themePalette.setColor(QPalette::AlternateBase, QColor(53, 53, 53)); + themePalette.setColor(QPalette::ToolTipBase, Qt::white); + themePalette.setColor(QPalette::ToolTipText, Qt::white); + themePalette.setColor(QPalette::Text, Qt::white); + themePalette.setColor(QPalette::Button, QColor(53, 53, 53)); + themePalette.setColor(QPalette::ButtonText, Qt::white); + themePalette.setColor(QPalette::BrightText, Qt::red); + themePalette.setColor(QPalette::Link, QColor(42, 130, 218)); + themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); + themePalette.setColor(QPalette::HighlightedText, Qt::black); + qApp->setPalette(themePalette); + break; + + case Theme::Green: + mw_searchbar->setStyleSheet("background-color: #354535; /* Dark green background */" + "color: #ffffff; /* White text */" + "border: 1px solid #ffffff; /* White border */" + "padding: 5px;"); + themePalette.setColor(QPalette::Window, QColor(53, 69, 53)); // Dark green background + themePalette.setColor(QPalette::WindowText, Qt::white); // White text + themePalette.setColor(QPalette::Base, QColor(25, 40, 25)); // Darker green base + themePalette.setColor(QPalette::AlternateBase, + QColor(53, 69, 53)); // Dark green alternate base + themePalette.setColor(QPalette::ToolTipBase, Qt::white); // White tooltip background + themePalette.setColor(QPalette::ToolTipText, Qt::white); // White tooltip text + themePalette.setColor(QPalette::Text, Qt::white); // White text + themePalette.setColor(QPalette::Button, QColor(53, 69, 53)); // Dark green button + themePalette.setColor(QPalette::ButtonText, Qt::white); // White button text + themePalette.setColor(QPalette::BrightText, Qt::red); // Bright red text for alerts + themePalette.setColor(QPalette::Link, QColor(42, 130, 218)); // Light blue links + themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); // Light blue highlight + themePalette.setColor(QPalette::HighlightedText, Qt::black); // Black highlighted text + + qApp->setPalette(themePalette); + break; + + case Theme::Blue: + mw_searchbar->setStyleSheet("background-color: #283c5a; /* Dark blue background */" + "color: #ffffff; /* White text */" + "border: 1px solid #ffffff; /* White border */" + "padding: 5px;"); + themePalette.setColor(QPalette::Window, QColor(40, 60, 90)); // Dark blue background + themePalette.setColor(QPalette::WindowText, Qt::white); // White text + themePalette.setColor(QPalette::Base, QColor(20, 40, 60)); // Darker blue base + themePalette.setColor(QPalette::AlternateBase, + QColor(40, 60, 90)); // Dark blue alternate base + themePalette.setColor(QPalette::ToolTipBase, Qt::white); // White tooltip background + themePalette.setColor(QPalette::ToolTipText, Qt::white); // White tooltip text + themePalette.setColor(QPalette::Text, Qt::white); // White text + themePalette.setColor(QPalette::Button, QColor(40, 60, 90)); // Dark blue button + themePalette.setColor(QPalette::ButtonText, Qt::white); // White button text + themePalette.setColor(QPalette::BrightText, Qt::red); // Bright red text for alerts + themePalette.setColor(QPalette::Link, QColor(42, 130, 218)); // Light blue links + themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); // Light blue highlight + themePalette.setColor(QPalette::HighlightedText, Qt::black); // Black highlighted text + + qApp->setPalette(themePalette); + break; + + case Theme::Violet: + mw_searchbar->setStyleSheet("background-color: #643278; /* Violet background */" + "color: #ffffff; /* White text */" + "border: 1px solid #ffffff; /* White border */" + "padding: 5px;"); + themePalette.setColor(QPalette::Window, QColor(100, 50, 120)); // Violet background + themePalette.setColor(QPalette::WindowText, Qt::white); // White text + themePalette.setColor(QPalette::Base, QColor(80, 30, 90)); // Darker violet base + themePalette.setColor(QPalette::AlternateBase, + QColor(100, 50, 120)); // Violet alternate base + themePalette.setColor(QPalette::ToolTipBase, Qt::white); // White tooltip background + themePalette.setColor(QPalette::ToolTipText, Qt::white); // White tooltip text + themePalette.setColor(QPalette::Text, Qt::white); // White text + themePalette.setColor(QPalette::Button, QColor(100, 50, 120)); // Violet button + themePalette.setColor(QPalette::ButtonText, Qt::white); // White button text + themePalette.setColor(QPalette::BrightText, Qt::red); // Bright red text for alerts + themePalette.setColor(QPalette::Link, QColor(42, 130, 218)); // Light blue links + themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); // Light blue highlight + themePalette.setColor(QPalette::HighlightedText, Qt::black); // Black highlighted text + + qApp->setPalette(themePalette); + break; + } +} \ No newline at end of file diff --git a/src/qt_gui/main_window_themes.h b/src/qt_gui/main_window_themes.h new file mode 100644 index 00000000..8b87fbce --- /dev/null +++ b/src/qt_gui/main_window_themes.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include +#include +#include + +enum class Theme : int { + Light, + Dark, + Green, + Blue, + Violet, +}; + +class WindowThemes : public QObject { + Q_OBJECT +public Q_SLOTS: + void SetWindowTheme(Theme theme, QLineEdit* mw_searchbar); +}; diff --git a/src/qt_gui/main_window_ui.h b/src/qt_gui/main_window_ui.h new file mode 100644 index 00000000..c467911d --- /dev/null +++ b/src/qt_gui/main_window_ui.h @@ -0,0 +1,279 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +/******************************************************************************** +** Form generated from reading UI file 'main_window.ui' +** +** Created by: Qt User Interface Compiler version 6.6.1 +** +** WARNING! All changes made in this file will be lost when recompiling UI file! +********************************************************************************/ + +#ifndef MAIN_WINDOW_UI_H +#define MAIN_WINDOW_UI_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class Ui_MainWindow { +public: + QAction* bootInstallPkgAct; + QAction* exitAct; + QAction* showGameListAct; + QAction* refreshGameListAct; + QAction* setIconSizeTinyAct; + QAction* setIconSizeSmallAct; + QAction* setIconSizeMediumAct; + QAction* setIconSizeLargeAct; + QAction* setlistModeListAct; + QAction* setlistModeGridAct; + QAction* gameInstallPathAct; + QAction* setThemeLight; + QAction* setThemeDark; + QAction* setThemeGreen; + QAction* setThemeBlue; + QAction* setThemeViolet; + QWidget* centralWidget; + QLineEdit* mw_searchbar; + + QWidget* sizeSliderContainer; + QHBoxLayout* sizeSliderContainer_layout; + QSlider* sizeSlider; + QMenuBar* menuBar; + QMenu* menuFile; + QMenu* menuView; + QMenu* menuGame_List_Icons; + QMenu* menuGame_List_Mode; + QMenu* menuSettings; + QMenu* menuThemes; + QToolBar* toolBar; + + void setupUi(QMainWindow* MainWindow) { + if (MainWindow->objectName().isEmpty()) + MainWindow->setObjectName("MainWindow"); + MainWindow->resize(1058, 580); + QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + sizePolicy.setHorizontalStretch(0); + sizePolicy.setVerticalStretch(0); + sizePolicy.setHeightForWidth(MainWindow->sizePolicy().hasHeightForWidth()); + MainWindow->setSizePolicy(sizePolicy); + MainWindow->setMinimumSize(QSize(4, 0)); + MainWindow->setAutoFillBackground(false); + MainWindow->setAnimated(true); + MainWindow->setDockNestingEnabled(true); + MainWindow->setDockOptions(QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks | + QMainWindow::AnimatedDocks | QMainWindow::GroupedDragging); + bootInstallPkgAct = new QAction(MainWindow); + bootInstallPkgAct->setObjectName("bootInstallPkgAct"); + exitAct = new QAction(MainWindow); + exitAct->setObjectName("exitAct"); + showGameListAct = new QAction(MainWindow); + showGameListAct->setObjectName("showGameListAct"); + showGameListAct->setCheckable(true); + refreshGameListAct = new QAction(MainWindow); + refreshGameListAct->setObjectName("refreshGameListAct"); + setIconSizeTinyAct = new QAction(MainWindow); + setIconSizeTinyAct->setObjectName("setIconSizeTinyAct"); + setIconSizeTinyAct->setCheckable(true); + setIconSizeSmallAct = new QAction(MainWindow); + setIconSizeSmallAct->setObjectName("setIconSizeSmallAct"); + setIconSizeSmallAct->setCheckable(true); + setIconSizeSmallAct->setChecked(true); + setIconSizeMediumAct = new QAction(MainWindow); + setIconSizeMediumAct->setObjectName("setIconSizeMediumAct"); + setIconSizeMediumAct->setCheckable(true); + setIconSizeLargeAct = new QAction(MainWindow); + setIconSizeLargeAct->setObjectName("setIconSizeLargeAct"); + setIconSizeLargeAct->setCheckable(true); + setlistModeListAct = new QAction(MainWindow); + setlistModeListAct->setObjectName("setlistModeListAct"); + setlistModeListAct->setCheckable(true); + setlistModeListAct->setChecked(true); + setlistModeGridAct = new QAction(MainWindow); + setlistModeGridAct->setObjectName("setlistModeGridAct"); + setlistModeGridAct->setCheckable(true); + gameInstallPathAct = new QAction(MainWindow); + gameInstallPathAct->setObjectName("gameInstallPathAct"); + setThemeLight = new QAction(MainWindow); + setThemeLight->setObjectName("setThemeLight"); + setThemeLight->setCheckable(true); + setThemeLight->setChecked(true); + setThemeDark = new QAction(MainWindow); + setThemeDark->setObjectName("setThemeDark"); + setThemeDark->setCheckable(true); + setThemeGreen = new QAction(MainWindow); + setThemeGreen->setObjectName("setThemeGreen"); + setThemeGreen->setCheckable(true); + setThemeBlue = new QAction(MainWindow); + setThemeBlue->setObjectName("setThemeBlue"); + setThemeBlue->setCheckable(true); + setThemeViolet = new QAction(MainWindow); + setThemeViolet->setObjectName("setThemeViolet"); + setThemeViolet->setCheckable(true); + centralWidget = new QWidget(MainWindow); + centralWidget->setObjectName("centralWidget"); + sizePolicy.setHeightForWidth(centralWidget->sizePolicy().hasHeightForWidth()); + centralWidget->setSizePolicy(sizePolicy); + mw_searchbar = new QLineEdit(centralWidget); + mw_searchbar->setObjectName("mw_searchbar"); + mw_searchbar->setGeometry(QRect(480, 10, 150, 31)); + sizePolicy.setHeightForWidth(mw_searchbar->sizePolicy().hasHeightForWidth()); + mw_searchbar->setSizePolicy(sizePolicy); + mw_searchbar->setMaximumWidth(250); + QFont font; + font.setPointSize(10); + font.setBold(false); + mw_searchbar->setFont(font); + mw_searchbar->setFocusPolicy(Qt::ClickFocus); + mw_searchbar->setFrame(false); + mw_searchbar->setClearButtonEnabled(false); + + sizeSliderContainer = new QWidget(centralWidget); + sizeSliderContainer->setObjectName("sizeSliderContainer"); + sizeSliderContainer->setGeometry(QRect(280, 10, 181, 31)); + QSizePolicy sizePolicy1(QSizePolicy::Fixed, QSizePolicy::Expanding); + sizePolicy1.setHorizontalStretch(0); + sizePolicy1.setVerticalStretch(0); + sizePolicy1.setHeightForWidth(sizeSliderContainer->sizePolicy().hasHeightForWidth()); + sizeSliderContainer->setSizePolicy(sizePolicy1); + sizeSliderContainer_layout = new QHBoxLayout(sizeSliderContainer); + sizeSliderContainer_layout->setSpacing(0); + sizeSliderContainer_layout->setContentsMargins(11, 11, 11, 11); + sizeSliderContainer_layout->setObjectName("sizeSliderContainer_layout"); + sizeSliderContainer_layout->setContentsMargins(14, 0, 14, 0); + sizeSlider = new QSlider(sizeSliderContainer); + sizeSlider->setObjectName("sizeSlider"); + QSizePolicy sizePolicy2(QSizePolicy::Expanding, QSizePolicy::Preferred); + sizePolicy2.setHorizontalStretch(0); + sizePolicy2.setVerticalStretch(0); + sizePolicy2.setHeightForWidth(sizeSlider->sizePolicy().hasHeightForWidth()); + sizeSlider->setSizePolicy(sizePolicy2); + sizeSlider->setFocusPolicy(Qt::ClickFocus); + sizeSlider->setAutoFillBackground(false); + sizeSlider->setOrientation(Qt::Horizontal); + sizeSlider->setTickPosition(QSlider::NoTicks); + + sizeSliderContainer_layout->addWidget(sizeSlider); + + MainWindow->setCentralWidget(centralWidget); + menuBar = new QMenuBar(MainWindow); + menuBar->setObjectName("menuBar"); + menuBar->setGeometry(QRect(0, 0, 1058, 22)); + menuBar->setContextMenuPolicy(Qt::PreventContextMenu); + menuFile = new QMenu(menuBar); + menuFile->setObjectName("menuFile"); + menuView = new QMenu(menuBar); + menuView->setObjectName("menuView"); + menuGame_List_Icons = new QMenu(menuView); + menuGame_List_Icons->setObjectName("menuGame_List_Icons"); + menuGame_List_Mode = new QMenu(menuView); + menuGame_List_Mode->setObjectName("menuGame_List_Mode"); + menuSettings = new QMenu(menuBar); + menuSettings->setObjectName("menuSettings"); + menuThemes = new QMenu(menuView); + menuThemes->setObjectName("menuThemes"); + MainWindow->setMenuBar(menuBar); + toolBar = new QToolBar(MainWindow); + toolBar->setObjectName("toolBar"); + MainWindow->addToolBar(Qt::TopToolBarArea, toolBar); + + menuBar->addAction(menuFile->menuAction()); + menuBar->addAction(menuView->menuAction()); + menuBar->addAction(menuSettings->menuAction()); + menuFile->addAction(bootInstallPkgAct); + menuFile->addSeparator(); + menuFile->addAction(exitAct); + menuView->addAction(showGameListAct); + menuView->addSeparator(); + menuView->addAction(refreshGameListAct); + menuView->addAction(menuGame_List_Mode->menuAction()); + menuView->addAction(menuGame_List_Icons->menuAction()); + menuView->addAction(menuThemes->menuAction()); + menuThemes->addAction(setThemeLight); + menuThemes->addAction(setThemeDark); + menuThemes->addAction(setThemeGreen); + menuThemes->addAction(setThemeBlue); + menuThemes->addAction(setThemeViolet); + menuGame_List_Icons->addAction(setIconSizeTinyAct); + menuGame_List_Icons->addAction(setIconSizeSmallAct); + menuGame_List_Icons->addAction(setIconSizeMediumAct); + menuGame_List_Icons->addAction(setIconSizeLargeAct); + menuGame_List_Mode->addAction(setlistModeListAct); + menuGame_List_Mode->addAction(setlistModeGridAct); + menuSettings->addAction(gameInstallPathAct); + + retranslateUi(MainWindow); + + QMetaObject::connectSlotsByName(MainWindow); + } // setupUi + + void retranslateUi(QMainWindow* MainWindow) { + MainWindow->setWindowTitle(QCoreApplication::translate("MainWindow", "Shadps4", nullptr)); + bootInstallPkgAct->setText( + QCoreApplication::translate("MainWindow", "Install Packages (PKG)", nullptr)); +#if QT_CONFIG(tooltip) + bootInstallPkgAct->setToolTip(QCoreApplication::translate( + "MainWindow", "Install application from a .pkg file", nullptr)); +#endif // QT_CONFIG(tooltip) + exitAct->setText(QCoreApplication::translate("MainWindow", "Exit", nullptr)); +#if QT_CONFIG(tooltip) + exitAct->setToolTip(QCoreApplication::translate("MainWindow", "Exit Shadps4", nullptr)); +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(statustip) + exitAct->setStatusTip( + QCoreApplication::translate("MainWindow", "Exit the application.", nullptr)); +#endif // QT_CONFIG(statustip) + showGameListAct->setText( + QCoreApplication::translate("MainWindow", "Show Game List", nullptr)); + refreshGameListAct->setText( + QCoreApplication::translate("MainWindow", "Game List Refresh", nullptr)); + setIconSizeTinyAct->setText(QCoreApplication::translate("MainWindow", "Tiny", nullptr)); + setIconSizeSmallAct->setText(QCoreApplication::translate("MainWindow", "Small", nullptr)); + setIconSizeMediumAct->setText(QCoreApplication::translate("MainWindow", "Medium", nullptr)); + setIconSizeLargeAct->setText(QCoreApplication::translate("MainWindow", "Large", nullptr)); + setlistModeListAct->setText( + QCoreApplication::translate("MainWindow", "List View", nullptr)); + setlistModeGridAct->setText( + QCoreApplication::translate("MainWindow", "Grid View", nullptr)); + gameInstallPathAct->setText( + QCoreApplication::translate("MainWindow", "Game Install Directory", nullptr)); + mw_searchbar->setPlaceholderText( + QCoreApplication::translate("MainWindow", "Search...", nullptr)); + // darkModeSwitch->setText( + // QCoreApplication::translate("MainWindow", "Game", nullptr)); + menuFile->setTitle(QCoreApplication::translate("MainWindow", "File", nullptr)); + menuView->setTitle(QCoreApplication::translate("MainWindow", "View", nullptr)); + menuGame_List_Icons->setTitle( + QCoreApplication::translate("MainWindow", "Game List Icons", nullptr)); + menuGame_List_Mode->setTitle( + QCoreApplication::translate("MainWindow", "Game List Mode", nullptr)); + menuSettings->setTitle(QCoreApplication::translate("MainWindow", "Settings", nullptr)); + menuThemes->setTitle(QCoreApplication::translate("MainWindow", "Themes", nullptr)); + setThemeLight->setText(QCoreApplication::translate("MainWindow", "Light", nullptr)); + setThemeDark->setText(QCoreApplication::translate("MainWindow", "Dark", nullptr)); + setThemeGreen->setText(QCoreApplication::translate("MainWindow", "Green", nullptr)); + setThemeBlue->setText(QCoreApplication::translate("MainWindow", "Blue", nullptr)); + setThemeViolet->setText(QCoreApplication::translate("MainWindow", "Violet", nullptr)); + toolBar->setWindowTitle(QCoreApplication::translate("MainWindow", "toolBar", nullptr)); + } // retranslateUi +}; + +namespace Ui { +class MainWindow : public Ui_MainWindow {}; +} // namespace Ui + +QT_END_NAMESPACE + +#endif // MAIN_WINDOW_UI_H diff --git a/src/qt_gui/qt_utils.h b/src/qt_gui/qt_utils.h new file mode 100644 index 00000000..3964923e --- /dev/null +++ b/src/qt_gui/qt_utils.h @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +namespace gui { +namespace utils { +template +void stop_future_watcher(QFutureWatcher& watcher, bool cancel) { + if (watcher.isStarted() || watcher.isRunning()) { + if (cancel) { + watcher.cancel(); + } + watcher.waitForFinished(); + } +} +} // namespace utils +} // namespace gui diff --git a/src/qt_gui/settings.cpp b/src/qt_gui/settings.cpp new file mode 100644 index 00000000..b428bcda --- /dev/null +++ b/src/qt_gui/settings.cpp @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "settings.h" + +Settings::Settings(QObject* parent) : QObject(parent), m_settings_dir(ComputeSettingsDir()) {} + +Settings::~Settings() { + if (m_settings) { + m_settings->sync(); + } +} + +QString Settings::GetSettingsDir() const { + return m_settings_dir.absolutePath(); +} + +QString Settings::ComputeSettingsDir() { + return ""; // TODO currently we configure same dir , make it configurable +} + +void Settings::RemoveValue(const QString& key, const QString& name) const { + if (m_settings) { + m_settings->beginGroup(key); + m_settings->remove(name); + m_settings->endGroup(); + } +} + +void Settings::RemoveValue(const GuiSave& entry) const { + RemoveValue(entry.key, entry.name); +} + +QVariant Settings::GetValue(const QString& key, const QString& name, const QVariant& def) const { + return m_settings ? m_settings->value(key + "/" + name, def) : def; +} + +QVariant Settings::GetValue(const GuiSave& entry) const { + return GetValue(entry.key, entry.name, entry.def); +} + +QVariant Settings::List2Var(const q_pair_list& list) { + QByteArray ba; + QDataStream stream(&ba, QIODevice::WriteOnly); + stream << list; + return QVariant(ba); +} + +q_pair_list Settings::Var2List(const QVariant& var) { + q_pair_list list; + QByteArray ba = var.toByteArray(); + QDataStream stream(&ba, QIODevice::ReadOnly); + stream >> list; + return list; +} + +void Settings::SetValue(const GuiSave& entry, const QVariant& value) const { + if (m_settings) { + m_settings->beginGroup(entry.key); + m_settings->setValue(entry.name, value); + m_settings->endGroup(); + } +} + +void Settings::SetValue(const QString& key, const QVariant& value) const { + if (m_settings) { + m_settings->setValue(key, value); + } +} + +void Settings::SetValue(const QString& key, const QString& name, const QVariant& value) const { + if (m_settings) { + m_settings->beginGroup(key); + m_settings->setValue(name, value); + m_settings->endGroup(); + } +} diff --git a/src/qt_gui/settings.h b/src/qt_gui/settings.h new file mode 100644 index 00000000..1e6d1a65 --- /dev/null +++ b/src/qt_gui/settings.h @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include +#include +#include + +#include "gui_save.h" + +typedef QPair q_string_pair; +typedef QPair q_size_pair; +typedef QList q_pair_list; +typedef QList q_size_list; + +// Parent Class for GUI settings +class Settings : public QObject { + Q_OBJECT + +public: + explicit Settings(QObject* parent = nullptr); + ~Settings(); + + QString GetSettingsDir() const; + + QVariant GetValue(const QString& key, const QString& name, const QVariant& def) const; + QVariant GetValue(const GuiSave& entry) const; + static QVariant List2Var(const q_pair_list& list); + static q_pair_list Var2List(const QVariant& var); + +public Q_SLOTS: + /** Remove entry */ + void RemoveValue(const QString& key, const QString& name) const; + void RemoveValue(const GuiSave& entry) const; + + /** Write value to entry */ + void SetValue(const GuiSave& entry, const QVariant& value) const; + void SetValue(const QString& key, const QVariant& value) const; + void SetValue(const QString& key, const QString& name, const QVariant& value) const; + +protected: + static QString ComputeSettingsDir(); + + std::unique_ptr m_settings; + QDir m_settings_dir; +}; \ No newline at end of file