mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-20 11:03:56 +00:00
feat: Add grid view with round icons for game list
- Implement toggle between list and grid view modes (Ctrl+G) - Add round icon rendering with anti-aliased circular clipping - Display game titles below icons in grid layout - Create flat model for grid view showing only games - Add View menu option and Ctrl+G hotkey for toggling - Implement separate filtering logic for both view modes - Update grid layout with proper spacing and text alignment - Maintain existing tree view functionality and features - Support double-click to launch and right-click context menus in both modes The grid view provides a more visual game browsing experience while preserving all existing functionality of the original list view. Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
|
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <regex>
|
#include <regex>
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/file_sys/patch_manager.h"
|
#include "core/file_sys/patch_manager.h"
|
||||||
#include "core/file_sys/registered_cache.h"
|
#include "core/file_sys/registered_cache.h"
|
||||||
@@ -187,12 +189,99 @@ void GameList::OnItemExpanded(const QModelIndex& item) {
|
|||||||
// Event in order to filter the gamelist after editing the searchfield
|
// Event in order to filter the gamelist after editing the searchfield
|
||||||
void GameList::OnTextChanged(const QString& new_text) {
|
void GameList::OnTextChanged(const QString& new_text) {
|
||||||
QString edit_filter_text = new_text.toLower();
|
QString edit_filter_text = new_text.toLower();
|
||||||
|
|
||||||
|
if (list_view->isVisible()) {
|
||||||
|
// Grid view filtering - simpler approach
|
||||||
|
FilterGridView(edit_filter_text);
|
||||||
|
} else {
|
||||||
|
// Tree view filtering - existing logic
|
||||||
|
FilterTreeView(edit_filter_text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameList::FilterGridView(const QString& filter_text) {
|
||||||
|
// Repopulate the grid view with filtered items
|
||||||
|
QStandardItemModel* hierarchical_model = item_model;
|
||||||
|
|
||||||
|
// Create a new flat model for grid view
|
||||||
|
QStandardItemModel* flat_model = new QStandardItemModel(this);
|
||||||
|
|
||||||
|
int visible_count = 0;
|
||||||
|
int total_count = 0;
|
||||||
|
|
||||||
|
// Collect all games from the hierarchical model
|
||||||
|
for (int i = 0; i < hierarchical_model->rowCount(); ++i) {
|
||||||
|
QStandardItem* folder = hierarchical_model->item(i, 0);
|
||||||
|
if (!folder) continue;
|
||||||
|
|
||||||
|
// Skip non-game folders in grid view, but include favorites
|
||||||
|
const auto folder_type = folder->data(GameListItem::TypeRole).value<GameListItemType>();
|
||||||
|
if (folder_type == GameListItemType::AddDir) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add games from this folder to the flat model
|
||||||
|
for (int j = 0; j < folder->rowCount(); ++j) {
|
||||||
|
QStandardItem* game_item = folder->child(j, 0);
|
||||||
|
if (!game_item) continue;
|
||||||
|
|
||||||
|
const auto game_type = game_item->data(GameListItem::TypeRole).value<GameListItemType>();
|
||||||
|
if (game_type == GameListItemType::Game) {
|
||||||
|
total_count++;
|
||||||
|
|
||||||
|
bool should_show = true;
|
||||||
|
|
||||||
|
if (!filter_text.isEmpty()) {
|
||||||
|
const QString file_path = game_item->data(GameListItemPath::FullPathRole).toString().toLower();
|
||||||
|
const QString file_title = game_item->data(GameListItemPath::TitleRole).toString().toLower();
|
||||||
|
const auto program_id = game_item->data(GameListItemPath::ProgramIdRole).toULongLong();
|
||||||
|
const QString file_program_id = QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char{'0'});
|
||||||
|
|
||||||
|
const QString file_name = file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} + file_title;
|
||||||
|
|
||||||
|
should_show = ContainsAllWords(file_name, filter_text) ||
|
||||||
|
(file_program_id.count() == 16 && file_program_id.contains(filter_text));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (should_show) {
|
||||||
|
// Clone the game item for the flat model
|
||||||
|
QStandardItem* cloned_item = game_item->clone();
|
||||||
|
|
||||||
|
// Set display text to just the game title for grid view
|
||||||
|
QString game_title = game_item->data(GameListItemPath::TitleRole).toString();
|
||||||
|
if (game_title.isEmpty()) {
|
||||||
|
// Fallback to filename if no title
|
||||||
|
std::string filename;
|
||||||
|
Common::SplitPath(game_item->data(GameListItemPath::FullPathRole).toString().toStdString(),
|
||||||
|
nullptr, &filename, nullptr);
|
||||||
|
game_title = QString::fromStdString(filename);
|
||||||
|
}
|
||||||
|
cloned_item->setText(game_title);
|
||||||
|
|
||||||
|
flat_model->appendRow(cloned_item);
|
||||||
|
visible_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the flat model for the list view
|
||||||
|
list_view->setModel(flat_model);
|
||||||
|
|
||||||
|
// Update grid size based on icon size
|
||||||
|
const u32 icon_size = UISettings::values.game_icon_size.GetValue();
|
||||||
|
list_view->setGridSize(QSize(icon_size + 60, icon_size + 80)); // More padding for round icons and text
|
||||||
|
|
||||||
|
search_field->setFilterResult(visible_count, total_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameList::FilterTreeView(const QString& filter_text) {
|
||||||
QStandardItem* folder;
|
QStandardItem* folder;
|
||||||
int children_total = 0;
|
int children_total = 0;
|
||||||
|
|
||||||
// If the searchfield is empty every item is visible
|
// If the searchfield is empty every item is visible
|
||||||
// Otherwise the filter gets applied
|
// Otherwise the filter gets applied
|
||||||
if (edit_filter_text.isEmpty()) {
|
if (filter_text.isEmpty()) {
|
||||||
tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
|
tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
|
||||||
UISettings::values.favorited_ids.size() == 0);
|
UISettings::values.favorited_ids.size() == 0);
|
||||||
for (int i = 1; i < item_model->rowCount() - 1; ++i) {
|
for (int i = 1; i < item_model->rowCount() - 1; ++i) {
|
||||||
@@ -234,8 +323,8 @@ void GameList::OnTextChanged(const QString& new_text) {
|
|||||||
const QString file_name =
|
const QString file_name =
|
||||||
file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} +
|
file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} +
|
||||||
file_title;
|
file_title;
|
||||||
if (ContainsAllWords(file_name, edit_filter_text) ||
|
if (ContainsAllWords(file_name, filter_text) ||
|
||||||
(file_program_id.count() == 16 && file_program_id.contains(edit_filter_text))) {
|
(file_program_id.count() == 16 && file_program_id.contains(filter_text))) {
|
||||||
tree_view->setRowHidden(j, folder_index, false);
|
tree_view->setRowHidden(j, folder_index, false);
|
||||||
++result_count;
|
++result_count;
|
||||||
} else {
|
} else {
|
||||||
@@ -322,11 +411,14 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
|
|||||||
this->main_window = parent;
|
this->main_window = parent;
|
||||||
layout = new QVBoxLayout;
|
layout = new QVBoxLayout;
|
||||||
tree_view = new QTreeView;
|
tree_view = new QTreeView;
|
||||||
|
list_view = new QListView;
|
||||||
controller_navigation = new ControllerNavigation(system.HIDCore(), this);
|
controller_navigation = new ControllerNavigation(system.HIDCore(), this);
|
||||||
search_field = new GameListSearchField(this);
|
search_field = new GameListSearchField(this);
|
||||||
item_model = new QStandardItemModel(tree_view);
|
item_model = new QStandardItemModel(tree_view);
|
||||||
tree_view->setModel(item_model);
|
tree_view->setModel(item_model);
|
||||||
|
list_view->setModel(item_model);
|
||||||
|
|
||||||
|
// Configure tree view
|
||||||
tree_view->setAlternatingRowColors(true);
|
tree_view->setAlternatingRowColors(true);
|
||||||
tree_view->setSelectionMode(QHeaderView::SingleSelection);
|
tree_view->setSelectionMode(QHeaderView::SingleSelection);
|
||||||
tree_view->setSelectionBehavior(QHeaderView::SelectRows);
|
tree_view->setSelectionBehavior(QHeaderView::SelectRows);
|
||||||
@@ -337,6 +429,23 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
|
|||||||
tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
|
tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }"));
|
tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }"));
|
||||||
|
|
||||||
|
// Configure list view for grid display
|
||||||
|
list_view->setViewMode(QListView::IconMode);
|
||||||
|
list_view->setResizeMode(QListView::Adjust);
|
||||||
|
list_view->setUniformItemSizes(true);
|
||||||
|
list_view->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||||
|
list_view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||||
|
list_view->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||||
|
list_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||||
|
list_view->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
|
list_view->setStyleSheet(QStringLiteral("QListView{ border: none; background: transparent; } QListView::item { text-align: center; padding: 5px; }"));
|
||||||
|
list_view->setGridSize(QSize(140, 160)); // Initial size for round icons and text
|
||||||
|
list_view->setSpacing(10);
|
||||||
|
list_view->setWordWrap(true);
|
||||||
|
list_view->setTextElideMode(Qt::ElideRight);
|
||||||
|
list_view->setFlow(QListView::LeftToRight);
|
||||||
|
list_view->setWrapping(true);
|
||||||
|
|
||||||
item_model->insertColumns(0, COLUMN_COUNT);
|
item_model->insertColumns(0, COLUMN_COUNT);
|
||||||
RetranslateUI();
|
RetranslateUI();
|
||||||
|
|
||||||
@@ -350,6 +459,8 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
|
|||||||
connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu);
|
connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu);
|
||||||
connect(tree_view, &QTreeView::expanded, this, &GameList::OnItemExpanded);
|
connect(tree_view, &QTreeView::expanded, this, &GameList::OnItemExpanded);
|
||||||
connect(tree_view, &QTreeView::collapsed, this, &GameList::OnItemExpanded);
|
connect(tree_view, &QTreeView::collapsed, this, &GameList::OnItemExpanded);
|
||||||
|
connect(list_view, &QListView::activated, this, &GameList::ValidateEntry);
|
||||||
|
connect(list_view, &QListView::customContextMenuRequested, this, &GameList::PopupContextMenu);
|
||||||
connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent,
|
connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent,
|
||||||
[this](Qt::Key key) {
|
[this](Qt::Key key) {
|
||||||
// Avoid pressing buttons while playing
|
// Avoid pressing buttons while playing
|
||||||
@@ -361,6 +472,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
|
|||||||
}
|
}
|
||||||
QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
|
QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
|
||||||
QCoreApplication::postEvent(tree_view, event);
|
QCoreApplication::postEvent(tree_view, event);
|
||||||
|
QCoreApplication::postEvent(list_view, event);
|
||||||
});
|
});
|
||||||
|
|
||||||
// We must register all custom types with the Qt Automoc system so that we are able to use
|
// We must register all custom types with the Qt Automoc system so that we are able to use
|
||||||
@@ -370,8 +482,12 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
|
|||||||
layout->setContentsMargins(0, 0, 0, 0);
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
layout->setSpacing(0);
|
layout->setSpacing(0);
|
||||||
layout->addWidget(tree_view);
|
layout->addWidget(tree_view);
|
||||||
|
layout->addWidget(list_view);
|
||||||
layout->addWidget(search_field);
|
layout->addWidget(search_field);
|
||||||
setLayout(layout);
|
setLayout(layout);
|
||||||
|
|
||||||
|
// Set initial view mode
|
||||||
|
SetViewMode(UISettings::values.game_list_grid_view.GetValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameList::UnloadController() {
|
void GameList::UnloadController() {
|
||||||
@@ -505,11 +621,22 @@ void GameList::DonePopulating(const QStringList& watch_list) {
|
|||||||
item_model->sort(tree_view->header()->sortIndicatorSection(),
|
item_model->sort(tree_view->header()->sortIndicatorSection(),
|
||||||
tree_view->header()->sortIndicatorOrder());
|
tree_view->header()->sortIndicatorOrder());
|
||||||
|
|
||||||
|
// Update grid view if it's currently active
|
||||||
|
if (list_view->isVisible()) {
|
||||||
|
PopulateGridView();
|
||||||
|
}
|
||||||
|
|
||||||
emit PopulatingCompleted();
|
emit PopulatingCompleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameList::PopupContextMenu(const QPoint& menu_location) {
|
void GameList::PopupContextMenu(const QPoint& menu_location) {
|
||||||
QModelIndex item = tree_view->indexAt(menu_location);
|
QModelIndex item;
|
||||||
|
if (tree_view->isVisible()) {
|
||||||
|
item = tree_view->indexAt(menu_location);
|
||||||
|
} else {
|
||||||
|
item = list_view->indexAt(menu_location);
|
||||||
|
}
|
||||||
|
|
||||||
if (!item.isValid())
|
if (!item.isValid())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -535,7 +662,12 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
|
|
||||||
|
if (tree_view->isVisible()) {
|
||||||
|
context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
|
||||||
|
} else {
|
||||||
|
context_menu.exec(list_view->viewport()->mapToGlobal(menu_location));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path) {
|
void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path) {
|
||||||
@@ -847,6 +979,7 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
|
|||||||
|
|
||||||
void GameList::SaveInterfaceLayout() {
|
void GameList::SaveInterfaceLayout() {
|
||||||
UISettings::values.gamelist_header_state = tree_view->header()->saveState();
|
UISettings::values.gamelist_header_state = tree_view->header()->saveState();
|
||||||
|
UISettings::values.game_list_grid_view.SetValue(list_view->isVisible());
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameList::LoadInterfaceLayout() {
|
void GameList::LoadInterfaceLayout() {
|
||||||
@@ -887,6 +1020,12 @@ void GameList::ToggleFavorite(u64 program_id) {
|
|||||||
tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
|
tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update grid view if it's currently active
|
||||||
|
if (list_view->isVisible()) {
|
||||||
|
PopulateGridView();
|
||||||
|
}
|
||||||
|
|
||||||
SaveConfig();
|
SaveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -968,3 +1107,76 @@ void GameListPlaceholder::changeEvent(QEvent* event) {
|
|||||||
void GameListPlaceholder::RetranslateUI() {
|
void GameListPlaceholder::RetranslateUI() {
|
||||||
text->setText(tr("Double-click to add a new folder to the game list"));
|
text->setText(tr("Double-click to add a new folder to the game list"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameList::SetViewMode(bool grid_view) {
|
||||||
|
if (grid_view) {
|
||||||
|
// Create a flat model for grid view showing only games
|
||||||
|
PopulateGridView();
|
||||||
|
tree_view->setVisible(false);
|
||||||
|
list_view->setVisible(true);
|
||||||
|
list_view->setCurrentIndex(list_view->model()->index(0, 0));
|
||||||
|
} else {
|
||||||
|
// Restore the hierarchical model for tree view
|
||||||
|
list_view->setVisible(false);
|
||||||
|
tree_view->setVisible(true);
|
||||||
|
tree_view->setCurrentIndex(item_model->index(0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameList::PopulateGridView() {
|
||||||
|
// Store the current hierarchical model
|
||||||
|
QStandardItemModel* hierarchical_model = item_model;
|
||||||
|
|
||||||
|
// Create a new flat model for grid view
|
||||||
|
QStandardItemModel* flat_model = new QStandardItemModel(this);
|
||||||
|
|
||||||
|
// Collect all games from the hierarchical model
|
||||||
|
for (int i = 0; i < hierarchical_model->rowCount(); ++i) {
|
||||||
|
QStandardItem* folder = hierarchical_model->item(i, 0);
|
||||||
|
if (!folder) continue;
|
||||||
|
|
||||||
|
// Skip non-game folders in grid view, but include favorites
|
||||||
|
const auto folder_type = folder->data(GameListItem::TypeRole).value<GameListItemType>();
|
||||||
|
if (folder_type == GameListItemType::AddDir) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add games from this folder to the flat model
|
||||||
|
for (int j = 0; j < folder->rowCount(); ++j) {
|
||||||
|
QStandardItem* game_item = folder->child(j, 0);
|
||||||
|
if (!game_item) continue;
|
||||||
|
|
||||||
|
const auto game_type = game_item->data(GameListItem::TypeRole).value<GameListItemType>();
|
||||||
|
if (game_type == GameListItemType::Game) {
|
||||||
|
// Clone the game item for the flat model
|
||||||
|
QStandardItem* cloned_item = game_item->clone();
|
||||||
|
|
||||||
|
// Set display text to just the game title for grid view
|
||||||
|
QString game_title = game_item->data(GameListItemPath::TitleRole).toString();
|
||||||
|
if (game_title.isEmpty()) {
|
||||||
|
// Fallback to filename if no title
|
||||||
|
std::string filename;
|
||||||
|
Common::SplitPath(game_item->data(GameListItemPath::FullPathRole).toString().toStdString(),
|
||||||
|
nullptr, &filename, nullptr);
|
||||||
|
game_title = QString::fromStdString(filename);
|
||||||
|
}
|
||||||
|
cloned_item->setText(game_title);
|
||||||
|
|
||||||
|
flat_model->appendRow(cloned_item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the flat model for the list view
|
||||||
|
list_view->setModel(flat_model);
|
||||||
|
|
||||||
|
// Update grid size based on icon size
|
||||||
|
const u32 icon_size = UISettings::values.game_icon_size.GetValue();
|
||||||
|
list_view->setGridSize(QSize(icon_size + 60, icon_size + 80)); // More padding for round icons and text
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameList::ToggleViewMode() {
|
||||||
|
bool current_grid_view = UISettings::values.game_list_grid_view.GetValue();
|
||||||
|
UISettings::values.game_list_grid_view.SetValue(!current_grid_view);
|
||||||
|
SetViewMode(!current_grid_view);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
|
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -7,6 +8,7 @@
|
|||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
|
#include <QListView>
|
||||||
#include <QStandardItemModel>
|
#include <QStandardItemModel>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QTreeView>
|
#include <QTreeView>
|
||||||
@@ -98,6 +100,9 @@ public:
|
|||||||
void SaveInterfaceLayout();
|
void SaveInterfaceLayout();
|
||||||
void LoadInterfaceLayout();
|
void LoadInterfaceLayout();
|
||||||
|
|
||||||
|
void SetViewMode(bool grid_view);
|
||||||
|
void ToggleViewMode();
|
||||||
|
|
||||||
QStandardItemModel* GetModel() const;
|
QStandardItemModel* GetModel() const;
|
||||||
|
|
||||||
/// Disables events from the emulated controller
|
/// Disables events from the emulated controller
|
||||||
@@ -152,6 +157,11 @@ private:
|
|||||||
void AddFavorite(u64 program_id);
|
void AddFavorite(u64 program_id);
|
||||||
void RemoveFavorite(u64 program_id);
|
void RemoveFavorite(u64 program_id);
|
||||||
|
|
||||||
|
void PopulateGridView();
|
||||||
|
|
||||||
|
void FilterGridView(const QString& filter_text);
|
||||||
|
void FilterTreeView(const QString& filter_text);
|
||||||
|
|
||||||
void PopupContextMenu(const QPoint& menu_location);
|
void PopupContextMenu(const QPoint& menu_location);
|
||||||
void AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path);
|
void AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path);
|
||||||
void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected);
|
void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected);
|
||||||
@@ -167,6 +177,7 @@ private:
|
|||||||
GMainWindow* main_window = nullptr;
|
GMainWindow* main_window = nullptr;
|
||||||
QVBoxLayout* layout = nullptr;
|
QVBoxLayout* layout = nullptr;
|
||||||
QTreeView* tree_view = nullptr;
|
QTreeView* tree_view = nullptr;
|
||||||
|
QListView* list_view = nullptr;
|
||||||
QStandardItemModel* item_model = nullptr;
|
QStandardItemModel* item_model = nullptr;
|
||||||
std::unique_ptr<GameListWorker> current_worker;
|
std::unique_ptr<GameListWorker> current_worker;
|
||||||
QFileSystemWatcher* watcher = nullptr;
|
QFileSystemWatcher* watcher = nullptr;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
|
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -11,6 +12,8 @@
|
|||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
#include <QStandardItem>
|
#include <QStandardItem>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
@@ -45,6 +48,31 @@ static QPixmap GetDefaultIcon(u32 size) {
|
|||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a round icon from a square pixmap
|
||||||
|
* @param pixmap The source pixmap
|
||||||
|
* @param size The desired size
|
||||||
|
* @return QPixmap round icon
|
||||||
|
*/
|
||||||
|
static QPixmap CreateRoundIcon(const QPixmap& pixmap, u32 size) {
|
||||||
|
QPixmap rounded(size, size);
|
||||||
|
rounded.fill(Qt::transparent);
|
||||||
|
|
||||||
|
QPainter painter(&rounded);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
|
// Create a circular clipping path
|
||||||
|
QPainterPath path;
|
||||||
|
path.addEllipse(0, 0, size, size);
|
||||||
|
painter.setClipPath(path);
|
||||||
|
|
||||||
|
// Draw the scaled pixmap
|
||||||
|
QPixmap scaled = pixmap.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||||
|
painter.drawPixmap(0, 0, scaled);
|
||||||
|
|
||||||
|
return rounded;
|
||||||
|
}
|
||||||
|
|
||||||
class GameListItem : public QStandardItem {
|
class GameListItem : public QStandardItem {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -85,9 +113,11 @@ public:
|
|||||||
if (!picture.loadFromData(picture_data.data(), static_cast<u32>(picture_data.size()))) {
|
if (!picture.loadFromData(picture_data.data(), static_cast<u32>(picture_data.size()))) {
|
||||||
picture = GetDefaultIcon(size);
|
picture = GetDefaultIcon(size);
|
||||||
}
|
}
|
||||||
picture = picture.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
|
||||||
|
|
||||||
setData(picture, Qt::DecorationRole);
|
// Create a round icon
|
||||||
|
QPixmap round_picture = CreateRoundIcon(picture, size);
|
||||||
|
|
||||||
|
setData(round_picture, Qt::DecorationRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
int type() const override {
|
int type() const override {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
|
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
@@ -1344,6 +1345,7 @@ void GMainWindow::InitializeHotkeys() {
|
|||||||
LinkActionShortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation"));
|
LinkActionShortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation"));
|
||||||
LinkActionShortcut(ui->action_Stop, QStringLiteral("Stop Emulation"));
|
LinkActionShortcut(ui->action_Stop, QStringLiteral("Stop Emulation"));
|
||||||
LinkActionShortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar"));
|
LinkActionShortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar"));
|
||||||
|
LinkActionShortcut(ui->action_Toggle_Grid_View, QStringLiteral("Toggle Grid View"));
|
||||||
LinkActionShortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar"));
|
LinkActionShortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar"));
|
||||||
LinkActionShortcut(ui->action_Fullscreen, QStringLiteral("Fullscreen"));
|
LinkActionShortcut(ui->action_Fullscreen, QStringLiteral("Fullscreen"));
|
||||||
LinkActionShortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot"));
|
LinkActionShortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot"));
|
||||||
@@ -1441,6 +1443,8 @@ void GMainWindow::RestoreUIState() {
|
|||||||
ui->action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar.GetValue());
|
ui->action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar.GetValue());
|
||||||
game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked());
|
game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked());
|
||||||
|
|
||||||
|
ui->action_Toggle_Grid_View->setChecked(UISettings::values.game_list_grid_view.GetValue());
|
||||||
|
|
||||||
ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue());
|
ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue());
|
||||||
statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked());
|
statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked());
|
||||||
Debugger::ToggleConsole();
|
Debugger::ToggleConsole();
|
||||||
@@ -1558,6 +1562,7 @@ void GMainWindow::ConnectMenuEvents() {
|
|||||||
connect_menu(ui->action_Display_Dock_Widget_Headers, &GMainWindow::OnDisplayTitleBars);
|
connect_menu(ui->action_Display_Dock_Widget_Headers, &GMainWindow::OnDisplayTitleBars);
|
||||||
connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar);
|
connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar);
|
||||||
connect_menu(ui->action_Show_Status_Bar, &GMainWindow::OnToggleStatusBar);
|
connect_menu(ui->action_Show_Status_Bar, &GMainWindow::OnToggleStatusBar);
|
||||||
|
connect_menu(ui->action_Toggle_Grid_View, &GMainWindow::OnToggleGridView);
|
||||||
|
|
||||||
connect_menu(ui->action_Reset_Window_Size_720, &GMainWindow::ResetWindowSize720);
|
connect_menu(ui->action_Reset_Window_Size_720, &GMainWindow::ResetWindowSize720);
|
||||||
connect_menu(ui->action_Reset_Window_Size_900, &GMainWindow::ResetWindowSize900);
|
connect_menu(ui->action_Reset_Window_Size_900, &GMainWindow::ResetWindowSize900);
|
||||||
@@ -5341,3 +5346,7 @@ int main(int argc, char* argv[]) {
|
|||||||
detached_tasks.WaitForAllTasks();
|
detached_tasks.WaitForAllTasks();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GMainWindow::OnToggleGridView() {
|
||||||
|
game_list->ToggleViewMode();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
|
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -383,6 +384,7 @@ private slots:
|
|||||||
void OnInstallDecryptionKeys();
|
void OnInstallDecryptionKeys();
|
||||||
void OnAbout();
|
void OnAbout();
|
||||||
void OnToggleFilterBar();
|
void OnToggleFilterBar();
|
||||||
|
void OnToggleGridView();
|
||||||
void OnToggleStatusBar();
|
void OnToggleStatusBar();
|
||||||
void OnDisplayTitleBars(bool);
|
void OnDisplayTitleBars(bool);
|
||||||
void InitializeHotkeys();
|
void InitializeHotkeys();
|
||||||
|
|||||||
@@ -124,6 +124,7 @@
|
|||||||
<addaction name="action_Display_Dock_Widget_Headers"/>
|
<addaction name="action_Display_Dock_Widget_Headers"/>
|
||||||
<addaction name="action_Show_Filter_Bar"/>
|
<addaction name="action_Show_Filter_Bar"/>
|
||||||
<addaction name="action_Show_Status_Bar"/>
|
<addaction name="action_Show_Status_Bar"/>
|
||||||
|
<addaction name="action_Toggle_Grid_View"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="menu_Reset_Window_Size"/>
|
<addaction name="menu_Reset_Window_Size"/>
|
||||||
<addaction name="menu_View_Debugging"/>
|
<addaction name="menu_View_Debugging"/>
|
||||||
@@ -287,6 +288,17 @@
|
|||||||
<string>Show Status Bar</string>
|
<string>Show Status Bar</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_Toggle_Grid_View">
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>&Grid View</string>
|
||||||
|
</property>
|
||||||
|
<property name="iconText">
|
||||||
|
<string>Grid View</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
<action name="action_View_Lobby">
|
<action name="action_View_Lobby">
|
||||||
<property name="enabled">
|
<property name="enabled">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ struct Values {
|
|||||||
Setting<u32> folder_icon_size{linkage, 48, "folder_icon_size", Category::UiGameList};
|
Setting<u32> folder_icon_size{linkage, 48, "folder_icon_size", Category::UiGameList};
|
||||||
Setting<u8> row_1_text_id{linkage, 3, "row_1_text_id", Category::UiGameList};
|
Setting<u8> row_1_text_id{linkage, 3, "row_1_text_id", Category::UiGameList};
|
||||||
Setting<u8> row_2_text_id{linkage, 2, "row_2_text_id", Category::UiGameList};
|
Setting<u8> row_2_text_id{linkage, 2, "row_2_text_id", Category::UiGameList};
|
||||||
|
Setting<bool> game_list_grid_view{linkage, false, "game_list_grid_view", Category::UiGameList};
|
||||||
std::atomic_bool is_game_list_reload_pending{false};
|
std::atomic_bool is_game_list_reload_pending{false};
|
||||||
Setting<bool> cache_game_list{linkage, true, "cache_game_list", Category::UiGameList};
|
Setting<bool> cache_game_list{linkage, true, "cache_game_list", Category::UiGameList};
|
||||||
Setting<bool> favorites_expanded{linkage, true, "favorites_expanded", Category::UiGameList};
|
Setting<bool> favorites_expanded{linkage, true, "favorites_expanded", Category::UiGameList};
|
||||||
@@ -229,7 +230,7 @@ void RestoreWindowState(std::unique_ptr<QtConfig>& qtConfig);
|
|||||||
// This must be in alphabetical order according to action name as it must have the same order as
|
// This must be in alphabetical order according to action name as it must have the same order as
|
||||||
// UISetting::values.shortcuts, which is alphabetically ordered.
|
// UISetting::values.shortcuts, which is alphabetically ordered.
|
||||||
// clang-format off
|
// clang-format off
|
||||||
const std::array<Shortcut, 28> default_hotkeys{{
|
const std::array<Shortcut, 29> default_hotkeys{{
|
||||||
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Mute/Unmute")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+M"), std::string("Home+Dpad_Right"), Qt::WindowShortcut, false}},
|
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Mute/Unmute")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+M"), std::string("Home+Dpad_Right"), Qt::WindowShortcut, false}},
|
||||||
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Volume Down")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("-"), std::string("Home+Dpad_Down"), Qt::ApplicationShortcut, true}},
|
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Volume Down")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("-"), std::string("Home+Dpad_Down"), Qt::ApplicationShortcut, true}},
|
||||||
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Volume Up")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("="), std::string("Home+Dpad_Up"), Qt::ApplicationShortcut, true}},
|
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Volume Up")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("="), std::string("Home+Dpad_Up"), Qt::ApplicationShortcut, true}},
|
||||||
@@ -254,6 +255,7 @@ const std::array<Shortcut, 28> default_hotkeys{{
|
|||||||
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Reset")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F6"), std::string(""), Qt::ApplicationShortcut, false}},
|
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Reset")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F6"), std::string(""), Qt::ApplicationShortcut, false}},
|
||||||
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Start/Stop")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F5"), std::string(""), Qt::ApplicationShortcut, false}},
|
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Start/Stop")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F5"), std::string(""), Qt::ApplicationShortcut, false}},
|
||||||
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Filter Bar")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F"), std::string(""), Qt::WindowShortcut, false}},
|
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Filter Bar")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F"), std::string(""), Qt::WindowShortcut, false}},
|
||||||
|
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Grid View")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+G"), std::string(""), Qt::WindowShortcut, false}},
|
||||||
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Framerate Limit")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+U"), std::string("Home+Y"), Qt::ApplicationShortcut, false}},
|
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Framerate Limit")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+U"), std::string("Home+Y"), Qt::ApplicationShortcut, false}},
|
||||||
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Mouse Panning")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F9"), std::string(""), Qt::ApplicationShortcut, false}},
|
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Mouse Panning")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F9"), std::string(""), Qt::ApplicationShortcut, false}},
|
||||||
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Renderdoc Capture")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string(""), std::string(""), Qt::ApplicationShortcut, false}},
|
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Renderdoc Capture")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string(""), std::string(""), Qt::ApplicationShortcut, false}},
|
||||||
|
|||||||
Reference in New Issue
Block a user