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