mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-22 19:08:58 +03:00
Merge branch 'master' into device-verification
This commit is contained in:
commit
657f4073e9
5 changed files with 177 additions and 2 deletions
20
src/CompletionModel.h
Normal file
20
src/CompletionModel.h
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -15,9 +15,11 @@
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <QAbstractItemView>
|
||||||
#include <QAbstractTextDocumentLayout>
|
#include <QAbstractTextDocumentLayout>
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
|
#include <QCompleter>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
#include <QMimeDatabase>
|
#include <QMimeDatabase>
|
||||||
|
@ -28,9 +30,12 @@
|
||||||
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
#include "ChatPage.h"
|
#include "ChatPage.h"
|
||||||
|
#include "CompletionModel.h"
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
#include "TextInputWidget.h"
|
#include "TextInputWidget.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
#include "emoji/EmojiSearchModel.h"
|
||||||
|
#include "emoji/Provider.h"
|
||||||
#include "ui/FlatButton.h"
|
#include "ui/FlatButton.h"
|
||||||
#include "ui/LoadingIndicator.h"
|
#include "ui/LoadingIndicator.h"
|
||||||
|
|
||||||
|
@ -61,6 +66,23 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
|
||||||
connect(this, &QTextEdit::textChanged, this, &FilteredTextEdit::textChanged);
|
connect(this, &QTextEdit::textChanged, this, &FilteredTextEdit::textChanged);
|
||||||
setAcceptRichText(false);
|
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_ = new QTimer(this);
|
||||||
typingTimer_->setInterval(1000);
|
typingTimer_->setInterval(1000);
|
||||||
typingTimer_->setSingleShot(true);
|
typingTimer_->setSingleShot(true);
|
||||||
|
@ -101,6 +123,18 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
|
||||||
previewDialog_.hide();
|
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
|
void
|
||||||
FilteredTextEdit::showResults(const std::vector<SearchResult> &results)
|
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()) {
|
switch (event->key()) {
|
||||||
case Qt::Key_At:
|
case Qt::Key_At:
|
||||||
atTriggerPosition_ = textCursor().position();
|
atTriggerPosition_ = textCursor().position();
|
||||||
|
@ -195,8 +244,25 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
|
||||||
|
|
||||||
break;
|
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_Return:
|
||||||
case Qt::Key_Enter:
|
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)) {
|
if (!(event->modifiers() & Qt::ShiftModifier)) {
|
||||||
stopTyping();
|
stopTyping();
|
||||||
submit();
|
submit();
|
||||||
|
@ -243,6 +309,21 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
|
||||||
if (isModifier)
|
if (isModifier)
|
||||||
return;
|
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) {
|
if (textCursor().position() == 0) {
|
||||||
resetAnchor();
|
resetAnchor();
|
||||||
closeSuggestions();
|
closeSuggestions();
|
||||||
|
@ -352,6 +433,29 @@ FilteredTextEdit::stopTyping()
|
||||||
emit stoppedTyping();
|
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
|
QSize
|
||||||
FilteredTextEdit::sizeHint() const
|
FilteredTextEdit::sizeHint() const
|
||||||
{
|
{
|
||||||
|
|
|
@ -33,8 +33,10 @@
|
||||||
|
|
||||||
struct SearchResult;
|
struct SearchResult;
|
||||||
|
|
||||||
|
class CompletionModel;
|
||||||
class FlatButton;
|
class FlatButton;
|
||||||
class LoadingIndicator;
|
class LoadingIndicator;
|
||||||
|
class QCompleter;
|
||||||
|
|
||||||
class FilteredTextEdit : public QTextEdit
|
class FilteredTextEdit : public QTextEdit
|
||||||
{
|
{
|
||||||
|
@ -80,8 +82,12 @@ protected:
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool emoji_popup_open_ = false;
|
||||||
|
CompletionModel *emoji_completion_model_;
|
||||||
std::deque<QString> true_history_, working_history_;
|
std::deque<QString> true_history_, working_history_;
|
||||||
|
int trigger_pos_; // Where emoji completer was triggered
|
||||||
size_t history_index_;
|
size_t history_index_;
|
||||||
|
QCompleter *completer_;
|
||||||
QTimer *typingTimer_;
|
QTimer *typingTimer_;
|
||||||
|
|
||||||
SuggestionsPopup suggestionsPopup_;
|
SuggestionsPopup suggestionsPopup_;
|
||||||
|
@ -103,19 +109,27 @@ private:
|
||||||
{
|
{
|
||||||
return pos == atTriggerPosition_ + anchorWidth(anchor);
|
return pos == atTriggerPosition_ + anchorWidth(anchor);
|
||||||
}
|
}
|
||||||
|
QRect completerRect();
|
||||||
QString query()
|
QString query()
|
||||||
{
|
{
|
||||||
auto cursor = textCursor();
|
auto cursor = textCursor();
|
||||||
cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor);
|
cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor);
|
||||||
return cursor.selectedText();
|
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_;
|
dialogs::PreviewUploadOverlay previewDialog_;
|
||||||
|
|
||||||
//! Latest position of the '@' character that triggers the username completer.
|
//! Latest position of the '@' character that triggers the username completer.
|
||||||
int atTriggerPosition_ = -1;
|
int atTriggerPosition_ = -1;
|
||||||
|
|
||||||
|
void insertCompletion(QString completion);
|
||||||
void textChanged();
|
void textChanged();
|
||||||
void uploadData(const QByteArray data, const QString &media, const QString &filename);
|
void uploadData(const QByteArray data, const QString &media, const QString &filename);
|
||||||
void afterCompletion(int);
|
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>
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -53,7 +53,7 @@ EventStore::EventStore(std::string room_id, QObject *)
|
||||||
this,
|
this,
|
||||||
[this](const mtx::responses::Messages &res) {
|
[this](const mtx::responses::Messages &res) {
|
||||||
uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res);
|
uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res);
|
||||||
if (newFirst == first)
|
if (newFirst == first && !res.chunk.empty())
|
||||||
fetchMore();
|
fetchMore();
|
||||||
else {
|
else {
|
||||||
emit beginInsertRows(toExternalIdx(newFirst),
|
emit beginInsertRows(toExternalIdx(newFirst),
|
||||||
|
|
Loading…
Reference in a new issue