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"