diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5cb0d38f9..3787ace99 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -32,7 +32,6 @@ add_executable(neochat chatboxhelper.cpp commandmodel.cpp webshortcutmodel.cpp - spellcheckhighlighter.cpp blurhash.cpp blurhashimageprovider.cpp joinrulesevent.cpp diff --git a/src/main.cpp b/src/main.cpp index 50eb3bc8c..5d0a2649f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -69,7 +69,6 @@ #include "roomlistmodel.h" #include "roommanager.h" #include "sortfilterroomlistmodel.h" -#include "spellcheckhighlighter.h" #include "userdirectorylistmodel.h" #include "userlistmodel.h" #include "webshortcutmodel.h" @@ -198,7 +197,6 @@ int main(int argc, char *argv[]) qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Quotient::AccountRegistry::instance()); qmlRegisterType("org.kde.neochat", 1, 0, "ActionsHandler"); qmlRegisterType("org.kde.neochat", 1, 0, "ChatDocumentHandler"); - qmlRegisterType("org.kde.neochat", 1, 0, "SpellcheckHighlighter"); qmlRegisterType("org.kde.neochat", 1, 0, "RoomListModel"); qmlRegisterType("org.kde.neochat", 1, 0, "WebShortcutModel"); qmlRegisterType("org.kde.neochat", 1, 0, "UserListModel"); diff --git a/src/spellcheckhighlighter.cpp b/src/spellcheckhighlighter.cpp deleted file mode 100644 index 32ef4a7e1..000000000 --- a/src/spellcheckhighlighter.cpp +++ /dev/null @@ -1,363 +0,0 @@ -// SPDX-FileCopyrightText: 2013 Aurélien Gâteau -// SPDX-FileCopyrightText: 2020 Christian Mollekopf -// SPDX-FileCopyrightText: 2021 Carl Schwan -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "spellcheckhighlighter.h" - -#include -#include - -// Cache of previously-determined languages (when using AutoDetectLanguage) -// There is one such cache per block (paragraph) -class LanguageCache : public QTextBlockUserData -{ -public: - // Key: QPair - // Value: language name - QMap, QString> languages; - - // Remove all cached language information after @p pos - void invalidate(int pos) - { - QMutableMapIterator, QString> it(languages); - it.toBack(); - while (it.hasPrevious()) { - it.previous(); - if (it.key().first + it.key().second >= pos) { - it.remove(); - } else { - break; - } - } - } - - QString languageAtPos(int pos) const - { - // The data structure isn't really great for such lookups... - QMapIterator, QString> it(languages); - while (it.hasNext()) { - it.next(); - if (it.key().first <= pos && it.key().first + it.key().second >= pos) { - return it.value(); - } - } - return QString(); - } -}; - -QVector split(QTextBoundaryFinder::BoundaryType boundary, const QString &text, int reasonMask = 0) -{ - QVector parts; - QTextBoundaryFinder boundaryFinder(boundary, text); - - while (boundaryFinder.position() < text.length()) { - const int start = boundaryFinder.position(); - - // Advance until we find a break that matches the mask or are at the end - for (;;) { - if (boundaryFinder.toNextBoundary() == -1) { - boundaryFinder.toEnd(); - break; - } - if (!reasonMask || boundaryFinder.boundaryReasons() & reasonMask) { - break; - } - } - - const auto length = boundaryFinder.position() - start; - - if (length < 1) { - continue; - } - parts << QStringRef{&text, start, length}; - } - return parts; -} - -SpellcheckHighlighter::SpellcheckHighlighter(QObject *parent) - : QSyntaxHighlighter(parent) -#ifndef Q_OS_ANDROID - , mSpellchecker{new Sonnet::Speller()} - , mLanguageGuesser -{ - new Sonnet::GuessLanguage() -} -#endif -, m_document(nullptr), m_cursorPosition(-1), m_selectionStart(-1), m_selectionEnd(-1) -{ - // Danger red from our color scheme - mErrorFormat.setForeground(QColor(0xED, 0x15, 0x15)); - mErrorFormat.setUnderlineColor(QColor(0xED, 0x15, 0x15)); - mErrorFormat.setUnderlineStyle(QTextCharFormat::SingleUnderline); - mQuoteFormat.setForeground(QColor(0x7F, 0x8C, 0x8D)); -#ifndef Q_OS_ANDROID - if (!mSpellchecker->isValid()) { - qWarning() << "Spellchecker is invalid"; - } -#endif -} - -void SpellcheckHighlighter::autodetectLanguage(const QString &sentence) -{ -#ifndef Q_OS_ANDROID - const auto lang = mLanguageGuesser->identify(sentence, mSpellchecker->availableLanguages()); - if (lang.isEmpty()) { - return; - } - mSpellchecker->setLanguage(lang); -#endif -} - -static bool isSpellcheckable(const QStringRef &token) -{ - if (token.isNull() || token.isEmpty()) { - return false; - } - if (!token.at(0).isLetter() || token.at(0).isUpper() || token.startsWith(QStringLiteral("http"))) { - return false; - } - // part of a slash command - if (token.contains("rainbowme") || token.contains("lenny") || token.contains("tableflip") || token.contains("unflip")) { - return false; - } - // TODO ignore urls and uppercase? - return true; -} - -void SpellcheckHighlighter::highlightBlock(const QString &text) -{ - // Avoid spellchecking quotes - if (text.isEmpty() || text.at(0) == QLatin1Char('>')) { - setFormat(0, text.length(), mQuoteFormat); - return; - } - // Don't spell check certain commands - if (text.startsWith("/join") || text.startsWith("/part") || text.startsWith("/invite")) { - setFormat(0, text.length(), QTextCharFormat{}); - return; - } -#ifndef Q_OS_ANDROID - for (const auto &sentenceRef : split(QTextBoundaryFinder::Sentence, text)) { - // Avoid spellchecking quotes - if (sentenceRef.isEmpty() || sentenceRef.at(0) == QLatin1Char('>')) { - continue; - } - - const auto sentence = QString::fromRawData(sentenceRef.data(), sentenceRef.length()); - - autodetectLanguage(sentence); - - const int offset = sentenceRef.position(); - for (const auto &wordRef : split(QTextBoundaryFinder::Word, sentence)) { - // Avoid spellchecking words in progress - // FIXME this will also prevent spellchecking a single word on a line. - if (offset + wordRef.position() + wordRef.length() >= text.length()) { - continue; - } - if (isSpellcheckable(wordRef)) { - const auto word = QString::fromRawData(wordRef.data(), wordRef.length()); - const auto format = mSpellchecker->isMisspelled(word) ? mErrorFormat : QTextCharFormat{}; - setFormat(offset + wordRef.position(), wordRef.length(), format); - } - } - } -#endif -} - -QStringList SpellcheckHighlighter::suggestions(int mousePosition, int max) -{ -#ifndef Q_OS_ANDROID - QTextCursor cursor = textCursor(); - - QTextCursor cursorAtMouse(textDocument()); - cursorAtMouse.setPosition(mousePosition); - - // Check if the user clicked a selected word - /* clang-format off */ - const bool selectedWordClicked = cursor.hasSelection() - && mousePosition >= cursor.selectionStart() - && mousePosition <= cursor.selectionEnd(); - /* clang-format on */ - - // Get the word under the (mouse-)cursor and see if it is misspelled. - // Don't include apostrophes at the start/end of the word in the selection. - QTextCursor wordSelectCursor(cursorAtMouse); - wordSelectCursor.clearSelection(); - wordSelectCursor.select(QTextCursor::WordUnderCursor); - m_selectedWord = wordSelectCursor.selectedText(); - - // Clear the selection again, we re-select it below (without the apostrophes). - wordSelectCursor.setPosition(wordSelectCursor.position() - m_selectedWord.size()); - if (m_selectedWord.startsWith(QLatin1Char('\'')) || m_selectedWord.startsWith(QLatin1Char('\"'))) { - m_selectedWord = m_selectedWord.right(m_selectedWord.size() - 1); - wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor); - } - if (m_selectedWord.endsWith(QLatin1Char('\'')) || m_selectedWord.endsWith(QLatin1Char('\"'))) { - m_selectedWord.chop(1); - } - - wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, m_selectedWord.size()); - - int endSelection = wordSelectCursor.selectionEnd(); - Q_EMIT wordUnderMouseChanged(); - - bool isMouseCursorInsideWord = true; - if ((mousePosition < wordSelectCursor.selectionStart() || mousePosition >= wordSelectCursor.selectionEnd()) // - && (m_selectedWord.length() > 1)) { - isMouseCursorInsideWord = false; - } - - wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, m_selectedWord.size()); - - m_wordIsMisspelled = isMouseCursorInsideWord && !m_selectedWord.isEmpty() && mSpellchecker->isMisspelled(m_selectedWord); - Q_EMIT wordIsMisspelledChanged(); - - if (!m_wordIsMisspelled || selectedWordClicked) { - return QStringList{}; - } - - if (!selectedWordClicked) { - Q_EMIT changeCursorPosition(wordSelectCursor.selectionStart(), endSelection); - } - - LanguageCache *cache = dynamic_cast(cursor.block().userData()); - if (cache) { - const QString cachedLanguage = cache->languageAtPos(cursor.positionInBlock()); - if (!cachedLanguage.isEmpty()) { - mSpellchecker->setLanguage(cachedLanguage); - } - } - QStringList suggestions = mSpellchecker->suggest(m_selectedWord); - if (max >= 0 && suggestions.count() > max) { - suggestions = suggestions.mid(0, max); - } - - return suggestions; -#else - return QStringList(); -#endif -} - -void SpellcheckHighlighter::addWordToDictionary(const QString &word) -{ -#ifndef Q_OS_ANDROID - mSpellchecker->addToPersonal(word); - rehighlight(); -#endif -} - -void SpellcheckHighlighter::ignoreWord(const QString &word) -{ -#ifndef Q_OS_ANDROID - mSpellchecker->addToSession(word); - rehighlight(); -#endif -} - -void SpellcheckHighlighter::replaceWord(const QString &replacement) -{ -#ifndef Q_OS_ANDROID - textCursor().insertText(replacement); -#endif -} - -QQuickTextDocument *SpellcheckHighlighter::quickDocument() const -{ - return m_document; -} - -void SpellcheckHighlighter::setQuickDocument(QQuickTextDocument *document) -{ - if (document == m_document) { - return; - } - - if (m_document) { - m_document->textDocument()->disconnect(this); - } - m_document = document; - setDocument(document->textDocument()); - Q_EMIT documentChanged(); -} - -int SpellcheckHighlighter::cursorPosition() const -{ - return m_cursorPosition; -} - -void SpellcheckHighlighter::setCursorPosition(int position) -{ - if (position == m_cursorPosition) { - return; - } - - m_cursorPosition = position; - Q_EMIT cursorPositionChanged(); -} - -int SpellcheckHighlighter::selectionStart() const -{ - return m_selectionStart; -} - -void SpellcheckHighlighter::setSelectionStart(int position) -{ - if (position == m_selectionStart) { - return; - } - - m_selectionStart = position; - Q_EMIT selectionStartChanged(); -} - -int SpellcheckHighlighter::selectionEnd() const -{ - return m_selectionEnd; -} - -void SpellcheckHighlighter::setSelectionEnd(int position) -{ - if (position == m_selectionEnd) { - return; - } - - m_selectionEnd = position; - Q_EMIT selectionEndChanged(); -} - -QTextCursor SpellcheckHighlighter::textCursor() const -{ - QTextDocument *doc = textDocument(); - if (!doc) { - return QTextCursor(); - } - - QTextCursor cursor(doc); - if (m_selectionStart != m_selectionEnd) { - cursor.setPosition(m_selectionStart); - cursor.setPosition(m_selectionEnd, QTextCursor::KeepAnchor); - } else { - cursor.setPosition(m_cursorPosition); - } - return cursor; -} - -QTextDocument *SpellcheckHighlighter::textDocument() const -{ - if (!m_document) { - return nullptr; - } - - return m_document->textDocument(); -} - -bool SpellcheckHighlighter::wordIsMisspelled() const -{ - return m_wordIsMisspelled; -} - -QString SpellcheckHighlighter::wordUnderMouse() const -{ - return m_selectedWord; -} diff --git a/src/spellcheckhighlighter.h b/src/spellcheckhighlighter.h deleted file mode 100644 index 38a1f3bf4..000000000 --- a/src/spellcheckhighlighter.h +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-FileCopyrightText: 2013 Aurélien Gâteau -// SPDX-FileCopyrightText: 2020 Christian Mollekopf -// SPDX-FileCopyrightText: 2021 Carl Schwan -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include -#include -#include -#ifndef Q_OS_ANDROID -#include -#include -#endif - -class SpellcheckHighlighter : public QSyntaxHighlighter -{ - Q_OBJECT - Q_PROPERTY(QQuickTextDocument *document READ quickDocument WRITE setQuickDocument NOTIFY documentChanged) - Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged) - Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged) - Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionEndChanged) - Q_PROPERTY(bool wordIsMisspelled READ wordIsMisspelled NOTIFY wordIsMisspelledChanged) - Q_PROPERTY(QString wordUnderMouse READ wordUnderMouse NOTIFY wordUnderMouseChanged) - -public: - SpellcheckHighlighter(QObject *parent = nullptr); - - Q_INVOKABLE QStringList suggestions(int position, int max = 5); - Q_INVOKABLE void ignoreWord(const QString &word); - Q_INVOKABLE void addWordToDictionary(const QString &word); - Q_INVOKABLE void replaceWord(const QString &word); - - [[nodiscard]] QQuickTextDocument *quickDocument() const; - void setQuickDocument(QQuickTextDocument *document); - - [[nodiscard]] int cursorPosition() const; - void setCursorPosition(int position); - - [[nodiscard]] int selectionStart() const; - void setSelectionStart(int position); - - [[nodiscard]] int selectionEnd() const; - void setSelectionEnd(int position); - - [[nodiscard]] bool wordIsMisspelled() const; - [[nodiscard]] QString wordUnderMouse() const; - -protected: - void highlightBlock(const QString &text) override; - -Q_SIGNALS: - void documentChanged(); - void cursorPositionChanged(); - void selectionStartChanged(); - void selectionEndChanged(); - void wordIsMisspelledChanged(); - void wordUnderMouseChanged(); - void changeCursorPosition(int start, int end); - -private: - [[nodiscard]] QTextCursor textCursor() const; - [[nodiscard]] QTextDocument *textDocument() const; - - void autodetectLanguage(const QString &sentence); - QTextCharFormat mErrorFormat; - QTextCharFormat mQuoteFormat; -#ifndef Q_OS_ANDROID - QScopedPointer mSpellchecker; - QScopedPointer mLanguageGuesser; -#endif - QString m_selectedWord; - QQuickTextDocument *m_document; - int m_cursorPosition; - int m_selectionStart; - int m_selectionEnd; - - int m_autoCompleteBeginPosition = -1; - int m_autoCompleteEndPosition = -1; - int m_wordIsMisspelled = false; -};