Skip to content

Commit

Permalink
Store search history
Browse files Browse the repository at this point in the history
  • Loading branch information
glassez committed Jan 28, 2025
1 parent 3978137 commit 2861156
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 7 deletions.
15 changes: 15 additions & 0 deletions src/base/preferences.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/base/preferences.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/gui/optionsdialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
37 changes: 37 additions & 0 deletions src/gui/optionsdialog.ui
Original file line number Diff line number Diff line change
Expand Up @@ -3272,6 +3272,43 @@ Disable encryption: Only connect to peers without protocol encryption</string>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="searchHistoryLayout">
<item>
<widget class="QLabel" name="searchHistoryLengthLabel">
<property name="text">
<string>History length</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="searchHistoryLengthSpinBox">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::PlusMinus</enum>
</property>
<property name="maximum">
<number>99</number>
</property>
<property name="stepType">
<enum>QAbstractSpinBox::StepType::DefaultStepType</enum>
</property>
</widget>
</item>
<item>
<spacer name="searchHistoryLengthSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
Expand Down
167 changes: 161 additions & 6 deletions src/gui/search/searchwidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

#include <utility>

#include <QCompleter>
#include <QDebug>
#include <QEvent>
#include <QJsonArray>
Expand All @@ -48,6 +49,9 @@
#include <QObject>
#include <QRegularExpression>
#include <QShortcut>
#include <QSortFilterProxyModel>
#include <QStringList>
#include <QStringListModel>
#include <QThread>

#include "base/global.h"
Expand All @@ -56,6 +60,7 @@
#include "base/profile.h"
#include "base/search/searchhandler.h"
#include "base/search/searchpluginmanager.h"
#include "base/utils/bytearray.h"
#include "base/utils/datetime.h"
#include "base/utils/fs.h"
#include "base/utils/foreignapps.h"
Expand All @@ -67,7 +72,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;
Expand Down Expand Up @@ -132,10 +142,27 @@ namespace
return tabName;
}

nonstd::expected<QStringList, QString> 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<SessionData, QString> 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)
Expand Down Expand Up @@ -191,8 +218,7 @@ namespace

nonstd::expected<QList<SearchResult>, 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)
Expand Down Expand Up @@ -285,8 +311,12 @@ class SearchWidget::DataStorage final : public QObject
void removeSession();
void storeTab(const QString &tabID, const QList<SearchResult> &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<SearchResult> &searchResults);
};
Expand All @@ -295,7 +325,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);

Expand Down Expand Up @@ -379,6 +409,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);
Expand All @@ -388,6 +419,7 @@ SearchWidget::SearchWidget(IGUIApplication *app, QWidget *parent)
m_ioThread->setObjectName("SearchWidget m_ioThread");
m_ioThread->start();

loadHistory();
restoreSession();
}

Expand Down Expand Up @@ -437,7 +469,7 @@ void SearchWidget::onPreferencesChanged()
}
else
{
QMetaObject::invokeMethod(m_dataStorage, [this] { m_dataStorage->removeSession(); });
QMetaObject::invokeMethod(m_dataStorage, &SearchWidget::DataStorage::removeSession);
}
}

Expand Down Expand Up @@ -469,6 +501,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()
Expand Down Expand Up @@ -552,6 +614,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)
Expand All @@ -570,6 +680,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 QSortFilterProxyModel(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)
Expand Down Expand Up @@ -750,6 +874,7 @@ void SearchWidget::searchButtonClicked()
m_ui->tabWidget->setTabIcon(tabIndex, UIThemeManager::instance()->getIcon(statusIconName(newTab->status())));
m_ui->tabWidget->setCurrentWidget(newTab);
adjustSearchButton();
updateHistory(pattern);
saveSession();
}

Expand Down Expand Up @@ -918,4 +1043,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"
Loading

0 comments on commit 2861156

Please sign in to comment.