diff --git a/src/TextInputWidget.cc b/src/TextInputWidget.cc
index 3f3d5cd9..e184d8b4 100644
--- a/src/TextInputWidget.cc
+++ b/src/TextInputWidget.cc
@@ -15,6 +15,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+#include <thread>
+
#include <QAbstractTextDocumentLayout>
#include <QApplication>
#include <QBuffer>
@@ -28,17 +30,23 @@
#include <QPainter>
#include <QStyleOption>
+#include <variant.hpp>
+
#include "Config.h"
+#include "RoomState.h"
#include "TextInputWidget.h"
+#include "Utils.h"
static constexpr size_t INPUT_HISTORY_SIZE = 127;
static constexpr int MAX_TEXTINPUT_HEIGHT = 120;
static constexpr int InputHeight = 26;
static constexpr int ButtonHeight = 24;
+static constexpr int MaxPopupItems = 5;
FilteredTextEdit::FilteredTextEdit(QWidget *parent)
: QTextEdit{parent}
, history_index_{0}
+ , popup_{parent}
, previewDialog_{parent}
{
setFrameStyle(QFrame::NoFrame);
@@ -64,10 +72,44 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
this,
&FilteredTextEdit::uploadData);
+ qRegisterMetaType<SearchResult>();
+ qRegisterMetaType<QVector<SearchResult>>();
+ connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults);
+ connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) {
+ popup_.hide();
+
+ auto cursor = textCursor();
+ const int end = cursor.position();
+
+ cursor.setPosition(atTriggerPosition_, QTextCursor::MoveAnchor);
+ cursor.setPosition(end, QTextCursor::KeepAnchor);
+ cursor.removeSelectedText();
+ cursor.insertText(text);
+ });
+
previewDialog_.hide();
}
void
+FilteredTextEdit::showResults(const QVector<SearchResult> &results)
+{
+ QPoint pos;
+
+ if (atTriggerPosition_ != -1) {
+ auto cursor = textCursor();
+ cursor.setPosition(atTriggerPosition_);
+ pos = viewport()->mapToGlobal(cursorRect(cursor).topLeft());
+ } else {
+ auto rect = cursorRect();
+ pos = viewport()->mapToGlobal(rect.topLeft());
+ }
+
+ popup_.addUsers(results);
+ popup_.move(pos.x(), pos.y() - popup_.height() - 10);
+ popup_.show();
+}
+
+void
FilteredTextEdit::keyPressEvent(QKeyEvent *event)
{
const bool isModifier = (event->modifiers() != Qt::NoModifier);
@@ -79,7 +121,34 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
typingTimer_->start();
}
+ // calculate the new query
+ if (textCursor().position() < atTriggerPosition_ || atTriggerPosition_ == -1) {
+ resetAnchor();
+ closeSuggestions();
+ }
+
+ if (popup_.isVisible()) {
+ switch (event->key()) {
+ case Qt::Key_Enter:
+ case Qt::Key_Return:
+ case Qt::Key_Escape:
+ case Qt::Key_Tab:
+ case Qt::Key_Space:
+ case Qt::Key_Backtab: {
+ closeSuggestions();
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
switch (event->key()) {
+ case Qt::Key_At:
+ atTriggerPosition_ = textCursor().position();
+
+ QTextEdit::keyPressEvent(event);
+ break;
case Qt::Key_Return:
case Qt::Key_Enter:
if (!(event->modifiers() & Qt::ShiftModifier)) {
@@ -124,6 +193,30 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
}
default:
QTextEdit::keyPressEvent(event);
+
+ // Check if the current word should be autocompleted.
+ auto cursor = textCursor();
+ cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor);
+ auto word = cursor.selectedText();
+
+ if (cursor.position() == 0) {
+ closeSuggestions();
+ return;
+ }
+
+ if (cursor.position() == atTriggerPosition_ + 1) {
+ const auto q = query();
+
+ if (q.isEmpty()) {
+ closeSuggestions();
+ return;
+ }
+
+ emit showSuggestions(query());
+ } else {
+ closeSuggestions();
+ }
+
break;
}
}
@@ -340,6 +433,52 @@ TextInputWidget::TextInputWidget(QWidget *parent)
setFixedHeight(widgetHeight);
input_->setFixedHeight(textInputHeight);
});
+ connect(input_, &FilteredTextEdit::showSuggestions, this, [this](const QString &q) {
+ if (q.isEmpty() || currState_.isNull())
+ return;
+
+ std::thread worker([this, q = q.toLower().toStdString()]() {
+ std::multimap<int, std::pair<std::string, std::string>> items;
+
+ auto get_name = [](auto membership) {
+ auto name = membership.second.content.display_name;
+ auto key = membership.first;
+
+ // Remove the leading '@' character.
+ if (name.empty()) {
+ key.erase(0, 1);
+ name = key;
+ }
+
+ return std::make_pair(key, name);
+ };
+
+ for (const auto &m : currState_->memberships) {
+ const auto user = get_name(m);
+ const int score = utils::levenshtein_distance(q, user.second);
+
+ items.emplace(score, user);
+ }
+
+ QVector<SearchResult> results;
+ auto end = items.begin();
+
+ if (items.size() >= MaxPopupItems)
+ std::advance(end, MaxPopupItems);
+
+ for (auto it = items.begin(); it != end; it++) {
+ const auto user = it->second;
+
+ results.push_back(
+ SearchResult{QString::fromStdString(user.first),
+ QString::fromStdString(user.second)});
+ }
+
+ emit input_->resultsRetrieved(results);
+ });
+
+ worker.detach();
+ });
sendMessageBtn_ = new FlatButton(this);
|