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
26class KReplaceNextDialog : public QDialog
27{
28 Q_OBJECT
29public:
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
37private:
38 QLabel *m_mainLabel = nullptr;
39 QPushButton *m_allButton = nullptr;
40 QPushButton *m_skipButton = nullptr;
41 QPushButton *m_replaceButton = nullptr;
42};
43
44KReplaceNextDialog::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
74void KReplaceNextDialog::setLabel(const QString &pattern, const QString &replacement)
75{
76 m_mainLabel->setText(i18n("Replace '%1' with '%2'?", pattern, replacement));
77}
78
79QPushButton *KReplaceNextDialog::replaceAllButton() const
80{
81 return m_allButton;
82}
83
84QPushButton *KReplaceNextDialog::skipButton() const
85{
86 return m_skipButton;
87}
88
89QPushButton *KReplaceNextDialog::replaceButton() const
90{
91 return m_replaceButton;
92}
93
94////
95
96class KReplacePrivate : public KFindPrivate
97{
98 Q_DECLARE_PUBLIC(KReplace)
99
100public:
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
121KReplace::KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent)
122 : KFind(*new KReplacePrivate(this, replacement), pattern, options, parent)
123{
124}
125
126KReplace::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
131KReplace::~KReplace() = default;
132
133int KReplace::numReplacements() const
134{
135 Q_D(const KReplace);
136
137 return d->m_replacements;
138}
139
140QDialog *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
150KReplaceNextDialog *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
173void 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
184static 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
206KFind::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
268int 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
285void 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
295void 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
312void 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
325void 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
355void KReplace::resetCounts()
356{
357 Q_D(KReplace);
358
359 KFind::resetCounts();
360 d->m_replacements = 0;
361}
362
363bool 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
403void KReplace::closeReplaceNextDialog()
404{
405 closeFindNextDialog();
406}
407
408#include "kreplace.moc"
409#include "moc_kreplace.cpp"
410

source code of ktextwidgets/src/findreplace/kreplace.cpp