File indexing completed on 2024-04-21 15:07:23

0001 /*
0002  * spellcheckdecorator.h
0003  *
0004  * SPDX-FileCopyrightText: 2013 Aurélien Gâteau <agateau@kde.org>
0005  *
0006  * SPDX-License-Identifier: LGPL-2.1-or-later
0007  */
0008 #include "spellcheckdecorator.h"
0009 
0010 // Local
0011 #include <highlighter.h>
0012 
0013 // Qt
0014 #include <QContextMenuEvent>
0015 #include <QMenu>
0016 #include <QPlainTextEdit>
0017 #include <QTextEdit>
0018 
0019 namespace Sonnet
0020 {
0021 class SpellCheckDecoratorPrivate
0022 {
0023 public:
0024     SpellCheckDecoratorPrivate(SpellCheckDecorator *installer, QPlainTextEdit *textEdit)
0025         : q(installer)
0026         , m_plainTextEdit(textEdit)
0027     {
0028         createDefaultHighlighter();
0029         // Catch pressing the "menu" key
0030         m_plainTextEdit->installEventFilter(q);
0031         // Catch right-click
0032         m_plainTextEdit->viewport()->installEventFilter(q);
0033     }
0034 
0035     SpellCheckDecoratorPrivate(SpellCheckDecorator *installer, QTextEdit *textEdit)
0036         : q(installer)
0037         , m_textEdit(textEdit)
0038     {
0039         createDefaultHighlighter();
0040         // Catch pressing the "menu" key
0041         m_textEdit->installEventFilter(q);
0042         // Catch right-click
0043         m_textEdit->viewport()->installEventFilter(q);
0044     }
0045 
0046     ~SpellCheckDecoratorPrivate()
0047     {
0048         if (m_plainTextEdit) {
0049             m_plainTextEdit->removeEventFilter(q);
0050             m_plainTextEdit->removeEventFilter(q);
0051         }
0052         if (m_textEdit) {
0053             m_textEdit->removeEventFilter(q);
0054             m_textEdit->viewport()->removeEventFilter(q);
0055         }
0056     }
0057 
0058     bool onContextMenuEvent(QContextMenuEvent *event);
0059     void execSuggestionMenu(const QPoint &pos, const QString &word, const QTextCursor &cursor);
0060     void createDefaultHighlighter();
0061 
0062     SpellCheckDecorator *const q;
0063     QTextEdit *m_textEdit = nullptr;
0064     QPlainTextEdit *m_plainTextEdit = nullptr;
0065     Highlighter *m_highlighter = nullptr;
0066 };
0067 
0068 bool SpellCheckDecoratorPrivate::onContextMenuEvent(QContextMenuEvent *event)
0069 {
0070     if (!m_highlighter) {
0071         createDefaultHighlighter();
0072     }
0073 
0074     // Obtain the cursor at the mouse position and the current cursor
0075     QTextCursor cursorAtMouse;
0076     if (m_textEdit) {
0077         cursorAtMouse = m_textEdit->cursorForPosition(event->pos());
0078     } else {
0079         cursorAtMouse = m_plainTextEdit->cursorForPosition(event->pos());
0080     }
0081     const int mousePos = cursorAtMouse.position();
0082     QTextCursor cursor;
0083     if (m_textEdit) {
0084         cursor = m_textEdit->textCursor();
0085     } else {
0086         cursor = m_plainTextEdit->textCursor();
0087     }
0088 
0089     // Check if the user clicked a selected word
0090     /* clang-format off */
0091     const bool selectedWordClicked = cursor.hasSelection()
0092                                      && mousePos >= cursor.selectionStart()
0093                                      && mousePos <= cursor.selectionEnd();
0094     /* clang-format on */
0095 
0096     // Get the word under the (mouse-)cursor and see if it is misspelled.
0097     // Don't include apostrophes at the start/end of the word in the selection.
0098     QTextCursor wordSelectCursor(cursorAtMouse);
0099     wordSelectCursor.clearSelection();
0100     wordSelectCursor.select(QTextCursor::WordUnderCursor);
0101     QString selectedWord = wordSelectCursor.selectedText();
0102 
0103     bool isMouseCursorInsideWord = true;
0104     if ((mousePos < wordSelectCursor.selectionStart() || mousePos >= wordSelectCursor.selectionEnd()) //
0105         && (selectedWord.length() > 1)) {
0106         isMouseCursorInsideWord = false;
0107     }
0108 
0109     // Clear the selection again, we re-select it below (without the apostrophes).
0110     wordSelectCursor.setPosition(wordSelectCursor.position() - selectedWord.size());
0111     if (selectedWord.startsWith(QLatin1Char('\'')) || selectedWord.startsWith(QLatin1Char('\"'))) {
0112         selectedWord = selectedWord.right(selectedWord.size() - 1);
0113         wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor);
0114     }
0115     if (selectedWord.endsWith(QLatin1Char('\'')) || selectedWord.endsWith(QLatin1Char('\"'))) {
0116         selectedWord.chop(1);
0117     }
0118 
0119     wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, selectedWord.size());
0120 
0121     /* clang-format off */
0122     const bool wordIsMisspelled = isMouseCursorInsideWord
0123                                   && m_highlighter
0124                                   && m_highlighter->isActive()
0125                                   && !selectedWord.isEmpty()
0126                                   && m_highlighter->isWordMisspelled(selectedWord);
0127     /* clang-format on */
0128 
0129     // If the user clicked a selected word, do nothing.
0130     // If the user clicked somewhere else, move the cursor there.
0131     // If the user clicked on a misspelled word, select that word.
0132     // Same behavior as in OpenOffice Writer.
0133     bool checkBlock = q->isSpellCheckingEnabledForBlock(cursorAtMouse.block().text());
0134     if (!selectedWordClicked) {
0135         if (wordIsMisspelled && checkBlock) {
0136             if (m_textEdit) {
0137                 m_textEdit->setTextCursor(wordSelectCursor);
0138             } else {
0139                 m_plainTextEdit->setTextCursor(wordSelectCursor);
0140             }
0141         } else {
0142             if (m_textEdit) {
0143                 m_textEdit->setTextCursor(cursorAtMouse);
0144             } else {
0145                 m_plainTextEdit->setTextCursor(cursorAtMouse);
0146             }
0147         }
0148         if (m_textEdit) {
0149             cursor = m_textEdit->textCursor();
0150         } else {
0151             cursor = m_plainTextEdit->textCursor();
0152         }
0153     }
0154 
0155     // Use standard context menu for already selected words, correctly spelled
0156     // words and words inside quotes.
0157     if (!wordIsMisspelled || selectedWordClicked || !checkBlock) {
0158         return false;
0159     }
0160     execSuggestionMenu(event->globalPos(), selectedWord, cursor);
0161     return true;
0162 }
0163 
0164 void SpellCheckDecoratorPrivate::execSuggestionMenu(const QPoint &pos, const QString &selectedWord, const QTextCursor &_cursor)
0165 {
0166     QTextCursor cursor = _cursor;
0167     QMenu menu; // don't use KMenu here we don't want auto management accelerator
0168 
0169     // Add the suggestions to the menu
0170     const QStringList reps = m_highlighter->suggestionsForWord(selectedWord, cursor);
0171     if (reps.isEmpty()) {
0172         QAction *suggestionsAction = menu.addAction(SpellCheckDecorator::tr("No suggestions for %1").arg(selectedWord));
0173         suggestionsAction->setEnabled(false);
0174     } else {
0175         QStringList::const_iterator end(reps.constEnd());
0176         for (QStringList::const_iterator it = reps.constBegin(); it != end; ++it) {
0177             menu.addAction(*it);
0178         }
0179     }
0180 
0181     menu.addSeparator();
0182 
0183     QAction *ignoreAction = menu.addAction(SpellCheckDecorator::tr("Ignore"));
0184     QAction *addToDictAction = menu.addAction(SpellCheckDecorator::tr("Add to Dictionary"));
0185     // Execute the popup inline
0186     const QAction *selectedAction = menu.exec(pos);
0187 
0188     if (selectedAction) {
0189         // Fails when we're in the middle of a compose-key sequence
0190         // Q_ASSERT(cursor.selectedText() == selectedWord);
0191 
0192         if (selectedAction == ignoreAction) {
0193             m_highlighter->ignoreWord(selectedWord);
0194             m_highlighter->rehighlight();
0195         } else if (selectedAction == addToDictAction) {
0196             m_highlighter->addWordToDictionary(selectedWord);
0197             m_highlighter->rehighlight();
0198         }
0199         // Other actions can only be one of the suggested words
0200         else {
0201             const QString replacement = selectedAction->text();
0202             Q_ASSERT(reps.contains(replacement));
0203             cursor.insertText(replacement);
0204             if (m_textEdit) {
0205                 m_textEdit->setTextCursor(cursor);
0206             } else {
0207                 m_plainTextEdit->setTextCursor(cursor);
0208             }
0209         }
0210     }
0211 }
0212 
0213 void SpellCheckDecoratorPrivate::createDefaultHighlighter()
0214 {
0215     if (m_textEdit) {
0216         m_highlighter = new Highlighter(m_textEdit);
0217     } else {
0218         m_highlighter = new Highlighter(m_plainTextEdit);
0219     }
0220 }
0221 
0222 SpellCheckDecorator::SpellCheckDecorator(QTextEdit *textEdit)
0223     : QObject(textEdit)
0224     , d(std::make_unique<SpellCheckDecoratorPrivate>(this, textEdit))
0225 {
0226 }
0227 
0228 SpellCheckDecorator::SpellCheckDecorator(QPlainTextEdit *textEdit)
0229     : QObject(textEdit)
0230     , d(std::make_unique<SpellCheckDecoratorPrivate>(this, textEdit))
0231 {
0232 }
0233 
0234 SpellCheckDecorator::~SpellCheckDecorator() = default;
0235 
0236 void SpellCheckDecorator::setHighlighter(Highlighter *highlighter)
0237 {
0238     d->m_highlighter = highlighter;
0239 }
0240 
0241 Highlighter *SpellCheckDecorator::highlighter() const
0242 {
0243     if (!d->m_highlighter) {
0244         d->createDefaultHighlighter();
0245     }
0246     return d->m_highlighter;
0247 }
0248 
0249 bool SpellCheckDecorator::eventFilter(QObject * /*obj*/, QEvent *event)
0250 {
0251     if (event->type() == QEvent::ContextMenu) {
0252         return d->onContextMenuEvent(static_cast<QContextMenuEvent *>(event));
0253     }
0254     return false;
0255 }
0256 
0257 bool SpellCheckDecorator::isSpellCheckingEnabledForBlock(const QString &textBlock) const
0258 {
0259     Q_UNUSED(textBlock);
0260     if (d->m_textEdit) {
0261         return d->m_textEdit->isEnabled();
0262     } else {
0263         return d->m_plainTextEdit->isEnabled();
0264     }
0265 }
0266 } // namespace
0267 
0268 #include "moc_spellcheckdecorator.cpp"