From a173d964f7e555da263dcbc1b4c81df9a8d3f811 Mon Sep 17 00:00:00 2001 From: Jussi Kuokkanen Date: Fri, 28 Aug 2020 23:32:23 +0300 Subject: [PATCH 01/10] add emoji completer to text input --- CMakeLists.txt | 2 + src/CompletionModel.h | 16 ++++++ src/TextInputWidget.cpp | 106 ++++++++++++++++++++++++++++++++++- src/TextInputWidget.h | 24 +++++++- src/emoji/EmojiSearchModel.h | 37 ++++++++++++ 5 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 src/CompletionModel.h create mode 100644 src/emoji/EmojiSearchModel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1be11fa3..5b82e285 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -245,6 +245,7 @@ set(SRC_FILES src/emoji/Category.cpp src/emoji/EmojiModel.cpp src/emoji/ItemDelegate.cpp + src/emoji/KeyboardSelector.cpp src/emoji/Panel.cpp src/emoji/PickButton.cpp src/emoji/Provider.cpp @@ -458,6 +459,7 @@ qt5_wrap_cpp(MOC_HEADERS src/emoji/Category.h src/emoji/EmojiModel.h src/emoji/ItemDelegate.h + src/emoji/KeyboardSelector.h src/emoji/Panel.h src/emoji/PickButton.h src/emoji/Provider.h 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 + +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 #include #include +#include #include #include #include @@ -25,12 +26,18 @@ #include #include #include +#include +#include #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::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); @@ -101,6 +125,17 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent) previewDialog_.hide(); } +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 &results) { @@ -123,7 +158,7 @@ FilteredTextEdit::showResults(const std::vector &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 #include #include @@ -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 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 +#include +#include +#include +#include + +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(); + } +}; + +} From 7acd4b3307fafd986b7dd7c6f29403038b8f3604 Mon Sep 17 00:00:00 2001 From: Jussi Kuokkanen Date: Fri, 28 Aug 2020 23:59:27 +0300 Subject: [PATCH 02/10] lint --- src/CompletionModel.h | 20 +++-- src/TextInputWidget.cpp | 170 ++++++++++++++++++----------------- src/TextInputWidget.h | 28 +++--- src/emoji/EmojiSearchModel.h | 39 ++++---- 4 files changed, 135 insertions(+), 122 deletions(-) diff --git a/src/CompletionModel.h b/src/CompletionModel.h index 66d300b0..ed021051 100644 --- a/src/CompletionModel.h +++ b/src/CompletionModel.h @@ -4,13 +4,17 @@ #include -class CompletionModel : public 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; - } + 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 bdc430f5..17018392 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -68,22 +68,22 @@ 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::of(&QCompleter::activated), - [this](auto &index) { - emoji_popup_open_ = false; - auto emoji = index.data(emoji::EmojiModel::Unicode).toString(); - insertCompletion(emoji); - }); + 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::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); @@ -126,14 +126,15 @@ 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); +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 @@ -158,7 +159,7 @@ FilteredTextEdit::showResults(const std::vector &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) && @@ -202,20 +203,20 @@ 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; - } - } + 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: @@ -246,20 +247,20 @@ 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; - } + 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 (emoji_popup_open_) { + event->ignore(); + return; + } if (!(event->modifiers() & Qt::ShiftModifier)) { stopTyping(); @@ -305,24 +306,26 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) QTextEdit::keyPressEvent(event); if (isModifier) - 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()); - } + return; - 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 (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(); @@ -436,22 +439,23 @@ FilteredTextEdit::stopTyping() 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; + // 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 diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index 8cd61b6a..4ae68798 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -109,7 +109,7 @@ private: { return pos == atTriggerPosition_ + anchorWidth(anchor); } - QRect completerRect(); + QRect completerRect(); QString query() { auto cursor = textCursor(); @@ -118,18 +118,18 @@ private: } 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; + 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_; @@ -137,7 +137,7 @@ private: //! Latest position of the '@' character that triggers the username completer. int atTriggerPosition_ = -1; - void insertCompletion(QString completion); + 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 index bce96998..87fa0403 100644 --- a/src/emoji/EmojiSearchModel.h +++ b/src/emoji/EmojiSearchModel.h @@ -11,27 +11,32 @@ namespace emoji { // Map emoji data to searchable data -class EmojiSearchModel : public QSortFilterProxyModel { +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); + 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(); - } + QString toShortcode(QString shortname) const + { + return shortname.replace(" ", "-").replace(":", "-").replace("--", "-").toLower(); + } }; } From 9ad9c8ddf07495ac6581edf7ca11463d7c0f78c9 Mon Sep 17 00:00:00 2001 From: Jussi Kuokkanen Date: Sat, 29 Aug 2020 00:26:45 +0300 Subject: [PATCH 03/10] fix build and remove commented code --- CMakeLists.txt | 2 -- scripts/emoji_codegen.py | 9 ++++++--- src/TextInputWidget.cpp | 5 +---- src/emoji/EmojiSearchModel.h | 7 +------ 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d2dffccd..7295cc54 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -245,7 +245,6 @@ set(SRC_FILES src/emoji/Category.cpp src/emoji/EmojiModel.cpp src/emoji/ItemDelegate.cpp - src/emoji/KeyboardSelector.cpp src/emoji/Panel.cpp src/emoji/PickButton.cpp src/emoji/Provider.cpp @@ -460,7 +459,6 @@ qt5_wrap_cpp(MOC_HEADERS src/emoji/Category.h src/emoji/EmojiModel.h src/emoji/ItemDelegate.h - src/emoji/KeyboardSelector.h src/emoji/Panel.h src/emoji/PickButton.h src/emoji/Provider.h diff --git a/scripts/emoji_codegen.py b/scripts/emoji_codegen.py index b39feb34..466246ed 100755 --- a/scripts/emoji_codegen.py +++ b/scripts/emoji_codegen.py @@ -11,12 +11,15 @@ class Emoji(object): self.code = repr(code.encode('utf-8'))[1:].strip("'") self.shortname = shortname +def to_shortcode(shortname): + return shortname.replace(" ", "-").replace(":", "-") + def generate_code(emojis, category): tmpl = Template(''' const std::vector emoji::Provider::{{ category }} = { // {{ category.capitalize() }} {%- for e in emoji %} - Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::EmojiCategory::{{ category.capitalize() }}}, + Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", "{{ to_shortcode(e.shortname) }}", emoji::EmojiCategory::{{ category.capitalize() }}}, {%- endfor %} }; ''') @@ -30,7 +33,7 @@ const QVector emoji::Provider::emoji = { {%- for c in kwargs.items() %} // {{ c[0].capitalize() }} {%- for e in c[1] %} - Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::EmojiCategory::{{ c[0].capitalize() }}}, + Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", "{{ to_shortcode(e.shortname) }}", emoji::EmojiCategory::{{ c[0].capitalize() }}}, {%- endfor %} {%- endfor %} }; @@ -101,4 +104,4 @@ if __name__ == '__main__': generate_code(objects, 'objects') generate_code(symbols, 'symbols') generate_code(flags, 'flags') - generate_qml_list(people=people, nature=nature, food=food, activity=activity, travel=travel, objects=objects, symbols=symbols, flags=flags) \ No newline at end of file + generate_qml_list(people=people, nature=nature, food=food, activity=activity, travel=travel, objects=objects, symbols=symbols, flags=flags) diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 17018392..08883cca 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -15,6 +15,7 @@ * along with this program. If not, see . */ +#include #include #include #include @@ -36,7 +37,6 @@ #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" @@ -250,7 +250,6 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) 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; @@ -310,9 +309,7 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) 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()); diff --git a/src/emoji/EmojiSearchModel.h b/src/emoji/EmojiSearchModel.h index 87fa0403..1ff5f4e9 100644 --- a/src/emoji/EmojiSearchModel.h +++ b/src/emoji/EmojiSearchModel.h @@ -5,8 +5,6 @@ #include #include #include -#include -#include namespace emoji { @@ -28,10 +26,7 @@ public: } 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 { From f40d8d15b59378c90d0847f0cef7328d9be73332 Mon Sep 17 00:00:00 2001 From: Jussi Kuokkanen Date: Sat, 29 Aug 2020 00:29:46 +0300 Subject: [PATCH 04/10] undo changes to emoji_codegen.py --- scripts/emoji_codegen.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/scripts/emoji_codegen.py b/scripts/emoji_codegen.py index 466246ed..b39feb34 100755 --- a/scripts/emoji_codegen.py +++ b/scripts/emoji_codegen.py @@ -11,15 +11,12 @@ class Emoji(object): self.code = repr(code.encode('utf-8'))[1:].strip("'") self.shortname = shortname -def to_shortcode(shortname): - return shortname.replace(" ", "-").replace(":", "-") - def generate_code(emojis, category): tmpl = Template(''' const std::vector emoji::Provider::{{ category }} = { // {{ category.capitalize() }} {%- for e in emoji %} - Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", "{{ to_shortcode(e.shortname) }}", emoji::EmojiCategory::{{ category.capitalize() }}}, + Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::EmojiCategory::{{ category.capitalize() }}}, {%- endfor %} }; ''') @@ -33,7 +30,7 @@ const QVector emoji::Provider::emoji = { {%- for c in kwargs.items() %} // {{ c[0].capitalize() }} {%- for e in c[1] %} - Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", "{{ to_shortcode(e.shortname) }}", emoji::EmojiCategory::{{ c[0].capitalize() }}}, + Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::EmojiCategory::{{ c[0].capitalize() }}}, {%- endfor %} {%- endfor %} }; @@ -104,4 +101,4 @@ if __name__ == '__main__': generate_code(objects, 'objects') generate_code(symbols, 'symbols') generate_code(flags, 'flags') - generate_qml_list(people=people, nature=nature, food=food, activity=activity, travel=travel, objects=objects, symbols=symbols, flags=flags) + generate_qml_list(people=people, nature=nature, food=food, activity=activity, travel=travel, objects=objects, symbols=symbols, flags=flags) \ No newline at end of file From aed8d23aca2434a70f09d01aea5d89c363de0f79 Mon Sep 17 00:00:00 2001 From: Jussi Kuokkanen Date: Sat, 29 Aug 2020 23:05:40 +0300 Subject: [PATCH 05/10] don't select emoji completion by default and add minimum string length before showing completions --- src/TextInputWidget.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 08883cca..770aaca1 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -250,15 +250,18 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) QTextEdit::keyPressEvent(event); emoji_popup_open_ = true; emoji_completion_model_->setFilterRegExp(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 (!completer_->popup()->currentIndex().isValid()) { + // No completion to select, do normal behavior + completer_->popup()->hide(); + emoji_popup_open_ = false; + } + else + event->ignore(); } if (!(event->modifiers() & Qt::ShiftModifier)) { @@ -307,11 +310,9 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) if (isModifier) return; - if (emoji_popup_open_) { + if (emoji_popup_open_ && wordUnderCursor().length() > 2) { // Update completion emoji_completion_model_->setFilterRegExp(wordUnderCursor()); - completer_->popup()->setCurrentIndex( - completer_->completionModel()->index(0, 0)); completer_->complete(completerRect()); } From 254b7549eba383fb9e842b9524d5388d466696b4 Mon Sep 17 00:00:00 2001 From: Jussi Kuokkanen Date: Mon, 31 Aug 2020 12:21:47 +0300 Subject: [PATCH 06/10] ignore enter keypress when completion is selected --- src/TextInputWidget.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 770aaca1..47e239cd 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -256,12 +256,13 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) case Qt::Key_Enter: if (emoji_popup_open_) { if (!completer_->popup()->currentIndex().isValid()) { - // No completion to select, do normal behavior - completer_->popup()->hide(); - emoji_popup_open_ = false; - } - else - event->ignore(); + // No completion to select, do normal behavior + completer_->popup()->hide(); + emoji_popup_open_ = false; + } else { + event->ignore(); + return; + } } if (!(event->modifiers() & Qt::ShiftModifier)) { From beec2607fc111b17c02f32755b618c72a55e49f1 Mon Sep 17 00:00:00 2001 From: Jussi Kuokkanen Date: Mon, 31 Aug 2020 18:04:59 +0300 Subject: [PATCH 07/10] get completion string based on trigger position instead of current word --- src/TextInputWidget.cpp | 17 +++++++++-------- src/TextInputWidget.h | 12 ++++++++++-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 47e239cd..ac76d5b0 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -129,10 +129,10 @@ void FilteredTextEdit::insertCompletion(QString completion) { // Paint the current word and replace it with 'completion' - auto cur_word = wordUnderCursor(); + auto cur_text = textAfterPosition(trigger_pos_); auto tc = textCursor(); - tc.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, cur_word.length()); - tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, cur_word.length()); + tc.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, cur_text.length()); + tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, cur_text.length()); tc.insertText(completion); setTextCursor(tc); } @@ -248,8 +248,8 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) } case Qt::Key_Colon: { QTextEdit::keyPressEvent(event); + trigger_pos_ = textCursor().position() - 1; emoji_popup_open_ = true; - emoji_completion_model_->setFilterRegExp(wordUnderCursor()); break; } case Qt::Key_Return: @@ -311,15 +311,15 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) if (isModifier) return; - if (emoji_popup_open_ && wordUnderCursor().length() > 2) { + if (emoji_popup_open_ && textAfterPosition(trigger_pos_).length() > 2) { // Update completion - emoji_completion_model_->setFilterRegExp(wordUnderCursor()); + emoji_completion_model_->setFilterRegExp(textAfterPosition(trigger_pos_)); completer_->complete(completerRect()); } if (emoji_popup_open_ && (completer_->completionCount() < 1 || - !wordUnderCursor().contains(QRegExp(":[^\r\n\t\f\v :]+$")))) { + !textAfterPosition(trigger_pos_).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; @@ -441,7 +441,8 @@ 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()); + cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, + textAfterPosition(trigger_pos_).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); diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index 4ae68798..e4bd9b96 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -86,6 +86,7 @@ private: bool emoji_popup_open_ = false; CompletionModel *emoji_completion_model_; std::deque true_history_, working_history_; + int trigger_pos_; // Where emoji completer was triggered size_t history_index_; QCompleter *completer_; QTimer *typingTimer_; @@ -116,7 +117,14 @@ private: cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); return cursor.selectedText(); } - QString wordUnderCursor() + QString textAfterPosition(int pos) + { + auto tc = textCursor(); + tc.setPosition(pos); + tc.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); + return tc.selectedText(); + } + /*QString wordUnderCursor() { auto tc = textCursor(); auto editor_text = toPlainText(); @@ -130,7 +138,7 @@ private: // Revert back std::reverse(text.begin(), text.end()); return text; - } + }*/ dialogs::PreviewUploadOverlay previewDialog_; From bb4636885df0dbfe9d2f632d02636c8e35947ae9 Mon Sep 17 00:00:00 2001 From: Jussi Kuokkanen Date: Mon, 31 Aug 2020 18:07:29 +0300 Subject: [PATCH 08/10] remove comment --- src/TextInputWidget.h | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index e4bd9b96..6003551e 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -124,21 +124,6 @@ private: tc.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); return tc.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_; From 8f872f1961115a580057c2a5c89fe7f0cb20e55d Mon Sep 17 00:00:00 2001 From: Jussi Kuokkanen Date: Mon, 31 Aug 2020 18:24:03 +0300 Subject: [PATCH 09/10] remove unused includes and use QRegularExpression --- src/TextInputWidget.cpp | 12 +++++------- src/TextInputWidget.h | 1 - 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index ac76d5b0..6d57a5f1 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -27,8 +27,6 @@ #include #include #include -#include -#include #include "Cache.h" #include "ChatPage.h" @@ -317,9 +315,9 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) completer_->complete(completerRect()); } - if (emoji_popup_open_ && - (completer_->completionCount() < 1 || - !textAfterPosition(trigger_pos_).contains(QRegExp(":[^\r\n\t\f\v :]+$")))) { + if (emoji_popup_open_ && (completer_->completionCount() < 1 || + !textAfterPosition(trigger_pos_) + .contains(QRegularExpression(":[^\r\n\t\f\v :]+$")))) { // No completions for this word or another word than the completer was // started with emoji_popup_open_ = false; @@ -441,8 +439,8 @@ FilteredTextEdit::completerRect() // Move left edge to the beginning of the word auto cursor = textCursor(); auto rect = cursorRect(); - cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, - textAfterPosition(trigger_pos_).length()); + cursor.movePosition( + QTextCursor::Left, QTextCursor::MoveAnchor, textAfterPosition(trigger_pos_).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); diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index 6003551e..3aa05c39 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -17,7 +17,6 @@ #pragma once -#include #include #include From 8d14a058c67a857821c4d7c3c9c7811595828ef5 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 2 Sep 2020 19:38:07 +0200 Subject: [PATCH 10/10] Fix endless pagination, when old history is inaccessible --- src/timeline/EventStore.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index fca1d31d..20487b28 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -55,7 +55,7 @@ EventStore::EventStore(std::string room_id, QObject *) [this](const mtx::responses::Messages &res) { // uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res); - if (newFirst == first) + if (newFirst == first && !res.chunk.empty()) fetchMore(); else { emit beginInsertRows(toExternalIdx(newFirst),