Merge pull request #267 from Lurkki14/master

Added emoji completion in text input
This commit is contained in:
DeepBlueV7.X 2020-08-31 16:32:14 +00:00 committed by GitHub
commit eb554e8266
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 176 additions and 1 deletions

20
src/CompletionModel.h Normal file
View file

@ -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;
}
};

View file

@ -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);
@ -101,6 +123,18 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
previewDialog_.hide();
}
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)
{
@ -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
{

View file

@ -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);

View file

@ -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();
}
};
}