diff --git a/.gitignore b/.gitignore index ba8935de..517c46c6 100644 --- a/.gitignore +++ b/.gitignore @@ -400,3 +400,4 @@ FodyWeavers.xsd /shadPS4/psf.txt /tools/pkg extractor/game/* /tools/pkg extractor/*.pkg +/shadPS4/shadps4.ini diff --git a/shadPS4/gui/custom_dock_widget.h b/shadPS4/gui/custom_dock_widget.h new file mode 100644 index 00000000..ece514c1 --- /dev/null +++ b/shadPS4/gui/custom_dock_widget.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include + +class custom_dock_widget : public QDockWidget +{ +private: + std::shared_ptr m_title_bar_widget; + bool m_is_title_bar_visible = true; + +public: + explicit custom_dock_widget(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/shadPS4/gui/custom_table_widget_item.cpp b/shadPS4/gui/custom_table_widget_item.cpp index 6b418046..26a78815 100644 --- a/shadPS4/gui/custom_table_widget_item.cpp +++ b/shadPS4/gui/custom_table_widget_item.cpp @@ -3,7 +3,7 @@ #include custom_table_widget_item::custom_table_widget_item(const std::string& text, int sort_role, const QVariant& sort_value) - : QTableWidgetItem(QString::fromStdString(text).simplified()) // simplified() forces single line text + : game_list_item(QString::fromStdString(text).simplified()) // simplified() forces single line text { if (sort_role != Qt::DisplayRole) { @@ -12,7 +12,7 @@ custom_table_widget_item::custom_table_widget_item(const std::string& text, int } custom_table_widget_item::custom_table_widget_item(const QString& text, int sort_role, const QVariant& sort_value) - : QTableWidgetItem(text.simplified()) // simplified() forces single line text + : game_list_item(text.simplified()) // simplified() forces single line text { if (sort_role != Qt::DisplayRole) { diff --git a/shadPS4/gui/custom_table_widget_item.h b/shadPS4/gui/custom_table_widget_item.h index 00ffc18a..0ca88e48 100644 --- a/shadPS4/gui/custom_table_widget_item.h +++ b/shadPS4/gui/custom_table_widget_item.h @@ -1,7 +1,8 @@ #pragma once +#include "game_list_item.h" #include -class custom_table_widget_item : public QTableWidgetItem +class custom_table_widget_item : public game_list_item { private: int m_sort_role = Qt::DisplayRole; diff --git a/shadPS4/gui/game_list_frame.cpp b/shadPS4/gui/game_list_frame.cpp index f01313e8..f5a10083 100644 --- a/shadPS4/gui/game_list_frame.cpp +++ b/shadPS4/gui/game_list_frame.cpp @@ -1,6 +1,511 @@ #include "game_list_frame.h" #include "gui_settings.h" +#include "custom_table_widget_item.h" +#include "qt_utils.h" +#include "../emulator/fileFormat/PSF.h" +#include +#include +game_list_frame::game_list_frame(std::shared_ptr gui_settings, QWidget* parent) + : custom_dock_widget(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 game_list_grid(QSize(), m_icon_color, m_margin_factor, m_text_factor, false); + + m_game_list = new game_list_table(); + 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->horizontalHeader()->setDefaultSectionSize(150); + m_game_list->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft); + m_game_list->setContextMenuPolicy(Qt::CustomContextMenu); + m_game_list->setAlternatingRowColors(true); + m_game_list->installEventFilter(this); + m_game_list->setColumnCount(gui::column_count); + + 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) + { + m_game_list->setHorizontalHeaderItem(col, new QTableWidgetItem(header_text)); + 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_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, &game_list_frame::OnHeaderColumnClicked); + connect(&m_repaint_watcher, &QFutureWatcher::resultReadyAt, this, [this](int index) + { + if (!m_is_list_layout) return; + if (game_list_item* item = m_repaint_watcher.resultAt(index)) + { + item->call_icon_func(); + } + }); + connect(&m_repaint_watcher, &QFutureWatcher::finished, this, &game_list_frame::OnRepaintFinished); + + connect(&m_refresh_watcher, &QFutureWatcher::finished, this, &game_list_frame::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(); + }); +} +game_list_frame::~game_list_frame() { + gui::utils::stop_future_watcher(m_repaint_watcher, true); + gui::utils::stop_future_watcher(m_refresh_watcher, true); + SaveSettings(); +} + +void game_list_frame::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 game_list_frame::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); + } +} + +bool game_list_frame::IsEntryVisible(const game_info& game) +{ + const QString serial = QString::fromStdString(game->info.serial); + return SearchMatchesApp(QString::fromStdString(game->info.name), serial); +} + +void game_list_frame::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 game_list_grid(image_size, image_color, m_margin_factor, m_text_factor * 2, show_text); + } + else + { + m_game_grid = new game_list_grid(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)); + + game_list_item* 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) + { + game_list_item* empty_item = new game_list_item(); + 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 game_list_frame::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(QString::fromStdString(QDir::currentPath().toStdString() + "/game/")); + QFileInfoList fList = parent_folder.entryInfoList(QDir::AllDirs | 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"); + game.icon_path = iconpath.toStdString(); + game.name = psf.get_string("TITLE"); + game.serial = psf.get_string("TITLE_ID"); + game.fw = (QString("%1").arg(psf.get_integer("SYSTEM_VER"), 8, 16, QLatin1Char('0'))).mid(1, 3).insert(1, '.').toStdString(); + game.version = psf.get_string("APP_VER"); + game.category = psf.get_string("CATEGORY"); + + m_titles.insert(QString::fromStdString(game.serial), QString::fromStdString(game.name)); + + gui_game_info 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 game_list_frame::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; + } + + const QString serial = QString::fromStdString(game->info.serial); + const QString title = m_titles.value(serial, QString::fromStdString(game->info.name)); + + // Icon + custom_table_widget_item* icon_item = new custom_table_widget_item; + 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)); + + // Title + custom_table_widget_item* title_item = new custom_table_widget_item(title); + + // Serial + custom_table_widget_item* serial_item = new custom_table_widget_item(serial); + + // Version + QString app_version = QString::fromStdString(game->info.version); + + m_game_list->setItem(row, gui::column_icon, icon_item); + m_game_list->setItem(row, gui::column_name, title_item); + m_game_list->setItem(row, gui::column_serial, serial_item); + m_game_list->setItem(row, gui::column_firmware, new custom_table_widget_item(game->info.fw)); + m_game_list->setItem(row, gui::column_version, new custom_table_widget_item(app_version)); + m_game_list->setItem(row, gui::column_category, new custom_table_widget_item(game->info.category)); + m_game_list->setItem(row, gui::column_path, new custom_table_widget_item(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 game_list_frame::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 game_list_frame::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) -> game_list_item* + { + 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 game_list_frame::FixNarrowColumns() const { @@ -44,6 +549,26 @@ void game_list_frame::ResizeColumnsToContents(int spacing) const } } +void game_list_frame::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 game_list_frame::SortGameList() const { // Back-up old header sizes to handle unwanted column resize in case of zero search results @@ -109,4 +634,150 @@ void game_list_frame::SortGameList() const // Shorten the last section to remove horizontal scrollbar if possible m_game_list->resizeColumnToContents(gui::column_count - 1); -} \ No newline at end of file +} + +QPixmap game_list_frame::PaintedPixmap(const QPixmap& icon) const +{ + const qreal device_pixel_ratio = devicePixelRatioF(); + QSize canvas_size(320, 176); + 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 PS3's ICON0.PNG + if (icon_size.width() < 320 || icon_size.height() < 176) + { + icon_size.scale(320, 176, Qt::KeepAspectRatio); + } + + canvas_size = icon_size; + + // Calculate the centered size and position of the icon on our canvas. + if (icon_size.width() != 320 || icon_size.height() != 176) + { + constexpr double target_ratio = 320.0 / 176.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 game_list_frame::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 game_list_frame::SetSearchText(const QString& text) +{ + m_search_text = text; + Refresh(); +} +void game_list_frame::closeEvent(QCloseEvent* event) +{ + QDockWidget::closeEvent(event); + Q_EMIT GameListFrameClosed(); +} + +void game_list_frame::resizeEvent(QResizeEvent* event) +{ + if (!m_is_list_layout) + { + Refresh(false, m_game_grid->selectedItems().count()); + } + QDockWidget::resizeEvent(event); +} +void game_list_frame::ResizeIcons(const int& slider_pos) +{ + m_icon_size_index = slider_pos; + m_icon_size = gui_settings::SizeFromSlider(slider_pos); + + RepaintIcons(); +} + +void game_list_frame::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 game_list_frame::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 game_list_frame::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/shadPS4/gui/game_list_frame.h b/shadPS4/gui/game_list_frame.h index 0443ed9f..b9e76785 100644 --- a/shadPS4/gui/game_list_frame.h +++ b/shadPS4/gui/game_list_frame.h @@ -1,33 +1,104 @@ #pragma once #include "game_list_table.h" +#include "custom_dock_widget.h" #include "shadps4gui.h" - +#include "game_list_grid.h" +#include "game_list_item.h" #include +#include +#include +#include +#include +#include +#include -class game_list_frame +class game_list_frame : public custom_dock_widget { + Q_OBJECT public : + explicit game_list_frame(std::shared_ptr gui_settings,QWidget* parent = nullptr); + ~game_list_frame(); /** 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(); +Q_SIGNALS: + void GameListFrameClosed(); + void RequestIconSizeChange(const int& val); +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); + + // 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 + game_list_grid* m_game_grid = nullptr; // Game List game_list_table* m_game_list = nullptr; QList m_columnActs; Qt::SortOrder m_col_sort_order; int m_sort_column; + QMap m_titles; + + // 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::deque 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; }; diff --git a/shadPS4/gui/game_list_grid.cpp b/shadPS4/gui/game_list_grid.cpp index aeef3e50..bc090b99 100644 --- a/shadPS4/gui/game_list_grid.cpp +++ b/shadPS4/gui/game_list_grid.cpp @@ -1,6 +1,6 @@ #include "game_list_grid.h" #include "game_list_grid_delegate.h" - +#include "game_list_item.h" #include #include @@ -56,47 +56,49 @@ void game_list_grid::setIconSize(const QSize& size) const } } -QTableWidgetItem* game_list_grid::addItem(const game_info& app, const QString& name, const QString& movie_path, const int& row, const int& col) +game_list_item* game_list_grid::addItem(const game_info& app, const QString& name,const int& row, const int& col) { - 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) + game_list_item* item = new game_list_item; + item->set_icon_func([this, app, item](int) { - 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; - } + const qreal device_pixel_ratio = devicePixelRatioF(); - // 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(); + // 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; + } - // 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); + // 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 background for image - QImage bg_img(app->pxmap.size(), QImage::Format_ARGB32); - bg_img.setDevicePixelRatio(device_pixel_ratio); - bg_img.fill(m_icon_color); + // 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); - // 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 background for image + QImage bg_img(app->pxmap.size(), QImage::Format_ARGB32); + bg_img.setDevicePixelRatio(device_pixel_ratio); + bg_img.fill(m_icon_color); - // create item with expanded image, title and position - QTableWidgetItem* item = new QTableWidgetItem(); - item->setData(Qt::ItemDataRole::DecorationRole, QPixmap::fromImage(exp_img)); + // 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); diff --git a/shadPS4/gui/game_list_grid.h b/shadPS4/gui/game_list_grid.h index 179c2cf7..b403da5f 100644 --- a/shadPS4/gui/game_list_grid.h +++ b/shadPS4/gui/game_list_grid.h @@ -19,7 +19,7 @@ public: void enableText(const bool& enabled); void setIconSize(const QSize& size) const; - QTableWidgetItem* addItem(const game_info& app, const QString& name, const QString& movie_path, const int& row, const int& col); + game_list_item* addItem(const game_info& app, const QString& name,const int& row, const int& col); [[nodiscard]] qreal getMarginFactor() const; diff --git a/shadPS4/gui/game_list_item.h b/shadPS4/gui/game_list_item.h new file mode 100644 index 00000000..e000be0a --- /dev/null +++ b/shadPS4/gui/game_list_item.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +#include + +using icon_callback_t = std::function; + +class game_list_item : public QTableWidgetItem +{ +public: + game_list_item() : QTableWidgetItem() + { + } + game_list_item(const QString& text, int type = Type) : QTableWidgetItem(text, type) + { + } + game_list_item(const QIcon& icon, const QString& text, int type = Type) : QTableWidgetItem(icon, text, type) + { + } + + ~game_list_item() + { + + } + + 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/shadPS4/gui/game_list_table.h b/shadPS4/gui/game_list_table.h index 3678a08c..26a16bf7 100644 --- a/shadPS4/gui/game_list_table.h +++ b/shadPS4/gui/game_list_table.h @@ -3,12 +3,14 @@ #include #include #include "../emulator/gameInfo.h" +#include "game_list_item.h" struct gui_game_info { GameInfo info{}; QPixmap icon; QPixmap pxmap; + game_list_item* item = nullptr; }; typedef std::shared_ptr game_info; diff --git a/shadPS4/gui/gui_save.h b/shadPS4/gui/gui_save.h new file mode 100644 index 00000000..66970172 --- /dev/null +++ b/shadPS4/gui/gui_save.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +struct gui_save +{ + QString key; + QString name; + QVariant def; + + gui_save() + { + key = ""; + name = ""; + def = QVariant(); + } + + gui_save(const QString& k, const QString& n, const QVariant& d) + { + key = k; + name = n; + def = d; + } + + bool operator==(const gui_save& rhs) const noexcept + { + return key == rhs.key && name == rhs.name && def == rhs.def; + } +}; diff --git a/shadPS4/gui/gui_settings.cpp b/shadPS4/gui/gui_settings.cpp index 4baf9373..d3353631 100644 --- a/shadPS4/gui/gui_settings.cpp +++ b/shadPS4/gui/gui_settings.cpp @@ -3,6 +3,24 @@ gui_settings::gui_settings(QObject* parent) { - m_settings.reset(new QSettings("shadps4.ini", QSettings::Format::IniFormat, parent)); + m_settings.reset(new QSettings("shadps4.ini", QSettings::Format::IniFormat, parent)); //TODO make the path configurable } +void gui_settings::SetGamelistColVisibility(int col, bool val) const +{ + SetValue(GetGuiSaveForColumn(col), val); +} + +bool gui_settings::GetGamelistColVisibility(int col) const +{ + return GetValue(GetGuiSaveForColumn(col)).toBool(); +} + +gui_save gui_settings::GetGuiSaveForColumn(int col) +{ + return gui_save{ gui::game_list, "visibility_" + gui::get_game_list_column_name(static_cast(col)), true }; +} +QSize gui_settings::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/shadPS4/gui/gui_settings.h b/shadPS4/gui/gui_settings.h index 831b6183..6ddb2d31 100644 --- a/shadPS4/gui/gui_settings.h +++ b/shadPS4/gui/gui_settings.h @@ -1,9 +1,15 @@ #pragma once -#include +#include "settings.h" +#include namespace gui { + enum custom_roles + { + game_role = Qt::UserRole + 1337, + }; + enum game_list_columns { column_icon, @@ -17,15 +23,81 @@ namespace gui 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_version: + return "column_version"; + case column_category: + return "column_category"; + case column_path: + return "column_path"; + case column_count: + return ""; + } + + throw std::exception("get_game_list_column_name: Invalid column"); + } + + const QSize game_list_icon_size_min = QSize(40, 22); + const QSize game_list_icon_size_small = QSize(80, 44); + const QSize game_list_icon_size_medium = QSize(160, 88); + const QSize game_list_icon_size_max = QSize(320, 176); + + 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 QColor game_list_icon_color = QColor(240, 240, 240, 255); + + const gui_save main_window_gamelist_visible = gui_save(main_window, "gamelistVisible", true); + const gui_save main_window_geometry = gui_save(main_window, "geometry", QByteArray()); + const gui_save main_window_windowState = gui_save(main_window, "windowState", QByteArray()); + const gui_save main_window_mwState = gui_save(main_window, "mwState", QByteArray()); + + const gui_save game_list_sortAsc = gui_save(game_list, "sortAsc", true); + const gui_save game_list_sortCol = gui_save(game_list, "sortCol", 1); + const gui_save game_list_state = gui_save(game_list, "state", QByteArray()); + const gui_save game_list_iconSize = gui_save(game_list, "iconSize", get_Index(game_list_icon_size_small)); + const gui_save game_list_iconSizeGrid = gui_save(game_list, "iconSizeGrid", get_Index(game_list_icon_size_small)); + const gui_save game_list_iconColor = gui_save(game_list, "iconColor", game_list_icon_color); + const gui_save game_list_listMode = gui_save(game_list, "listMode", true); + const gui_save game_list_textFactor = gui_save(game_list, "textFactor", qreal{ 2.0 }); + const gui_save game_list_marginFactor = gui_save(game_list, "marginFactor", qreal{ 0.09 }); + + } -class gui_settings +class gui_settings : public settings { + Q_OBJECT public: explicit gui_settings(QObject* parent = nullptr); -private: - std::unique_ptr m_settings; + bool GetGamelistColVisibility(int col) const; + + +public Q_SLOTS: + void SetGamelistColVisibility(int col, bool val) const; + static gui_save GetGuiSaveForColumn(int col); + static QSize SizeFromSlider(int pos); }; diff --git a/shadPS4/gui/main_window.cpp b/shadPS4/gui/main_window.cpp new file mode 100644 index 00000000..fb6b1700 --- /dev/null +++ b/shadPS4/gui/main_window.cpp @@ -0,0 +1,232 @@ +#include "game_list_frame.h" +#include "main_window.h" +#include "gui_settings.h" +#include "ui_main_window.h" + +main_window::main_window(std::shared_ptr gui_settings, QWidget* parent) + : QMainWindow(parent) + , ui(new Ui::main_window) + , m_gui_settings(std::move(gui_settings)) +{ + + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose); +} + +main_window::~main_window() +{ + SaveWindowState(); +} + +bool main_window::Init() +{ + // add toolbar widgets + 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(); + + 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 main_window::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); + +} + +void main_window::CreateDockWindows() +{ + m_main_window = new QMainWindow(); + m_main_window->setContextMenuPolicy(Qt::PreventContextMenu); + + m_game_list_frame = new game_list_frame(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, &game_list_frame::GameListFrameClosed, this, [this]() + { + if (ui->showGameListAct->isChecked()) + { + ui->showGameListAct->setChecked(false); + m_gui_settings->SetValue(gui::main_window_gamelist_visible, false); + } + }); +} +void main_window::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, &game_list_frame::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, &main_window::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, &game_list_frame::SetSearchText); +} + +void main_window::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 main_window::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 main_window::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 main_window::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(); +} \ No newline at end of file diff --git a/shadPS4/gui/main_window.h b/shadPS4/gui/main_window.h new file mode 100644 index 00000000..9a945e34 --- /dev/null +++ b/shadPS4/gui/main_window.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +class gui_settings; +class game_list_frame; + +namespace Ui +{ + class main_window; +} + +class main_window : 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 main_window(std::shared_ptr gui_settings,QWidget* parent = nullptr); + ~main_window(); + bool Init(); + +private Q_SLOTS: + void ConfigureGuiFromSettings(); + void SetIconSizeActions(int idx) const; + void ResizeIcons(int index); + void SaveWindowState() const; +private: + void CreateActions(); + void CreateDockWindows(); + void CreateConnects(); + + QActionGroup* m_icon_size_act_group = nullptr; + QActionGroup* m_list_mode_act_group = nullptr; + + // Dockable widget frames + QMainWindow* m_main_window = nullptr; + game_list_frame* m_game_list_frame = nullptr; + + std::shared_ptr m_gui_settings; + +}; + diff --git a/shadPS4/gui/main_window.ui b/shadPS4/gui/main_window.ui new file mode 100644 index 00000000..bbe343a4 --- /dev/null +++ b/shadPS4/gui/main_window.ui @@ -0,0 +1,297 @@ + + + main_window + + + + 0 + 0 + 1058 + 580 + + + + + 0 + 0 + + + + + 4 + 0 + + + + RPCS3 + + + false + + + true + + + true + + + QMainWindow::AllowNestedDocks|QMainWindow::AllowTabbedDocks|QMainWindow::AnimatedDocks|QMainWindow::GroupedDragging + + + + + 0 + 0 + + + + + + 480 + 10 + 251 + 31 + + + + + 0 + 0 + + + + + 10 + false + + + + Qt::ClickFocus + + + false + + + Search... + + + false + + + + + + 280 + 10 + 181 + 31 + + + + + 0 + 0 + + + + + 0 + + + 14 + + + 0 + + + 14 + + + 0 + + + + + + 0 + 0 + + + + Qt::ClickFocus + + + false + + + Qt::Horizontal + + + QSlider::NoTicks + + + + + + + + + + 0 + 0 + 1058 + 22 + + + + Qt::PreventContextMenu + + + + File + + + + + + + + View + + + + Game List Icons + + + + + + + + + Game List Mode + + + + + + + + + + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + Install Packages (PKG) + + + Install application from a .pkg file + + + + + Install Firmware + + + Install firmware from PS3UPDAT.PUP + + + + + Exit + + + Exit RPCS3 + + + Exit the application. + + + + + true + + + Show Game List + + + + + Game List Refresh + + + + + true + + + Tiny + + + + + true + + + true + + + Small + + + + + true + + + Medium + + + + + true + + + Large + + + + + true + + + true + + + List View + + + + + true + + + Grid View + + + + + + + + + diff --git a/shadPS4/gui/qt_utils.h b/shadPS4/gui/qt_utils.h new file mode 100644 index 00000000..00ad829b --- /dev/null +++ b/shadPS4/gui/qt_utils.h @@ -0,0 +1,23 @@ +#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(); + } + } + } // utils +} // gui + diff --git a/shadPS4/gui/settings.cpp b/shadPS4/gui/settings.cpp new file mode 100644 index 00000000..9867b2d7 --- /dev/null +++ b/shadPS4/gui/settings.cpp @@ -0,0 +1,94 @@ +#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 gui_save& 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 gui_save& 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 gui_save& 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/shadPS4/gui/settings.h b/shadPS4/gui/settings.h new file mode 100644 index 00000000..7116edab --- /dev/null +++ b/shadPS4/gui/settings.h @@ -0,0 +1,48 @@ +#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 gui_save& 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 gui_save& entry) const; + + /** Write value to entry */ + void SetValue(const gui_save& 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 diff --git a/shadPS4/gui/shadps4gui.cpp b/shadPS4/gui/shadps4gui.cpp index 3b2b4214..849aebdb 100644 --- a/shadPS4/gui/shadps4gui.cpp +++ b/shadPS4/gui/shadps4gui.cpp @@ -12,11 +12,15 @@ shadps4gui::shadps4gui(std::shared_ptr gui_settings, QWidget* pare , m_gui_settings(std::move(gui_settings)) { ui.setupUi(this); - game_list = new GameListViewer(); - game_list->SetGamePath(QDir::currentPath() + "/game/"); - ui.horizontalLayout->addWidget(game_list); + //game_list = new GameListViewer(); + //game_list->SetGamePath(QDir::currentPath() + "/game/"); + //ui.horizontalLayout->addWidget(game_list); + //show(); + //game_list->PopulateAsync(); + game_list_frame* game_list2 = new game_list_frame(m_gui_settings); + ui.horizontalLayout->addWidget(game_list2); + game_list2->LoadSettings(); show(); - game_list->PopulateAsync(); } shadps4gui::~shadps4gui() diff --git a/shadPS4/gui/shadps4gui.h b/shadPS4/gui/shadps4gui.h index 9f65ae78..ed559002 100644 --- a/shadPS4/gui/shadps4gui.h +++ b/shadPS4/gui/shadps4gui.h @@ -4,6 +4,7 @@ #include "ui_shadps4gui.h" #include "GameListViewer.h" #include "gui_settings.h" +#include "game_list_frame.h" class shadps4gui : public QMainWindow { diff --git a/shadPS4/main.cpp b/shadPS4/main.cpp index baaf71a0..ebed39f5 100644 --- a/shadPS4/main.cpp +++ b/shadPS4/main.cpp @@ -1,13 +1,15 @@ #include "gui/shadps4gui.h" #include #include "gui/gui_settings.h" +#include "gui/main_window.h" int main(int argc, char* argv[]) { QApplication a(argc, argv); std::shared_ptr m_gui_settings; m_gui_settings.reset(new gui_settings()); - shadps4gui w(m_gui_settings,nullptr); - w.show(); + main_window* m_main_window = new main_window(m_gui_settings, nullptr); + m_main_window->Init(); + return a.exec(); } \ No newline at end of file diff --git a/shadPS4/shadPS4.vcxproj b/shadPS4/shadPS4.vcxproj index 817400ef..86b503fa 100644 --- a/shadPS4/shadPS4.vcxproj +++ b/shadPS4/shadPS4.vcxproj @@ -22,6 +22,8 @@ + + @@ -29,6 +31,7 @@ + @@ -40,12 +43,18 @@ + - + + - + + + + + @@ -70,12 +79,12 @@ 6.4.2 - core;gui;widgets + core;gui;widgets;concurrent debug 6.4.2 - core;gui;widgets + core;gui;widgets;concurrent release diff --git a/shadPS4/shadPS4.vcxproj.filters b/shadPS4/shadPS4.vcxproj.filters index 8280e965..eff56824 100644 --- a/shadPS4/shadPS4.vcxproj.filters +++ b/shadPS4/shadPS4.vcxproj.filters @@ -33,6 +33,12 @@ {ed31734c-f010-4590-9f01-18e0b2497ffb} + + {b4b58418-b124-41bb-96ef-610faa1c8812} + + + {731089fd-7d11-4f10-8f55-4854c2e42e9e} + @@ -59,26 +65,35 @@ gui - - gui - - - gui - - - gui - - + gui - gui + gui\game list + + + gui\game list + + + gui\game list + + + gui\game list + + + gui\game list + + + gui\main window Form Files + + gui\main window + @@ -87,9 +102,21 @@ gui - + gui + + gui + + + gui\game list + + + gui\game list + + + gui\main window + @@ -107,23 +134,29 @@ emulator\fileFormat - - gui - Header Files - + + gui + + + Header Files + + + Header Files + + gui - gui + gui\game list - gui + gui\game list - - gui + + gui\game list \ No newline at end of file