File indexing completed on 2025-01-19 10:02:08
0001 /* 0002 This file is part of the KDE project 0003 SPDX-FileCopyrightText: 2001 S.R. Haque <srhaque@iee.org>. 0004 SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com> 0005 0006 SPDX-License-Identifier: LGPL-2.0-only 0007 */ 0008 0009 #include "kreplace.h" 0010 0011 #include "kfind_p.h" 0012 #include "kreplacedialog.h" 0013 0014 #if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 70) 0015 #include <QRegExp> 0016 #endif 0017 0018 #include <QDialogButtonBox> 0019 #include <QLabel> 0020 #include <QPushButton> 0021 #include <QRegularExpression> 0022 #include <QVBoxLayout> 0023 0024 #include <KLocalizedString> 0025 #include <KMessageBox> 0026 0027 //#define DEBUG_REPLACE 0028 #define INDEX_NOMATCH -1 0029 0030 class KReplaceNextDialog : public QDialog 0031 { 0032 Q_OBJECT 0033 public: 0034 explicit KReplaceNextDialog(QWidget *parent); 0035 void setLabel(const QString &pattern, const QString &replacement); 0036 0037 QPushButton *replaceAllButton() const; 0038 QPushButton *skipButton() const; 0039 QPushButton *replaceButton() const; 0040 0041 private: 0042 QLabel *m_mainLabel = nullptr; 0043 QPushButton *m_allButton = nullptr; 0044 QPushButton *m_skipButton = nullptr; 0045 QPushButton *m_replaceButton = nullptr; 0046 }; 0047 0048 KReplaceNextDialog::KReplaceNextDialog(QWidget *parent) 0049 : QDialog(parent) 0050 { 0051 setModal(false); 0052 setWindowTitle(i18n("Replace")); 0053 0054 QVBoxLayout *layout = new QVBoxLayout(this); 0055 0056 m_mainLabel = new QLabel(this); 0057 layout->addWidget(m_mainLabel); 0058 0059 m_allButton = new QPushButton(i18nc("@action:button Replace all occurrences", "&All")); 0060 m_allButton->setObjectName(QStringLiteral("allButton")); 0061 m_skipButton = new QPushButton(i18n("&Skip")); 0062 m_skipButton->setObjectName(QStringLiteral("skipButton")); 0063 m_replaceButton = new QPushButton(i18n("Replace")); 0064 m_replaceButton->setObjectName(QStringLiteral("replaceButton")); 0065 m_replaceButton->setDefault(true); 0066 0067 QDialogButtonBox *buttonBox = new QDialogButtonBox(this); 0068 buttonBox->addButton(m_allButton, QDialogButtonBox::ActionRole); 0069 buttonBox->addButton(m_skipButton, QDialogButtonBox::ActionRole); 0070 buttonBox->addButton(m_replaceButton, QDialogButtonBox::ActionRole); 0071 buttonBox->setStandardButtons(QDialogButtonBox::Close); 0072 layout->addWidget(buttonBox); 0073 0074 connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); 0075 connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); 0076 } 0077 0078 void KReplaceNextDialog::setLabel(const QString &pattern, const QString &replacement) 0079 { 0080 m_mainLabel->setText(i18n("Replace '%1' with '%2'?", pattern, replacement)); 0081 } 0082 0083 QPushButton *KReplaceNextDialog::replaceAllButton() const 0084 { 0085 return m_allButton; 0086 } 0087 0088 QPushButton *KReplaceNextDialog::skipButton() const 0089 { 0090 return m_skipButton; 0091 } 0092 0093 QPushButton *KReplaceNextDialog::replaceButton() const 0094 { 0095 return m_replaceButton; 0096 } 0097 0098 //// 0099 0100 class KReplacePrivate : public KFindPrivate 0101 { 0102 Q_DECLARE_PUBLIC(KReplace) 0103 0104 public: 0105 KReplacePrivate(KReplace *q, const QString &replacement) 0106 : KFindPrivate(q) 0107 , m_replacement(replacement) 0108 { 0109 } 0110 0111 KReplaceNextDialog *nextDialog(); 0112 void doReplace(); 0113 0114 void slotSkip(); 0115 void slotReplace(); 0116 void slotReplaceAll(); 0117 0118 QString m_replacement; 0119 int m_replacements = 0; 0120 QRegularExpressionMatch m_match; 0121 }; 0122 0123 //// 0124 0125 KReplace::KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent) 0126 : KFind(*new KReplacePrivate(this, replacement), pattern, options, parent) 0127 { 0128 } 0129 0130 KReplace::KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent, QWidget *dlg) 0131 : KFind(*new KReplacePrivate(this, replacement), pattern, options, parent, dlg) 0132 { 0133 } 0134 0135 KReplace::~KReplace() = default; 0136 0137 int KReplace::numReplacements() const 0138 { 0139 Q_D(const KReplace); 0140 0141 return d->m_replacements; 0142 } 0143 0144 QDialog *KReplace::replaceNextDialog(bool create) 0145 { 0146 Q_D(KReplace); 0147 0148 if (d->dialog || create) { 0149 return d->nextDialog(); 0150 } 0151 return nullptr; 0152 } 0153 0154 KReplaceNextDialog *KReplacePrivate::nextDialog() 0155 { 0156 Q_Q(KReplace); 0157 0158 if (!dialog) { 0159 auto *nextDialog = new KReplaceNextDialog(q->parentWidget()); 0160 q->connect(nextDialog->replaceAllButton(), &QPushButton::clicked, q, [this]() { 0161 slotReplaceAll(); 0162 }); 0163 q->connect(nextDialog->skipButton(), &QPushButton::clicked, q, [this]() { 0164 slotSkip(); 0165 }); 0166 q->connect(nextDialog->replaceButton(), &QPushButton::clicked, q, [this]() { 0167 slotReplace(); 0168 }); 0169 q->connect(nextDialog, &QDialog::finished, q, [this]() { 0170 slotDialogClosed(); 0171 }); 0172 dialog = nextDialog; 0173 } 0174 return static_cast<KReplaceNextDialog *>(dialog); 0175 } 0176 0177 void KReplace::displayFinalDialog() const 0178 { 0179 Q_D(const KReplace); 0180 0181 if (!d->m_replacements) { 0182 KMessageBox::information(parentWidget(), i18n("No text was replaced.")); 0183 } else { 0184 KMessageBox::information(parentWidget(), i18np("1 replacement done.", "%1 replacements done.", d->m_replacements)); 0185 } 0186 } 0187 0188 #if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 70) 0189 static int replaceHelper(QString &text, const QString &replacement, int index, long options, int length, const QRegExp *regExp) 0190 { 0191 QString rep(replacement); 0192 if (options & KReplaceDialog::BackReference) { 0193 // Backreferences: replace \0 with the right portion of 'text' 0194 rep.replace(QLatin1String("\\0"), text.mid(index, length)); 0195 0196 // Other backrefs 0197 if (regExp) { 0198 const QStringList caps = regExp->capturedTexts(); 0199 for (int i = 0; i < caps.count(); ++i) { 0200 rep.replace(QLatin1String("\\") + QString::number(i), caps.at(i)); 0201 } 0202 } 0203 } 0204 0205 // Then replace rep into the text 0206 text.replace(index, length, rep); 0207 return rep.length(); 0208 } 0209 #endif 0210 0211 static int replaceHelper(QString &text, const QString &replacement, int index, long options, const QRegularExpressionMatch *match, int length) 0212 { 0213 QString rep(replacement); 0214 if (options & KReplaceDialog::BackReference) { 0215 // Handle backreferences 0216 if (options & KFind::RegularExpression) { // regex search 0217 Q_ASSERT(match); 0218 const int capNum = match->regularExpression().captureCount(); 0219 for (int i = 0; i <= capNum; ++i) { 0220 rep.replace(QLatin1String("\\") + QString::number(i), match->captured(i)); 0221 } 0222 } else { // with non-regex search only \0 is supported, replace it with the 0223 // right portion of 'text' 0224 rep.replace(QLatin1String("\\0"), text.mid(index, length)); 0225 } 0226 } 0227 0228 // Then replace rep into the text 0229 text.replace(index, length, rep); 0230 return rep.length(); 0231 } 0232 0233 KFind::Result KReplace::replace() 0234 { 0235 Q_D(KReplace); 0236 0237 #ifdef DEBUG_REPLACE 0238 // qDebug() << "d->index=" << d->index; 0239 #endif 0240 if (d->index == INDEX_NOMATCH && d->lastResult == Match) { 0241 d->lastResult = NoMatch; 0242 return NoMatch; 0243 } 0244 0245 do { // this loop is only because validateMatch can fail 0246 #ifdef DEBUG_REPLACE 0247 // qDebug() << "beginning of loop: d->index=" << d->index; 0248 #endif 0249 // Find the next match. 0250 d->index = KFind::find(d->text, d->pattern, d->index, d->options, &d->matchedLength, d->options & KFind::RegularExpression ? &d->m_match : nullptr); 0251 0252 #ifdef DEBUG_REPLACE 0253 // qDebug() << "KFind::find returned d->index=" << d->index; 0254 #endif 0255 if (d->index != -1) { 0256 // Flexibility: the app can add more rules to validate a possible match 0257 if (validateMatch(d->text, d->index, d->matchedLength)) { 0258 if (d->options & KReplaceDialog::PromptOnReplace) { 0259 #ifdef DEBUG_REPLACE 0260 // qDebug() << "PromptOnReplace"; 0261 #endif 0262 // Display accurate initial string and replacement string, they can vary 0263 QString matchedText(d->text.mid(d->index, d->matchedLength)); 0264 QString rep(matchedText); 0265 replaceHelper(rep, d->m_replacement, 0, d->options, d->options & KFind::RegularExpression ? &d->m_match : nullptr, d->matchedLength); 0266 d->nextDialog()->setLabel(matchedText, rep); 0267 d->nextDialog()->show(); // TODO kde5: virtual void showReplaceNextDialog(QString,QString), so that kreplacetest can skip the show() 0268 0269 // Tell the world about the match we found, in case someone wants to 0270 // highlight it. 0271 #if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 81) 0272 Q_EMIT highlight(d->text, d->index, d->matchedLength); 0273 #endif 0274 Q_EMIT textFound(d->text, d->index, d->matchedLength); 0275 0276 d->lastResult = Match; 0277 return Match; 0278 } else { 0279 d->doReplace(); // this moves on too 0280 } 0281 } else { 0282 // not validated -> move on 0283 if (d->options & KFind::FindBackwards) { 0284 d->index--; 0285 } else { 0286 d->index++; 0287 } 0288 } 0289 } else { 0290 d->index = INDEX_NOMATCH; // will exit the loop 0291 } 0292 } while (d->index != INDEX_NOMATCH); 0293 0294 d->lastResult = NoMatch; 0295 return NoMatch; 0296 } 0297 0298 int KReplace::replace(QString &text, const QString &pattern, const QString &replacement, int index, long options, int *replacedLength) 0299 { 0300 int matchedLength; 0301 QRegularExpressionMatch match; 0302 index = KFind::find(text, pattern, index, options, &matchedLength, options & KFind::RegularExpression ? &match : nullptr); 0303 0304 if (index != -1) { 0305 *replacedLength = replaceHelper(text, replacement, index, options, options & KFind::RegularExpression ? &match : nullptr, matchedLength); 0306 if (options & KFind::FindBackwards) { 0307 index--; 0308 } else { 0309 index += *replacedLength; 0310 } 0311 } 0312 return index; 0313 } 0314 0315 #if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 70) 0316 int KReplace::replace(QString &text, const QRegExp &pattern, const QString &replacement, int index, long options, int *replacedLength) 0317 { 0318 int matchedLength; 0319 0320 index = KFind::find(text, pattern, index, options, &matchedLength); 0321 if (index != -1) { 0322 *replacedLength = replaceHelper(text, replacement, index, options, matchedLength, &pattern); 0323 if (options & KFind::FindBackwards) { 0324 index--; 0325 } else { 0326 index += *replacedLength; 0327 } 0328 } 0329 return index; 0330 } 0331 #endif 0332 0333 void KReplacePrivate::slotReplaceAll() 0334 { 0335 Q_Q(KReplace); 0336 0337 doReplace(); 0338 options &= ~KReplaceDialog::PromptOnReplace; 0339 Q_EMIT q->optionsChanged(); 0340 Q_EMIT q->findNext(); 0341 } 0342 0343 void KReplacePrivate::slotSkip() 0344 { 0345 Q_Q(KReplace); 0346 0347 if (options & KFind::FindBackwards) { 0348 index--; 0349 } else { 0350 index++; 0351 } 0352 if (dialogClosed) { 0353 dialog->deleteLater(); 0354 dialog = nullptr; // hide it again 0355 } else { 0356 Q_EMIT q->findNext(); 0357 } 0358 } 0359 0360 void KReplacePrivate::slotReplace() 0361 { 0362 Q_Q(KReplace); 0363 0364 doReplace(); 0365 if (dialogClosed) { 0366 dialog->deleteLater(); 0367 dialog = nullptr; // hide it again 0368 } else { 0369 Q_EMIT q->findNext(); 0370 } 0371 } 0372 0373 void KReplacePrivate::doReplace() 0374 { 0375 Q_Q(KReplace); 0376 0377 Q_ASSERT(index >= 0); 0378 const int replacedLength = replaceHelper(text, m_replacement, index, options, &m_match, matchedLength); 0379 0380 // Tell the world about the replacement we made, in case someone wants to 0381 // highlight it. 0382 #if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 83) 0383 Q_EMIT q->replace(text, index, replacedLength, matchedLength); 0384 #endif 0385 Q_EMIT q->textReplaced(text, index, replacedLength, matchedLength); 0386 0387 #ifdef DEBUG_REPLACE 0388 // qDebug() << "after replace() signal: d->index=" << d->index << " replacedLength=" << replacedLength; 0389 #endif 0390 m_replacements++; 0391 if (options & KFind::FindBackwards) { 0392 Q_ASSERT(index >= 0); 0393 index--; 0394 } else { 0395 index += replacedLength; 0396 // when replacing the empty pattern, move on. See also kjs/regexp.cpp for how this should be done for regexps. 0397 if (pattern.isEmpty()) { 0398 ++index; 0399 } 0400 } 0401 #ifdef DEBUG_REPLACE 0402 // qDebug() << "after adjustment: d->index=" << d->index; 0403 #endif 0404 } 0405 0406 void KReplace::resetCounts() 0407 { 0408 Q_D(KReplace); 0409 0410 KFind::resetCounts(); 0411 d->m_replacements = 0; 0412 } 0413 0414 bool KReplace::shouldRestart(bool forceAsking, bool showNumMatches) const 0415 { 0416 Q_D(const KReplace); 0417 0418 // Only ask if we did a "find from cursor", otherwise it's pointless. 0419 // ... Or if the prompt-on-replace option was set. 0420 // Well, unless the user can modify the document during a search operation, 0421 // hence the force boolean. 0422 if (!forceAsking && (d->options & KFind::FromCursor) == 0 && (d->options & KReplaceDialog::PromptOnReplace) == 0) { 0423 displayFinalDialog(); 0424 return false; 0425 } 0426 QString message; 0427 if (showNumMatches) { 0428 if (!d->m_replacements) { 0429 message = i18n("No text was replaced."); 0430 } else { 0431 message = i18np("1 replacement done.", "%1 replacements done.", d->m_replacements); 0432 } 0433 } else { 0434 if (d->options & KFind::FindBackwards) { 0435 message = i18n("Beginning of document reached."); 0436 } else { 0437 message = i18n("End of document reached."); 0438 } 0439 } 0440 0441 message += QLatin1Char('\n'); 0442 // Hope this word puzzle is ok, it's a different sentence 0443 message += 0444 (d->options & KFind::FindBackwards) ? i18n("Do you want to restart search from the end?") : i18n("Do you want to restart search at the beginning?"); 0445 0446 int ret = KMessageBox::questionTwoActions(parentWidget(), 0447 message, 0448 QString(), 0449 KGuiItem(i18nc("@action:button Restart find & replace", "Restart")), 0450 KGuiItem(i18nc("@action:button Stop find & replace", "Stop"))); 0451 return (ret == KMessageBox::PrimaryAction); 0452 } 0453 0454 void KReplace::closeReplaceNextDialog() 0455 { 0456 closeFindNextDialog(); 0457 } 0458 0459 #include "kreplace.moc" 0460 #include "moc_kreplace.cpp"