summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorDeepBlueV7.X <nicolas.werner@hotmail.de>2020-09-03 18:11:17 +0000
committerGitHub <noreply@github.com>2020-09-03 18:11:17 +0000
commit657f4073e98176dab3de85a3868ed633d8544ede (patch)
tree5614ee224aa8acb0fa492a154f7e456ad4d33990 /src
parentAdd support for Encrypted to-device verification messages (diff)
parentFix endless pagination, when old history is inaccessible (diff)
downloadnheko-657f4073e98176dab3de85a3868ed633d8544ede.tar.xz
Merge branch 'master' into device-verification
Diffstat (limited to 'src')
-rw-r--r--src/CompletionModel.h20
-rw-r--r--src/TextInputWidget.cpp104
-rw-r--r--src/TextInputWidget.h16
-rw-r--r--src/emoji/EmojiSearchModel.h37
-rw-r--r--src/timeline/EventStore.cpp2
5 files changed, 177 insertions, 2 deletions
diff --git a/src/CompletionModel.h b/src/CompletionModel.h
new file mode 100644
index 00000000..ed021051
--- /dev/null
+++ b/src/CompletionModel.h
@@ -0,0 +1,20 @@
+#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 4edd8376..6d57a5f1 100644
--- a/src/TextInputWidget.cpp
+++ b/src/TextInputWidget.cpp
@@ -15,9 +15,11 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <QAbstractItemView>
 #include <QAbstractTextDocumentLayout>
 #include <QBuffer>
 #include <QClipboard>
+#include <QCompleter>
 #include <QFileDialog>
 #include <QMimeData>
 #include <QMimeDatabase>
@@ -28,9 +30,12 @@
 
 #include "Cache.h"
 #include "ChatPage.h"
+#include "CompletionModel.h"
 #include "Logging.h"
 #include "TextInputWidget.h"
 #include "Utils.h"
+#include "emoji/EmojiSearchModel.h"
+#include "emoji/Provider.h"
 #include "ui/FlatButton.h"
 #include "ui/LoadingIndicator.h"
 
@@ -61,6 +66,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 +124,18 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
 }
 
 void
+FilteredTextEdit::insertCompletion(QString completion)
+{
+        // Paint the current word and replace it with 'completion'
+        auto cur_text = textAfterPosition(trigger_pos_);
+        auto tc       = textCursor();
+        tc.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, cur_text.length());
+        tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, cur_text.length());
+        tc.insertText(completion);
+        setTextCursor(tc);
+}
+
+void
 FilteredTextEdit::showResults(const std::vector<SearchResult> &results)
 {
         QPoint pos;
@@ -167,6 +201,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 +244,25 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
 
                 break;
         }
+        case Qt::Key_Colon: {
+                QTextEdit::keyPressEvent(event);
+                trigger_pos_      = textCursor().position() - 1;
+                emoji_popup_open_ = true;
+                break;
+        }
         case Qt::Key_Return:
         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();
+                                return;
+                        }
+                }
+
                 if (!(event->modifiers() & Qt::ShiftModifier)) {
                         stopTyping();
                         submit();
@@ -243,6 +309,21 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
                 if (isModifier)
                         return;
 
+                if (emoji_popup_open_ && textAfterPosition(trigger_pos_).length() > 2) {
+                        // Update completion
+                        emoji_completion_model_->setFilterRegExp(textAfterPosition(trigger_pos_));
+                        completer_->complete(completerRect());
+                }
+
+                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;
+                        completer_->popup()->hide();
+                }
+
                 if (textCursor().position() == 0) {
                         resetAnchor();
                         closeSuggestions();
@@ -352,6 +433,29 @@ 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, 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);
+        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 2473c13a..3aa05c39 100644
--- a/src/TextInputWidget.h
+++ b/src/TextInputWidget.h
@@ -33,8 +33,10 @@
 
 struct SearchResult;
 
+class CompletionModel;
 class FlatButton;
 class LoadingIndicator;
+class QCompleter;
 
 class FilteredTextEdit : public QTextEdit
 {
@@ -80,8 +82,12 @@ protected:
         }
 
 private:
+        bool emoji_popup_open_ = false;
+        CompletionModel *emoji_completion_model_;
         std::deque<QString> true_history_, working_history_;
+        int trigger_pos_; // Where emoji completer was triggered
         size_t history_index_;
+        QCompleter *completer_;
         QTimer *typingTimer_;
 
         SuggestionsPopup suggestionsPopup_;
@@ -103,19 +109,27 @@ private:
         {
                 return pos == atTriggerPosition_ + anchorWidth(anchor);
         }
-
+        QRect completerRect();
         QString query()
         {
                 auto cursor = textCursor();
                 cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor);
                 return cursor.selectedText();
         }
+        QString textAfterPosition(int pos)
+        {
+                auto tc = textCursor();
+                tc.setPosition(pos);
+                tc.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
+                return tc.selectedText();
+        }
 
         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..1ff5f4e9
--- /dev/null
+++ b/src/emoji/EmojiSearchModel.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "EmojiModel.h"
+
+#include <QDebug>
+#include <QEvent>
+#include <QSortFilterProxyModel>
+
+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);
+        }
+
+private:
+        QString toShortcode(QString shortname) const
+        {
+                return shortname.replace(" ", "-").replace(":", "-").replace("--", "-").toLower();
+        }
+};
+
+}
diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 3ecd4c75..bfc16a02 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -53,7 +53,7 @@ EventStore::EventStore(std::string room_id, QObject *)
                 this,
                 [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),