File indexing completed on 2023-05-30 12:24:17

0001 /*
0002   This file is part of Lokalize
0003 
0004   SPDX-FileCopyrightText: 2007-2009 Nick Shaforostoff <shafff@ukr.net>
0005   SPDX-FileCopyrightText: 2018-2019 Simon Depiets <sdepiets@gmail.com>
0006 
0007   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0008 */
0009 
0010 #include "lokalize_debug.h"
0011 
0012 #include "editortab.h"
0013 #include "editorview.h"
0014 #include "catalog.h"
0015 #include "pos.h"
0016 #include "cmd.h"
0017 #include "project.h"
0018 #include "prefs_lokalize.h"
0019 #include "ui_kaider_findextension.h"
0020 #include "stemming.h"
0021 
0022 
0023 #include <klocalizedstring.h>
0024 #include <kmessagebox.h>
0025 #include <kreplacedialog.h>
0026 #include <kreplace.h>
0027 
0028 #include <sonnet/backgroundchecker.h>
0029 #include <sonnet/dialog.h>
0030 
0031 #include <QTimer>
0032 #include <QPointer>
0033 #include <QElapsedTimer>
0034 
0035 
0036 #define IGNOREACCELS KFind::MinimumUserOption
0037 #define INCLUDENOTES KFind::MinimumUserOption*2
0038 
0039 static long makeOptions(long options, const Ui_findExtension* ui_findExtension)
0040 {
0041     return options
0042            + IGNOREACCELS * ui_findExtension->m_ignoreAccelMarks->isChecked()
0043            + INCLUDENOTES * ui_findExtension->m_notes->isChecked();
0044     //bool skipMarkup(){return ui_findExtension->m_skipTags->isChecked();}
0045 }
0046 
0047 class EntryFindDialog: public KFindDialog
0048 {
0049 public:
0050     EntryFindDialog(QWidget* parent);
0051     ~EntryFindDialog();
0052     long options() const
0053     {
0054         return makeOptions(KFindDialog::options(), ui_findExtension);
0055     }
0056     static EntryFindDialog* instance(QWidget* parent = nullptr);
0057 private:
0058     static QPointer<EntryFindDialog> _instance;
0059     static void cleanup()
0060     {
0061         delete EntryFindDialog::_instance;
0062     }
0063 private:
0064     Ui_findExtension* ui_findExtension;
0065 };
0066 
0067 QPointer<EntryFindDialog> EntryFindDialog::_instance = nullptr;
0068 EntryFindDialog* EntryFindDialog::instance(QWidget* parent)
0069 {
0070     if (_instance == nullptr) {
0071         _instance = new EntryFindDialog(parent);
0072         qAddPostRoutine(EntryFindDialog::cleanup);
0073     }
0074     return _instance;
0075 }
0076 
0077 EntryFindDialog::EntryFindDialog(QWidget* parent)
0078     : KFindDialog(parent)
0079     , ui_findExtension(new Ui_findExtension)
0080 {
0081     ui_findExtension->setupUi(findExtension());
0082     setHasSelection(false);
0083 
0084     KConfig config;
0085     KConfigGroup stateGroup(&config, "FindReplace");
0086     setOptions(stateGroup.readEntry("FindOptions", (qlonglong)0));
0087     setFindHistory(stateGroup.readEntry("FindHistory", QStringList()));
0088 }
0089 
0090 EntryFindDialog::~EntryFindDialog()
0091 {
0092     KConfig config;
0093     KConfigGroup stateGroup(&config, "FindReplace");
0094     stateGroup.writeEntry("FindOptions", (qlonglong)options());
0095     stateGroup.writeEntry("FindHistory", findHistory());
0096 
0097     delete ui_findExtension;
0098 }
0099 
0100 //BEGIN EntryReplaceDialog
0101 class EntryReplaceDialog: public KReplaceDialog
0102 {
0103 public:
0104     EntryReplaceDialog(QWidget* parent);
0105     ~EntryReplaceDialog();
0106     long options() const
0107     {
0108         return makeOptions(KReplaceDialog::options(), ui_findExtension);
0109     }
0110     static EntryReplaceDialog* instance(QWidget* parent = nullptr);
0111 private:
0112     static QPointer<EntryReplaceDialog> _instance;
0113     static void cleanup()
0114     {
0115         delete EntryReplaceDialog::_instance;
0116     }
0117 private:
0118     Ui_findExtension* ui_findExtension;
0119 };
0120 
0121 QPointer<EntryReplaceDialog> EntryReplaceDialog::_instance = nullptr;
0122 EntryReplaceDialog* EntryReplaceDialog::instance(QWidget* parent)
0123 {
0124     if (_instance == nullptr) {
0125         _instance = new EntryReplaceDialog(parent);
0126         qAddPostRoutine(EntryReplaceDialog::cleanup);
0127     }
0128     return _instance;
0129 }
0130 
0131 EntryReplaceDialog::EntryReplaceDialog(QWidget* parent)
0132     : KReplaceDialog(parent)
0133     , ui_findExtension(new Ui_findExtension)
0134 {
0135     ui_findExtension->setupUi(findExtension());
0136     //ui_findExtension->m_notes->hide();
0137     setHasSelection(false);
0138 
0139     KConfig config;
0140     KConfigGroup stateGroup(&config, "FindReplace");
0141     setOptions(stateGroup.readEntry("ReplaceOptions", (qlonglong)0));
0142     setFindHistory(stateGroup.readEntry("ReplacePatternHistory", QStringList()));
0143     setReplacementHistory(stateGroup.readEntry("ReplacementHistory", QStringList()));
0144 }
0145 
0146 EntryReplaceDialog::~EntryReplaceDialog()
0147 {
0148     KConfig config;
0149     KConfigGroup stateGroup(&config, "FindReplace");
0150     stateGroup.writeEntry("ReplaceOptions", (qlonglong)options());
0151     stateGroup.writeEntry("ReplacePatternHistory", findHistory());
0152     stateGroup.writeEntry("ReplacementHistory", replacementHistory());
0153 
0154     delete ui_findExtension;
0155 }
0156 //END EntryReplaceDialog
0157 
0158 //TODO &amp;, &nbsp;
0159 static void calcOffsetWithAccels(const QString& data, int& offset, int& length)
0160 {
0161     int i = 0;
0162     for (; i < offset; ++i)
0163         if (Q_UNLIKELY(data.at(i) == '&'))
0164             ++offset;
0165 
0166     //if & is inside highlighted word
0167     int limit = offset + length;
0168     for (i = offset; i < limit; ++i)
0169         if (Q_UNLIKELY(data.at(i) == '&')) {
0170             ++length;
0171             limit = qMin(data.size(), offset + length); //just safety
0172         }
0173 }
0174 static bool determineStartingPos(Catalog* m_catalog, KFind* find, DocPosition& pos) //search or replace
0175 //called from find() and findNext()
0176 {
0177     if (find->options() & KFind::FindBackwards) {
0178         pos.entry = m_catalog->numberOfEntries() - 1;
0179         pos.form = (m_catalog->isPlural(pos.entry)) ?
0180                    m_catalog->numberOfPluralForms() - 1 : 0;
0181     } else {
0182         pos.entry = 0;
0183         pos.form = 0;
0184     }
0185     return true;
0186 }
0187 
0188 void EditorTab::find()
0189 {
0190     //QWidget* p=0; QWidget* next=qobject_cast<QWidget*>(parent()); while(next) { p=next; next=qobject_cast<QWidget*>(next->parent()); }
0191     EntryFindDialog::instance(nativeParentWidget());
0192 
0193     QString sel = selectionInTarget();
0194     if (!(sel.isEmpty() && selectionInSource().isEmpty())) {
0195         if (sel.isEmpty())
0196             sel = selectionInSource();
0197         if (m_find && m_find->options()&IGNOREACCELS)
0198             sel.remove('&');
0199         EntryFindDialog::instance()->setPattern(sel);
0200     }
0201 
0202     if (EntryFindDialog::instance()->exec() != QDialog::Accepted)
0203         return;
0204 
0205     if (m_find) {
0206         m_find->resetCounts();
0207         m_find->setPattern(EntryFindDialog::instance()->pattern());
0208         m_find->setOptions(EntryFindDialog::instance()->options());
0209 
0210     } else { // This creates a find-next-prompt dialog if needed.
0211         m_find = new KFind(EntryFindDialog::instance()->pattern(), EntryFindDialog::instance()->options(), this, EntryFindDialog::instance());
0212         connect(m_find, QOverload<const QString &, int, int>::of(&KFind::highlight), this, &EditorTab::highlightFound);
0213         connect(m_find, &KFind::findNext, this, QOverload<>::of(&EditorTab::findNext));
0214         m_find->closeFindNextDialog();
0215     }
0216 
0217     DocPosition pos;
0218     if (m_find->options() & KFind::FromCursor)
0219         pos = m_currentPos;
0220     else if (!determineStartingPos(m_catalog, m_find, pos))
0221         return;
0222 
0223 
0224     findNext(pos);
0225 }
0226 
0227 void EditorTab::findNext(const DocPosition& startingPos)
0228 {
0229     Catalog& catalog = *m_catalog;
0230     KFind& find = *m_find;
0231 
0232     if (Q_UNLIKELY(catalog.numberOfEntries() <= startingPos.entry))
0233         return;//for the case when app wasn't able to process event before file close
0234 
0235     bool anotherEntry = _searchingPos.entry != m_currentPos.entry;
0236     _searchingPos = startingPos;
0237 
0238     if (anotherEntry)
0239         _searchingPos.offset = 0;
0240 
0241 
0242     QRegExp rx("[^(\\\\n)>]\n");
0243     //_searchingPos.part=DocPosition::Source;
0244     bool ignoreaccels = m_find->options()&IGNOREACCELS;
0245     bool includenotes = m_find->options()&INCLUDENOTES;
0246     int switchOptions = DocPosition::Source | DocPosition::Target | (includenotes * DocPosition::Comment);
0247     int flag = 1;
0248     while (flag) {
0249 
0250         flag = 0;
0251         KFind::Result res = KFind::NoMatch;
0252         while (true) {
0253             if (find.needData() || anotherEntry || m_view->m_modifiedAfterFind) {
0254                 anotherEntry = false;
0255                 m_view->m_modifiedAfterFind = false;
0256 
0257                 QString data;
0258                 if (_searchingPos.part == DocPosition::Comment)
0259                     data = catalog.notes(_searchingPos).at(_searchingPos.form).content;
0260                 else
0261                     data = catalog.catalogString(_searchingPos).string;
0262 
0263                 if (ignoreaccels)
0264                     data.remove('&');
0265                 find.setData(data);
0266             }
0267 
0268             res = find.find();
0269             //offset=-1;
0270             if (res != KFind::NoMatch)
0271                 break;
0272 
0273             if (!(
0274                     (find.options()&KFind::FindBackwards) ?
0275                     switchPrev(m_catalog, _searchingPos, switchOptions) :
0276                     switchNext(m_catalog, _searchingPos, switchOptions)
0277                 ))
0278                 break;
0279         }
0280 
0281         if (res == KFind::NoMatch) {
0282             //file-wide search
0283             if (find.shouldRestart(true, true)) {
0284                 flag = 1;
0285                 determineStartingPos(m_catalog, m_find, _searchingPos);
0286             }
0287             find.resetCounts();
0288         }
0289     }
0290 }
0291 
0292 void EditorTab::findNext()
0293 {
0294     if (m_find) {
0295         findNext((m_currentPos.entry == _searchingPos.entry && _searchingPos.part == DocPosition::Comment) ?
0296                  _searchingPos : m_currentPos);
0297     } else
0298         find();
0299 
0300 }
0301 
0302 void EditorTab::findPrev()
0303 {
0304 
0305     if (m_find) {
0306         m_find->setOptions(m_find->options() ^ KFind::FindBackwards);
0307         findNext(m_currentPos);
0308     } else {
0309         find();
0310     }
0311 
0312 }
0313 
0314 void EditorTab::highlightFound(const QString &, int matchingIndex, int matchedLength)
0315 {
0316     if (m_find->options()&IGNOREACCELS && _searchingPos.part != DocPosition::Comment) {
0317         QString data = m_catalog->catalogString(_searchingPos).string;
0318         calcOffsetWithAccels(data, matchingIndex, matchedLength);
0319     }
0320 
0321     _searchingPos.offset = matchingIndex;
0322     gotoEntry(_searchingPos, matchedLength);
0323 }
0324 
0325 void EditorTab::replace()
0326 {
0327     EntryReplaceDialog::instance(nativeParentWidget());
0328 
0329     if (!m_view->selectionInTarget().isEmpty()) {
0330         if (m_replace && m_replace->options()&IGNOREACCELS) {
0331             QString tmp(m_view->selectionInTarget());
0332             tmp.remove('&');
0333             EntryReplaceDialog::instance()->setPattern(tmp);
0334         } else
0335             EntryReplaceDialog::instance()->setPattern(m_view->selectionInTarget());
0336     }
0337 
0338 
0339     if (EntryReplaceDialog::instance()->exec() != QDialog::Accepted)
0340         return;
0341 
0342 
0343     if (m_replace) m_replace->deleteLater();// _replace=0;
0344 
0345     // This creates a find-next-prompt dialog if needed.
0346     {
0347         m_replace = new KReplace(EntryReplaceDialog::instance()->pattern(), EntryReplaceDialog::instance()->replacement(), EntryReplaceDialog::instance()->options(), this, EntryReplaceDialog::instance());
0348         connect(m_replace, QOverload<const QString &, int, int>::of(&KReplace::highlight), this, &EditorTab::highlightFound_);
0349         connect(m_replace, &KReplace::findNext, this, QOverload<>::of(&EditorTab::replaceNext));
0350         connect(m_replace, QOverload<const QString &, int, int, int>::of(&KReplace::replace), this, &EditorTab::doReplace);
0351         connect(m_replace, &KReplace::dialogClosed, this, &EditorTab::cleanupReplace);
0352 //         _replace->closeReplaceNextDialog();
0353     }
0354 //     else
0355 //     {
0356 //         _replace->resetCounts();
0357 //         _replace->setPattern(EntryReplaceDialog::instance()->pattern());
0358 //         _replace->setOptions(EntryReplaceDialog::instance()->options());
0359 //     }
0360 
0361     //m_catalog->beginMacro(i18nc("@item Undo action item","Replace"));
0362     m_doReplaceCalled = false;
0363 
0364     if (m_replace->options() & KFind::FromCursor)
0365         replaceNext(m_currentPos);
0366     else {
0367         DocPosition pos;
0368         if (!determineStartingPos(m_catalog, m_replace, pos)) return;
0369         replaceNext(pos);
0370     }
0371 
0372 }
0373 
0374 
0375 void EditorTab::replaceNext(const DocPosition& startingPos)
0376 {
0377     bool anotherEntry = m_currentPos.entry != _replacingPos.entry;
0378     _replacingPos = startingPos;
0379 
0380     if (anotherEntry)
0381         _replacingPos.offset = 0;
0382 
0383 
0384     int flag = 1;
0385     bool ignoreaccels = m_replace->options()&IGNOREACCELS;
0386     bool includenotes = m_replace->options()&INCLUDENOTES;
0387     qCWarning(LOKALIZE_LOG) << "includenotes" << includenotes;
0388     int switchOptions = DocPosition::Target | (includenotes * DocPosition::Comment);
0389     while (flag) {
0390         flag = 0;
0391         KFind::Result res = KFind::NoMatch;
0392         while (1) {
0393             if (m_replace->needData() || anotherEntry/*||m_view->m_modifiedAfterFind*/) {
0394                 anotherEntry = false;
0395                 //m_view->m_modifiedAfterFind=false;//NOTE TEST THIS
0396 
0397                 QString data;
0398                 if (_replacingPos.part == DocPosition::Comment)
0399                     data = m_catalog->notes(_replacingPos).at(_replacingPos.form).content;
0400                 else {
0401                     data = m_catalog->targetWithTags(_replacingPos).string;
0402                     if (ignoreaccels) data.remove('&');
0403                 }
0404                 m_replace->setData(data);
0405             }
0406             res = m_replace->replace();
0407             if (res != KFind::NoMatch)
0408                 break;
0409 
0410             if (!(
0411                     (m_replace->options()&KFind::FindBackwards) ?
0412                     switchPrev(m_catalog, _replacingPos, switchOptions) :
0413                     switchNext(m_catalog, _replacingPos, switchOptions)
0414                 ))
0415                 break;
0416         }
0417 
0418         if (res == KFind::NoMatch) {
0419             if ((m_replace->options()&KFind::FromCursor)
0420                 && m_replace->shouldRestart(true)) {
0421                 flag = 1;
0422                 determineStartingPos(m_catalog, m_replace, _replacingPos);
0423             } else {
0424                 if (!(m_replace->options() & KFind::FromCursor))
0425                     m_replace->displayFinalDialog();
0426 
0427                 m_replace->closeReplaceNextDialog();
0428                 cleanupReplace();
0429             }
0430             m_replace->resetCounts();
0431         }
0432     }
0433 }
0434 
0435 void EditorTab::cleanupReplace()
0436 {
0437     if (m_doReplaceCalled) {
0438         m_doReplaceCalled = false;
0439         m_catalog->endMacro();
0440     }
0441 }
0442 
0443 void EditorTab::replaceNext()
0444 {
0445     replaceNext(m_currentPos);
0446 }
0447 
0448 void EditorTab::highlightFound_(const QString &, int matchingIndex, int matchedLength)
0449 {
0450     if (m_replace->options()&IGNOREACCELS) {
0451         QString data = m_catalog->targetWithTags(_replacingPos).string;
0452         calcOffsetWithAccels(data, matchingIndex, matchedLength);
0453     }
0454 
0455     _replacingPos.offset = matchingIndex;
0456     gotoEntry(_replacingPos, matchedLength);
0457 }
0458 
0459 
0460 void EditorTab::doReplace(const QString &newStr, int offset, int newLen, int remLen)
0461 {
0462     if (!m_doReplaceCalled) {
0463         m_doReplaceCalled = true;
0464         m_catalog->beginMacro(i18nc("@item Undo action item", "Replace"));
0465     }
0466     DocPosition pos = _replacingPos;
0467     if (_replacingPos.part == DocPosition::Comment)
0468         m_catalog->push(new SetNoteCmd(m_catalog, pos, newStr));
0469     else {
0470         QString oldStr = m_catalog->target(_replacingPos);
0471 
0472         if (m_replace->options()&IGNOREACCELS)
0473             calcOffsetWithAccels(oldStr, offset, remLen);
0474 
0475         pos.offset = offset;
0476         m_catalog->push(new DelTextCmd(m_catalog, pos, oldStr.mid(offset, remLen)));
0477 
0478         if (newLen)
0479             m_catalog->push(new InsTextCmd(m_catalog, pos, newStr.mid(offset, newLen)));
0480     }
0481     if (pos.entry == m_currentPos.entry) {
0482         pos.offset += newLen;
0483         m_view->gotoEntry(pos, 0);
0484     }
0485 }
0486 
0487 void EditorTab::spellcheck()
0488 {
0489     if (!m_sonnetDialog) {
0490         m_sonnetChecker = new Sonnet::BackgroundChecker(this);
0491         m_sonnetChecker->changeLanguage(enhanceLangCode(Project::instance()->langCode()));
0492         m_sonnetChecker->setAutoDetectLanguageDisabled(true);
0493         m_sonnetDialog = new Sonnet::Dialog(m_sonnetChecker, this);
0494         connect(m_sonnetDialog, &Sonnet::Dialog::spellCheckDone, this, &EditorTab::spellcheckNext);
0495         connect(m_sonnetDialog, &Sonnet::Dialog::replace, this, &EditorTab::spellcheckReplace);
0496         connect(m_sonnetDialog, &Sonnet::Dialog::stop, this, &EditorTab::spellcheckStop);
0497         connect(m_sonnetDialog, &Sonnet::Dialog::cancel, this, &EditorTab::spellcheckCancel);
0498 
0499         connect(m_sonnetDialog/*m_sonnetChecker*/, &Sonnet::Dialog::misspelling, this, &EditorTab::spellcheckShow);
0500 //         disconnect(/*m_sonnetDialog*/m_sonnetChecker,SIGNAL(misspelling(QString,int)),
0501 //             m_sonnetDialog,SLOT(slotMisspelling(QString,int)));
0502 //
0503 //     connect( d->checker, SIGNAL(misspelling(const QString&, int)),
0504 //              SLOT(slotMisspelling(const QString&, int)) );
0505     }
0506 
0507     QString text = m_catalog->msgstr(m_currentPos);
0508     if (!m_view->selectionInTarget().isEmpty())
0509         text = m_view->selectionInTarget();
0510     text.remove('&');
0511     m_sonnetDialog->setBuffer(text);
0512 
0513     _spellcheckPos = m_currentPos;
0514     _spellcheckStartPos = m_currentPos;
0515     m_spellcheckStop = false;
0516     //m_catalog->beginMacro(i18n("Spellcheck"));
0517     m_spellcheckStartUndoIndex = m_catalog->index();
0518     m_sonnetDialog->show();
0519 
0520 }
0521 
0522 
0523 void EditorTab::spellcheckNext()
0524 {
0525     if (m_spellcheckStop)
0526         return;
0527 
0528     do {
0529         if (!switchNext(m_catalog, _spellcheckPos)) {
0530             qCDebug(LOKALIZE_LOG) << _spellcheckStartPos.entry;
0531             qCDebug(LOKALIZE_LOG) << _spellcheckStartPos.form;
0532             bool continueFromStart =
0533                 !(_spellcheckStartPos.entry == 0 && _spellcheckStartPos.form == 0)
0534                 && KMessageBox::questionYesNo(this, i18n("Lokalize has reached end of document. Do you want to continue from start?"),
0535                                               i18nc("@title", "Spellcheck")) == KMessageBox::Yes;
0536             if (continueFromStart) {
0537                 _spellcheckStartPos.entry = 0;
0538                 _spellcheckStartPos.form = 0;
0539                 _spellcheckPos = _spellcheckStartPos;
0540             } else {
0541                 KMessageBox::information(this, i18n("Lokalize has finished spellchecking"), i18nc("@title", "Spellcheck"));
0542                 return;
0543             }
0544         }
0545     } while (m_catalog->msgstr(_spellcheckPos).isEmpty() || !m_catalog->isApproved(_spellcheckPos.entry));
0546 
0547     m_sonnetDialog->setBuffer(m_catalog->msgstr(_spellcheckPos).remove(Project::instance()->accel()));
0548 }
0549 
0550 void EditorTab::spellcheckStop()
0551 {
0552     m_spellcheckStop = true;
0553 }
0554 
0555 void EditorTab::spellcheckCancel()
0556 {
0557     m_catalog->setIndex(m_spellcheckStartUndoIndex);
0558     gotoEntry(_spellcheckPos);
0559 }
0560 
0561 void EditorTab::spellcheckShow(const QString &word, int offset)
0562 {
0563     const Project& project = *Project::instance();
0564     const QString accel = project.accel();
0565 
0566     QString source = m_catalog->source(_spellcheckPos);
0567     source.remove(accel);
0568     if (source.contains(word) && project.targetLangCode().leftRef(2) != project.sourceLangCode().leftRef(2)) {
0569         m_sonnetDialog->setUpdatesEnabled(false);
0570         m_sonnetChecker->continueChecking();
0571         return;
0572     }
0573 
0574     m_sonnetDialog->setUpdatesEnabled(true);
0575 
0576     show();
0577 
0578     DocPosition pos = _spellcheckPos;
0579     int length = word.length();
0580     calcOffsetWithAccels(m_catalog->target(pos), offset, length);
0581     pos.offset = offset;
0582 
0583     gotoEntry(pos, length);
0584 }
0585 
0586 void EditorTab::spellcheckReplace(QString oldWord, int offset, const QString &newWord)
0587 {
0588     DocPosition pos = _spellcheckPos;
0589     int length = oldWord.length();
0590     calcOffsetWithAccels(m_catalog->target(pos), offset, length);
0591     pos.offset = offset;
0592     if (length > oldWord.length()) //replaced word contains accel mark
0593         oldWord = m_catalog->target(pos).mid(offset, length);
0594 
0595     m_catalog->push(new DelTextCmd(m_catalog, pos, oldWord));
0596     m_catalog->push(new InsTextCmd(m_catalog, pos, newWord));
0597 
0598 
0599     gotoEntry(pos, newWord.length());
0600 }
0601 
0602 
0603 
0604 
0605