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 &, 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