File indexing completed on 2024-04-14 15:03:02

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