From 3c20eb7ca8802496f3a0f4c1d63a2a17745f2438 Mon Sep 17 00:00:00 2001 From: Slava Monich Date: Thu, 9 Dec 2021 01:46:04 +0200 Subject: [PATCH] Improve chat search performance Simple text search performs significantly better that regular expressions. Disconnecting search filter model from the source when search is off should be good for performance because the filter model won't have to unnecessarily react to the source model changes. --- harbour-fernschreiber.pro | 2 + qml/pages/OverviewPage.qml | 26 +++------- src/chatlistmodel.cpp | 7 ++- src/chatlistmodel.h | 8 +-- src/harbour-fernschreiber.cpp | 7 +-- src/textfiltermodel.cpp | 98 +++++++++++++++++++++++++++++++++++ src/textfiltermodel.h | 58 +++++++++++++++++++++ 7 files changed, 178 insertions(+), 28 deletions(-) create mode 100644 src/textfiltermodel.cpp create mode 100644 src/textfiltermodel.h diff --git a/harbour-fernschreiber.pro b/harbour-fernschreiber.pro index 34d593d..d19ecd1 100644 --- a/harbour-fernschreiber.pro +++ b/harbour-fernschreiber.pro @@ -38,6 +38,7 @@ SOURCES += src/harbour-fernschreiber.cpp \ src/tdlibfile.cpp \ src/tdlibreceiver.cpp \ src/tdlibwrapper.cpp \ + src/textfiltermodel.cpp \ src/tgsplugin.cpp DISTFILES += qml/harbour-fernschreiber.qml \ @@ -228,6 +229,7 @@ HEADERS += \ src/tdlibreceiver.h \ src/tdlibsecrets.h \ src/tdlibwrapper.h \ + src/textfiltermodel.h \ src/tgsplugin.h # https://github.com/Samsung/rlottie.git diff --git a/qml/pages/OverviewPage.qml b/qml/pages/OverviewPage.qml index 4911a34..1fd3104 100644 --- a/qml/pages/OverviewPage.qml +++ b/qml/pages/OverviewPage.qml @@ -103,19 +103,11 @@ Page { } } - Timer { - id: searchChatTimer - interval: 300 - running: false - repeat: false - onTriggered: { - if (chatSearchField.text === "") { - chatListView.model = chatListModel; - } else { - chatListView.model = chatListProxyModel; - } - chatListProxyModel.setFilterWildcard("*" + chatSearchField.text + "*"); - } + TextFilterModel { + id: chatListProxyModel + sourceModel: (chatSearchField.opacity > 0) ? chatListModel : null + filterRoleName: "filter" + filterText: chatSearchField.text } function openMessage(chatId, messageId) { @@ -355,10 +347,6 @@ Page { placeholderText: qsTr("Filter your chats...") canHide: text === "" - onTextChanged: { - searchChatTimer.restart(); - } - onHideClicked: { resetFocus(); } @@ -380,7 +368,7 @@ Page { clip: true opacity: (overviewPage.chatListCreated && !overviewPage.logoutLoading) ? 1 : 0 Behavior on opacity { FadeAnimation {} } - model: chatListModel + model: chatListProxyModel.sourceModel ? chatListProxyModel : chatListModel delegate: ChatListViewItem { ownUserId: overviewPage.ownUserId isVerified: is_verified @@ -394,7 +382,7 @@ Page { ViewPlaceholder { enabled: chatListView.count === 0 - text: chatSearchField.text === "" ? qsTr("You don't have any chats yet.") : qsTr("No matching chats found.") + text: chatListModel.count === 0 ? qsTr("You don't have any chats yet.") : qsTr("No matching chats found.") hintText: qsTr("You can search public chats or create a new chat via the pull-down menu.") } diff --git a/src/chatlistmodel.cpp b/src/chatlistmodel.cpp index cb3ae62..9cc589e 100644 --- a/src/chatlistmodel.cpp +++ b/src/chatlistmodel.cpp @@ -320,6 +320,7 @@ QVector ChatListModel::ChatData::updateLastMessage(const QVariantMap &messa changedRoles.append(RoleLastMessageDate); } if (prevSenderMessageText != senderMessageText()) { + changedRoles.append(RoleFilter); changedRoles.append(RoleLastMessageText); } if (prevSenderMessageStatus != senderMessageStatus()) { @@ -388,6 +389,9 @@ ChatListModel::ChatListModel(TDLibWrapper *tdLibWrapper, AppSettings *appSetting relativeTimeRefreshTimer->setSingleShot(false); relativeTimeRefreshTimer->setInterval(30000); connect(relativeTimeRefreshTimer, SIGNAL(timeout()), SLOT(handleRelativeTimeRefreshTimer())); + connect(this, SIGNAL(rowsInserted(QModelIndex,int,int)), SIGNAL(countChanged())); + connect(this, SIGNAL(rowsRemoved(QModelIndex,int,int)), SIGNAL(countChanged())); + connect(this, SIGNAL(modelReset()), SIGNAL(countChanged())); } ChatListModel::~ChatListModel() @@ -457,7 +461,7 @@ QVariant ChatListModel::data(const QModelIndex &index, int role) const case ChatListModel::RoleIsChannel: return data->isChannel(); case ChatListModel::RoleIsMarkedAsUnread: return data->isMarkedAsUnread(); case ChatListModel::RoleIsPinned: return data->isPinned(); - case ChatListModel::RoleFilter: return QString(data->title() + " " + data->senderMessageText()).trimmed(); + case ChatListModel::RoleFilter: return data->title() + " " + data->senderMessageText(); case ChatListModel::RoleDraftMessageText: return data->draftMessageText(); case ChatListModel::RoleDraftMessageDate: return data->draftMessageDate(); } @@ -891,6 +895,7 @@ void ChatListModel::handleChatTitleUpdated(const QString &chatId, const QString chat->chatData.insert(TITLE, title); QVector changedRoles; changedRoles.append(ChatListModel::RoleTitle); + changedRoles.append(ChatListModel::RoleFilter); const QModelIndex modelIndex(index(chatIndex)); emit dataChanged(modelIndex, modelIndex, changedRoles); } else { diff --git a/src/chatlistmodel.h b/src/chatlistmodel.h index 8c17d75..f3659b7 100644 --- a/src/chatlistmodel.h +++ b/src/chatlistmodel.h @@ -28,6 +28,7 @@ class ChatListModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(bool showAllChats READ showAllChats WRITE setShowAllChats NOTIFY showAllChatsChanged) + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) public: @@ -57,9 +58,9 @@ public: ChatListModel(TDLibWrapper *tdLibWrapper, AppSettings *appSettings); ~ChatListModel() override; - virtual QHash roleNames() const override; - virtual int rowCount(const QModelIndex&) const override; - virtual QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const Q_DECL_OVERRIDE; + int rowCount(const QModelIndex &index = QModelIndex()) const Q_DECL_OVERRIDE; + QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; Q_INVOKABLE void redrawModel(); Q_INVOKABLE QVariantMap get(int row); @@ -90,6 +91,7 @@ private slots: void handleRelativeTimeRefreshTimer(); signals: + void countChanged(); void showAllChatsChanged(); void chatChanged(const qlonglong &changedChatId); void chatJoined(const qlonglong &chatId, const QString &chatTitle); diff --git a/src/harbour-fernschreiber.cpp b/src/harbour-fernschreiber.cpp index 51febb6..1754fcf 100644 --- a/src/harbour-fernschreiber.cpp +++ b/src/harbour-fernschreiber.cpp @@ -42,6 +42,7 @@ #include "dbusadaptor.h" #include "processlauncher.h" #include "stickermanager.h" +#include "textfiltermodel.h" #include "tgsplugin.h" #include "fernschreiberutils.h" #include "knownusersmodel.h" @@ -69,6 +70,7 @@ int main(int argc, char *argv[]) const char *uri = "WerkWolf.Fernschreiber"; qmlRegisterType(uri, 1, 0, "TDLibFile"); qmlRegisterType(uri, 1, 0, "NamedAction"); + qmlRegisterType(uri, 1, 0, "TextFilterModel"); qmlRegisterSingletonType(uri, 1, 0, "DebugLog", DebugLogJS::createSingleton); AppSettings *appSettings = new AppSettings(view.data()); @@ -89,11 +91,6 @@ int main(int argc, char *argv[]) ChatListModel chatListModel(tdLibWrapper, appSettings); context->setContextProperty("chatListModel", &chatListModel); - QSortFilterProxyModel chatListProxyModel(view.data()); - chatListProxyModel.setSourceModel(&chatListModel); - chatListProxyModel.setFilterRole(ChatListModel::RoleFilter); - chatListProxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive); - context->setContextProperty("chatListProxyModel", &chatListProxyModel); ChatModel chatModel(tdLibWrapper); context->setContextProperty("chatModel", &chatModel); diff --git a/src/textfiltermodel.cpp b/src/textfiltermodel.cpp new file mode 100644 index 0000000..d747d30 --- /dev/null +++ b/src/textfiltermodel.cpp @@ -0,0 +1,98 @@ +/* + This file is part of Fernschreiber. + + Fernschreiber is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Fernschreiber is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Fernschreiber. If not, see . +*/ + +#include "textfiltermodel.h" + +#define DEBUG_MODULE TextFilterModel +#include "debuglog.h" + +TextFilterModel::TextFilterModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + setDynamicSortFilter(true); + setFilterCaseSensitivity(Qt::CaseInsensitive); + setFilterFixedString(QString()); +} + +void TextFilterModel::setSource(QObject *model) +{ + setSourceModel(qobject_cast(model)); +} + +void TextFilterModel::setSourceModel(QAbstractItemModel *model) +{ + if (sourceModel() != model) { + LOG(model); + QSortFilterProxyModel::setSourceModel(model); + updateFilterRole(); + emit sourceChanged(); + } +} + +QString TextFilterModel::getFilterRoleName() const +{ + return filterRoleName; +} + +void TextFilterModel::setFilterRoleName(QString role) +{ + if (filterRoleName != role) { + filterRoleName = role; + LOG(role); + updateFilterRole(); + emit filterRoleNameChanged(); + } +} + +QString TextFilterModel::getFilterText() const +{ + return filterText; +} + +void TextFilterModel::setFilterText(QString text) +{ + if (filterText.compare(text, Qt::CaseInsensitive) != 0) { + filterText = text; + LOG(text); + setFilterFixedString(text); + emit filterTextChanged(); + } +} + +int TextFilterModel::findRole(QAbstractItemModel *model, QString role) +{ + if (model && !role.isEmpty()) { + const QByteArray roleName(role.toUtf8()); + const QHash roleMap(model->roleNames()); + const QList roles(roleMap.keys()); + const int n = roles.count(); + for (int i = 0; i < n; i++) { + const QByteArray name(roleMap.value(roles.at(i))); + if (name == roleName) { + LOG(role << roles.at(i)); + return roles.at(i); + } + } + LOG("Unknown role" << role); + } + return -1; +} + +void TextFilterModel::updateFilterRole() +{ + const int role = findRole(sourceModel(), filterRoleName); + setFilterRole((role >= 0) ? role : Qt::DisplayRole); +} diff --git a/src/textfiltermodel.h b/src/textfiltermodel.h new file mode 100644 index 0000000..7f296c0 --- /dev/null +++ b/src/textfiltermodel.h @@ -0,0 +1,58 @@ +/* + This file is part of Fernschreiber. + + Fernschreiber is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Fernschreiber is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Fernschreiber. If not, see . +*/ + +#ifndef TEXTFILTERMODEL_H +#define TEXTFILTERMODEL_H + +#include + +class TextFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(QString filterRoleName READ getFilterRoleName WRITE setFilterRoleName NOTIFY filterRoleNameChanged) + Q_PROPERTY(QString filterText READ getFilterText WRITE setFilterText NOTIFY filterTextChanged) + Q_PROPERTY(QObject* sourceModel READ sourceModel WRITE setSource NOTIFY sourceChanged) + +public: + TextFilterModel(QObject *parent = Q_NULLPTR); + + void setSource(QObject* model); + void setSourceModel(QAbstractItemModel *model) Q_DECL_OVERRIDE; + + QString getFilterRoleName() const; + void setFilterRoleName(QString role); + + QString getFilterText() const; + void setFilterText(QString text); + +signals: + void sourceChanged(); + void filterRoleNameChanged(); + void filterTextChanged(); + +private slots: + void updateFilterRole(); + +private: + static int findRole(QAbstractItemModel *model, QString role); + +private: + QString filterRoleName; + QString filterText; +}; + +#endif // TEXTFILTERMODEL_H