From efbfc3374eef954028a4e0c5f2bb9d03b34032a7 Mon Sep 17 00:00:00 2001 From: luddens Date: Wed, 29 Apr 2020 18:36:15 +0200 Subject: [PATCH] fluid suggestions list UX - add a delay before searching suggestions A timer of 100ms is started each time the text is edited, if the timer time-outs the suggestion's search is executed. - foreach search a worker with a token is launched in another thread Once the search is done, the worker sends the suggestions list and the corresponding urls list to the main thread. if the token of the worker matches with the last token used, the main thread displays the suggestions. --- kiwix-desktop.pro | 2 + src/searchbar.cpp | 92 +++++++++++++++++++----------------- src/searchbar.h | 10 +++- src/suggestionlistworker.cpp | 50 ++++++++++++++++++++ src/suggestionlistworker.h | 23 +++++++++ src/tabbar.cpp | 6 ++- 6 files changed, 138 insertions(+), 45 deletions(-) create mode 100644 src/suggestionlistworker.cpp create mode 100644 src/suggestionlistworker.h diff --git a/kiwix-desktop.pro b/kiwix-desktop.pro index 4aa3d58..191cefb 100644 --- a/kiwix-desktop.pro +++ b/kiwix-desktop.pro @@ -43,6 +43,7 @@ DEFINES += QT_DEPRECATED_WARNINGS SOURCES += \ src/contenttypefilter.cpp \ src/findinpagebar.cpp \ + src/suggestionlistworker.cpp \ src/translation.cpp \ src/main.cpp \ src/mainwindow.cpp \ @@ -75,6 +76,7 @@ SOURCES += \ HEADERS += \ src/contenttypefilter.h \ src/findinpagebar.h \ + src/suggestionlistworker.h \ src/translation.h \ src/mainwindow.h \ src/kiwixapp.h \ diff --git a/src/searchbar.cpp b/src/searchbar.cpp index e535f18..eca5c6d 100644 --- a/src/searchbar.cpp +++ b/src/searchbar.cpp @@ -1,10 +1,10 @@ #include "searchbar.h" #include -#include #include #include "kiwixapp.h" +#include "suggestionlistworker.h" SearchButton::SearchButton(QWidget *parent) : QPushButton(parent), @@ -62,6 +62,8 @@ SearchBar::SearchBar(QWidget *parent) : m_completer(&m_completionModel, this), m_button(this) { + mp_typingTimer = new QTimer(this); + mp_typingTimer->setSingleShot(true); setPlaceholderText(gt("search")); m_completer.setCompletionMode(QCompleter::UnfilteredPopupCompletion); m_completer.setCaseSensitivity(Qt::CaseInsensitive); @@ -75,13 +77,15 @@ SearchBar::SearchBar(QWidget *parent) : QString style(byteContent); m_completer.popup()->setStyleSheet(style); - connect(this, &QLineEdit::textEdited, this, &SearchBar::updateCompletion); + qRegisterMetaType>("QVector"); + connect(mp_typingTimer, &QTimer::timeout, this, &SearchBar::updateCompletion); connect(KiwixApp::instance(), &KiwixApp::currentTitleChanged, this, &SearchBar::on_currentTitleChanged); connect(this, &QLineEdit::textEdited, this, [=](const QString &text) { m_searchbarInput = text; m_returnPressed = false; + mp_typingTimer->start(100); }); connect(this, &QLineEdit::textChanged, this, [=](const QString &text) { @@ -94,6 +98,18 @@ SearchBar::SearchBar(QWidget *parent) : }); } +void SearchBar::hideSuggestions() +{ + m_completer.popup()->hide(); +} + +void SearchBar::clearSuggestions() +{ + QStringList empty; + m_completionModel.setStringList(empty); + m_urlList.clear(); +} + void SearchBar::on_currentTitleChanged(const QString& title) { if (this->hasFocus()) { @@ -117,7 +133,7 @@ void SearchBar::focusInEvent( QFocusEvent* event) if (event->reason() == Qt::ActiveWindowFocusReason || event->reason() == Qt::MouseFocusReason) { connect(&m_completer, QOverload::of(&QCompleter::activated), - this, &SearchBar::openCompletion); + this, QOverload::of(&SearchBar::openCompletion)); } QLineEdit::focusInEvent(event); m_button.set_searchMode(true); @@ -134,56 +150,46 @@ void SearchBar::focusOutEvent(QFocusEvent* event) return QLineEdit::focusOutEvent(event); } -void SearchBar::updateCompletion(const QString &text) +void SearchBar::updateCompletion() { - QStringList wordList; - m_urlList.clear(); + mp_typingTimer->stop(); + clearSuggestions(); auto currentWidget = KiwixApp::instance()->getTabWidget()->currentWebView(); - if (!currentWidget) { - m_completionModel.setStringList(wordList); + if (!currentWidget || currentWidget->url().isEmpty() || m_searchbarInput.isEmpty()) { + hideSuggestions(); return; } - auto qurl = currentWidget->url(); - qInfo() << "Search bar url is " << qurl; - auto currentZimId = qurl.host().split(".")[0]; - auto reader = KiwixApp::instance()->getLibrary()->getReader(currentZimId); - QUrl url; - url.setScheme("zim"); - if (reader) { - url.setHost(currentZimId + ".zim"); - reader->searchSuggestionsSmart(text.toStdString(), 15); - std::string title, path; - while (reader->getNextSuggestion(title, path)) { - url.setPath(QString::fromStdString(path)); - wordList << QString::fromStdString(title); - m_urlList.push_back(url); + m_token++; + auto suggestionWorker = new SuggestionListWorker(m_searchbarInput, m_token, this); + connect(suggestionWorker, &SuggestionListWorker::searchFinished, this, + [=] (const QStringList& suggestions, const QVector& urlList, int token) { + if (token != m_token) { + return; } - } - QUrlQuery query; - url.setPath(""); - if (reader) { - // The host is used to determine the currentZimId - // The content query item is used to know in which zim search (as for kiwix-serve) - url.setHost(currentZimId + ".search"); - query.addQueryItem("content", currentZimId); - } else { - // We do not allow multi zim search for now. - // We don't have a correct UI to select on which zim search, - // how to display results, ... - //url.setHost("library.search"); - } - query.addQueryItem("pattern", text); - url.setQuery(query); - wordList << text + " (" + gt("fulltext-search") + ")"; - m_urlList.push_back(url); - m_completionModel.setStringList(wordList); + m_urlList = urlList; + if (m_returnPressed) { + openCompletion(suggestions.first(), 0); + return; + } + m_completionModel.setStringList(suggestions); + m_completer.complete(); + }); + connect(suggestionWorker, &SuggestionListWorker::finished, suggestionWorker, &QObject::deleteLater); + suggestionWorker->start(); } void SearchBar::openCompletion(const QModelIndex &index) +{ + if (m_urlList.size() != 0) { + openCompletion(index.data().toString(), index.row()); + } +} + +void SearchBar::openCompletion(const QString& text, int index) { QUrl url; - if (this->text().compare(index.data().toString(), Qt::CaseInsensitive) == 0) { - url = m_urlList.at(index.row()); + if (this->text().compare(text, Qt::CaseInsensitive) == 0) { + url = m_urlList.at(index); } else { url = m_urlList.last(); } diff --git a/src/searchbar.h b/src/searchbar.h index 1ed9773..378200a 100644 --- a/src/searchbar.h +++ b/src/searchbar.h @@ -7,6 +7,8 @@ #include #include #include +#include +#include class SearchButton : public QPushButton { Q_OBJECT @@ -26,9 +28,12 @@ class SearchBar : public QLineEdit Q_OBJECT public: SearchBar(QWidget *parent = nullptr); + void hideSuggestions(); public slots: void on_currentTitleChanged(const QString &title); + void clearSuggestions(); + protected: virtual void focusInEvent(QFocusEvent *); virtual void focusOutEvent(QFocusEvent *); @@ -40,10 +45,13 @@ private: QString m_title; QString m_searchbarInput; bool m_returnPressed = false; + QTimer* mp_typingTimer; + int m_token; private slots: - void updateCompletion(const QString& text); + void updateCompletion(); void openCompletion(const QModelIndex& index); + void openCompletion(const QString& text, int index); }; #endif // SEARCHBAR_H diff --git a/src/suggestionlistworker.cpp b/src/suggestionlistworker.cpp new file mode 100644 index 0000000..4f8bce4 --- /dev/null +++ b/src/suggestionlistworker.cpp @@ -0,0 +1,50 @@ +#include "suggestionlistworker.h" +#include "kiwixapp.h" + +SuggestionListWorker::SuggestionListWorker(const QString& text, int token, QObject *parent) +: QThread(parent), + m_text(text), + m_token(token) +{ +} + +void SuggestionListWorker::run() +{ + QStringList suggestionList; + QVector urlList; + + auto currentWidget = KiwixApp::instance()->getTabWidget()->currentWebView(); + auto qurl = currentWidget->url(); + auto currentZimId = qurl.host().split(".")[0]; + auto reader = KiwixApp::instance()->getLibrary()->getReader(currentZimId); + QUrl url; + url.setScheme("zim"); + if (reader) { + url.setHost(currentZimId + ".zim"); + reader->searchSuggestionsSmart(m_text.toStdString(), 15); + std::string title, path; + while (reader->getNextSuggestion(title, path)) { + url.setPath(QString::fromStdString(path)); + suggestionList.append(QString::fromStdString(title)); + urlList.append(url); + } + } + QUrlQuery query; + url.setPath(""); + if (reader) { + // The host is used to determine the currentZimId + // The content query item is used to know in which zim search (as for kiwix-serve) + url.setHost(currentZimId + ".search"); + query.addQueryItem("content", currentZimId); + } else { + // We do not allow multi zim search for now. + // We don't have a correct UI to select on which zim search, + // how to display results, ... + //url.setHost("library.search"); + } + query.addQueryItem("pattern", m_text); + url.setQuery(query); + suggestionList.append(m_text + " (" + gt("fulltext-search") + ")"); + urlList.append(url); + emit(searchFinished(suggestionList, urlList, m_token)); +} \ No newline at end of file diff --git a/src/suggestionlistworker.h b/src/suggestionlistworker.h new file mode 100644 index 0000000..a0e0566 --- /dev/null +++ b/src/suggestionlistworker.h @@ -0,0 +1,23 @@ +#ifndef SUGGESTIONLISTWORKER_H +#define SUGGESTIONLISTWORKER_H + +#include +#include +#include + +class SuggestionListWorker : public QThread +{ + Q_OBJECT +public: + SuggestionListWorker(const QString& text, int token, QObject *parent = nullptr); + void run() override; + +signals: + void searchFinished(const QStringList& suggestions, const QVector& urlList, int token); + +private: + QString m_text; + int m_token = 0; +}; + +#endif // SUGGESTIONLISTWORKER_H diff --git a/src/tabbar.cpp b/src/tabbar.cpp index f3ec3d1..7c635ea 100644 --- a/src/tabbar.cpp +++ b/src/tabbar.cpp @@ -27,7 +27,11 @@ TabBar::TabBar(QWidget *parent) : connect(app->getAction(KiwixApp::NewTabAction), &QAction::triggered, this, [=]() { this->createNewTab(true); - KiwixApp::instance()->getMainWindow()->getTopWidget()->getSearchBar().setFocus(Qt::MouseFocusReason); + auto topWidget = KiwixApp::instance()->getMainWindow()->getTopWidget(); + topWidget->getSearchBar().setFocus(Qt::MouseFocusReason); + topWidget->getSearchBar().clear(); + topWidget->getSearchBar().clearSuggestions(); + topWidget->getSearchBar().hideSuggestions(); }); connect(app->getAction(KiwixApp::CloseTabAction), &QAction::triggered, this, [=]() {