From 748bf0cd988c103e66919db753d40fe19a784022 Mon Sep 17 00:00:00 2001 From: Jussi Kuokkanen Date: Wed, 2 Sep 2020 13:32:57 +0300 Subject: rename CompletionModel to CompletionProxyModel --- src/CompletionProxyModel.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/CompletionProxyModel.h (limited to 'src/CompletionProxyModel.h') diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h new file mode 100644 index 00000000..ee38075e --- /dev/null +++ b/src/CompletionProxyModel.h @@ -0,0 +1,20 @@ +#pragma once + +// Class for showing a limited amount of completions at a time + +#include + +class CompletionProxyModel : public QSortFilterProxyModel +{ +public: + CompletionProxyModel(QAbstractItemModel *model, QObject *parent = nullptr) + : QSortFilterProxyModel(parent) + { + setSourceModel(model); + } + int rowCount(const QModelIndex &parent) const override + { + auto row_count = QSortFilterProxyModel::rowCount(parent); + return (row_count < 7) ? row_count : 7; + } +}; -- cgit 1.5.1 From add5903fb0abc76d77ce4369c4679a95be03b433 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 20 Nov 2020 02:38:08 +0100 Subject: Working User completer --- CMakeLists.txt | 2 ++ resources/qml/Completer.qml | 8 +++++++- resources/qml/MessageInput.qml | 20 ++++++++++++++++---- src/CompletionModel.h | 20 -------------------- src/CompletionModelRoles.h | 2 +- src/CompletionProxyModel.h | 21 ++++++++++++++++++++- src/UsersModel.cpp | 28 ++++++++++++++++++++++------ src/UsersModel.h | 12 +++++++----- src/timeline/InputBar.cpp | 8 ++++++++ 9 files changed, 83 insertions(+), 38 deletions(-) delete mode 100644 src/CompletionModel.h (limited to 'src/CompletionProxyModel.h') diff --git a/CMakeLists.txt b/CMakeLists.txt index aa81f285..7e68db77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -492,6 +492,7 @@ qt5_wrap_cpp(MOC_HEADERS src/ChatPage.h src/CommunitiesList.h src/CommunitiesListItem.h + src/CompletionProxyModel.h src/DeviceVerificationFlow.h src/InviteeItem.h src/LoginPage.h @@ -508,6 +509,7 @@ qt5_wrap_cpp(MOC_HEADERS src/TrayIcon.h src/UserInfoWidget.h src/UserSettingsPage.h + src/UsersModel.h src/WebRTCSession.h src/WelcomePage.h src/popups/PopupItem.h diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml index d53eae62..2c520dec 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml @@ -34,11 +34,17 @@ Popup { onCompleterNameChanged: { if (completerName) completer = TimelineManager.timeline.input.completerFor(completerName); - + else + completer = undefined; } padding: 0 onAboutToShow: currentIndex = -1 + Connections { + onTimelineChanged: completer = null + target: TimelineManager + } + ColumnLayout { anchors.fill: parent spacing: 0 diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index a4a47a3e..50ff7324 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -77,14 +77,13 @@ Rectangle { onTextChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onCursorPositionChanged: { TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text); - if (cursorPosition < completerTriggeredAt) { + if (cursorPosition <= completerTriggeredAt) { completerTriggeredAt = -1; popup.close(); } } onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) - // Ensure that we get escape key press events first. Keys.onShortcutOverride: event.accepted = (completerTriggeredAt != -1 && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter)) Keys.onPressed: { if (event.matches(StandardKey.Paste)) { @@ -97,18 +96,20 @@ Rectangle { } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) { textArea.text = TimelineManager.timeline.input.nextText(); } else if (event.key == Qt.Key_At) { - completerTriggeredAt = cursorPosition + 1; + completerTriggeredAt = cursorPosition; popup.completerName = "user"; popup.open(); } else if (event.key == Qt.Key_Escape && popup.opened) { completerTriggeredAt = -1; + popup.completerName = ""; event.accepted = true; popup.close(); } else if (event.matches(StandardKey.InsertParagraphSeparator) && popup.opened) { var currentCompletion = popup.currentCompletion(); + popup.completerName = ""; popup.close(); if (currentCompletion) { - textArea.remove(completerTriggeredAt - 1, cursorPosition); + textArea.remove(completerTriggeredAt, cursorPosition); textArea.insert(cursorPosition, currentCompletion); event.accepted = true; return ; @@ -129,6 +130,17 @@ Rectangle { } } + Connections { + onTimelineChanged: { + textArea.clear(); + textArea.append(TimelineManager.timeline.input.text()); + textArea.completerTriggeredAt = -1; + popup.completerName = ""; + } + target: TimelineManager + } + // Ensure that we get escape key press events first. + Completer { id: popup diff --git a/src/CompletionModel.h b/src/CompletionModel.h deleted file mode 100644 index ed021051..00000000 --- a/src/CompletionModel.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -// Class for showing a limited amount of completions at a time - -#include - -class CompletionModel : public QSortFilterProxyModel -{ -public: - CompletionModel(QAbstractItemModel *model, QObject *parent = nullptr) - : QSortFilterProxyModel(parent) - { - setSourceModel(model); - } - int rowCount(const QModelIndex &parent) const override - { - auto row_count = QSortFilterProxyModel::rowCount(parent); - return (row_count < 7) ? row_count : 7; - } -}; diff --git a/src/CompletionModelRoles.h b/src/CompletionModelRoles.h index bd6c4114..7c7307d3 100644 --- a/src/CompletionModelRoles.h +++ b/src/CompletionModelRoles.h @@ -10,6 +10,6 @@ enum Roles { CompletionRole = Qt::UserRole * 2, // The string to replace the active completion SearchRole, // String completer uses for search + SearchRole2, // Secondary string completer uses for search }; - } diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h index ee38075e..757aa990 100644 --- a/src/CompletionProxyModel.h +++ b/src/CompletionProxyModel.h @@ -4,17 +4,36 @@ #include +#include "CompletionModelRoles.h" + class CompletionProxyModel : public QSortFilterProxyModel { + Q_OBJECT + public: CompletionProxyModel(QAbstractItemModel *model, QObject *parent = nullptr) : QSortFilterProxyModel(parent) { setSourceModel(model); } - int rowCount(const QModelIndex &parent) const override + + QHash roleNames() const override + { + return this->sourceModel()->roleNames(); + } + + int rowCount(const QModelIndex &parent = QModelIndex()) const override { auto row_count = QSortFilterProxyModel::rowCount(parent); return (row_count < 7) ? row_count : 7; } + +public slots: + QVariant completionAt(int i) const + { + if (i >= 0 && i < rowCount()) + return data(index(i, 0), CompletionModel::CompletionRole); + else + return {}; + } }; diff --git a/src/UsersModel.cpp b/src/UsersModel.cpp index c63292bb..f102cff1 100644 --- a/src/UsersModel.cpp +++ b/src/UsersModel.cpp @@ -3,12 +3,23 @@ #include "Cache.h" #include "CompletionModelRoles.h" -#include - UsersModel::UsersModel(const std::string &roomId, QObject *parent) : QAbstractListModel(parent) + , room_id(roomId) +{ + roomMembers_ = cache::roomMembers(roomId); +} + +QHash +UsersModel::roleNames() const { - roomMembers_ = cache::getMembers(roomId, 0, 9999); + return { + {CompletionModel::CompletionRole, "completionRole"}, + {CompletionModel::SearchRole, "searchRole"}, + {CompletionModel::SearchRole2, "searchRole2"}, + {Roles::DisplayName, "displayName"}, + {Roles::AvatarUrl, "avatarUrl"}, + }; } QVariant @@ -19,9 +30,14 @@ UsersModel::data(const QModelIndex &index, int role) const case CompletionModel::CompletionRole: case CompletionModel::SearchRole: case Qt::DisplayRole: - return roomMembers_[index.row()].display_name; - case Avatar: - return roomMembers_[index.row()].avatar; + case Roles::DisplayName: + return QString::fromStdString( + cache::displayName(room_id, roomMembers_[index.row()])); + case CompletionModel::SearchRole2: + return QString::fromStdString(roomMembers_[index.row()]); + case Roles::AvatarUrl: + return cache::avatarUrl(QString::fromStdString(room_id), + QString::fromStdString(roomMembers_[index.row()])); } } return {}; diff --git a/src/UsersModel.h b/src/UsersModel.h index 09ccbf25..6ee8261f 100644 --- a/src/UsersModel.h +++ b/src/UsersModel.h @@ -2,23 +2,25 @@ #include -class RoomMember; - class UsersModel : public QAbstractListModel { public: enum Roles { - Avatar = Qt::UserRole // QImage avatar + AvatarUrl = Qt::UserRole, + DisplayName, }; UsersModel(const std::string &roomId, QObject *parent = nullptr); + QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override { - return (parent == QModelIndex()) ? roomMembers_.size() : 0; + (void)parent; + return roomMembers_.size(); } QVariant data(const QModelIndex &index, int role) const override; private: - std::vector roomMembers_; + std::string room_id; + std::vector roomMembers_; }; diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 82649faa..641d8379 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -14,12 +14,14 @@ #include "Cache.h" #include "CallManager.h" #include "ChatPage.h" +#include "CompletionProxyModel.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" #include "Olm.h" #include "TimelineModel.h" #include "UserSettingsPage.h" +#include "UsersModel.h" #include "Utils.h" #include "dialogs/PlaceCall.h" #include "dialogs/PreviewUploadOverlay.h" @@ -166,6 +168,12 @@ InputBar::nextText() QObject * InputBar::completerFor(QString completerName) { + if (completerName == "user") { + auto userModel = new UsersModel(room->roomId().toStdString()); + auto proxy = new CompletionProxyModel(userModel); + userModel->setParent(proxy); + return proxy; + } return nullptr; } -- cgit 1.5.1 From ecc7759973ae49ea718aea34c5469d2d1ba205d1 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 20 Nov 2020 04:05:31 +0100 Subject: Custom completer filtering --- resources/qml/MessageInput.qml | 3 ++ src/CompletionProxyModel.h | 96 ++++++++++++++++++++++++++++++++++++++++++ src/UsersModel.cpp | 9 ++-- src/UsersModel.h | 2 + 4 files changed, 107 insertions(+), 3 deletions(-) (limited to 'src/CompletionProxyModel.h') diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 50ff7324..d9592e14 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -128,6 +128,9 @@ Rectangle { textArea.clear(); event.accepted = true; } + if (popup.opened) + popup.completer.setSearchString(textArea.getText(completerTriggeredAt, cursorPosition)); + } Connections { diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h index 757aa990..ee8236e3 100644 --- a/src/CompletionProxyModel.h +++ b/src/CompletionProxyModel.h @@ -5,6 +5,7 @@ #include #include "CompletionModelRoles.h" +#include "Utils.h" class CompletionProxyModel : public QSortFilterProxyModel { @@ -15,6 +16,20 @@ public: : QSortFilterProxyModel(parent) { setSourceModel(model); + sort(0, Qt::AscendingOrder); + setFilterRole(CompletionModel::SearchRole); + + connect( + this, + &CompletionProxyModel::newSearchString, + this, + [this](QString s) { + s.remove(":"); + s.remove("@"); + searchString = s.toLower(); + invalidate(); + }, + Qt::QueuedConnection); } QHash roleNames() const override @@ -28,6 +43,79 @@ public: return (row_count < 7) ? row_count : 7; } + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const + { + if (searchString.size() < 1) + return true; + + auto source_index = sourceModel()->index(source_row, 0, source_parent); + auto role1 = sourceModel() + ->data(source_index, CompletionModel::SearchRole) + .toString() + .toLower(); + + if (role1.contains(searchString)) + return true; + // auto score = + // utils::levenshtein_distance(searchString, role1.toLower().toStdString()); + // if ((size_t)role1.size() >= searchString.size() && + // ((size_t)score) < (size_t)role1.size() - searchString.size() + 2) + // return true; + + auto role2 = sourceModel() + ->data(source_index, CompletionModel::SearchRole2) + .toString() + .toLower(); + if (role2.contains(searchString)) + return true; + // if (!role2.isEmpty()) { + // score = + // utils::levenshtein_distance(searchString, + // role2.toLower().toStdString()); + // if ((size_t)role2.size() >= searchString.size() && + // ((size_t)score) < (size_t)role2.size() - searchString.size() + 2) + // return true; + //} + + return false; + } + + bool lessThan(const QModelIndex &source_left, + const QModelIndex &source_right) const override + { + if (searchString.size() < 1) + return false; + + auto left1 = + sourceModel()->data(source_left, CompletionModel::SearchRole).toString(); + auto left2 = + sourceModel()->data(source_left, CompletionModel::SearchRole2).toString(); + auto left = left1.toLower().indexOf(searchString); + // utils::levenshtein_distance(searchString, left1.toLower().toStdString()); + if (!left2.isEmpty()) { + // left = std::min( + // utils::levenshtein_distance(searchString, + // left2.toLower().toStdString()), left); + left = std::min(left2.toLower().indexOf(searchString), left); + } + + auto right1 = + sourceModel()->data(source_right, CompletionModel::SearchRole).toString(); + auto right2 = + sourceModel()->data(source_right, CompletionModel::SearchRole2).toString(); + auto right = right1.toLower().indexOf(searchString); + // auto right = + // utils::levenshtein_distance(searchString, right1.toLower().toStdString()); + if (!right2.isEmpty()) { + // right = std::min( + // utils::levenshtein_distance(searchString, + // right2.toLower().toStdString()), right); + right = std::min(right2.toLower().indexOf(searchString), right); + } + + return left < right; + } + public slots: QVariant completionAt(int i) const { @@ -36,4 +124,12 @@ public slots: else return {}; } + + void setSearchString(QString s) { emit newSearchString(s); } + +signals: + void newSearchString(QString); + +private: + QString searchString; }; diff --git a/src/UsersModel.cpp b/src/UsersModel.cpp index f102cff1..cb9f8f75 100644 --- a/src/UsersModel.cpp +++ b/src/UsersModel.cpp @@ -8,6 +8,10 @@ UsersModel::UsersModel(const std::string &roomId, QObject *parent) , room_id(roomId) { roomMembers_ = cache::roomMembers(roomId); + for (const auto &m : roomMembers_) { + displayNames.push_back(QString::fromStdString(cache::displayName(room_id, m))); + userids.push_back(QString::fromStdString(m)); + } } QHash @@ -31,10 +35,9 @@ UsersModel::data(const QModelIndex &index, int role) const case CompletionModel::SearchRole: case Qt::DisplayRole: case Roles::DisplayName: - return QString::fromStdString( - cache::displayName(room_id, roomMembers_[index.row()])); + return displayNames[index.row()]; case CompletionModel::SearchRole2: - return QString::fromStdString(roomMembers_[index.row()]); + return userids[index.row()]; case Roles::AvatarUrl: return cache::avatarUrl(QString::fromStdString(room_id), QString::fromStdString(roomMembers_[index.row()])); diff --git a/src/UsersModel.h b/src/UsersModel.h index 6ee8261f..cddcdd84 100644 --- a/src/UsersModel.h +++ b/src/UsersModel.h @@ -23,4 +23,6 @@ public: private: std::string room_id; std::vector roomMembers_; + std::vector displayNames; + std::vector userids; }; -- cgit 1.5.1 From 67dcc74c79aebc3d6f117f1cc08a44a778b1192e Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 20 Nov 2020 04:14:19 +0100 Subject: Simple prefix match for completions --- resources/qml/MessageInput.qml | 2 +- src/CompletionProxyModel.h | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src/CompletionProxyModel.h') diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index d9592e14..1366689c 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -129,7 +129,7 @@ Rectangle { event.accepted = true; } if (popup.opened) - popup.completer.setSearchString(textArea.getText(completerTriggeredAt, cursorPosition)); + popup.completer.setSearchString(textArea.getText(completerTriggeredAt, cursorPosition) + event.text); } diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h index ee8236e3..2bc22875 100644 --- a/src/CompletionProxyModel.h +++ b/src/CompletionProxyModel.h @@ -90,27 +90,27 @@ public: sourceModel()->data(source_left, CompletionModel::SearchRole).toString(); auto left2 = sourceModel()->data(source_left, CompletionModel::SearchRole2).toString(); - auto left = left1.toLower().indexOf(searchString); + auto left = (size_t)left1.toLower().indexOf(searchString); // utils::levenshtein_distance(searchString, left1.toLower().toStdString()); if (!left2.isEmpty()) { // left = std::min( // utils::levenshtein_distance(searchString, // left2.toLower().toStdString()), left); - left = std::min(left2.toLower().indexOf(searchString), left); + left = std::min((size_t)left2.toLower().indexOf(searchString), left); } auto right1 = sourceModel()->data(source_right, CompletionModel::SearchRole).toString(); auto right2 = sourceModel()->data(source_right, CompletionModel::SearchRole2).toString(); - auto right = right1.toLower().indexOf(searchString); + auto right = (size_t)right1.toLower().indexOf(searchString); // auto right = // utils::levenshtein_distance(searchString, right1.toLower().toStdString()); if (!right2.isEmpty()) { // right = std::min( // utils::levenshtein_distance(searchString, // right2.toLower().toStdString()), right); - right = std::min(right2.toLower().indexOf(searchString), right); + right = std::min((size_t)right2.toLower().indexOf(searchString), right); } return left < right; -- cgit 1.5.1 From c8fa40a2dffad4814bf96248bad0e60c1b2209e9 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 23 Nov 2020 05:08:17 +0100 Subject: Use a trie for filtering completions (not fuzzy yet) --- resources/qml/Completer.qml | 4 +- src/CompletionProxyModel.h | 201 ++++++++++++++++++++++++++++---------------- 2 files changed, 131 insertions(+), 74 deletions(-) (limited to 'src/CompletionProxyModel.h') diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml index 0557a2e7..dda5b896 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml @@ -55,7 +55,7 @@ Popup { model: completer delegate: Rectangle { - color: model.index == popup.currentIndex ? colors.window : colors.base + color: model.index == popup.currentIndex ? colors.window : colors.alternateBase height: chooser.childrenRect.height + 4 width: chooser.childrenRect.width + 4 @@ -141,7 +141,7 @@ Popup { } background: Rectangle { - color: colors.base + color: colors.alternateBase implicitHeight: popup.contentHeight implicitWidth: popup.contentWidth } diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h index 2bc22875..fa22c61e 100644 --- a/src/CompletionProxyModel.h +++ b/src/CompletionProxyModel.h @@ -2,22 +2,106 @@ // Class for showing a limited amount of completions at a time -#include +#include #include "CompletionModelRoles.h" +#include "Logging.h" #include "Utils.h" -class CompletionProxyModel : public QSortFilterProxyModel +template +struct trie +{ + std::vector values; + std::map next; + + void insert(const QVector &keys, const Value &v) + { + auto t = this; + for (const auto k : keys) { + t = &t->next[k]; + } + + t->values.push_back(v); + } + + std::vector valuesAndSubvalues(size_t limit = -1) const + { + std::vector ret; + if (limit < 200) + ret.reserve(limit); + + for (const auto &v : values) { + if (ret.size() >= limit) + return ret; + else + ret.push_back(v); + } + + for (const auto &[k, t] : next) { + (void)k; + if (ret.size() >= limit) + return ret; + else { + auto temp = t.valuesAndSubvalues(limit - ret.size()); + ret.insert(ret.end(), temp.begin(), temp.end()); + } + } + + return ret; + } + + std::vector search(const QVector &keys, size_t limit) const + { + std::vector ret; + auto t = this; + int i = 0; + for (; i < (int)keys.size(); i++) { + if (auto e = t->next.find(keys[i]); e != t->next.end()) { + t = &e->second; + } else { + t = nullptr; + break; + } + } + + if (t) { + ret = t->valuesAndSubvalues(limit); + } + + return ret; + } +}; + +class CompletionProxyModel : public QAbstractProxyModel { Q_OBJECT public: CompletionProxyModel(QAbstractItemModel *model, QObject *parent = nullptr) - : QSortFilterProxyModel(parent) + : QAbstractProxyModel(parent) { setSourceModel(model); - sort(0, Qt::AscendingOrder); - setFilterRole(CompletionModel::SearchRole); + + for (int i = 0; i < sourceModel()->rowCount(); i++) { + if (i < 7) + mapping.push_back(i); + + auto string1 = + sourceModel() + ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole) + .toString() + .toLower(); + trie_.insert(string1.toUcs4(), i); + + auto string2 = + sourceModel() + ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2) + .toString() + .toLower(); + + if (!string2.isEmpty()) + trie_.insert(string2.toUcs4(), i); + } connect( this, @@ -32,6 +116,20 @@ public: Qt::QueuedConnection); } + void invalidate() + { + auto key = searchString.toUcs4(); + beginResetModel(); + mapping = trie_.search(key, 7); + endResetModel(); + + std::string temp; + for (auto v : mapping) { + temp += std::to_string(v) + ", "; + } + nhlog::ui()->debug("mapping: {}", temp); + }; + QHash roleNames() const override { return this->sourceModel()->roleNames(); @@ -39,83 +137,40 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override { - auto row_count = QSortFilterProxyModel::rowCount(parent); - return (row_count < 7) ? row_count : 7; + (void)parent; + return (int)mapping.size(); } - bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override { - if (searchString.size() < 1) - return true; - - auto source_index = sourceModel()->index(source_row, 0, source_parent); - auto role1 = sourceModel() - ->data(source_index, CompletionModel::SearchRole) - .toString() - .toLower(); - - if (role1.contains(searchString)) - return true; - // auto score = - // utils::levenshtein_distance(searchString, role1.toLower().toStdString()); - // if ((size_t)role1.size() >= searchString.size() && - // ((size_t)score) < (size_t)role1.size() - searchString.size() + 2) - // return true; - - auto role2 = sourceModel() - ->data(source_index, CompletionModel::SearchRole2) - .toString() - .toLower(); - if (role2.contains(searchString)) - return true; - // if (!role2.isEmpty()) { - // score = - // utils::levenshtein_distance(searchString, - // role2.toLower().toStdString()); - // if ((size_t)role2.size() >= searchString.size() && - // ((size_t)score) < (size_t)role2.size() - searchString.size() + 2) - // return true; - //} - - return false; + for (int i = 0; i < (int)mapping.size(); i++) { + if (mapping[i] == sourceIndex.row()) { + return index(i, 0); + } + } + return QModelIndex(); } - bool lessThan(const QModelIndex &source_left, - const QModelIndex &source_right) const override + QModelIndex mapToSource(const QModelIndex &proxyIndex) const override { - if (searchString.size() < 1) - return false; - - auto left1 = - sourceModel()->data(source_left, CompletionModel::SearchRole).toString(); - auto left2 = - sourceModel()->data(source_left, CompletionModel::SearchRole2).toString(); - auto left = (size_t)left1.toLower().indexOf(searchString); - // utils::levenshtein_distance(searchString, left1.toLower().toStdString()); - if (!left2.isEmpty()) { - // left = std::min( - // utils::levenshtein_distance(searchString, - // left2.toLower().toStdString()), left); - left = std::min((size_t)left2.toLower().indexOf(searchString), left); - } + auto row = proxyIndex.row(); + if (row < 0 || row >= (int)mapping.size()) + return QModelIndex(); - auto right1 = - sourceModel()->data(source_right, CompletionModel::SearchRole).toString(); - auto right2 = - sourceModel()->data(source_right, CompletionModel::SearchRole2).toString(); - auto right = (size_t)right1.toLower().indexOf(searchString); - // auto right = - // utils::levenshtein_distance(searchString, right1.toLower().toStdString()); - if (!right2.isEmpty()) { - // right = std::min( - // utils::levenshtein_distance(searchString, - // right2.toLower().toStdString()), right); - right = std::min((size_t)right2.toLower().indexOf(searchString), right); - } + return sourceModel()->index(mapping[row], 0); + } - return left < right; + QModelIndex index(int row, + int column, + const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return createIndex(row, column); } + QModelIndex parent(const QModelIndex &) const override { return QModelIndex{}; } + int columnCount(const QModelIndex &) const override { return sourceModel()->columnCount(); } + public slots: QVariant completionAt(int i) const { @@ -132,4 +187,6 @@ signals: private: QString searchString; + trie trie_; + std::vector mapping; }; -- cgit 1.5.1 From 29625ae25307aa6746df90cdecc208adc2c36ccb Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 24 Nov 2020 02:35:38 +0100 Subject: Fix some issues with completer --- resources/qml/ActiveCallBar.qml | 1 - resources/qml/Completer.qml | 119 ++++++++++++++++++++++------------------ resources/qml/MessageView.qml | 1 - resources/qml/TimelineView.qml | 2 - src/CompletionProxyModel.h | 89 ++++++++++++++++++++++++++---- 5 files changed, 144 insertions(+), 68 deletions(-) (limited to 'src/CompletionProxyModel.h') diff --git a/resources/qml/ActiveCallBar.qml b/resources/qml/ActiveCallBar.qml index cbe36e6d..49c0e255 100644 --- a/resources/qml/ActiveCallBar.qml +++ b/resources/qml/ActiveCallBar.qml @@ -53,7 +53,6 @@ Rectangle { Connections { target: TimelineManager - onCallStateChanged: { switch (state) { case WebRTCState.INITIATING: diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml index dda5b896..7074de81 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml @@ -9,105 +9,120 @@ Popup { property int currentIndex: -1 property string completerName property var completer + property bool bottomToTop: true function up() { + if (bottomToTop) + down_(); + else + up_(); + } + + function down() { + if (bottomToTop) + up_(); + else + down_(); + } + + function up_() { currentIndex = currentIndex - 1; if (currentIndex == -2) - currentIndex = repeater.count - 1; + currentIndex = listView.count - 1; } - function down() { + function down_() { currentIndex = currentIndex + 1; - if (currentIndex >= repeater.count) + if (currentIndex >= listView.count) currentIndex = -1; } function currentCompletion() { - if (currentIndex > -1 && currentIndex < repeater.count) + if (currentIndex > -1 && currentIndex < listView.count) return completer.completionAt(currentIndex); else return null; } onCompleterNameChanged: { - if (completerName) + if (completerName) { completer = TimelineManager.timeline.input.completerFor(completerName); - else + completer.setSearchString(""); + } else { completer = undefined; + } } padding: 0 onAboutToShow: currentIndex = -1 + height: listView.contentHeight Connections { onTimelineChanged: completer = null target: TimelineManager } - ColumnLayout { - anchors.fill: parent - spacing: 0 - - Repeater { - id: repeater - - model: completer + ListView { + id: listView - delegate: Rectangle { - color: model.index == popup.currentIndex ? colors.window : colors.alternateBase - height: chooser.childrenRect.height + 4 - width: chooser.childrenRect.width + 4 + anchors.fill: parent + implicitWidth: contentItem.childrenRect.width + model: completer + verticalLayoutDirection: popup.bottomToTop ? ListView.BottomToTop : ListView.TopToBottom - DelegateChooser { - id: chooser + delegate: Rectangle { + color: model.index == popup.currentIndex ? colors.highlight : colors.base + height: chooser.childrenRect.height + 4 + implicitWidth: chooser.childrenRect.width + 4 - roleValue: popup.completerName - anchors.centerIn: parent + DelegateChooser { + id: chooser - DelegateChoice { - roleValue: "user" + roleValue: popup.completerName + anchors.centerIn: parent - RowLayout { - id: del + DelegateChoice { + roleValue: "user" - anchors.centerIn: parent + RowLayout { + id: del - Avatar { - height: 24 - width: 24 - displayName: model.displayName - url: model.avatarUrl.replace("mxc://", "image://MxcImage/") - } + anchors.centerIn: parent - Label { - text: model.displayName - color: colors.text - } + Avatar { + height: 24 + width: 24 + displayName: model.displayName + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + } + Label { + text: model.displayName + color: model.index == popup.currentIndex ? colors.highlightedText : colors.text } } - DelegateChoice { - roleValue: "emoji" + } - RowLayout { - id: del + DelegateChoice { + roleValue: "emoji" - anchors.centerIn: parent + RowLayout { + id: del - Label { - text: model.unicode - color: colors.text - font: Settings.emojiFont - } + anchors.centerIn: parent - Label { - text: model.shortName - color: colors.text - } + Label { + text: model.unicode + color: model.index == popup.currentIndex ? colors.highlightedText : colors.text + font: Settings.emojiFont + } + Label { + text: model.shortName + color: model.index == popup.currentIndex ? colors.highlightedText : colors.text } } @@ -141,7 +156,7 @@ Popup { } background: Rectangle { - color: colors.alternateBase + color: colors.base implicitHeight: popup.contentHeight implicitWidth: popup.contentWidth } diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 71c85276..679c1f50 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -182,7 +182,6 @@ ListView { Connections { target: chat - onMovementEnded: { if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) chat.model.currentIndex = index; diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index c23564c1..ac998a81 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -131,7 +131,6 @@ Page { Connections { target: TimelineManager - onNewDeviceVerificationRequest: { var dialog = deviceVerificationDialog.createObject(timelineRoot, { "flow": flow @@ -142,7 +141,6 @@ Page { Connections { target: TimelineManager.timeline - onOpenProfile: { var userProfile = userProfileComponent.createObject(timelineRoot, { "profile": profile diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h index fa22c61e..ba28e84c 100644 --- a/src/CompletionProxyModel.h +++ b/src/CompletionProxyModel.h @@ -43,29 +43,94 @@ struct trie return ret; else { auto temp = t.valuesAndSubvalues(limit - ret.size()); - ret.insert(ret.end(), temp.begin(), temp.end()); + for (auto &&v : temp) { + if (ret.size() >= limit) + return ret; + + if (std::find(ret.begin(), ret.end(), v) == ret.end()) { + ret.push_back(std::move(v)); + } + } } } return ret; } - std::vector search(const QVector &keys, size_t limit) const + std::vector search(const QVector &keys, + size_t limit, + size_t max_distance = 2) const { std::vector ret; - auto t = this; - int i = 0; - for (; i < (int)keys.size(); i++) { - if (auto e = t->next.find(keys[i]); e != t->next.end()) { - t = &e->second; - } else { - t = nullptr; - break; + if (!limit) + return ret; + + auto append = [&ret, limit](std::vector &&in) { + for (auto &&v : in) { + if (ret.size() >= limit) + return; + + if (std::find(ret.begin(), ret.end(), v) == ret.end()) { + ret.push_back(std::move(v)); + } + } + }; + + { + auto t = this; + int i = 0; + for (; i < (int)keys.size(); i++) { + if (auto e = t->next.find(keys[i]); e != t->next.end()) { + t = &e->second; + } else { + t = nullptr; + break; + } + } + + if (t) { + ret = t->valuesAndSubvalues(limit); } } - if (t) { - ret = t->valuesAndSubvalues(limit); + if (max_distance && keys.size() < static_cast(limit) && keys.size() > 1) { + max_distance -= 1; + + // swap chars case + if (keys.size() >= 2) { + auto t = this; + for (int i = 1; i >= 0; i--) { + if (auto e = t->next.find(keys[i]); e != t->next.end()) { + t = &e->second; + } else { + t = nullptr; + break; + } + } + + if (t) { + append(t->search( + keys.mid(2), (limit - ret.size()) * 2, max_distance)); + } + } + + // delete character case + append(this->search(keys.mid(1), (limit - ret.size()) * 2, max_distance)); + + // substitute and insert cases + for (const auto &[k, t] : this->next) { + if (k == keys[0] || ret.size() >= limit) + break; + + // substitute + append(this->search(keys.mid(1), limit - ret.size(), max_distance)); + + if (ret.size() >= limit) + break; + + // insert + append(this->search(keys, limit - ret.size(), max_distance)); + } } return ret; -- cgit 1.5.1 From 8922a4777683e08dc0cccfaaff37bb693bd642a6 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 24 Nov 2020 02:56:14 +0100 Subject: Fix completer fuzzy match not applying after prefix match and insert being broken --- src/CompletionProxyModel.h | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) (limited to 'src/CompletionProxyModel.h') diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h index ba28e84c..91f937a4 100644 --- a/src/CompletionProxyModel.h +++ b/src/CompletionProxyModel.h @@ -57,7 +57,7 @@ struct trie return ret; } - std::vector search(const QVector &keys, + std::vector search(const QVector &keys, //< TODO(Nico): replace this with a span size_t limit, size_t max_distance = 2) const { @@ -65,6 +65,9 @@ struct trie if (!limit) return ret; + if (keys.isEmpty()) + return valuesAndSubvalues(limit); + auto append = [&ret, limit](std::vector &&in) { for (auto &&v : in) { if (ret.size() >= limit) @@ -76,24 +79,11 @@ struct trie } }; - { - auto t = this; - int i = 0; - for (; i < (int)keys.size(); i++) { - if (auto e = t->next.find(keys[i]); e != t->next.end()) { - t = &e->second; - } else { - t = nullptr; - break; - } - } - - if (t) { - ret = t->valuesAndSubvalues(limit); - } + if (auto e = this->next.find(keys[0]); e != this->next.end()) { + append(e->second.search(keys.mid(1), limit, max_distance)); } - if (max_distance && keys.size() < static_cast(limit) && keys.size() > 1) { + if (max_distance && ret.size() < limit) { max_distance -= 1; // swap chars case @@ -123,13 +113,13 @@ struct trie break; // substitute - append(this->search(keys.mid(1), limit - ret.size(), max_distance)); + append(t.search(keys.mid(1), limit - ret.size(), max_distance)); if (ret.size() >= limit) break; // insert - append(this->search(keys, limit - ret.size(), max_distance)); + append(t.search(keys, limit - ret.size(), max_distance)); } } -- cgit 1.5.1 From 9c8850a46c8f8f16bb476fcd7d3d529e82796048 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 24 Nov 2020 13:40:23 +0100 Subject: Match on each word in the completer --- src/CompletionProxyModel.h | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'src/CompletionProxyModel.h') diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h index 91f937a4..f4ec6a96 100644 --- a/src/CompletionProxyModel.h +++ b/src/CompletionProxyModel.h @@ -3,6 +3,7 @@ // Class for showing a limited amount of completions at a time #include +#include #include "CompletionModelRoles.h" #include "Logging.h" @@ -136,6 +137,7 @@ public: : QAbstractProxyModel(parent) { setSourceModel(model); + QRegularExpression splitPoints("\\s+|-"); for (int i = 0; i < sourceModel()->rowCount(); i++) { if (i < 7) @@ -148,14 +150,23 @@ public: .toLower(); trie_.insert(string1.toUcs4(), i); + for (const auto &e : string1.split(splitPoints, Qt::SkipEmptyParts)) { + trie_.insert(e.toUcs4(), i); + } + auto string2 = sourceModel() ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2) .toString() .toLower(); - if (!string2.isEmpty()) + if (!string2.isEmpty()) { trie_.insert(string2.toUcs4(), i); + for (const auto &e : + string2.split(splitPoints, Qt::SkipEmptyParts)) { + trie_.insert(e.toUcs4(), i); + } + } } connect( -- cgit 1.5.1 From 7cf66ea4f30a9f6daa15e79300292dd5e720c970 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 24 Nov 2020 15:35:56 +0100 Subject: Split completion model into header and source --- CMakeLists.txt | 1 + src/CompletionProxyModel.cpp | 133 +++++++++++++++++++++++++++++++++++++++++++ src/CompletionProxyModel.h | 121 ++++----------------------------------- 3 files changed, 145 insertions(+), 110 deletions(-) create mode 100644 src/CompletionProxyModel.cpp (limited to 'src/CompletionProxyModel.h') diff --git a/CMakeLists.txt b/CMakeLists.txt index 52527312..bd42938a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -283,6 +283,7 @@ set(SRC_FILES src/ColorImageProvider.cpp src/CommunitiesList.cpp src/CommunitiesListItem.cpp + src/CompletionProxyModel.cpp src/DeviceVerificationFlow.cpp src/EventAccessors.cpp src/InviteeItem.cpp diff --git a/src/CompletionProxyModel.cpp b/src/CompletionProxyModel.cpp new file mode 100644 index 00000000..c520bb68 --- /dev/null +++ b/src/CompletionProxyModel.cpp @@ -0,0 +1,133 @@ +#include "CompletionProxyModel.h" + +#include + +#include "CompletionModelRoles.h" +#include "Logging.h" +#include "Utils.h" + +CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model, QObject *parent) + : QAbstractProxyModel(parent) +{ + setSourceModel(model); + QRegularExpression splitPoints("\\s+|-"); + + for (int i = 0; i < sourceModel()->rowCount(); i++) { + if (i < 7) + mapping.push_back(i); + + auto string1 = sourceModel() + ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole) + .toString() + .toLower(); + trie_.insert(string1.toUcs4(), i); + + for (const auto &e : string1.split(splitPoints, Qt::SkipEmptyParts)) { + trie_.insert(e.toUcs4(), i); + } + + auto string2 = sourceModel() + ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2) + .toString() + .toLower(); + + if (!string2.isEmpty()) { + trie_.insert(string2.toUcs4(), i); + for (const auto &e : string2.split(splitPoints, Qt::SkipEmptyParts)) { + trie_.insert(e.toUcs4(), i); + } + } + } + + connect( + this, + &CompletionProxyModel::newSearchString, + this, + [this](QString s) { + s.remove(":"); + s.remove("@"); + searchString = s.toLower(); + invalidate(); + }, + Qt::QueuedConnection); +} + +void +CompletionProxyModel::invalidate() +{ + auto key = searchString.toUcs4(); + beginResetModel(); + mapping = trie_.search(key, 7); + endResetModel(); + + std::string temp; + for (auto v : mapping) { + temp += std::to_string(v) + ", "; + } + nhlog::ui()->debug("mapping: {}", temp); +} + +QHash +CompletionProxyModel::roleNames() const +{ + return this->sourceModel()->roleNames(); +} + +int +CompletionProxyModel::rowCount(const QModelIndex &) const +{ + return (int)mapping.size(); +} + +QModelIndex +CompletionProxyModel::mapFromSource(const QModelIndex &sourceIndex) const +{ + for (int i = 0; i < (int)mapping.size(); i++) { + if (mapping[i] == sourceIndex.row()) { + return index(i, 0); + } + } + return QModelIndex(); +} + +QModelIndex +CompletionProxyModel::mapToSource(const QModelIndex &proxyIndex) const +{ + auto row = proxyIndex.row(); + if (row < 0 || row >= (int)mapping.size()) + return QModelIndex(); + + return sourceModel()->index(mapping[row], 0); +} + +QModelIndex +CompletionProxyModel::index(int row, int column, const QModelIndex &) const +{ + return createIndex(row, column); +} + +QModelIndex +CompletionProxyModel::parent(const QModelIndex &) const +{ + return QModelIndex{}; +} +int +CompletionProxyModel::columnCount(const QModelIndex &) const +{ + return sourceModel()->columnCount(); +} + +QVariant +CompletionProxyModel::completionAt(int i) const +{ + if (i >= 0 && i < rowCount()) + return data(index(i, 0), CompletionModel::CompletionRole); + else + return {}; +} + +void +CompletionProxyModel::setSearchString(QString s) +{ + emit newSearchString(s); +} diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h index f4ec6a96..1517505f 100644 --- a/src/CompletionProxyModel.h +++ b/src/CompletionProxyModel.h @@ -3,11 +3,6 @@ // Class for showing a limited amount of completions at a time #include -#include - -#include "CompletionModelRoles.h" -#include "Logging.h" -#include "Utils.h" template struct trie @@ -133,120 +128,26 @@ class CompletionProxyModel : public QAbstractProxyModel Q_OBJECT public: - CompletionProxyModel(QAbstractItemModel *model, QObject *parent = nullptr) - : QAbstractProxyModel(parent) - { - setSourceModel(model); - QRegularExpression splitPoints("\\s+|-"); - - for (int i = 0; i < sourceModel()->rowCount(); i++) { - if (i < 7) - mapping.push_back(i); - - auto string1 = - sourceModel() - ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole) - .toString() - .toLower(); - trie_.insert(string1.toUcs4(), i); - - for (const auto &e : string1.split(splitPoints, Qt::SkipEmptyParts)) { - trie_.insert(e.toUcs4(), i); - } + CompletionProxyModel(QAbstractItemModel *model, QObject *parent = nullptr); - auto string2 = - sourceModel() - ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2) - .toString() - .toLower(); - - if (!string2.isEmpty()) { - trie_.insert(string2.toUcs4(), i); - for (const auto &e : - string2.split(splitPoints, Qt::SkipEmptyParts)) { - trie_.insert(e.toUcs4(), i); - } - } - } + void invalidate(); - connect( - this, - &CompletionProxyModel::newSearchString, - this, - [this](QString s) { - s.remove(":"); - s.remove("@"); - searchString = s.toLower(); - invalidate(); - }, - Qt::QueuedConnection); - } + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &) const override; - void invalidate() - { - auto key = searchString.toUcs4(); - beginResetModel(); - mapping = trie_.search(key, 7); - endResetModel(); - - std::string temp; - for (auto v : mapping) { - temp += std::to_string(v) + ", "; - } - nhlog::ui()->debug("mapping: {}", temp); - }; - - QHash roleNames() const override - { - return this->sourceModel()->roleNames(); - } - - int rowCount(const QModelIndex &parent = QModelIndex()) const override - { - (void)parent; - return (int)mapping.size(); - } - - QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override - { - for (int i = 0; i < (int)mapping.size(); i++) { - if (mapping[i] == sourceIndex.row()) { - return index(i, 0); - } - } - return QModelIndex(); - } - - QModelIndex mapToSource(const QModelIndex &proxyIndex) const override - { - auto row = proxyIndex.row(); - if (row < 0 || row >= (int)mapping.size()) - return QModelIndex(); - - return sourceModel()->index(mapping[row], 0); - } + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; + QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; QModelIndex index(int row, int column, - const QModelIndex &parent = QModelIndex()) const override - { - (void)parent; - return createIndex(row, column); - } - - QModelIndex parent(const QModelIndex &) const override { return QModelIndex{}; } - int columnCount(const QModelIndex &) const override { return sourceModel()->columnCount(); } + const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &) const override; public slots: - QVariant completionAt(int i) const - { - if (i >= 0 && i < rowCount()) - return data(index(i, 0), CompletionModel::CompletionRole); - else - return {}; - } + QVariant completionAt(int i) const; - void setSearchString(QString s) { emit newSearchString(s); } + void setSearchString(QString s); signals: void newSearchString(QString); -- cgit 1.5.1