diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp
index 6a6b143ed..4cf0e9bef 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 7e07446d6..95cb873d7 100644
--- a/src/base/preferences.h
+++ b/src/base/preferences.h
@@ -173,6 +173,8 @@ public:
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 ba4ee232c..3527e4db9 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 94309cd9a..71906f13b 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 f8cd9ca59..4369be0b1 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 @@ public:
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 330450c37..a09493ef6 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 @@ private:
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 @@ private:
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;
};