File indexing completed on 2024-10-06 07:36:00

0001 // SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
0002 // SPDX-License-Identifier: GPL-3.0-or-later
0003 
0004 #include "chatdocumenthandler.h"
0005 
0006 #include <QQmlFile>
0007 #include <QQmlFileSelector>
0008 #include <QStringBuilder>
0009 #include <QSyntaxHighlighter>
0010 #include <QTextBlock>
0011 #include <QTextDocument>
0012 #include <QTimer>
0013 
0014 #include <Sonnet/BackgroundChecker>
0015 #include <Sonnet/Settings>
0016 
0017 #include "chatdocumenthandler_logging.h"
0018 
0019 class SyntaxHighlighter : public QSyntaxHighlighter
0020 {
0021 public:
0022     QTextCharFormat mentionFormat;
0023     QTextCharFormat errorFormat;
0024     Sonnet::BackgroundChecker *checker = new Sonnet::BackgroundChecker;
0025     Sonnet::Settings settings;
0026     QList<QPair<int, QString>> errors;
0027     QString previousText;
0028     QTimer rehighlightTimer;
0029     SyntaxHighlighter(QObject *parent)
0030         : QSyntaxHighlighter(parent)
0031     {
0032         mentionFormat.setFontWeight(QFont::Bold);
0033         mentionFormat.setForeground(Qt::blue);
0034 
0035         errorFormat.setForeground(Qt::red);
0036         errorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
0037 
0038         connect(checker, &Sonnet::BackgroundChecker::misspelling, this, [this](const QString &word, int start) {
0039             errors += {start, word};
0040             checker->continueChecking();
0041         });
0042         connect(checker, &Sonnet::BackgroundChecker::done, this, [this]() {
0043             rehighlightTimer.start();
0044         });
0045         rehighlightTimer.setInterval(100);
0046         rehighlightTimer.setSingleShot(true);
0047         rehighlightTimer.callOnTimeout(this, &QSyntaxHighlighter::rehighlight);
0048     }
0049     void highlightBlock(const QString &text) override
0050     {
0051         if (settings.checkerEnabledByDefault()) {
0052             if (text != previousText) {
0053                 previousText = text;
0054                 checker->stop();
0055                 errors.clear();
0056                 checker->setText(text);
0057             }
0058             for (const auto &error : errors) {
0059                 setFormat(error.first, error.second.size(), errorFormat);
0060             }
0061         }
0062         auto handler = dynamic_cast<ChatDocumentHandler *>(parent());
0063         auto room = handler->room();
0064         if (!room) {
0065             return;
0066         }
0067         auto mentions = handler->chatBarCache()->mentions();
0068         mentions->erase(std::remove_if(mentions->begin(),
0069                                        mentions->end(),
0070                                        [this](auto &mention) {
0071                                            if (document()->toPlainText().isEmpty()) {
0072                                                return false;
0073                                            }
0074 
0075                                            if (mention.cursor.position() == 0 && mention.cursor.anchor() == 0) {
0076                                                return true;
0077                                            }
0078 
0079                                            if (mention.cursor.position() - mention.cursor.anchor() != mention.text.size()) {
0080                                                mention.cursor.setPosition(mention.start);
0081                                                mention.cursor.setPosition(mention.cursor.anchor() + mention.text.size(), QTextCursor::KeepAnchor);
0082                                            }
0083 
0084                                            if (mention.cursor.selectedText() != mention.text) {
0085                                                return true;
0086                                            }
0087                                            if (currentBlock() == mention.cursor.block()) {
0088                                                mention.start = mention.cursor.anchor();
0089                                                mention.position = mention.cursor.position();
0090                                                setFormat(mention.cursor.selectionStart(), mention.cursor.selectedText().size(), mentionFormat);
0091                                            }
0092                                            return false;
0093                                        }),
0094                         mentions->end());
0095     }
0096 };
0097 
0098 ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
0099     : QObject(parent)
0100     , m_document(nullptr)
0101     , m_cursorPosition(-1)
0102     , m_highlighter(new SyntaxHighlighter(this))
0103     , m_completionModel(new CompletionModel(this))
0104 {
0105     connect(this, &ChatDocumentHandler::roomChanged, this, [this]() {
0106         m_completionModel->setRoom(m_room);
0107         static QPointer<NeoChatRoom> previousRoom = nullptr;
0108         if (previousRoom) {
0109             disconnect(m_chatBarCache, &ChatBarCache::textChanged, this, nullptr);
0110         }
0111         previousRoom = m_room;
0112         connect(m_chatBarCache, &ChatBarCache::textChanged, this, [this]() {
0113             int start = completionStartIndex();
0114             m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
0115         });
0116     });
0117     connect(this, &ChatDocumentHandler::documentChanged, this, [this]() {
0118         m_highlighter->setDocument(m_document->textDocument());
0119     });
0120     connect(this, &ChatDocumentHandler::cursorPositionChanged, this, [this]() {
0121         if (!m_room) {
0122             return;
0123         }
0124         int start = completionStartIndex();
0125         m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
0126     });
0127 }
0128 
0129 int ChatDocumentHandler::completionStartIndex() const
0130 {
0131     if (!m_room) {
0132         return 0;
0133     }
0134 
0135     const qsizetype cursor = cursorPosition();
0136     const auto &text = getText();
0137 
0138     auto start = std::min(cursor, text.size()) - 1;
0139     while (start > -1) {
0140         if (text.at(start) == QLatin1Char(' ')) {
0141             start++;
0142             break;
0143         }
0144         start--;
0145     }
0146     return start;
0147 }
0148 
0149 QQuickTextDocument *ChatDocumentHandler::document() const
0150 {
0151     return m_document;
0152 }
0153 
0154 void ChatDocumentHandler::setDocument(QQuickTextDocument *document)
0155 {
0156     if (document == m_document) {
0157         return;
0158     }
0159 
0160     if (m_document) {
0161         m_document->textDocument()->disconnect(this);
0162     }
0163     m_document = document;
0164     Q_EMIT documentChanged();
0165 }
0166 
0167 int ChatDocumentHandler::cursorPosition() const
0168 {
0169     return m_cursorPosition;
0170 }
0171 
0172 void ChatDocumentHandler::setCursorPosition(int position)
0173 {
0174     if (position == m_cursorPosition) {
0175         return;
0176     }
0177     if (m_room) {
0178         m_cursorPosition = position;
0179     }
0180     Q_EMIT cursorPositionChanged();
0181 }
0182 
0183 NeoChatRoom *ChatDocumentHandler::room() const
0184 {
0185     return m_room;
0186 }
0187 
0188 void ChatDocumentHandler::setRoom(NeoChatRoom *room)
0189 {
0190     if (m_room == room) {
0191         return;
0192     }
0193 
0194     m_room = room;
0195     Q_EMIT roomChanged();
0196 }
0197 
0198 ChatBarCache *ChatDocumentHandler::chatBarCache() const
0199 {
0200     return m_chatBarCache;
0201 }
0202 
0203 void ChatDocumentHandler::setChatBarCache(ChatBarCache *chatBarCache)
0204 {
0205     if (m_chatBarCache == chatBarCache) {
0206         return;
0207     }
0208     m_chatBarCache = chatBarCache;
0209     Q_EMIT chatBarCacheChanged();
0210 }
0211 
0212 void ChatDocumentHandler::complete(int index)
0213 {
0214     if (m_document == nullptr) {
0215         qCWarning(ChatDocumentHandling) << "complete called with m_document set to nullptr.";
0216         return;
0217     }
0218     if (m_completionModel->autoCompletionType() == CompletionModel::None) {
0219         qCWarning(ChatDocumentHandling) << "complete called with m_completionModel->autoCompletionType() == CompletionModel::None.";
0220         return;
0221     }
0222 
0223     if (m_completionModel->autoCompletionType() == CompletionModel::User) {
0224         auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::DisplayNameRole).toString();
0225         auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
0226         auto text = getText();
0227         auto at = text.lastIndexOf(QLatin1Char('@'), cursorPosition() - 1);
0228         QTextCursor cursor(document()->textDocument());
0229         cursor.setPosition(at);
0230         cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
0231         cursor.insertText(name + QStringLiteral(" "));
0232         cursor.setPosition(at);
0233         cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor);
0234         cursor.setKeepPositionOnInsert(true);
0235         pushMention({cursor, name, 0, 0, id});
0236         m_highlighter->rehighlight();
0237     } else if (m_completionModel->autoCompletionType() == CompletionModel::Command) {
0238         auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
0239         auto text = getText();
0240         auto at = text.lastIndexOf(QLatin1Char('/'));
0241         QTextCursor cursor(document()->textDocument());
0242         cursor.setPosition(at);
0243         cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
0244         cursor.insertText(QStringLiteral("/%1 ").arg(command));
0245     } else if (m_completionModel->autoCompletionType() == CompletionModel::Room) {
0246         auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
0247         auto text = getText();
0248         auto at = text.lastIndexOf(QLatin1Char('#'), cursorPosition() - 1);
0249         QTextCursor cursor(document()->textDocument());
0250         cursor.setPosition(at);
0251         cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
0252         cursor.insertText(alias + QStringLiteral(" "));
0253         cursor.setPosition(at);
0254         cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor);
0255         cursor.setKeepPositionOnInsert(true);
0256         pushMention({cursor, alias, 0, 0, alias});
0257         m_highlighter->rehighlight();
0258     } else if (m_completionModel->autoCompletionType() == CompletionModel::Emoji) {
0259         auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
0260         auto text = getText();
0261         auto at = text.lastIndexOf(QLatin1Char(':'));
0262         QTextCursor cursor(document()->textDocument());
0263         cursor.setPosition(at);
0264         cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
0265         cursor.insertText(shortcode);
0266     }
0267 }
0268 
0269 CompletionModel *ChatDocumentHandler::completionModel() const
0270 {
0271     return m_completionModel;
0272 }
0273 
0274 int ChatDocumentHandler::selectionStart() const
0275 {
0276     return m_selectionStart;
0277 }
0278 
0279 void ChatDocumentHandler::setSelectionStart(int position)
0280 {
0281     if (position == m_selectionStart) {
0282         return;
0283     }
0284 
0285     m_selectionStart = position;
0286     Q_EMIT selectionStartChanged();
0287 }
0288 
0289 int ChatDocumentHandler::selectionEnd() const
0290 {
0291     return m_selectionEnd;
0292 }
0293 
0294 void ChatDocumentHandler::setSelectionEnd(int position)
0295 {
0296     if (position == m_selectionEnd) {
0297         return;
0298     }
0299 
0300     m_selectionEnd = position;
0301     Q_EMIT selectionEndChanged();
0302 }
0303 
0304 QString ChatDocumentHandler::getText() const
0305 {
0306     if (!m_chatBarCache) {
0307         qCWarning(ChatDocumentHandling) << "getText called with m_chatBarCache set to nullptr.";
0308         return {};
0309     }
0310     return m_chatBarCache->text();
0311 }
0312 
0313 void ChatDocumentHandler::pushMention(const Mention mention) const
0314 {
0315     if (!m_chatBarCache) {
0316         qCWarning(ChatDocumentHandling) << "pushMention called with m_chatBarCache set to nullptr.";
0317         return;
0318     }
0319     m_chatBarCache->mentions()->push_back(mention);
0320 }
0321 
0322 QColor ChatDocumentHandler::mentionColor() const
0323 {
0324     return m_mentionColor;
0325 }
0326 
0327 void ChatDocumentHandler::setMentionColor(const QColor &color)
0328 {
0329     if (m_mentionColor == color) {
0330         return;
0331     }
0332     m_mentionColor = color;
0333     m_highlighter->mentionFormat.setForeground(m_mentionColor);
0334     m_highlighter->rehighlight();
0335     Q_EMIT mentionColorChanged();
0336 }
0337 
0338 QColor ChatDocumentHandler::errorColor() const
0339 {
0340     return m_errorColor;
0341 }
0342 
0343 void ChatDocumentHandler::setErrorColor(const QColor &color)
0344 {
0345     if (m_errorColor == color) {
0346         return;
0347     }
0348     m_errorColor = color;
0349     m_highlighter->errorFormat.setForeground(m_errorColor);
0350     m_highlighter->rehighlight();
0351     Q_EMIT errorColorChanged();
0352 }
0353 
0354 #include "moc_chatdocumenthandler.cpp"