summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorJussi Kuokkanen <jussi.kuokkanen@protonmail.com>2020-08-28 23:32:23 +0300
committerJussi Kuokkanen <jussi.kuokkanen@protonmail.com>2020-08-28 23:32:23 +0300
commita173d964f7e555da263dcbc1b4c81df9a8d3f811 (patch)
tree54a93262e06ea12853320d950380b6ad67186d5c /src
parentMerge pull request #237 from trilene/voip (diff)
downloadnheko-a173d964f7e555da263dcbc1b4c81df9a8d3f811.tar.xz
add emoji completer to text input
Diffstat (limited to 'src')
-rw-r--r--src/CompletionModel.h16
-rw-r--r--src/TextInputWidget.cpp106
-rw-r--r--src/TextInputWidget.h24
-rw-r--r--src/emoji/EmojiSearchModel.h37
4 files changed, 180 insertions, 3 deletions
diff --git a/src/CompletionModel.h b/src/CompletionModel.h
new file mode 100644

index 00000000..66d300b0 --- /dev/null +++ b/src/CompletionModel.h
@@ -0,0 +1,16 @@ +#pragma once + +// Class for showing a limited amount of completions at a time + +#include <QSortFilterProxyModel> + +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/TextInputWidget.cpp b/src/TextInputWidget.cpp
index a3392170..9af7de26 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp
@@ -18,6 +18,7 @@ #include <QAbstractTextDocumentLayout> #include <QBuffer> #include <QClipboard> +#include <QCompleter> #include <QFileDialog> #include <QMimeData> #include <QMimeDatabase> @@ -25,12 +26,18 @@ #include <QPainter> #include <QStyleOption> #include <QtConcurrent> +#include <qnamespace.h> +#include <qregexp.h> #include "Cache.h" #include "ChatPage.h" +#include "CompletionModel.h" #include "Logging.h" #include "TextInputWidget.h" #include "Utils.h" +#include "emoji/EmojiSearchModel.h" +#include "emoji/KeyboardSelector.h" +#include "emoji/Provider.h" #include "ui/FlatButton.h" #include "ui/LoadingIndicator.h" @@ -61,6 +68,23 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent) connect(this, &QTextEdit::textChanged, this, &FilteredTextEdit::textChanged); setAcceptRichText(false); + completer_ = new QCompleter(this); + completer_->setWidget(this); + auto model = new emoji::EmojiSearchModel(this); + model->sort(0, Qt::AscendingOrder); + completer_->setModel((emoji_completion_model_ = new CompletionModel(model, this))); + completer_->setModelSorting(QCompleter::UnsortedModel); + completer_->popup()->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + completer_->popup()->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + connect(completer_, QOverload<const QModelIndex&>::of(&QCompleter::activated), + [this](auto &index) { + emoji_popup_open_ = false; + auto emoji = index.data(emoji::EmojiModel::Unicode).toString(); + insertCompletion(emoji); + }); + + typingTimer_ = new QTimer(this); typingTimer_->setInterval(1000); typingTimer_->setSingleShot(true); @@ -102,6 +126,17 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent) } void +FilteredTextEdit::insertCompletion(QString completion) { + // Paint the current word and replace it with 'completion' + auto cur_word = wordUnderCursor(); + auto tc = textCursor(); + tc.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, cur_word.length()); + tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, cur_word.length()); + tc.insertText(completion); + setTextCursor(tc); +} + +void FilteredTextEdit::showResults(const std::vector<SearchResult> &results) { QPoint pos; @@ -123,7 +158,7 @@ FilteredTextEdit::showResults(const std::vector<SearchResult> &results) void FilteredTextEdit::keyPressEvent(QKeyEvent *event) { - const bool isModifier = (event->modifiers() != Qt::NoModifier); + const bool isModifier = (event->modifiers() != Qt::NoModifier); #if defined(Q_OS_MAC) if (event->modifiers() == (Qt::ControlModifier | Qt::MetaModifier) && @@ -167,6 +202,21 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) } } + if (emoji_popup_open_) { + auto fake_key = (event->key() == Qt::Key_Backtab) ? Qt::Key_Up : Qt::Key_Down; + switch (event->key()) { + case Qt::Key_Backtab: + case Qt::Key_Tab: { + // Simulate up/down arrow press + auto ev = new QKeyEvent(QEvent::KeyPress, fake_key, Qt::NoModifier); + QCoreApplication::postEvent(completer_->popup(), ev); + return; + } + default: + break; + } + } + switch (event->key()) { case Qt::Key_At: atTriggerPosition_ = textCursor().position(); @@ -195,8 +245,22 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) break; } + case Qt::Key_Colon: { + QTextEdit::keyPressEvent(event); + emoji_popup_open_ = true; + emoji_completion_model_->setFilterRegExp(wordUnderCursor()); + //completer_->setCompletionPrefix(wordUnderCursor()); + completer_->popup()->setCurrentIndex(completer_->completionModel()->index(0, 0)); + completer_->complete(completerRect()); + break; + } case Qt::Key_Return: case Qt::Key_Enter: + if (emoji_popup_open_) { + event->ignore(); + return; + } + if (!(event->modifiers() & Qt::ShiftModifier)) { stopTyping(); submit(); @@ -241,7 +305,24 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) QTextEdit::keyPressEvent(event); if (isModifier) - return; + return; + + + if (emoji_popup_open_) { + // Update completion + + emoji_completion_model_->setFilterRegExp(wordUnderCursor()); + //completer_->setCompletionPrefix(wordUnderCursor()); + completer_->popup()->setCurrentIndex(completer_->completionModel()->index(0, 0)); + completer_->complete(completerRect()); + } + + if (emoji_popup_open_ && (completer_->completionCount() < 1 || + !wordUnderCursor().contains(QRegExp(":[^\r\n\t\f\v :]+$")))) { + // No completions for this word or another word than the completer was started with + emoji_popup_open_ = false; + completer_->popup()->hide(); + } if (textCursor().position() == 0) { resetAnchor(); @@ -352,6 +433,27 @@ FilteredTextEdit::stopTyping() emit stoppedTyping(); } +QRect +FilteredTextEdit::completerRect() +{ + // Move left edge to the beginning of the word + auto cursor = textCursor(); + auto rect = cursorRect(); + cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, wordUnderCursor().length()); + auto cursor_global_x = viewport()->mapToGlobal(cursorRect(cursor).topLeft()).x(); + auto rect_global_left = viewport()->mapToGlobal(rect.bottomLeft()).x(); + auto dx = qAbs(rect_global_left - cursor_global_x); + rect.moveLeft(rect.left() - dx); + + auto item_height = completer_->popup()->sizeHintForRow(0); + auto max_height = item_height * completer_->maxVisibleItems(); + auto height = (completer_->completionCount() > completer_->maxVisibleItems()) ? max_height : + completer_->completionCount() * item_height; + rect.setWidth(completer_->popup()->sizeHintForColumn(0)); + rect.moveBottom(-height); + return rect; +} + QSize FilteredTextEdit::sizeHint() const { diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h
index 27dff57f..9e70f498 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h
@@ -17,6 +17,7 @@ #pragma once +#include <algorithm> #include <deque> #include <optional> @@ -33,8 +34,10 @@ struct SearchResult; +class CompletionModel; class FlatButton; class LoadingIndicator; +class QCompleter; class FilteredTextEdit : public QTextEdit { @@ -80,8 +83,11 @@ protected: } private: + bool emoji_popup_open_ = false; + CompletionModel *emoji_completion_model_; std::deque<QString> true_history_, working_history_; size_t history_index_; + QCompleter *completer_; QTimer *typingTimer_; SuggestionsPopup suggestionsPopup_; @@ -103,19 +109,35 @@ private: { return pos == atTriggerPosition_ + anchorWidth(anchor); } - + QRect completerRect(); QString query() { auto cursor = textCursor(); cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); return cursor.selectedText(); } + QString wordUnderCursor() + { + auto tc = textCursor(); + auto editor_text = toPlainText(); + // Text before cursor + auto text = editor_text.chopped(editor_text.length() - tc.position()); + // Revert to find the first space (last before cursor in the original) + std::reverse(text.begin(), text.end()); + auto space_idx = text.indexOf(" "); + if (space_idx > -1) + text.chop(text.length() - space_idx); + // Revert back + std::reverse(text.begin(), text.end()); + return text; + } dialogs::PreviewUploadOverlay previewDialog_; //! Latest position of the '@' character that triggers the username completer. int atTriggerPosition_ = -1; + void insertCompletion(QString completion); void textChanged(); void uploadData(const QByteArray data, const QString &media, const QString &filename); void afterCompletion(int); diff --git a/src/emoji/EmojiSearchModel.h b/src/emoji/EmojiSearchModel.h new file mode 100644
index 00000000..bce96998 --- /dev/null +++ b/src/emoji/EmojiSearchModel.h
@@ -0,0 +1,37 @@ +#pragma once + +#include "EmojiModel.h" + +#include <QDebug> +#include <QEvent> +#include <QSortFilterProxyModel> +#include <qabstractitemmodel.h> +#include <qsortfilterproxymodel.h> + +namespace emoji { + +// Map emoji data to searchable data +class EmojiSearchModel : public QSortFilterProxyModel { +public: + EmojiSearchModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) { + setSourceModel(new EmojiModel(this)); + } + QVariant data(const QModelIndex &index, int role = Qt::UserRole + 1) const override { + if (role == Qt::DisplayRole) { + auto emoji = QSortFilterProxyModel::data(index, role).toString(); + return emoji + " :" + toShortcode(data(index, EmojiModel::ShortName).toString()) + + ":"; + } + return QSortFilterProxyModel::data(index, role); + } + /*int rowCount(const QModelIndex &parent) const override { + auto row_count = QSortFilterProxyModel::rowCount(parent); + return (row_count < 7) ? row_count : 7; + }*/ +private: + QString toShortcode(QString shortname) const { + return shortname.replace(" ", "-").replace(":", "-").replace("--", "-").toLower(); + } +}; + +}