From b76054beba214298eaa1b63fd0176574e51192a8 Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Thu, 30 Jan 2025 08:59:10 +0300 Subject: [PATCH] Store search history PR #22208. --- src/base/preferences.cpp | 15 +++ src/base/preferences.h | 2 + src/gui/optionsdialog.cpp | 3 + src/gui/optionsdialog.ui | 37 +++++++ src/gui/search/searchwidget.cpp | 186 ++++++++++++++++++++++++++++++-- src/gui/search/searchwidget.h | 10 +- 6 files changed, 246 insertions(+), 7 deletions(-) diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index 6a6b143edf70..4cf0e9bef5ea 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -655,6 +655,21 @@ void Preferences::setSearchEnabled(const bool enabled) setValue(u"Preferences/Search/SearchEnabled"_s, enabled); } +int Preferences::searchHistoryLength() const +{ + const int val = value(u"Search/HistoryLength"_s, 50); + return std::clamp(val, 0, 99); +} + +void Preferences::setSearchHistoryLength(const int length) +{ + const int clampedLength = std::clamp(length, 0, 99); + if (clampedLength == searchHistoryLength()) + return; + + setValue(u"Search/HistoryLength"_s, clampedLength); +} + bool Preferences::storeOpenedSearchTabs() const { return value(u"Search/StoreOpenedSearchTabs"_s, false); diff --git a/src/base/preferences.h b/src/base/preferences.h index 7e07446d6541..95cb873d7a60 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -173,6 +173,8 @@ class Preferences final : public QObject void setSearchEnabled(bool enabled); // Search UI + int searchHistoryLength() const; + void setSearchHistoryLength(int length); bool storeOpenedSearchTabs() const; void setStoreOpenedSearchTabs(bool enabled); bool storeOpenedSearchTabResults() const; diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index ba4ee232c8a3..3527e4db9ed5 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -1281,9 +1281,11 @@ void OptionsDialog::loadSearchTabOptions() m_ui->groupStoreOpenedTabs->setChecked(pref->storeOpenedSearchTabs()); m_ui->checkStoreTabsSearchResults->setChecked(pref->storeOpenedSearchTabResults()); + m_ui->searchHistoryLengthSpinBox->setValue(pref->searchHistoryLength()); connect(m_ui->groupStoreOpenedTabs, &QGroupBox::toggled, this, &OptionsDialog::enableApplyButton); connect(m_ui->checkStoreTabsSearchResults, &QCheckBox::toggled, this, &OptionsDialog::enableApplyButton); + connect(m_ui->searchHistoryLengthSpinBox, qSpinBoxValueChanged, this, &OptionsDialog::enableApplyButton); } void OptionsDialog::saveSearchTabOptions() const @@ -1292,6 +1294,7 @@ void OptionsDialog::saveSearchTabOptions() const pref->setStoreOpenedSearchTabs(m_ui->groupStoreOpenedTabs->isChecked()); pref->setStoreOpenedSearchTabResults(m_ui->checkStoreTabsSearchResults->isChecked()); + pref->setSearchHistoryLength(m_ui->searchHistoryLengthSpinBox->value()); } #ifndef DISABLE_WEBUI diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index 94309cd9a802..71906f13b547 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -3272,6 +3272,43 @@ Disable encryption: Only connect to peers without protocol encryption + + + + + + History length + + + + + + + QAbstractSpinBox::ButtonSymbols::PlusMinus + + + 99 + + + QAbstractSpinBox::StepType::DefaultStepType + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + diff --git a/src/gui/search/searchwidget.cpp b/src/gui/search/searchwidget.cpp index f8cd9ca594a4..4369be0b1cf2 100644 --- a/src/gui/search/searchwidget.cpp +++ b/src/gui/search/searchwidget.cpp @@ -34,6 +34,7 @@ #include +#include #include #include #include @@ -48,6 +49,9 @@ #include #include #include +#include +#include +#include #include #include "base/global.h" @@ -56,6 +60,8 @@ #include "base/profile.h" #include "base/search/searchhandler.h" #include "base/search/searchpluginmanager.h" +#include "base/utils/bytearray.h" +#include "base/utils/compare.h" #include "base/utils/datetime.h" #include "base/utils/fs.h" #include "base/utils/foreignapps.h" @@ -67,7 +73,12 @@ #include "searchjobwidget.h" #include "ui_searchwidget.h" +const int HISTORY_FILE_MAX_SIZE = 10 * 1024 * 1024; +const int SESSION_FILE_MAX_SIZE = 10 * 1024 * 1024; +const int RESULTS_FILE_MAX_SIZE = 10 * 1024 * 1024; + const QString DATA_FOLDER_NAME = u"SearchUI"_s; +const QString HISTORY_FILE_NAME = u"History.txt"_s; const QString SESSION_FILE_NAME = u"Session.json"_s; const QString KEY_SESSION_TABS = u"Tabs"_s; @@ -86,6 +97,24 @@ const QString KEY_RESULT_PUBDATE = u"PubDate"_s; namespace { + class SearchHistorySortModel final : public QSortFilterProxyModel + { + Q_OBJECT + Q_DISABLE_COPY_MOVE(SearchHistorySortModel) + + public: + using QSortFilterProxyModel::QSortFilterProxyModel; + + private: + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override + { + const int result = m_naturalCompare(left.data(sortRole()).toString(), right.data(sortRole()).toString()); + return result < 0; + } + + Utils::Compare::NaturalCompare m_naturalCompare; + }; + struct TabData { QString tabID; @@ -132,10 +161,27 @@ namespace return tabName; } + nonstd::expected loadHistory(const Path &filePath) + { + const auto readResult = Utils::IO::readFile(filePath, HISTORY_FILE_MAX_SIZE); + if (!readResult) + { + if (readResult.error().status == Utils::IO::ReadError::NotExist) + return {}; + + return nonstd::make_unexpected(readResult.error().message); + } + + QStringList history; + for (const QByteArrayView line : asConst(Utils::ByteArray::splitToViews(readResult.value(), "\n"))) + history.append(QString::fromUtf8(line)); + + return history; + } + nonstd::expected loadSession(const Path &filePath) { - const int fileMaxSize = 10 * 1024 * 1024; - const auto readResult = Utils::IO::readFile(filePath, fileMaxSize); + const auto readResult = Utils::IO::readFile(filePath, SESSION_FILE_MAX_SIZE); if (!readResult) { if (readResult.error().status == Utils::IO::ReadError::NotExist) @@ -191,8 +237,7 @@ namespace nonstd::expected, QString> loadSearchResults(const Path &filePath) { - const int fileMaxSize = 10 * 1024 * 1024; - const auto readResult = Utils::IO::readFile(filePath, fileMaxSize); + const auto readResult = Utils::IO::readFile(filePath, RESULTS_FILE_MAX_SIZE); if (!readResult) { if (readResult.error().status != Utils::IO::ReadError::NotExist) @@ -285,8 +330,12 @@ class SearchWidget::DataStorage final : public QObject void removeSession(); void storeTab(const QString &tabID, const QList &searchResults); void removeTab(const QString &tabID); + void loadHistory(); + void storeHistory(const QStringList &history); + void removeHistory(); signals: + void historyLoaded(const QStringList &history); void sessionLoaded(const SessionData &sessionData); void tabLoaded(const QString &tabID, const QString &searchPattern, const QList &searchResults); }; @@ -295,7 +344,7 @@ SearchWidget::SearchWidget(IGUIApplication *app, QWidget *parent) : GUIApplicationComponent(app, parent) , m_ui {new Ui::SearchWidget()} , m_ioThread {new QThread} - , m_dataStorage {new DataStorage(this)} + , m_dataStorage {new DataStorage} { m_ui->setupUi(this); @@ -379,6 +428,7 @@ SearchWidget::SearchWidget(IGUIApplication *app, QWidget *parent) const auto *focusSearchHotkeyAlternative = new QShortcut((Qt::CTRL | Qt::Key_E), this); connect(focusSearchHotkeyAlternative, &QShortcut::activated, this, &SearchWidget::toggleFocusBetweenLineEdits); + m_historyLength = Preferences::instance()->searchHistoryLength(); m_storeOpenedTabs = Preferences::instance()->storeOpenedSearchTabs(); m_storeOpenedTabsResults = Preferences::instance()->storeOpenedSearchTabResults(); connect(Preferences::instance(), &Preferences::changed, this, &SearchWidget::onPreferencesChanged); @@ -388,6 +438,7 @@ SearchWidget::SearchWidget(IGUIApplication *app, QWidget *parent) m_ioThread->setObjectName("SearchWidget m_ioThread"); m_ioThread->start(); + loadHistory(); restoreSession(); } @@ -437,7 +488,7 @@ void SearchWidget::onPreferencesChanged() } else { - QMetaObject::invokeMethod(m_dataStorage, [this] { m_dataStorage->removeSession(); }); + QMetaObject::invokeMethod(m_dataStorage, &SearchWidget::DataStorage::removeSession); } } @@ -469,6 +520,36 @@ void SearchWidget::onPreferencesChanged() } } } + + const int historyLength = pref->searchHistoryLength(); + if (historyLength != m_historyLength) + { + if (m_historyLength <= 0) + { + createSearchPatternCompleter(); + } + else + { + if (historyLength <= 0) + { + m_searchPatternCompleterModel->removeRows(0, m_searchPatternCompleterModel->rowCount()); + QMetaObject::invokeMethod(m_dataStorage, &SearchWidget::DataStorage::removeHistory); + } + else if (historyLength < m_historyLength) + { + if (const int rowCount = m_searchPatternCompleterModel->rowCount(); rowCount > historyLength) + { + m_searchPatternCompleterModel->removeRows(0, (rowCount - historyLength)); + QMetaObject::invokeMethod(m_dataStorage, [this] + { + m_dataStorage->storeHistory(m_searchPatternCompleterModel->stringList()); + }); + } + } + } + + m_historyLength = historyLength; + } } void SearchWidget::fillCatCombobox() @@ -552,6 +633,54 @@ int SearchWidget::addTab(const QString &tabID, SearchJobWidget *searchJobWdget) return m_ui->tabWidget->addTab(searchJobWdget, makeTabName(searchJobWdget)); } +void SearchWidget::updateHistory(const QString &newSearchPattern) +{ + if (m_historyLength <= 0) + return; + + if (m_searchPatternCompleterModel->stringList().contains(newSearchPattern)) + return; + + const int rowNum = m_searchPatternCompleterModel->rowCount(); + m_searchPatternCompleterModel->insertRow(rowNum); + m_searchPatternCompleterModel->setData(m_searchPatternCompleterModel->index(rowNum, 0), newSearchPattern); + if (m_searchPatternCompleterModel->rowCount() > m_historyLength) + m_searchPatternCompleterModel->removeRow(0); + + QMetaObject::invokeMethod(m_dataStorage, [this, history = m_searchPatternCompleterModel->stringList()] + { + m_dataStorage->storeHistory(history); + }); +} + +void SearchWidget::loadHistory() +{ + if (m_historyLength <= 0) + return; + + createSearchPatternCompleter(); + + connect(m_dataStorage, &DataStorage::historyLoaded, this, [this](const QStringList &storedHistory) + { + if (m_historyLength <= 0) + return; + + QStringList history = storedHistory; + for (const QString &newPattern : asConst(m_searchPatternCompleterModel->stringList())) + { + if (!history.contains(newPattern)) + history.append(newPattern); + } + + if (history.size() > m_historyLength) + history = history.mid(history.size() - m_historyLength); + + m_searchPatternCompleterModel->setStringList(history); + }); + + QMetaObject::invokeMethod(m_dataStorage, &SearchWidget::DataStorage::loadHistory); +} + void SearchWidget::saveSession() const { if (!m_storeOpenedTabs) @@ -570,6 +699,20 @@ void SearchWidget::saveSession() const QMetaObject::invokeMethod(m_dataStorage, [this, sessionData] { m_dataStorage->storeSession(sessionData); }); } +void SearchWidget::createSearchPatternCompleter() +{ + Q_ASSERT(!m_ui->lineEditSearchPattern->completer()); + + m_searchPatternCompleterModel = new QStringListModel(this); + auto *sortModel = new SearchHistorySortModel(this); + sortModel->setSourceModel(m_searchPatternCompleterModel); + sortModel->sort(0); + auto *completer = new QCompleter(sortModel, this); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel); + m_ui->lineEditSearchPattern->setCompleter(completer); +} + void SearchWidget::restoreSession() { if (!m_storeOpenedTabs) @@ -750,6 +893,7 @@ void SearchWidget::searchButtonClicked() m_ui->tabWidget->setTabIcon(tabIndex, UIThemeManager::instance()->getIcon(statusIconName(newTab->status()))); m_ui->tabWidget->setCurrentWidget(newTab); adjustSearchButton(); + updateHistory(pattern); saveSession(); } @@ -918,4 +1062,34 @@ void SearchWidget::DataStorage::removeTab(const QString &tabID) Utils::Fs::removeFile(makeDataFilePath(tabID + u".json")); } +void SearchWidget::DataStorage::loadHistory() +{ + const Path historyFilePath = makeDataFilePath(HISTORY_FILE_NAME); + const auto loadResult = ::loadHistory(historyFilePath); + if (!loadResult) + { + LogMsg(tr("Failed to load Search UI history. File: \"%1\". Error: \"%2\"") + .arg(historyFilePath.toString(), loadResult.error()), Log::WARNING); + return; + } + + emit historyLoaded(loadResult.value()); +} + +void SearchWidget::DataStorage::storeHistory(const QStringList &history) +{ + const Path filePath = makeDataFilePath(HISTORY_FILE_NAME); + const auto saveResult = Utils::IO::saveToFile(filePath, history.join(u'\n').toUtf8()); + if (!saveResult) + { + LogMsg(tr("Failed to save search history. File: \"%1\". Error: \"%2\"") + .arg(filePath.toString(), saveResult.error()), Log::WARNING); + } +} + +void SearchWidget::DataStorage::removeHistory() +{ + Utils::Fs::removeFile(makeDataFilePath(HISTORY_FILE_NAME)); +} + #include "searchwidget.moc" diff --git a/src/gui/search/searchwidget.h b/src/gui/search/searchwidget.h index 330450c374c3..a09493ef6f42 100644 --- a/src/gui/search/searchwidget.h +++ b/src/gui/search/searchwidget.h @@ -39,6 +39,7 @@ class QEvent; class QObject; +class QStringListModel; class SearchJobWidget; @@ -93,8 +94,12 @@ class SearchWidget : public GUIApplicationComponent QString generateTabID() const; int addTab(const QString &tabID, SearchJobWidget *searchJobWdget); - void saveSession() const; + void loadHistory(); void restoreSession(); + void updateHistory(const QString &newSearchPattern); + void saveSession() const; + + void createSearchPatternCompleter(); Ui::SearchWidget *m_ui = nullptr; QPointer m_currentSearchTab; // Selected tab @@ -103,9 +108,12 @@ class SearchWidget : public GUIApplicationComponent bool m_storeOpenedTabs = false; bool m_storeOpenedTabsResults = false; + int m_historyLength = 0; Utils::Thread::UniquePtr m_ioThread; class DataStorage; DataStorage *m_dataStorage = nullptr; + + QStringListModel *m_searchPatternCompleterModel = nullptr; };