// SPDX-FileCopyrightText: 2020 Carl Schwan // SPDX-License-Identifier: GPL-3.0-or-later #include "chatdocumenthandler.h" #include #include #include #include #include #include #include #include #include #include "chatdocumenthandler_logging.h" class SyntaxHighlighter : public QSyntaxHighlighter { public: QTextCharFormat mentionFormat; QTextCharFormat errorFormat; Sonnet::BackgroundChecker *checker = new Sonnet::BackgroundChecker; Sonnet::Settings settings; QList> 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 handler = dynamic_cast(parent()); auto room = handler->room(); if (!room) { return; } auto mentions = handler->chatBarCache()->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()); } }; ChatDocumentHandler::ChatDocumentHandler(QObject *parent) : QObject(parent) , m_document(nullptr) , m_cursorPosition(-1) , m_highlighter(new SyntaxHighlighter(this)) , m_completionModel(new CompletionModel(this)) { connect(this, &ChatDocumentHandler::roomChanged, this, [this]() { m_completionModel->setRoom(m_room); static QPointer previousRoom = nullptr; if (previousRoom) { disconnect(m_chatBarCache, &ChatBarCache::textChanged, this, nullptr); } previousRoom = m_room; connect(m_chatBarCache, &ChatBarCache::textChanged, 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; } const qsizetype cursor = cursorPosition(); 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; } 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(); } ChatBarCache *ChatDocumentHandler::chatBarCache() const { return m_chatBarCache; } void ChatDocumentHandler::setChatBarCache(ChatBarCache *chatBarCache) { if (m_chatBarCache == chatBarCache) { return; } m_chatBarCache = chatBarCache; Q_EMIT chatBarCacheChanged(); } void ChatDocumentHandler::complete(int index) { if (m_document == nullptr) { qCWarning(ChatDocumentHandling) << "complete called with m_document set to nullptr."; return; } if (m_completionModel->autoCompletionType() == CompletionModel::None) { qCWarning(ChatDocumentHandling) << "complete called with m_completionModel->autoCompletionType() == CompletionModel::None."; return; } 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); } } 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_chatBarCache) { qCWarning(ChatDocumentHandling) << "getText called with m_chatBarCache set to nullptr."; return {}; } return m_chatBarCache->text(); } void ChatDocumentHandler::pushMention(const Mention mention) const { if (!m_chatBarCache) { qCWarning(ChatDocumentHandling) << "pushMention called with m_chatBarCache set to nullptr."; return; } m_chatBarCache->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"