| 1 | /* |
| 2 | This file is part of the KDE project |
| 3 | SPDX-FileCopyrightText: 2001 S.R. Haque <srhaque@iee.org>. |
| 4 | SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com> |
| 5 | |
| 6 | SPDX-License-Identifier: LGPL-2.0-only |
| 7 | */ |
| 8 | |
| 9 | #include "kreplace.h" |
| 10 | |
| 11 | #include "kfind_p.h" |
| 12 | #include "kreplacedialog.h" |
| 13 | |
| 14 | #include <QDialogButtonBox> |
| 15 | #include <QLabel> |
| 16 | #include <QPushButton> |
| 17 | #include <QRegularExpression> |
| 18 | #include <QVBoxLayout> |
| 19 | |
| 20 | #include <KLocalizedString> |
| 21 | #include <KMessageBox> |
| 22 | |
| 23 | //#define DEBUG_REPLACE |
| 24 | #define INDEX_NOMATCH -1 |
| 25 | |
| 26 | class KReplaceNextDialog : public QDialog |
| 27 | { |
| 28 | Q_OBJECT |
| 29 | public: |
| 30 | explicit KReplaceNextDialog(QWidget *parent); |
| 31 | void setLabel(const QString &pattern, const QString &replacement); |
| 32 | |
| 33 | QPushButton *replaceAllButton() const; |
| 34 | QPushButton *skipButton() const; |
| 35 | QPushButton *replaceButton() const; |
| 36 | |
| 37 | private: |
| 38 | QLabel *m_mainLabel = nullptr; |
| 39 | QPushButton *m_allButton = nullptr; |
| 40 | QPushButton *m_skipButton = nullptr; |
| 41 | QPushButton *m_replaceButton = nullptr; |
| 42 | }; |
| 43 | |
| 44 | KReplaceNextDialog::KReplaceNextDialog(QWidget *parent) |
| 45 | : QDialog(parent) |
| 46 | { |
| 47 | setModal(false); |
| 48 | setWindowTitle(i18n("Replace" )); |
| 49 | |
| 50 | QVBoxLayout *layout = new QVBoxLayout(this); |
| 51 | |
| 52 | m_mainLabel = new QLabel(this); |
| 53 | layout->addWidget(m_mainLabel); |
| 54 | |
| 55 | m_allButton = new QPushButton(i18nc("@action:button Replace all occurrences" , "&All" )); |
| 56 | m_allButton->setObjectName(QStringLiteral("allButton" )); |
| 57 | m_skipButton = new QPushButton(i18n("&Skip" )); |
| 58 | m_skipButton->setObjectName(QStringLiteral("skipButton" )); |
| 59 | m_replaceButton = new QPushButton(i18n("Replace" )); |
| 60 | m_replaceButton->setObjectName(QStringLiteral("replaceButton" )); |
| 61 | m_replaceButton->setDefault(true); |
| 62 | |
| 63 | QDialogButtonBox *buttonBox = new QDialogButtonBox(this); |
| 64 | buttonBox->addButton(button: m_allButton, role: QDialogButtonBox::ActionRole); |
| 65 | buttonBox->addButton(button: m_skipButton, role: QDialogButtonBox::ActionRole); |
| 66 | buttonBox->addButton(button: m_replaceButton, role: QDialogButtonBox::ActionRole); |
| 67 | buttonBox->setStandardButtons(QDialogButtonBox::Close); |
| 68 | layout->addWidget(buttonBox); |
| 69 | |
| 70 | connect(sender: buttonBox, signal: &QDialogButtonBox::accepted, context: this, slot: &QDialog::accept); |
| 71 | connect(sender: buttonBox, signal: &QDialogButtonBox::rejected, context: this, slot: &QDialog::reject); |
| 72 | } |
| 73 | |
| 74 | void KReplaceNextDialog::setLabel(const QString &pattern, const QString &replacement) |
| 75 | { |
| 76 | m_mainLabel->setText(i18n("Replace '%1' with '%2'?" , pattern, replacement)); |
| 77 | } |
| 78 | |
| 79 | QPushButton *KReplaceNextDialog::replaceAllButton() const |
| 80 | { |
| 81 | return m_allButton; |
| 82 | } |
| 83 | |
| 84 | QPushButton *KReplaceNextDialog::skipButton() const |
| 85 | { |
| 86 | return m_skipButton; |
| 87 | } |
| 88 | |
| 89 | QPushButton *KReplaceNextDialog::replaceButton() const |
| 90 | { |
| 91 | return m_replaceButton; |
| 92 | } |
| 93 | |
| 94 | //// |
| 95 | |
| 96 | class KReplacePrivate : public KFindPrivate |
| 97 | { |
| 98 | Q_DECLARE_PUBLIC(KReplace) |
| 99 | |
| 100 | public: |
| 101 | KReplacePrivate(KReplace *q, const QString &replacement) |
| 102 | : KFindPrivate(q) |
| 103 | , m_replacement(replacement) |
| 104 | { |
| 105 | } |
| 106 | |
| 107 | KReplaceNextDialog *nextDialog(); |
| 108 | void doReplace(); |
| 109 | |
| 110 | void slotSkip(); |
| 111 | void slotReplace(); |
| 112 | void slotReplaceAll(); |
| 113 | |
| 114 | QString m_replacement; |
| 115 | int m_replacements = 0; |
| 116 | QRegularExpressionMatch m_match; |
| 117 | }; |
| 118 | |
| 119 | //// |
| 120 | |
| 121 | KReplace::KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent) |
| 122 | : KFind(*new KReplacePrivate(this, replacement), pattern, options, parent) |
| 123 | { |
| 124 | } |
| 125 | |
| 126 | KReplace::KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent, QWidget *dlg) |
| 127 | : KFind(*new KReplacePrivate(this, replacement), pattern, options, parent, dlg) |
| 128 | { |
| 129 | } |
| 130 | |
| 131 | KReplace::~KReplace() = default; |
| 132 | |
| 133 | int KReplace::numReplacements() const |
| 134 | { |
| 135 | Q_D(const KReplace); |
| 136 | |
| 137 | return d->m_replacements; |
| 138 | } |
| 139 | |
| 140 | QDialog *KReplace::replaceNextDialog(bool create) |
| 141 | { |
| 142 | Q_D(KReplace); |
| 143 | |
| 144 | if (d->dialog || create) { |
| 145 | return d->nextDialog(); |
| 146 | } |
| 147 | return nullptr; |
| 148 | } |
| 149 | |
| 150 | KReplaceNextDialog *KReplacePrivate::nextDialog() |
| 151 | { |
| 152 | Q_Q(KReplace); |
| 153 | |
| 154 | if (!dialog) { |
| 155 | auto *nextDialog = new KReplaceNextDialog(q->parentWidget()); |
| 156 | q->connect(sender: nextDialog->replaceAllButton(), signal: &QPushButton::clicked, context: q, slot: [this]() { |
| 157 | slotReplaceAll(); |
| 158 | }); |
| 159 | q->connect(sender: nextDialog->skipButton(), signal: &QPushButton::clicked, context: q, slot: [this]() { |
| 160 | slotSkip(); |
| 161 | }); |
| 162 | q->connect(sender: nextDialog->replaceButton(), signal: &QPushButton::clicked, context: q, slot: [this]() { |
| 163 | slotReplace(); |
| 164 | }); |
| 165 | q->connect(sender: nextDialog, signal: &QDialog::finished, context: q, slot: [this]() { |
| 166 | slotDialogClosed(); |
| 167 | }); |
| 168 | dialog = nextDialog; |
| 169 | } |
| 170 | return static_cast<KReplaceNextDialog *>(dialog); |
| 171 | } |
| 172 | |
| 173 | void KReplace::displayFinalDialog() const |
| 174 | { |
| 175 | Q_D(const KReplace); |
| 176 | |
| 177 | if (!d->m_replacements) { |
| 178 | KMessageBox::information(parent: parentWidget(), i18n("No text was replaced." )); |
| 179 | } else { |
| 180 | KMessageBox::information(parent: parentWidget(), i18np("1 replacement done." , "%1 replacements done." , d->m_replacements)); |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | static int replaceHelper(QString &text, const QString &replacement, int index, long options, const QRegularExpressionMatch *match, int length) |
| 185 | { |
| 186 | QString rep(replacement); |
| 187 | if (options & KReplaceDialog::BackReference) { |
| 188 | // Handle backreferences |
| 189 | if (options & KFind::RegularExpression) { // regex search |
| 190 | Q_ASSERT(match); |
| 191 | const int capNum = match->regularExpression().captureCount(); |
| 192 | for (int i = 0; i <= capNum; ++i) { |
| 193 | rep.replace(before: QLatin1String("\\" ) + QString::number(i), after: match->captured(nth: i)); |
| 194 | } |
| 195 | } else { // with non-regex search only \0 is supported, replace it with the |
| 196 | // right portion of 'text' |
| 197 | rep.replace(before: QLatin1String("\\0" ), after: text.mid(position: index, n: length)); |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | // Then replace rep into the text |
| 202 | text.replace(i: index, len: length, after: rep); |
| 203 | return rep.length(); |
| 204 | } |
| 205 | |
| 206 | KFind::Result KReplace::replace() |
| 207 | { |
| 208 | Q_D(KReplace); |
| 209 | |
| 210 | #ifdef DEBUG_REPLACE |
| 211 | // qDebug() << "d->index=" << d->index; |
| 212 | #endif |
| 213 | if (d->index == INDEX_NOMATCH && d->lastResult == Match) { |
| 214 | d->lastResult = NoMatch; |
| 215 | return NoMatch; |
| 216 | } |
| 217 | |
| 218 | do { // this loop is only because validateMatch can fail |
| 219 | #ifdef DEBUG_REPLACE |
| 220 | // qDebug() << "beginning of loop: d->index=" << d->index; |
| 221 | #endif |
| 222 | // Find the next match. |
| 223 | d->index = KFind::find(text: d->text, pattern: d->pattern, index: d->index, options: d->options, matchedLength: &d->matchedLength, rmatch: d->options & KFind::RegularExpression ? &d->m_match : nullptr); |
| 224 | |
| 225 | #ifdef DEBUG_REPLACE |
| 226 | // qDebug() << "KFind::find returned d->index=" << d->index; |
| 227 | #endif |
| 228 | if (d->index != -1) { |
| 229 | // Flexibility: the app can add more rules to validate a possible match |
| 230 | if (validateMatch(text: d->text, index: d->index, matchedlength: d->matchedLength)) { |
| 231 | if (d->options & KReplaceDialog::PromptOnReplace) { |
| 232 | #ifdef DEBUG_REPLACE |
| 233 | // qDebug() << "PromptOnReplace"; |
| 234 | #endif |
| 235 | // Display accurate initial string and replacement string, they can vary |
| 236 | QString matchedText(d->text.mid(position: d->index, n: d->matchedLength)); |
| 237 | QString rep(matchedText); |
| 238 | replaceHelper(text&: rep, replacement: d->m_replacement, index: 0, options: d->options, match: d->options & KFind::RegularExpression ? &d->m_match : nullptr, length: d->matchedLength); |
| 239 | d->nextDialog()->setLabel(pattern: matchedText, replacement: rep); |
| 240 | d->nextDialog()->show(); // TODO kde5: virtual void showReplaceNextDialog(QString,QString), so that kreplacetest can skip the show() |
| 241 | |
| 242 | // Tell the world about the match we found, in case someone wants to |
| 243 | // highlight it. |
| 244 | Q_EMIT textFound(text: d->text, matchingIndex: d->index, matchedLength: d->matchedLength); |
| 245 | |
| 246 | d->lastResult = Match; |
| 247 | return Match; |
| 248 | } else { |
| 249 | d->doReplace(); // this moves on too |
| 250 | } |
| 251 | } else { |
| 252 | // not validated -> move on |
| 253 | if (d->options & KFind::FindBackwards) { |
| 254 | d->index--; |
| 255 | } else { |
| 256 | d->index++; |
| 257 | } |
| 258 | } |
| 259 | } else { |
| 260 | d->index = INDEX_NOMATCH; // will exit the loop |
| 261 | } |
| 262 | } while (d->index != INDEX_NOMATCH); |
| 263 | |
| 264 | d->lastResult = NoMatch; |
| 265 | return NoMatch; |
| 266 | } |
| 267 | |
| 268 | int KReplace::replace(QString &text, const QString &pattern, const QString &replacement, int index, long options, int *replacedLength) |
| 269 | { |
| 270 | int matchedLength; |
| 271 | QRegularExpressionMatch match; |
| 272 | index = KFind::find(text, pattern, index, options, matchedLength: &matchedLength, rmatch: options & KFind::RegularExpression ? &match : nullptr); |
| 273 | |
| 274 | if (index != -1) { |
| 275 | *replacedLength = replaceHelper(text, replacement, index, options, match: options & KFind::RegularExpression ? &match : nullptr, length: matchedLength); |
| 276 | if (options & KFind::FindBackwards) { |
| 277 | index--; |
| 278 | } else { |
| 279 | index += *replacedLength; |
| 280 | } |
| 281 | } |
| 282 | return index; |
| 283 | } |
| 284 | |
| 285 | void KReplacePrivate::slotReplaceAll() |
| 286 | { |
| 287 | Q_Q(KReplace); |
| 288 | |
| 289 | doReplace(); |
| 290 | options &= ~KReplaceDialog::PromptOnReplace; |
| 291 | Q_EMIT q->optionsChanged(); |
| 292 | Q_EMIT q->findNext(); |
| 293 | } |
| 294 | |
| 295 | void KReplacePrivate::slotSkip() |
| 296 | { |
| 297 | Q_Q(KReplace); |
| 298 | |
| 299 | if (options & KFind::FindBackwards) { |
| 300 | index--; |
| 301 | } else { |
| 302 | index++; |
| 303 | } |
| 304 | if (dialogClosed) { |
| 305 | dialog->deleteLater(); |
| 306 | dialog = nullptr; // hide it again |
| 307 | } else { |
| 308 | Q_EMIT q->findNext(); |
| 309 | } |
| 310 | } |
| 311 | |
| 312 | void KReplacePrivate::slotReplace() |
| 313 | { |
| 314 | Q_Q(KReplace); |
| 315 | |
| 316 | doReplace(); |
| 317 | if (dialogClosed) { |
| 318 | dialog->deleteLater(); |
| 319 | dialog = nullptr; // hide it again |
| 320 | } else { |
| 321 | Q_EMIT q->findNext(); |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | void KReplacePrivate::doReplace() |
| 326 | { |
| 327 | Q_Q(KReplace); |
| 328 | |
| 329 | Q_ASSERT(index >= 0); |
| 330 | const int replacedLength = replaceHelper(text, replacement: m_replacement, index, options, match: &m_match, length: matchedLength); |
| 331 | |
| 332 | // Tell the world about the replacement we made, in case someone wants to |
| 333 | // highlight it. |
| 334 | Q_EMIT q->textReplaced(text, replacementIndex: index, replacedLength, matchedLength); |
| 335 | |
| 336 | #ifdef DEBUG_REPLACE |
| 337 | // qDebug() << "after replace() signal: d->index=" << d->index << " replacedLength=" << replacedLength; |
| 338 | #endif |
| 339 | m_replacements++; |
| 340 | if (options & KFind::FindBackwards) { |
| 341 | Q_ASSERT(index >= 0); |
| 342 | index--; |
| 343 | } else { |
| 344 | index += replacedLength; |
| 345 | // when replacing the empty pattern, move on. See also kjs/regexp.cpp for how this should be done for regexps. |
| 346 | if (pattern.isEmpty()) { |
| 347 | ++index; |
| 348 | } |
| 349 | } |
| 350 | #ifdef DEBUG_REPLACE |
| 351 | // qDebug() << "after adjustment: d->index=" << d->index; |
| 352 | #endif |
| 353 | } |
| 354 | |
| 355 | void KReplace::resetCounts() |
| 356 | { |
| 357 | Q_D(KReplace); |
| 358 | |
| 359 | KFind::resetCounts(); |
| 360 | d->m_replacements = 0; |
| 361 | } |
| 362 | |
| 363 | bool KReplace::shouldRestart(bool forceAsking, bool showNumMatches) const |
| 364 | { |
| 365 | Q_D(const KReplace); |
| 366 | |
| 367 | // Only ask if we did a "find from cursor", otherwise it's pointless. |
| 368 | // ... Or if the prompt-on-replace option was set. |
| 369 | // Well, unless the user can modify the document during a search operation, |
| 370 | // hence the force boolean. |
| 371 | if (!forceAsking && (d->options & KFind::FromCursor) == 0 && (d->options & KReplaceDialog::PromptOnReplace) == 0) { |
| 372 | displayFinalDialog(); |
| 373 | return false; |
| 374 | } |
| 375 | QString message; |
| 376 | if (showNumMatches) { |
| 377 | if (!d->m_replacements) { |
| 378 | message = i18n("No text was replaced." ); |
| 379 | } else { |
| 380 | message = i18np("1 replacement done." , "%1 replacements done." , d->m_replacements); |
| 381 | } |
| 382 | } else { |
| 383 | if (d->options & KFind::FindBackwards) { |
| 384 | message = i18n("Beginning of document reached." ); |
| 385 | } else { |
| 386 | message = i18n("End of document reached." ); |
| 387 | } |
| 388 | } |
| 389 | |
| 390 | message += QLatin1Char('\n'); |
| 391 | // Hope this word puzzle is ok, it's a different sentence |
| 392 | message += |
| 393 | (d->options & KFind::FindBackwards) ? i18n("Do you want to restart search from the end?" ) : i18n("Do you want to restart search at the beginning?" ); |
| 394 | |
| 395 | int ret = KMessageBox::questionTwoActions(parent: parentWidget(), |
| 396 | text: message, |
| 397 | title: QString(), |
| 398 | primaryAction: KGuiItem(i18nc("@action:button Restart find & replace" , "Restart" )), |
| 399 | secondaryAction: KGuiItem(i18nc("@action:button Stop find & replace" , "Stop" ))); |
| 400 | return (ret == KMessageBox::PrimaryAction); |
| 401 | } |
| 402 | |
| 403 | void KReplace::closeReplaceNextDialog() |
| 404 | { |
| 405 | closeFindNextDialog(); |
| 406 | } |
| 407 | |
| 408 | #include "kreplace.moc" |
| 409 | #include "moc_kreplace.cpp" |
| 410 | |