mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-22 19:08:58 +03:00
add emoji completer to text input
This commit is contained in:
parent
3fece53eb7
commit
a173d964f7
5 changed files with 182 additions and 3 deletions
|
@ -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
|
||||
|
|
16
src/CompletionModel.h
Normal file
16
src/CompletionModel.h
Normal file
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
|
@ -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<SearchResult> &results)
|
||||
{
|
||||
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
37
src/emoji/EmojiSearchModel.h
Normal file
37
src/emoji/EmojiSearchModel.h
Normal file
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
Loading…
Reference in a new issue