File indexing completed on 2024-03-24 04:03:44
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->viewport()->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"