From a3c4fece7e012d865ac3343ed2c5a273ac0f238f Mon Sep 17 00:00:00 2001 From: Jussi Kuokkanen Date: Mon, 7 Sep 2020 12:51:28 +0300 Subject: add per-room user model --- src/UsersModel.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/UsersModel.cpp (limited to 'src/UsersModel.cpp') diff --git a/src/UsersModel.cpp b/src/UsersModel.cpp new file mode 100644 index 00000000..c63292bb --- /dev/null +++ b/src/UsersModel.cpp @@ -0,0 +1,28 @@ +#include "UsersModel.h" + +#include "Cache.h" +#include "CompletionModelRoles.h" + +#include + +UsersModel::UsersModel(const std::string &roomId, QObject *parent) + : QAbstractListModel(parent) +{ + roomMembers_ = cache::getMembers(roomId, 0, 9999); +} + +QVariant +UsersModel::data(const QModelIndex &index, int role) const +{ + if (hasIndex(index.row(), index.column(), index.parent())) { + switch (role) { + case CompletionModel::CompletionRole: + case CompletionModel::SearchRole: + case Qt::DisplayRole: + return roomMembers_[index.row()].display_name; + case Avatar: + return roomMembers_[index.row()].avatar; + } + } + return {}; +} -- 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/UsersModel.cpp') 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/UsersModel.cpp') 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 c07c3261414b537a21c0493f73d9790c36dde219 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 24 Nov 2020 19:01:52 +0100 Subject: Linkify username completion --- src/UsersModel.cpp | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/UsersModel.cpp') diff --git a/src/UsersModel.cpp b/src/UsersModel.cpp index cb9f8f75..aeabf467 100644 --- a/src/UsersModel.cpp +++ b/src/UsersModel.cpp @@ -32,6 +32,9 @@ UsersModel::data(const QModelIndex &index, int role) const if (hasIndex(index.row(), index.column(), index.parent())) { switch (role) { case CompletionModel::CompletionRole: + return QString("[%1](https://matrix.to/#/%2)") + .arg(displayNames[index.row()]) + .arg(userids[index.row()]); case CompletionModel::SearchRole: case Qt::DisplayRole: case Roles::DisplayName: -- cgit 1.5.1 From 37df79f7964795e03ffca10c4623cb9061aa3b9c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 24 Nov 2020 19:06:31 +0100 Subject: Show userid in completer --- resources/qml/Completer.qml | 5 +++++ src/UsersModel.cpp | 3 +++ src/UsersModel.h | 1 + 3 files changed, 9 insertions(+) (limited to 'src/UsersModel.cpp') diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml index 647e4d85..9703da64 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml @@ -111,6 +111,11 @@ Popup { color: model.index == popup.currentIndex ? colors.highlightedText : colors.text } + Label { + text: "(" + model.userid + ")" + color: model.index == popup.currentIndex ? colors.highlightedText : colors.buttonText + } + } } diff --git a/src/UsersModel.cpp b/src/UsersModel.cpp index aeabf467..4be37503 100644 --- a/src/UsersModel.cpp +++ b/src/UsersModel.cpp @@ -23,6 +23,7 @@ UsersModel::roleNames() const {CompletionModel::SearchRole2, "searchRole2"}, {Roles::DisplayName, "displayName"}, {Roles::AvatarUrl, "avatarUrl"}, + {Roles::UserID, "userid"}, }; } @@ -44,6 +45,8 @@ UsersModel::data(const QModelIndex &index, int role) const case Roles::AvatarUrl: return cache::avatarUrl(QString::fromStdString(room_id), QString::fromStdString(roomMembers_[index.row()])); + case Roles::UserID: + return userids[index.row()]; } } return {}; diff --git a/src/UsersModel.h b/src/UsersModel.h index cddcdd84..c60b34b8 100644 --- a/src/UsersModel.h +++ b/src/UsersModel.h @@ -9,6 +9,7 @@ public: { AvatarUrl = Qt::UserRole, DisplayName, + UserID, }; UsersModel(const std::string &roomId, QObject *parent = nullptr); -- cgit 1.5.1