This is natively integrated into the existing chat document handler, and emulates normal Sonnet behavior.
404 lines
13 KiB
C++
404 lines
13 KiB
C++
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include "chatdocumenthandler.h"
|
|
|
|
#include <QQmlFile>
|
|
#include <QQmlFileSelector>
|
|
#include <QStringBuilder>
|
|
#include <QSyntaxHighlighter>
|
|
#include <QTextBlock>
|
|
#include <QTextDocument>
|
|
#include <QTimer>
|
|
|
|
#include <Sonnet/BackgroundChecker>
|
|
#include <Sonnet/Settings>
|
|
|
|
class SyntaxHighlighter : public QSyntaxHighlighter
|
|
{
|
|
public:
|
|
QTextCharFormat mentionFormat;
|
|
QTextCharFormat errorFormat;
|
|
Sonnet::BackgroundChecker *checker = new Sonnet::BackgroundChecker;
|
|
Sonnet::Settings settings;
|
|
QList<QPair<int, QString>> errors;
|
|
QString previousText;
|
|
QTimer rehighlightTimer;
|
|
SyntaxHighlighter(QObject *parent)
|
|
: QSyntaxHighlighter(parent)
|
|
{
|
|
mentionFormat.setFontWeight(QFont::Bold);
|
|
mentionFormat.setForeground(Qt::blue);
|
|
|
|
errorFormat.setForeground(Qt::red);
|
|
errorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
|
|
|
|
connect(checker, &Sonnet::BackgroundChecker::misspelling, this, [this](const QString &word, int start) {
|
|
errors += {start, word};
|
|
checker->continueChecking();
|
|
});
|
|
connect(checker, &Sonnet::BackgroundChecker::done, this, [this]() {
|
|
rehighlightTimer.start();
|
|
});
|
|
rehighlightTimer.setInterval(100);
|
|
rehighlightTimer.setSingleShot(true);
|
|
rehighlightTimer.callOnTimeout(this, &QSyntaxHighlighter::rehighlight);
|
|
}
|
|
void highlightBlock(const QString &text) override
|
|
{
|
|
if (settings.checkerEnabledByDefault()) {
|
|
if (text != previousText) {
|
|
previousText = text;
|
|
checker->stop();
|
|
errors.clear();
|
|
checker->setText(text);
|
|
}
|
|
for (const auto &error : errors) {
|
|
setFormat(error.first, error.second.size(), errorFormat);
|
|
}
|
|
}
|
|
auto room = dynamic_cast<ChatDocumentHandler *>(parent())->room();
|
|
if (!room) {
|
|
return;
|
|
}
|
|
auto mentions = room->mentions();
|
|
mentions->erase(std::remove_if(mentions->begin(),
|
|
mentions->end(),
|
|
[this](auto &mention) {
|
|
if (document()->toPlainText().isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
if (mention.cursor.position() == 0 && mention.cursor.anchor() == 0) {
|
|
return true;
|
|
}
|
|
|
|
if (mention.cursor.position() - mention.cursor.anchor() != mention.text.size()) {
|
|
mention.cursor.setPosition(mention.start);
|
|
mention.cursor.setPosition(mention.cursor.anchor() + mention.text.size(), QTextCursor::KeepAnchor);
|
|
}
|
|
|
|
if (mention.cursor.selectedText() != mention.text) {
|
|
return true;
|
|
}
|
|
if (currentBlock() == mention.cursor.block()) {
|
|
mention.start = mention.cursor.anchor();
|
|
mention.position = mention.cursor.position();
|
|
setFormat(mention.cursor.selectionStart(), mention.cursor.selectedText().size(), mentionFormat);
|
|
}
|
|
return false;
|
|
}),
|
|
mentions->end());
|
|
}
|
|
|
|
QStringList suggestions(const QString &word) const
|
|
{
|
|
return checker->suggest(word);
|
|
}
|
|
};
|
|
|
|
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
|
: QObject(parent)
|
|
, m_document(nullptr)
|
|
, m_cursorPosition(-1)
|
|
, m_highlighter(new SyntaxHighlighter(this))
|
|
, m_completionModel(new CompletionModel())
|
|
{
|
|
connect(this, &ChatDocumentHandler::roomChanged, this, [this]() {
|
|
m_completionModel->setRoom(m_room);
|
|
static QPointer<NeoChatRoom> previousRoom = nullptr;
|
|
if (previousRoom) {
|
|
disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr);
|
|
disconnect(previousRoom, &NeoChatRoom::editTextChanged, this, nullptr);
|
|
}
|
|
previousRoom = m_room;
|
|
connect(m_room, &NeoChatRoom::chatBoxTextChanged, this, [this]() {
|
|
int start = completionStartIndex();
|
|
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
|
});
|
|
connect(m_room, &NeoChatRoom::editTextChanged, this, [this]() {
|
|
int start = completionStartIndex();
|
|
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
|
});
|
|
});
|
|
connect(this, &ChatDocumentHandler::documentChanged, this, [this]() {
|
|
m_highlighter->setDocument(m_document->textDocument());
|
|
});
|
|
connect(this, &ChatDocumentHandler::cursorPositionChanged, this, [this]() {
|
|
if (!m_room) {
|
|
return;
|
|
}
|
|
int start = completionStartIndex();
|
|
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
|
});
|
|
}
|
|
|
|
int ChatDocumentHandler::completionStartIndex() const
|
|
{
|
|
if (!m_room) {
|
|
return 0;
|
|
}
|
|
|
|
#if !defined(Q_OS_ANDROID)
|
|
const long long cursor = cursorPosition();
|
|
#else
|
|
const auto cursor = cursorPosition();
|
|
#endif
|
|
const auto &text = getText();
|
|
|
|
auto start = std::min(cursor, text.size()) - 1;
|
|
while (start > -1) {
|
|
if (text.at(start) == QLatin1Char(' ')) {
|
|
start++;
|
|
break;
|
|
}
|
|
start--;
|
|
}
|
|
return start;
|
|
}
|
|
|
|
bool ChatDocumentHandler::isEdit() const
|
|
{
|
|
return m_isEdit;
|
|
}
|
|
|
|
void ChatDocumentHandler::setIsEdit(bool edit)
|
|
{
|
|
if (edit == m_isEdit) {
|
|
return;
|
|
}
|
|
m_isEdit = edit;
|
|
Q_EMIT isEditChanged();
|
|
}
|
|
|
|
QQuickTextDocument *ChatDocumentHandler::document() const
|
|
{
|
|
return m_document;
|
|
}
|
|
|
|
void ChatDocumentHandler::setDocument(QQuickTextDocument *document)
|
|
{
|
|
if (document == m_document) {
|
|
return;
|
|
}
|
|
|
|
if (m_document) {
|
|
m_document->textDocument()->disconnect(this);
|
|
}
|
|
m_document = document;
|
|
Q_EMIT documentChanged();
|
|
}
|
|
|
|
int ChatDocumentHandler::cursorPosition() const
|
|
{
|
|
return m_cursorPosition;
|
|
}
|
|
|
|
void ChatDocumentHandler::setCursorPosition(int position)
|
|
{
|
|
if (position == m_cursorPosition) {
|
|
return;
|
|
}
|
|
if (m_room) {
|
|
m_cursorPosition = position;
|
|
}
|
|
Q_EMIT cursorPositionChanged();
|
|
}
|
|
|
|
NeoChatRoom *ChatDocumentHandler::room() const
|
|
{
|
|
return m_room;
|
|
}
|
|
|
|
void ChatDocumentHandler::setRoom(NeoChatRoom *room)
|
|
{
|
|
if (m_room == room) {
|
|
return;
|
|
}
|
|
|
|
m_room = room;
|
|
Q_EMIT roomChanged();
|
|
}
|
|
|
|
void ChatDocumentHandler::complete(int index)
|
|
{
|
|
if (m_completionModel->autoCompletionType() == CompletionModel::User) {
|
|
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::DisplayNameRole).toString();
|
|
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
|
|
auto text = getText();
|
|
auto at = text.lastIndexOf(QLatin1Char('@'), cursorPosition() - 1);
|
|
QTextCursor cursor(document()->textDocument());
|
|
cursor.setPosition(at);
|
|
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
|
cursor.insertText(name + QStringLiteral(" "));
|
|
cursor.setPosition(at);
|
|
cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor);
|
|
cursor.setKeepPositionOnInsert(true);
|
|
pushMention({cursor, name, 0, 0, id});
|
|
m_highlighter->rehighlight();
|
|
} else if (m_completionModel->autoCompletionType() == CompletionModel::Command) {
|
|
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
|
|
auto text = getText();
|
|
auto at = text.lastIndexOf(QLatin1Char('/'));
|
|
QTextCursor cursor(document()->textDocument());
|
|
cursor.setPosition(at);
|
|
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
|
cursor.insertText(QStringLiteral("/%1 ").arg(command));
|
|
} else if (m_completionModel->autoCompletionType() == CompletionModel::Room) {
|
|
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
|
|
auto text = getText();
|
|
auto at = text.lastIndexOf(QLatin1Char('#'), cursorPosition() - 1);
|
|
QTextCursor cursor(document()->textDocument());
|
|
cursor.setPosition(at);
|
|
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
|
cursor.insertText(alias + QStringLiteral(" "));
|
|
cursor.setPosition(at);
|
|
cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor);
|
|
cursor.setKeepPositionOnInsert(true);
|
|
pushMention({cursor, alias, 0, 0, alias});
|
|
m_highlighter->rehighlight();
|
|
} else if (m_completionModel->autoCompletionType() == CompletionModel::Emoji) {
|
|
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
|
|
auto text = getText();
|
|
auto at = text.lastIndexOf(QLatin1Char(':'));
|
|
QTextCursor cursor(document()->textDocument());
|
|
cursor.setPosition(at);
|
|
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
|
cursor.insertText(shortcode);
|
|
}
|
|
}
|
|
|
|
QStringList ChatDocumentHandler::getSuggestions(int mousePosition)
|
|
{
|
|
QTextCursor cursorAtMouse(document()->textDocument());
|
|
cursorAtMouse.setPosition(mousePosition);
|
|
|
|
// Get the word under the (mouse-)cursor and see if it is misspelled.
|
|
// Don't include apostrophes at the start/end of the word in the selection.
|
|
QTextCursor wordSelectCursor(cursorAtMouse);
|
|
wordSelectCursor.clearSelection();
|
|
wordSelectCursor.select(QTextCursor::WordUnderCursor);
|
|
m_selectedWord = wordSelectCursor.selectedText();
|
|
|
|
return m_highlighter->suggestions(m_selectedWord);
|
|
}
|
|
|
|
bool ChatDocumentHandler::getActive() const
|
|
{
|
|
return m_highlighter->settings.checkerEnabledByDefault();
|
|
}
|
|
|
|
bool ChatDocumentHandler::getIsWordIsMisspelled() const
|
|
{
|
|
return !m_highlighter->errors.isEmpty();
|
|
}
|
|
|
|
QString ChatDocumentHandler::getWordUnderMouse() const
|
|
{
|
|
return m_selectedWord;
|
|
}
|
|
|
|
void ChatDocumentHandler::replaceWord(const QString &word)
|
|
{
|
|
QTextCursor cursor(document()->textDocument());
|
|
const auto &text = m_room->chatBoxText();
|
|
|
|
auto at = text.indexOf(m_highlighter->previousText);
|
|
cursor.setPosition(at);
|
|
cursor.setPosition(at + m_highlighter->previousText.length(), QTextCursor::KeepAnchor);
|
|
cursor.insertText(word);
|
|
}
|
|
|
|
CompletionModel *ChatDocumentHandler::completionModel() const
|
|
{
|
|
return m_completionModel;
|
|
}
|
|
|
|
int ChatDocumentHandler::selectionStart() const
|
|
{
|
|
return m_selectionStart;
|
|
}
|
|
|
|
void ChatDocumentHandler::setSelectionStart(int position)
|
|
{
|
|
if (position == m_selectionStart) {
|
|
return;
|
|
}
|
|
|
|
m_selectionStart = position;
|
|
Q_EMIT selectionStartChanged();
|
|
}
|
|
|
|
int ChatDocumentHandler::selectionEnd() const
|
|
{
|
|
return m_selectionEnd;
|
|
}
|
|
|
|
void ChatDocumentHandler::setSelectionEnd(int position)
|
|
{
|
|
if (position == m_selectionEnd) {
|
|
return;
|
|
}
|
|
|
|
m_selectionEnd = position;
|
|
Q_EMIT selectionEndChanged();
|
|
}
|
|
|
|
QString ChatDocumentHandler::getText() const
|
|
{
|
|
if (!m_room) {
|
|
return QString();
|
|
}
|
|
if (m_isEdit) {
|
|
return m_room->editText();
|
|
} else {
|
|
return m_room->chatBoxText();
|
|
}
|
|
}
|
|
|
|
void ChatDocumentHandler::pushMention(const Mention mention) const
|
|
{
|
|
if (!m_room) {
|
|
return;
|
|
}
|
|
if (m_isEdit) {
|
|
m_room->editMentions()->push_back(mention);
|
|
} else {
|
|
m_room->mentions()->push_back(mention);
|
|
}
|
|
}
|
|
|
|
QColor ChatDocumentHandler::mentionColor() const
|
|
{
|
|
return m_mentionColor;
|
|
}
|
|
|
|
void ChatDocumentHandler::setMentionColor(const QColor &color)
|
|
{
|
|
if (m_mentionColor == color) {
|
|
return;
|
|
}
|
|
m_mentionColor = color;
|
|
m_highlighter->mentionFormat.setForeground(m_mentionColor);
|
|
m_highlighter->rehighlight();
|
|
Q_EMIT mentionColorChanged();
|
|
}
|
|
|
|
QColor ChatDocumentHandler::errorColor() const
|
|
{
|
|
return m_errorColor;
|
|
}
|
|
|
|
void ChatDocumentHandler::setErrorColor(const QColor &color)
|
|
{
|
|
if (m_errorColor == color) {
|
|
return;
|
|
}
|
|
m_errorColor = color;
|
|
m_highlighter->errorFormat.setForeground(m_errorColor);
|
|
m_highlighter->rehighlight();
|
|
Q_EMIT errorColorChanged();
|
|
}
|
|
|
|
#include "moc_chatdocumenthandler.cpp"
|