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"