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