summaryrefslogtreecommitdiffstats
path: root/src/citra_qt/game_list.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/citra_qt/game_list.cpp')
-rw-r--r--src/citra_qt/game_list.cpp278
1 files changed, 233 insertions, 45 deletions
diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp
index a9ec9e830..a8e3541cd 100644
--- a/src/citra_qt/game_list.cpp
+++ b/src/citra_qt/game_list.cpp
@@ -2,11 +2,12 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
+#include <QApplication>
#include <QFileInfo>
#include <QHeaderView>
+#include <QKeyEvent>
#include <QMenu>
#include <QThreadPool>
-#include <QVBoxLayout>
#include "common/common_paths.h"
#include "common/logging/log.h"
#include "common/string_util.h"
@@ -15,10 +16,192 @@
#include "game_list_p.h"
#include "ui_settings.h"
-GameList::GameList(QWidget* parent) : QWidget{parent} {
- QVBoxLayout* layout = new QVBoxLayout;
+GameList::SearchField::KeyReleaseEater::KeyReleaseEater(GameList* gamelist) {
+ this->gamelist = gamelist;
+ edit_filter_text_old = "";
+}
+
+// EventFilter in order to process systemkeys while editing the searchfield
+bool GameList::SearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* event) {
+ // If it isn't a KeyRelease event then continue with standard event processing
+ if (event->type() != QEvent::KeyRelease)
+ return QObject::eventFilter(obj, event);
+
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+ int rowCount = gamelist->tree_view->model()->rowCount();
+ QString edit_filter_text = gamelist->search_field->edit_filter->text().toLower();
+
+ // If the searchfield's text hasn't changed special function keys get checked
+ // If no function key changes the searchfield's text the filter doesn't need to get reloaded
+ if (edit_filter_text == edit_filter_text_old) {
+ switch (keyEvent->key()) {
+ // Escape: Resets the searchfield
+ case Qt::Key_Escape: {
+ if (edit_filter_text_old.isEmpty()) {
+ return QObject::eventFilter(obj, event);
+ } else {
+ gamelist->search_field->edit_filter->clear();
+ edit_filter_text = "";
+ }
+ break;
+ }
+ // Return and Enter
+ // If the enter key gets pressed first checks how many and which entry is visible
+ // If there is only one result launch this game
+ case Qt::Key_Return:
+ case Qt::Key_Enter: {
+ QStandardItemModel* item_model = new QStandardItemModel(gamelist->tree_view);
+ QModelIndex root_index = item_model->invisibleRootItem()->index();
+ QStandardItem* child_file;
+ QString file_path;
+ int resultCount = 0;
+ for (int i = 0; i < rowCount; ++i) {
+ if (!gamelist->tree_view->isRowHidden(i, root_index)) {
+ ++resultCount;
+ child_file = gamelist->item_model->item(i, 0);
+ file_path = child_file->data(GameListItemPath::FullPathRole).toString();
+ }
+ }
+ if (resultCount == 1) {
+ // To avoid loading error dialog loops while confirming them using enter
+ // Also users usually want to run a diffrent game after closing one
+ gamelist->search_field->edit_filter->setText("");
+ edit_filter_text = "";
+ emit gamelist->GameChosen(file_path);
+ } else {
+ return QObject::eventFilter(obj, event);
+ }
+ break;
+ }
+ default:
+ return QObject::eventFilter(obj, event);
+ }
+ }
+ edit_filter_text_old = edit_filter_text;
+ return QObject::eventFilter(obj, event);
+}
+
+void GameList::SearchField::setFilterResult(int visible, int total) {
+ QString result_of_text = tr("of");
+ QString result_text;
+ if (total == 1) {
+ result_text = tr("result");
+ } else {
+ result_text = tr("results");
+ }
+ label_filter_result->setText(
+ QString("%1 %2 %3 %4").arg(visible).arg(result_of_text).arg(total).arg(result_text));
+}
+
+void GameList::SearchField::clear() {
+ edit_filter->setText("");
+}
+
+void GameList::SearchField::setFocus() {
+ if (edit_filter->isVisible()) {
+ edit_filter->setFocus();
+ }
+}
+
+GameList::SearchField::SearchField(GameList* parent) : QWidget{parent} {
+ KeyReleaseEater* keyReleaseEater = new KeyReleaseEater(parent);
+ layout_filter = new QHBoxLayout;
+ layout_filter->setMargin(8);
+ label_filter = new QLabel;
+ label_filter->setText(tr("Filter:"));
+ edit_filter = new QLineEdit;
+ edit_filter->setText("");
+ edit_filter->setPlaceholderText(tr("Enter pattern to filter"));
+ edit_filter->installEventFilter(keyReleaseEater);
+ edit_filter->setClearButtonEnabled(true);
+ connect(edit_filter, SIGNAL(textChanged(const QString&)), parent,
+ SLOT(onTextChanged(const QString&)));
+ label_filter_result = new QLabel;
+ button_filter_close = new QToolButton(this);
+ button_filter_close->setText("X");
+ button_filter_close->setCursor(Qt::ArrowCursor);
+ button_filter_close->setStyleSheet("QToolButton{ border: none; padding: 0px; color: "
+ "#000000; font-weight: bold; background: #F0F0F0; }"
+ "QToolButton:hover{ border: none; padding: 0px; color: "
+ "#EEEEEE; font-weight: bold; background: #E81123}");
+ connect(button_filter_close, SIGNAL(clicked()), parent, SLOT(onFilterCloseClicked()));
+ layout_filter->setSpacing(10);
+ layout_filter->addWidget(label_filter);
+ layout_filter->addWidget(edit_filter);
+ layout_filter->addWidget(label_filter_result);
+ layout_filter->addWidget(button_filter_close);
+ setLayout(layout_filter);
+}
+
+/**
+ * Checks if all words separated by spaces are contained in another string
+ * This offers a word order insensitive search function
+ *
+ * @param String that gets checked if it contains all words of the userinput string
+ * @param String containing all words getting checked
+ * @return true if the haystack contains all words of userinput
+ */
+bool GameList::containsAllWords(QString haystack, QString userinput) {
+ QStringList userinput_split = userinput.split(" ", QString::SplitBehavior::SkipEmptyParts);
+ return std::all_of(userinput_split.begin(), userinput_split.end(),
+ [haystack](QString s) { return haystack.contains(s); });
+}
+
+// Event in order to filter the gamelist after editing the searchfield
+void GameList::onTextChanged(const QString& newText) {
+ int rowCount = tree_view->model()->rowCount();
+ QString edit_filter_text = newText.toLower();
+
+ QModelIndex root_index = item_model->invisibleRootItem()->index();
+
+ // If the searchfield is empty every item is visible
+ // Otherwise the filter gets applied
+ if (edit_filter_text.isEmpty()) {
+ for (int i = 0; i < rowCount; ++i) {
+ tree_view->setRowHidden(i, root_index, false);
+ }
+ search_field->setFilterResult(rowCount, rowCount);
+ } else {
+ QStandardItem* child_file;
+ QString file_path, file_name, file_title, file_programmid;
+ int result_count = 0;
+ for (int i = 0; i < rowCount; ++i) {
+ child_file = item_model->item(i, 0);
+ file_path = child_file->data(GameListItemPath::FullPathRole).toString().toLower();
+ file_name = file_path.mid(file_path.lastIndexOf("/") + 1);
+ file_title = child_file->data(GameListItemPath::TitleRole).toString().toLower();
+ file_programmid =
+ child_file->data(GameListItemPath::ProgramIdRole).toString().toLower();
+
+ // Only items which filename in combination with its title contains all words
+ // that are in the searchfiel will be visible in the gamelist
+ // The search is case insensitive because of toLower()
+ // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent
+ // multiple conversions of edit_filter_text for each game in the gamelist
+ if (containsAllWords(file_name.append(" ").append(file_title), edit_filter_text) ||
+ (file_programmid.count() == 16 && edit_filter_text.contains(file_programmid))) {
+ tree_view->setRowHidden(i, root_index, false);
+ ++result_count;
+ } else {
+ tree_view->setRowHidden(i, root_index, true);
+ }
+ search_field->setFilterResult(result_count, rowCount);
+ }
+ }
+}
+
+void GameList::onFilterCloseClicked() {
+ main_window->filterBarSetChecked(false);
+}
+GameList::GameList(GMainWindow* parent) : QWidget{parent} {
+ watcher = new QFileSystemWatcher(this);
+ connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory);
+
+ this->main_window = parent;
+ layout = new QVBoxLayout;
tree_view = new QTreeView;
+ search_field = new SearchField(this);
item_model = new QStandardItemModel(tree_view);
tree_view->setModel(item_model);
@@ -39,14 +222,15 @@ GameList::GameList(QWidget* parent) : QWidget{parent} {
connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry);
connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu);
- connect(&watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory);
// We must register all custom types with the Qt Automoc system so that we are able to use it
// with signals/slots. In this case, QList falls under the umbrells of custom types.
qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
layout->setContentsMargins(0, 0, 0, 0);
+ layout->setSpacing(0);
layout->addWidget(tree_view);
+ layout->addWidget(search_field);
setLayout(layout);
}
@@ -54,6 +238,20 @@ GameList::~GameList() {
emit ShouldCancelWorker();
}
+void GameList::setFilterFocus() {
+ if (tree_view->model()->rowCount() > 0) {
+ search_field->setFocus();
+ }
+}
+
+void GameList::setFilterVisible(bool visibility) {
+ search_field->setVisible(visibility);
+}
+
+void GameList::clearFilter() {
+ search_field->clear();
+}
+
void GameList::AddEntry(const QList<QStandardItem*>& entry_items) {
item_model->invisibleRootItem()->appendRow(entry_items);
}
@@ -69,11 +267,33 @@ void GameList::ValidateEntry(const QModelIndex& item) {
std::string std_file_path(file_path.toStdString());
if (!FileUtil::Exists(std_file_path) || FileUtil::IsDirectory(std_file_path))
return;
+ // Users usually want to run a diffrent game after closing one
+ search_field->clear();
emit GameChosen(file_path);
}
-void GameList::DonePopulating() {
+void GameList::DonePopulating(QStringList watch_list) {
+ // Clear out the old directories to watch for changes and add the new ones
+ auto watch_dirs = watcher->directories();
+ if (!watch_dirs.isEmpty()) {
+ watcher->removePaths(watch_dirs);
+ }
+ // Workaround: Add the watch paths in chunks to allow the gui to refresh
+ // This prevents the UI from stalling when a large number of watch paths are added
+ // Also artificially caps the watcher to a certain number of directories
+ constexpr int LIMIT_WATCH_DIRECTORIES = 5000;
+ constexpr int SLICE_SIZE = 25;
+ int len = std::min(watch_list.length(), LIMIT_WATCH_DIRECTORIES);
+ for (int i = 0; i < len; i += SLICE_SIZE) {
+ watcher->addPaths(watch_list.mid(i, i + SLICE_SIZE));
+ QCoreApplication::processEvents();
+ }
tree_view->setEnabled(true);
+ int rowCount = tree_view->model()->rowCount();
+ search_field->setFilterResult(rowCount, rowCount);
+ if (rowCount > 0) {
+ search_field->setFocus();
+ }
}
void GameList::PopupContextMenu(const QPoint& menu_location) {
@@ -97,6 +317,7 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
if (!FileUtil::Exists(dir_path.toStdString()) ||
!FileUtil::IsDirectory(dir_path.toStdString())) {
LOG_ERROR(Frontend, "Could not find game list folder at %s", dir_path.toLocal8Bit().data());
+ search_field->setFilterResult(0, 0);
return;
}
@@ -106,11 +327,6 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
emit ShouldCancelWorker();
- auto watch_dirs = watcher.directories();
- if (!watch_dirs.isEmpty()) {
- watcher.removePaths(watch_dirs);
- }
- UpdateWatcherList(dir_path.toStdString(), deep_scan ? 256 : 0);
GameListWorker* worker = new GameListWorker(dir_path, deep_scan);
connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
@@ -151,42 +367,11 @@ static bool HasSupportedFileExtension(const std::string& file_name) {
void GameList::RefreshGameDirectory() {
if (!UISettings::values.gamedir.isEmpty() && current_worker != nullptr) {
LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
+ search_field->clear();
PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
}
}
-/**
- * Adds the game list folder to the QFileSystemWatcher to check for updates.
- *
- * The file watcher will fire off an update to the game list when a change is detected in the game
- * list folder.
- *
- * Notice: This method is run on the UI thread because QFileSystemWatcher is not thread safe and
- * this function is fast enough to not stall the UI thread. If performance is an issue, it should
- * be moved to another thread and properly locked to prevent concurrency issues.
- *
- * @param dir folder to check for changes in
- * @param recursion 0 if recursion is disabled. Any positive number passed to this will add each
- * directory recursively to the watcher and will update the file list if any of the folders
- * change. The number determines how deep the recursion should traverse.
- */
-void GameList::UpdateWatcherList(const std::string& dir, unsigned int recursion) {
- const auto callback = [this, recursion](unsigned* num_entries_out, const std::string& directory,
- const std::string& virtual_name) -> bool {
- std::string physical_name = directory + DIR_SEP + virtual_name;
-
- if (FileUtil::IsDirectory(physical_name)) {
- UpdateWatcherList(physical_name, recursion - 1);
- }
- return true;
- };
-
- watcher.addPath(QString::fromStdString(dir));
- if (recursion > 0) {
- FileUtil::ForeachDirectoryEntry(nullptr, dir, callback);
- }
-}
-
void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) {
const auto callback = [this, recursion](unsigned* num_entries_out, const std::string& directory,
const std::string& virtual_name) -> bool {
@@ -195,7 +380,8 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
if (stop_processing)
return false; // Breaks the callback loop.
- if (!FileUtil::IsDirectory(physical_name) && HasSupportedFileExtension(physical_name)) {
+ bool is_dir = FileUtil::IsDirectory(physical_name);
+ if (!is_dir && HasSupportedFileExtension(physical_name)) {
std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(physical_name);
if (!loader)
return true;
@@ -212,7 +398,8 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
new GameListItemSize(FileUtil::GetSize(physical_name)),
});
- } else if (recursion > 0) {
+ } else if (is_dir && recursion > 0) {
+ watch_list.append(QString::fromStdString(physical_name));
AddFstEntriesToGameList(physical_name, recursion - 1);
}
@@ -224,8 +411,9 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
void GameListWorker::run() {
stop_processing = false;
+ watch_list.append(dir_path);
AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0);
- emit Finished();
+ emit Finished(watch_list);
}
void GameListWorker::Cancel() {