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"