File indexing completed on 2024-09-29 12:38:47
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"