File indexing completed on 2024-04-28 15:30:46
0001 /* 0002 SPDX-FileCopyrightText: 2009-2010 Michel Ludwig <michel.ludwig@kdemail.net> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "spellingmenu.h" 0008 0009 #include "katedocument.h" 0010 #include "kateglobal.h" 0011 #include "kateview.h" 0012 #include "ontheflycheck.h" 0013 #include "spellcheck/spellcheck.h" 0014 0015 #include "katepartdebug.h" 0016 0017 #include <QActionGroup> 0018 0019 #include <KLocalizedString> 0020 #include <KTextEditor/Range> 0021 0022 KateSpellingMenu::KateSpellingMenu(KTextEditor::ViewPrivate *view) 0023 : QObject(view) 0024 , m_view(view) 0025 , m_spellingMenuAction(nullptr) 0026 , m_ignoreWordAction(nullptr) 0027 , m_addToDictionaryAction(nullptr) 0028 , m_spellingMenu(nullptr) 0029 , m_currentMisspelledRange(nullptr) 0030 { 0031 } 0032 0033 KateSpellingMenu::~KateSpellingMenu() 0034 { 0035 m_currentMisspelledRange = nullptr; // it shouldn't be accessed anymore as it could 0036 } 0037 0038 bool KateSpellingMenu::isEnabled() const 0039 { 0040 if (!m_spellingMenuAction) { 0041 return false; 0042 } 0043 return m_spellingMenuAction->isEnabled(); 0044 } 0045 0046 bool KateSpellingMenu::isVisible() const 0047 { 0048 if (!m_spellingMenuAction) { 0049 return false; 0050 } 0051 return m_spellingMenuAction->isVisible(); 0052 } 0053 0054 void KateSpellingMenu::setEnabled(bool b) 0055 { 0056 if (m_spellingMenuAction) { 0057 m_spellingMenuAction->setEnabled(b); 0058 } 0059 } 0060 0061 void KateSpellingMenu::setVisible(bool b) 0062 { 0063 if (m_spellingMenuAction) { 0064 m_spellingMenuAction->setVisible(b); 0065 } 0066 } 0067 0068 void KateSpellingMenu::createActions(KActionCollection *ac) 0069 { 0070 m_spellingMenuAction = new KActionMenu(i18n("Spelling"), this); 0071 ac->addAction(QStringLiteral("spelling_suggestions"), m_spellingMenuAction); 0072 m_spellingMenu = m_spellingMenuAction->menu(); 0073 connect(m_spellingMenu, &QMenu::aboutToShow, this, &KateSpellingMenu::populateSuggestionsMenu); 0074 0075 m_ignoreWordAction = new QAction(i18n("Ignore Word"), this); 0076 connect(m_ignoreWordAction, &QAction::triggered, this, &KateSpellingMenu::ignoreCurrentWord); 0077 0078 m_addToDictionaryAction = new QAction(i18n("Add to Dictionary"), this); 0079 connect(m_addToDictionaryAction, &QAction::triggered, this, &KateSpellingMenu::addCurrentWordToDictionary); 0080 0081 m_dictionaryGroup = new QActionGroup(this); 0082 QMapIterator<QString, QString> i(Sonnet::Speller().preferredDictionaries()); 0083 while (i.hasNext()) { 0084 i.next(); 0085 QAction *action = m_dictionaryGroup->addAction(i.key()); 0086 action->setData(i.value()); 0087 } 0088 connect(m_dictionaryGroup, &QActionGroup::triggered, [this](QAction *action) { 0089 if (m_selectedRange.isValid() && !m_selectedRange.isEmpty()) { 0090 const bool blockmode = m_view->blockSelection(); 0091 m_view->doc()->setDictionary(action->data().toString(), m_selectedRange, blockmode); 0092 } 0093 }); 0094 0095 setVisible(false); 0096 } 0097 0098 void KateSpellingMenu::caretEnteredMisspelledRange(KTextEditor::MovingRange *range) 0099 { 0100 if (m_currentMisspelledRange == range) { 0101 return; 0102 } 0103 m_currentMisspelledRange = range; 0104 } 0105 0106 void KateSpellingMenu::caretExitedMisspelledRange(KTextEditor::MovingRange *range) 0107 { 0108 if (range != m_currentMisspelledRange) { 0109 // The order of 'exited' and 'entered' signals was wrong 0110 return; 0111 } 0112 m_currentMisspelledRange = nullptr; 0113 } 0114 0115 void KateSpellingMenu::rangeDeleted(KTextEditor::MovingRange *range) 0116 { 0117 if (m_currentMisspelledRange == range) { 0118 m_currentMisspelledRange = nullptr; 0119 } 0120 } 0121 0122 void KateSpellingMenu::cleanUpAfterShown() 0123 { 0124 // Ugly hack to avoid segfaults. 0125 // cleanUpAfterShown/ViewPrivate::aboutToHideContextMenu is called before 0126 // some action slot is processed. 0127 QTimer::singleShot(0, [this]() { 0128 if (m_currentMisspelledRangeNeedCleanUp) { 0129 m_currentMisspelledRange = nullptr; 0130 m_currentMisspelledRangeNeedCleanUp = false; 0131 } 0132 0133 // We need to remove our list or they will accumulated on next show event 0134 for (auto act : m_menuOnTopSuggestionList) { 0135 act->parentWidget()->removeAction(act); 0136 delete act; 0137 } 0138 m_menuOnTopSuggestionList.clear(); 0139 }); 0140 } 0141 0142 void KateSpellingMenu::prepareToBeShown(QMenu *contextMenu) 0143 { 0144 Q_ASSERT(contextMenu); 0145 0146 if (!m_view->doc()->onTheFlySpellChecker()) { 0147 // Nothing todo! 0148 return; 0149 } 0150 0151 m_selectedRange = m_view->selectionRange(); 0152 if (m_selectedRange.isValid() && !m_selectedRange.isEmpty()) { 0153 // Selected words need a special handling to work properly 0154 auto imv = m_view->doc()->onTheFlySpellChecker()->installedMovingRanges(m_selectedRange); 0155 for (int i = 0; i < imv.size(); ++i) { 0156 if (imv.at(i)->toRange() == m_selectedRange) { 0157 m_currentMisspelledRange = imv.at(i); 0158 m_currentMisspelledRangeNeedCleanUp = true; 0159 break; 0160 } 0161 } 0162 } 0163 0164 if (m_currentMisspelledRange != nullptr) { 0165 setVisible(true); 0166 m_selectedRange = m_currentMisspelledRange->toRange(); // Support actions of m_dictionaryGroup 0167 const QString &misspelledWord = m_view->doc()->text(*m_currentMisspelledRange); 0168 m_spellingMenuAction->setText(i18n("Spelling '%1'", misspelledWord)); 0169 // Add suggestions on top of menu 0170 m_currentDictionary = m_view->doc()->dictionaryForMisspelledRange(*m_currentMisspelledRange); 0171 m_currentSuggestions = KTextEditor::EditorPrivate::self()->spellCheckManager()->suggestions(misspelledWord, m_currentDictionary); 0172 int counter = 5; 0173 QFont boldFont; // Emphasize on-top suggestions, so does Falkon 0174 boldFont.setBold(true); 0175 for (QStringList::const_iterator i = m_currentSuggestions.cbegin(); i != m_currentSuggestions.cend() && counter > 0; ++i) { 0176 const QString &suggestion = *i; 0177 QAction *action = new QAction(suggestion, contextMenu); 0178 action->setFont(boldFont); 0179 m_menuOnTopSuggestionList.append(action); 0180 connect(action, &QAction::triggered, this, [suggestion, this]() { 0181 replaceWordBySuggestion(suggestion); 0182 }); 0183 m_spellingMenu->addAction(action); 0184 --counter; 0185 } 0186 contextMenu->insertActions(m_spellingMenuAction, m_menuOnTopSuggestionList); 0187 0188 } else if (m_selectedRange.isValid() && !m_selectedRange.isEmpty()) { 0189 setVisible(true); 0190 m_spellingMenuAction->setText(i18n("Spelling")); 0191 } else { 0192 setVisible(false); 0193 } 0194 } 0195 0196 void KateSpellingMenu::populateSuggestionsMenu() 0197 { 0198 m_spellingMenu->clear(); 0199 0200 if (m_currentMisspelledRange) { 0201 m_spellingMenu->addAction(m_ignoreWordAction); 0202 m_spellingMenu->addAction(m_addToDictionaryAction); 0203 0204 m_spellingMenu->addSeparator(); 0205 bool dictFound = false; 0206 for (auto action : m_dictionaryGroup->actions()) { 0207 action->setCheckable(true); 0208 if (action->data().toString() == m_currentDictionary) { 0209 dictFound = true; 0210 action->setChecked(true); 0211 } 0212 m_spellingMenu->addAction(action); 0213 } 0214 if (!dictFound && !m_currentDictionary.isEmpty()) { 0215 const QString dictName = Sonnet::Speller().availableDictionaries().key(m_currentDictionary); 0216 QAction *action = m_dictionaryGroup->addAction(dictName); 0217 action->setData(m_currentDictionary); 0218 action->setCheckable(true); 0219 action->setChecked(true); 0220 m_spellingMenu->addAction(action); 0221 } 0222 0223 m_spellingMenu->addSeparator(); 0224 int counter = 10; 0225 for (QStringList::const_iterator i = m_currentSuggestions.cbegin(); i != m_currentSuggestions.cend() && counter > 0; ++i) { 0226 const QString &suggestion = *i; 0227 QAction *action = new QAction(suggestion, m_spellingMenu); 0228 connect(action, &QAction::triggered, this, [suggestion, this]() { 0229 replaceWordBySuggestion(suggestion); 0230 }); 0231 m_spellingMenu->addAction(action); 0232 --counter; 0233 } 0234 0235 } else if (m_selectedRange.isValid() && !m_selectedRange.isEmpty()) { 0236 for (auto action : m_dictionaryGroup->actions()) { 0237 action->setCheckable(false); 0238 m_spellingMenu->addAction(action); 0239 } 0240 } 0241 } 0242 0243 void KateSpellingMenu::replaceWordBySuggestion(const QString &suggestion) 0244 { 0245 if (!m_currentMisspelledRange) { 0246 return; 0247 } 0248 // Ensure we keep some special dictionary setting... 0249 const QString dictionary = m_view->doc()->dictionaryForMisspelledRange(*m_currentMisspelledRange); 0250 KTextEditor::Range newRange = m_currentMisspelledRange->toRange(); 0251 newRange.setEnd(KTextEditor::Cursor(newRange.start().line(), newRange.start().column() + suggestion.size())); 0252 0253 KTextEditor::DocumentPrivate *doc = m_view->doc(); 0254 KTextEditor::EditorPrivate::self()->spellCheckManager()->replaceCharactersEncodedIfNecessary(suggestion, doc, *m_currentMisspelledRange); 0255 0256 // ...on the replaced word 0257 m_view->doc()->setDictionary(dictionary, newRange); 0258 m_view->clearSelection(); // Ensure cursor move and next right click works properly if there was a selection 0259 } 0260 0261 void KateSpellingMenu::addCurrentWordToDictionary() 0262 { 0263 if (!m_currentMisspelledRange) { 0264 return; 0265 } 0266 const QString &misspelledWord = m_view->doc()->text(*m_currentMisspelledRange); 0267 const QString dictionary = m_view->doc()->dictionaryForMisspelledRange(*m_currentMisspelledRange); 0268 KTextEditor::EditorPrivate::self()->spellCheckManager()->addToDictionary(misspelledWord, dictionary); 0269 m_view->doc()->clearMisspellingForWord(misspelledWord); // WARNING: 'm_currentMisspelledRange' is deleted here! 0270 m_view->clearSelection(); 0271 } 0272 0273 void KateSpellingMenu::ignoreCurrentWord() 0274 { 0275 if (!m_currentMisspelledRange) { 0276 return; 0277 } 0278 const QString &misspelledWord = m_view->doc()->text(*m_currentMisspelledRange); 0279 const QString dictionary = m_view->doc()->dictionaryForMisspelledRange(*m_currentMisspelledRange); 0280 KTextEditor::EditorPrivate::self()->spellCheckManager()->ignoreWord(misspelledWord, dictionary); 0281 m_view->doc()->clearMisspellingForWord(misspelledWord); // WARNING: 'm_currentMisspelledRange' is deleted here! 0282 m_view->clearSelection(); 0283 } 0284 0285 #include "moc_spellingmenu.cpp"