|
@@ -4,9 +4,9 @@
|
|
|
|
|
|
|
|
#include <QFileInfo>
|
|
#include <QFileInfo>
|
|
|
#include <QHeaderView>
|
|
#include <QHeaderView>
|
|
|
|
|
+#include <QKeyEvent>
|
|
|
#include <QMenu>
|
|
#include <QMenu>
|
|
|
#include <QThreadPool>
|
|
#include <QThreadPool>
|
|
|
-#include <QVBoxLayout>
|
|
|
|
|
#include "common/common_paths.h"
|
|
#include "common/common_paths.h"
|
|
|
#include "common/logging/log.h"
|
|
#include "common/logging/log.h"
|
|
|
#include "common/string_util.h"
|
|
#include "common/string_util.h"
|
|
@@ -15,10 +15,189 @@
|
|
|
#include "game_list_p.h"
|
|
#include "game_list_p.h"
|
|
|
#include "ui_settings.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 visable
|
|
|
|
|
+ // 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 visable, 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(visable).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} {
|
|
|
|
|
+ this->main_window = parent;
|
|
|
|
|
+ layout = new QVBoxLayout;
|
|
|
tree_view = new QTreeView;
|
|
tree_view = new QTreeView;
|
|
|
|
|
+ search_field = new SearchField(this);
|
|
|
item_model = new QStandardItemModel(tree_view);
|
|
item_model = new QStandardItemModel(tree_view);
|
|
|
tree_view->setModel(item_model);
|
|
tree_view->setModel(item_model);
|
|
|
|
|
|
|
@@ -46,7 +225,9 @@ GameList::GameList(QWidget* parent) : QWidget{parent} {
|
|
|
qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
|
|
qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
|
|
|
|
|
|
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
+ layout->setSpacing(0);
|
|
|
layout->addWidget(tree_view);
|
|
layout->addWidget(tree_view);
|
|
|
|
|
+ layout->addWidget(search_field);
|
|
|
setLayout(layout);
|
|
setLayout(layout);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -54,6 +235,18 @@ GameList::~GameList() {
|
|
|
emit ShouldCancelWorker();
|
|
emit ShouldCancelWorker();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+void GameList::setFilterFocus() {
|
|
|
|
|
+ search_field->setFocus();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void GameList::setFilterVisible(bool visablility) {
|
|
|
|
|
+ search_field->setVisible(visablility);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void GameList::clearFilter() {
|
|
|
|
|
+ search_field->clear();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
void GameList::AddEntry(const QList<QStandardItem*>& entry_items) {
|
|
void GameList::AddEntry(const QList<QStandardItem*>& entry_items) {
|
|
|
item_model->invisibleRootItem()->appendRow(entry_items);
|
|
item_model->invisibleRootItem()->appendRow(entry_items);
|
|
|
}
|
|
}
|
|
@@ -69,11 +262,16 @@ void GameList::ValidateEntry(const QModelIndex& item) {
|
|
|
std::string std_file_path(file_path.toStdString());
|
|
std::string std_file_path(file_path.toStdString());
|
|
|
if (!FileUtil::Exists(std_file_path) || FileUtil::IsDirectory(std_file_path))
|
|
if (!FileUtil::Exists(std_file_path) || FileUtil::IsDirectory(std_file_path))
|
|
|
return;
|
|
return;
|
|
|
|
|
+ // Users usually want to run a diffrent game after closing one
|
|
|
|
|
+ search_field->clear();
|
|
|
emit GameChosen(file_path);
|
|
emit GameChosen(file_path);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void GameList::DonePopulating() {
|
|
void GameList::DonePopulating() {
|
|
|
tree_view->setEnabled(true);
|
|
tree_view->setEnabled(true);
|
|
|
|
|
+ int rowCount = tree_view->model()->rowCount();
|
|
|
|
|
+ search_field->setFilterResult(rowCount, rowCount);
|
|
|
|
|
+ search_field->setFocus();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void GameList::PopupContextMenu(const QPoint& menu_location) {
|
|
void GameList::PopupContextMenu(const QPoint& menu_location) {
|
|
@@ -151,25 +349,26 @@ static bool HasSupportedFileExtension(const std::string& file_name) {
|
|
|
void GameList::RefreshGameDirectory() {
|
|
void GameList::RefreshGameDirectory() {
|
|
|
if (!UISettings::values.gamedir.isEmpty() && current_worker != nullptr) {
|
|
if (!UISettings::values.gamedir.isEmpty() && current_worker != nullptr) {
|
|
|
LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
|
|
LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
|
|
|
|
|
+ search_field->clear();
|
|
|
PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
|
|
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.
|
|
|
|
|
- */
|
|
|
|
|
|
|
+* 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) {
|
|
void GameList::UpdateWatcherList(const std::string& dir, unsigned int recursion) {
|
|
|
const auto callback = [this, recursion](unsigned* num_entries_out, const std::string& directory,
|
|
const auto callback = [this, recursion](unsigned* num_entries_out, const std::string& directory,
|
|
|
const std::string& virtual_name) -> bool {
|
|
const std::string& virtual_name) -> bool {
|