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 "kfinddialog.h"
10#include "kfinddialog_p.h"
11
12#include "kfind.h"
13
14#include <QCheckBox>
15#include <QDialogButtonBox>
16#include <QGridLayout>
17#include <QGroupBox>
18#include <QLabel>
19#include <QLineEdit>
20#include <QMenu>
21#include <QPushButton>
22#include <QRegularExpression>
23
24#include <KGuiItem>
25#include <KHistoryComboBox>
26#include <KLazyLocalizedString>
27#include <KLocalizedString>
28#include <KMessageBox>
29
30#include <assert.h>
31
32KFindDialog::KFindDialog(QWidget *parent, long options, const QStringList &findStrings, bool hasSelection, bool replaceDialog)
33 : KFindDialog(*new KFindDialogPrivate(this), parent, options, findStrings, hasSelection, replaceDialog)
34{
35 setWindowTitle(i18n("Find Text"));
36}
37
38KFindDialog::KFindDialog(KFindDialogPrivate &dd, QWidget *parent, long options, const QStringList &findStrings, bool hasSelection, bool replaceDialog)
39 : QDialog(parent)
40 , d_ptr(&dd)
41{
42 Q_D(KFindDialog);
43
44 d->init(forReplace: replaceDialog, findStrings, hasSelection);
45 setOptions(options);
46}
47
48KFindDialog::~KFindDialog() = default;
49
50QWidget *KFindDialog::findExtension() const
51{
52 Q_D(const KFindDialog);
53
54 if (!d->findExtension) {
55 d->findExtension = new QWidget(d->findGrp);
56 d->findLayout->addWidget(d->findExtension, row: 3, column: 0, rowSpan: 1, columnSpan: 2);
57 }
58
59 return d->findExtension;
60}
61
62QStringList KFindDialog::findHistory() const
63{
64 Q_D(const KFindDialog);
65
66 return d->find->historyItems();
67}
68
69void KFindDialogPrivate::init(bool forReplace, const QStringList &_findStrings, bool hasSelection)
70{
71 Q_Q(KFindDialog);
72
73 // Create common parts of dialog.
74 QVBoxLayout *topLayout = new QVBoxLayout(q);
75
76 findGrp = new QGroupBox(i18nc("@title:group", "Find"), q);
77 findLayout = new QGridLayout(findGrp);
78
79 QLabel *findLabel = new QLabel(i18n("&Text to find:"), findGrp);
80 find = new KHistoryComboBox(findGrp);
81 find->setMaxCount(10);
82 find->setDuplicatesEnabled(false);
83 regExp = new QCheckBox(i18n("Regular e&xpression"), findGrp);
84 regExpItem = new QPushButton(i18n("&Edit..."), findGrp);
85 regExpItem->setEnabled(false);
86
87 findLayout->addWidget(findLabel, row: 0, column: 0);
88 findLayout->addWidget(find, row: 1, column: 0, rowSpan: 1, columnSpan: 2);
89 findLayout->addWidget(regExp, row: 2, column: 0);
90 findLayout->addWidget(regExpItem, row: 2, column: 1);
91 topLayout->addWidget(findGrp);
92
93 replaceGrp = new QGroupBox(i18n("Replace With"), q);
94 replaceLayout = new QGridLayout(replaceGrp);
95
96 QLabel *replaceLabel = new QLabel(i18n("Replace&ment text:"), replaceGrp);
97 replace = new KHistoryComboBox(replaceGrp);
98 replace->setMaxCount(10);
99 replace->setDuplicatesEnabled(false);
100 backRef = new QCheckBox(i18n("Use p&laceholders"), replaceGrp);
101 backRefItem = new QPushButton(i18n("Insert Place&holder"), replaceGrp);
102 backRefItem->setEnabled(false);
103
104 replaceLayout->addWidget(replaceLabel, row: 0, column: 0);
105 replaceLayout->addWidget(replace, row: 1, column: 0, rowSpan: 1, columnSpan: 2);
106 replaceLayout->addWidget(backRef, row: 2, column: 0);
107 replaceLayout->addWidget(backRefItem, row: 2, column: 1);
108 topLayout->addWidget(replaceGrp);
109
110 QGroupBox *optionGrp = new QGroupBox(i18n("Options"), q);
111 QGridLayout *optionsLayout = new QGridLayout(optionGrp);
112
113 caseSensitive = new QCheckBox(i18n("C&ase sensitive"), optionGrp);
114 wholeWordsOnly = new QCheckBox(i18n("&Whole words only"), optionGrp);
115 fromCursor = new QCheckBox(i18n("From c&ursor"), optionGrp);
116 findBackwards = new QCheckBox(i18n("Find &backwards"), optionGrp);
117 selectedText = new QCheckBox(i18n("&Selected text"), optionGrp);
118 q->setHasSelection(hasSelection);
119 // If we have a selection, we make 'find in selection' default
120 // and if we don't, then the option has to be unchecked, obviously.
121 selectedText->setChecked(hasSelection);
122 slotSelectedTextToggled(hasSelection);
123
124 promptOnReplace = new QCheckBox(i18n("&Prompt on replace"), optionGrp);
125 promptOnReplace->setChecked(true);
126
127 optionsLayout->addWidget(caseSensitive, row: 0, column: 0);
128 optionsLayout->addWidget(wholeWordsOnly, row: 1, column: 0);
129 optionsLayout->addWidget(fromCursor, row: 2, column: 0);
130 optionsLayout->addWidget(findBackwards, row: 0, column: 1);
131 optionsLayout->addWidget(selectedText, row: 1, column: 1);
132 optionsLayout->addWidget(promptOnReplace, row: 2, column: 1);
133 topLayout->addWidget(optionGrp);
134
135 buttonBox = new QDialogButtonBox(q);
136 buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Close);
137 q->connect(sender: buttonBox, signal: &QDialogButtonBox::accepted, context: q, slot: [this]() {
138 slotOk();
139 });
140 q->connect(sender: buttonBox, signal: &QDialogButtonBox::rejected, context: q, slot: [this]() {
141 slotReject();
142 });
143 topLayout->addWidget(buttonBox);
144
145 // We delay creation of these until needed.
146 patterns = nullptr;
147 placeholders = nullptr;
148
149 // signals and slots connections
150 q->connect(sender: selectedText, signal: &QCheckBox::toggled, context: q, slot: [this](bool checked) {
151 slotSelectedTextToggled(checked);
152 });
153 q->connect(sender: regExp, signal: &QCheckBox::toggled, context: regExpItem, slot: &QWidget::setEnabled);
154 q->connect(sender: backRef, signal: &QCheckBox::toggled, context: backRefItem, slot: &QWidget::setEnabled);
155 q->connect(sender: regExpItem, signal: &QPushButton::clicked, context: q, slot: [this]() {
156 showPatterns();
157 });
158 q->connect(sender: backRefItem, signal: &QPushButton::clicked, context: q, slot: [this]() {
159 showPlaceholders();
160 });
161
162 q->connect(sender: find, signal: &KHistoryComboBox::editTextChanged, context: q, slot: [this](const QString &text) {
163 textSearchChanged(text);
164 });
165
166 q->connect(sender: regExp, signal: &QCheckBox::toggled, context: q, slot: &KFindDialog::optionsChanged);
167 q->connect(sender: backRef, signal: &QCheckBox::toggled, context: q, slot: &KFindDialog::optionsChanged);
168 q->connect(sender: caseSensitive, signal: &QCheckBox::toggled, context: q, slot: &KFindDialog::optionsChanged);
169 q->connect(sender: wholeWordsOnly, signal: &QCheckBox::toggled, context: q, slot: &KFindDialog::optionsChanged);
170 q->connect(sender: fromCursor, signal: &QCheckBox::toggled, context: q, slot: &KFindDialog::optionsChanged);
171 q->connect(sender: findBackwards, signal: &QCheckBox::toggled, context: q, slot: &KFindDialog::optionsChanged);
172 q->connect(sender: selectedText, signal: &QCheckBox::toggled, context: q, slot: &KFindDialog::optionsChanged);
173 q->connect(sender: promptOnReplace, signal: &QCheckBox::toggled, context: q, slot: &KFindDialog::optionsChanged);
174
175 // tab order
176 q->setTabOrder(find, regExp);
177 q->setTabOrder(regExp, regExpItem);
178 q->setTabOrder(regExpItem, replace); // findExtension widgets are inserted in showEvent()
179 q->setTabOrder(replace, backRef);
180 q->setTabOrder(backRef, backRefItem);
181 q->setTabOrder(backRefItem, caseSensitive);
182 q->setTabOrder(caseSensitive, wholeWordsOnly);
183 q->setTabOrder(wholeWordsOnly, fromCursor);
184 q->setTabOrder(fromCursor, findBackwards);
185 q->setTabOrder(findBackwards, selectedText);
186 q->setTabOrder(selectedText, promptOnReplace);
187
188 // buddies
189 findLabel->setBuddy(find);
190 replaceLabel->setBuddy(replace);
191
192 if (!forReplace) {
193 promptOnReplace->hide();
194 replaceGrp->hide();
195 }
196
197 findStrings = _findStrings;
198 find->setFocus();
199 QPushButton *buttonOk = buttonBox->button(which: QDialogButtonBox::Ok);
200 buttonOk->setEnabled(!q->pattern().isEmpty());
201
202 if (forReplace) {
203 KGuiItem::assign(button: buttonOk,
204 item: KGuiItem(i18n("&Replace"),
205 QString(),
206 i18n("Start replace"),
207 i18n("<qt>If you press the <b>Replace</b> button, the text you entered "
208 "above is searched for within the document and any occurrence is "
209 "replaced with the replacement text.</qt>")));
210 } else {
211 KGuiItem::assign(button: buttonOk,
212 item: KGuiItem(i18n("&Find"),
213 QStringLiteral("edit-find"),
214 i18n("Start searching"),
215 i18n("<qt>If you press the <b>Find</b> button, the text you entered "
216 "above is searched for within the document.</qt>")));
217 }
218
219 // QWhatsthis texts
220 find->setWhatsThis(i18n("Enter a pattern to search for, or select a previous pattern from the list."));
221 regExp->setWhatsThis(i18n("If enabled, search for a regular expression."));
222 regExpItem->setWhatsThis(i18n("Click here to edit your regular expression using a graphical editor."));
223 replace->setWhatsThis(i18n("Enter a replacement string, or select a previous one from the list."));
224 backRef->setWhatsThis(
225 i18n("<qt>If enabled, any occurrence of <code><b>\\N</b></code>, where "
226 "<code><b>N</b></code> is an integer number, will be replaced with "
227 "the corresponding capture (\"parenthesized substring\") from the "
228 "pattern.<p>To include (a literal <code><b>\\N</b></code> in your "
229 "replacement, put an extra backslash in front of it, like "
230 "<code><b>\\\\N</b></code>.</p></qt>"));
231 backRefItem->setWhatsThis(i18n("Click for a menu of available captures."));
232 wholeWordsOnly->setWhatsThis(i18n("Require word boundaries in both ends of a match to succeed."));
233 fromCursor->setWhatsThis(i18n("Start searching at the current cursor location rather than at the top."));
234 selectedText->setWhatsThis(i18n("Only search within the current selection."));
235 caseSensitive->setWhatsThis(i18n("Perform a case sensitive search: entering the pattern 'Joe' will not match 'joe' or 'JOE', only 'Joe'."));
236 findBackwards->setWhatsThis(i18n("Search backwards."));
237 promptOnReplace->setWhatsThis(i18n("Ask before replacing each match found."));
238
239 textSearchChanged(find->lineEdit()->text());
240}
241
242void KFindDialogPrivate::textSearchChanged(const QString &text)
243{
244 buttonBox->button(which: QDialogButtonBox::Ok)->setEnabled(!text.isEmpty());
245}
246
247void KFindDialog::showEvent(QShowEvent *e)
248{
249 Q_D(KFindDialog);
250
251 if (!d->initialShowDone) {
252 d->initialShowDone = true; // only once
253 // qDebug() << "showEvent\n";
254 if (!d->findStrings.isEmpty()) {
255 setFindHistory(d->findStrings);
256 }
257 d->findStrings = QStringList();
258 if (!d->pattern.isEmpty()) {
259 d->find->lineEdit()->setText(d->pattern);
260 d->find->lineEdit()->selectAll();
261 d->pattern.clear();
262 }
263 // maintain a user-friendly tab order
264 if (d->findExtension) {
265 QWidget *prev = d->regExpItem;
266 const auto children = d->findExtension->findChildren<QWidget *>();
267 for (QWidget *child : children) {
268 setTabOrder(prev, child);
269 prev = child;
270 }
271 setTabOrder(prev, d->replace);
272 }
273 }
274 d->find->setFocus();
275 QDialog::showEvent(e);
276}
277
278long KFindDialog::options() const
279{
280 Q_D(const KFindDialog);
281
282 long options = 0;
283
284 if (d->caseSensitive->isChecked()) {
285 options |= KFind::CaseSensitive;
286 }
287 if (d->wholeWordsOnly->isChecked()) {
288 options |= KFind::WholeWordsOnly;
289 }
290 if (d->fromCursor->isChecked()) {
291 options |= KFind::FromCursor;
292 }
293 if (d->findBackwards->isChecked()) {
294 options |= KFind::FindBackwards;
295 }
296 if (d->selectedText->isChecked()) {
297 options |= KFind::SelectedText;
298 }
299 if (d->regExp->isChecked()) {
300 options |= KFind::RegularExpression;
301 }
302 return options;
303}
304
305QString KFindDialog::pattern() const
306{
307 Q_D(const KFindDialog);
308
309 return d->find->currentText();
310}
311
312void KFindDialog::setPattern(const QString &pattern)
313{
314 Q_D(KFindDialog);
315
316 d->find->lineEdit()->setText(pattern);
317 d->find->lineEdit()->selectAll();
318 d->pattern = pattern;
319 // qDebug() << "setPattern " << pattern;
320}
321
322void KFindDialog::setFindHistory(const QStringList &strings)
323{
324 Q_D(KFindDialog);
325
326 if (!strings.isEmpty()) {
327 d->find->setHistoryItems(items: strings, setCompletionList: true);
328 d->find->lineEdit()->setText(strings.first());
329 d->find->lineEdit()->selectAll();
330 } else {
331 d->find->clearHistory();
332 }
333}
334
335void KFindDialog::setHasSelection(bool hasSelection)
336{
337 Q_D(KFindDialog);
338
339 if (hasSelection) {
340 d->enabled |= KFind::SelectedText;
341 } else {
342 d->enabled &= ~KFind::SelectedText;
343 }
344 d->selectedText->setEnabled(hasSelection);
345 if (!hasSelection) {
346 d->selectedText->setChecked(false);
347 d->slotSelectedTextToggled(hasSelection);
348 }
349}
350
351void KFindDialogPrivate::slotSelectedTextToggled(bool selec)
352{
353 // From cursor doesn't make sense if we have a selection
354 fromCursor->setEnabled(!selec && (enabled & KFind::FromCursor));
355 if (selec) { // uncheck if disabled
356 fromCursor->setChecked(false);
357 }
358}
359
360void KFindDialog::setHasCursor(bool hasCursor)
361{
362 Q_D(KFindDialog);
363
364 if (hasCursor) {
365 d->enabled |= KFind::FromCursor;
366 } else {
367 d->enabled &= ~KFind::FromCursor;
368 }
369 d->fromCursor->setEnabled(hasCursor);
370 d->fromCursor->setChecked(hasCursor && (options() & KFind::FromCursor));
371}
372
373void KFindDialog::setSupportsBackwardsFind(bool supports)
374{
375 Q_D(KFindDialog);
376
377 // ########## Shouldn't this hide the checkbox instead?
378 if (supports) {
379 d->enabled |= KFind::FindBackwards;
380 } else {
381 d->enabled &= ~KFind::FindBackwards;
382 }
383 d->findBackwards->setEnabled(supports);
384 d->findBackwards->setChecked(supports && (options() & KFind::FindBackwards));
385}
386
387void KFindDialog::setSupportsCaseSensitiveFind(bool supports)
388{
389 Q_D(KFindDialog);
390
391 // ########## This should hide the checkbox instead
392 if (supports) {
393 d->enabled |= KFind::CaseSensitive;
394 } else {
395 d->enabled &= ~KFind::CaseSensitive;
396 }
397 d->caseSensitive->setEnabled(supports);
398 d->caseSensitive->setChecked(supports && (options() & KFind::CaseSensitive));
399}
400
401void KFindDialog::setSupportsWholeWordsFind(bool supports)
402{
403 Q_D(KFindDialog);
404
405 // ########## This should hide the checkbox instead
406 if (supports) {
407 d->enabled |= KFind::WholeWordsOnly;
408 } else {
409 d->enabled &= ~KFind::WholeWordsOnly;
410 }
411 d->wholeWordsOnly->setEnabled(supports);
412 d->wholeWordsOnly->setChecked(supports && (options() & KFind::WholeWordsOnly));
413}
414
415void KFindDialog::setSupportsRegularExpressionFind(bool supports)
416{
417 Q_D(KFindDialog);
418
419 if (supports) {
420 d->enabled |= KFind::RegularExpression;
421 } else {
422 d->enabled &= ~KFind::RegularExpression;
423 }
424 d->regExp->setEnabled(supports);
425 d->regExp->setChecked(supports && (options() & KFind::RegularExpression));
426 if (!supports) {
427 d->regExpItem->hide();
428 d->regExp->hide();
429 } else {
430 d->regExpItem->show();
431 d->regExp->show();
432 }
433}
434
435void KFindDialog::setOptions(long options)
436{
437 Q_D(KFindDialog);
438
439 d->caseSensitive->setChecked((d->enabled & KFind::CaseSensitive) && (options & KFind::CaseSensitive));
440 d->wholeWordsOnly->setChecked((d->enabled & KFind::WholeWordsOnly) && (options & KFind::WholeWordsOnly));
441 d->fromCursor->setChecked((d->enabled & KFind::FromCursor) && (options & KFind::FromCursor));
442 d->findBackwards->setChecked((d->enabled & KFind::FindBackwards) && (options & KFind::FindBackwards));
443 d->selectedText->setChecked((d->enabled & KFind::SelectedText) && (options & KFind::SelectedText));
444 d->regExp->setChecked((d->enabled & KFind::RegularExpression) && (options & KFind::RegularExpression));
445}
446
447// Create a popup menu with a list of regular expression terms, to help the user
448// compose a regular expression search pattern.
449void KFindDialogPrivate::showPatterns()
450{
451 Q_Q(KFindDialog);
452
453 typedef struct {
454 const KLazyLocalizedString description;
455 const char *regExp;
456 int cursorAdjustment;
457 } Term;
458 static const Term items[] = {
459 {.description: kli18n(text: "Any Character"), .regExp: ".", .cursorAdjustment: 0},
460 {.description: kli18n(text: "Start of Line"), .regExp: "^", .cursorAdjustment: 0},
461 {.description: kli18n(text: "End of Line"), .regExp: "$", .cursorAdjustment: 0},
462 {.description: kli18n(text: "Set of Characters"), .regExp: "[]", .cursorAdjustment: -1},
463 {.description: kli18n(text: "Repeats, Zero or More Times"), .regExp: "*", .cursorAdjustment: 0},
464 {.description: kli18n(text: "Repeats, One or More Times"), .regExp: "+", .cursorAdjustment: 0},
465 {.description: kli18n(text: "Optional"), .regExp: "?", .cursorAdjustment: 0},
466 {.description: kli18n(text: "Escape"), .regExp: "\\", .cursorAdjustment: 0},
467 {.description: kli18n(text: "TAB"), .regExp: "\\t", .cursorAdjustment: 0},
468 {.description: kli18n(text: "Newline"), .regExp: "\\n", .cursorAdjustment: 0},
469 {.description: kli18n(text: "Carriage Return"), .regExp: "\\r", .cursorAdjustment: 0},
470 {.description: kli18n(text: "White Space"), .regExp: "\\s", .cursorAdjustment: 0},
471 {.description: kli18n(text: "Digit"), .regExp: "\\d", .cursorAdjustment: 0},
472 };
473
474 class RegExpAction : public QAction
475 {
476 public:
477 RegExpAction(QObject *parent, const QString &text, const QString &regExp, int cursor)
478 : QAction(text, parent)
479 , mText(text)
480 , mRegExp(regExp)
481 , mCursor(cursor)
482 {
483 }
484
485 QString text() const
486 {
487 return mText;
488 }
489 QString regExp() const
490 {
491 return mRegExp;
492 }
493 int cursor() const
494 {
495 return mCursor;
496 }
497
498 private:
499 QString mText;
500 QString mRegExp;
501 int mCursor;
502 };
503
504 // Populate the popup menu.
505 if (!patterns) {
506 patterns = new QMenu(q);
507 for (const Term &item : items) {
508 patterns->addAction(action: new RegExpAction(patterns, item.description.toString(), QLatin1String(item.regExp), item.cursorAdjustment));
509 }
510 }
511
512 // Insert the selection into the edit control.
513 QAction *action = patterns->exec(pos: regExpItem->mapToGlobal(regExpItem->rect().bottomLeft()));
514 if (action) {
515 RegExpAction *regExpAction = static_cast<RegExpAction *>(action);
516 if (regExpAction) {
517 QLineEdit *editor = find->lineEdit();
518
519 editor->insert(regExpAction->regExp());
520 editor->setCursorPosition(editor->cursorPosition() + regExpAction->cursor());
521 }
522 }
523}
524
525class PlaceHolderAction : public QAction
526{
527public:
528 PlaceHolderAction(QObject *parent, const QString &text, int id)
529 : QAction(text, parent)
530 , mText(text)
531 , mId(id)
532 {
533 }
534
535 QString text() const
536 {
537 return mText;
538 }
539 int id() const
540 {
541 return mId;
542 }
543
544private:
545 QString mText;
546 int mId;
547};
548
549// Create a popup menu with a list of backreference terms, to help the user
550// compose a regular expression replacement pattern.
551void KFindDialogPrivate::showPlaceholders()
552{
553 Q_Q(KFindDialog);
554
555 // Populate the popup menu.
556 if (!placeholders) {
557 placeholders = new QMenu(q);
558 q->connect(sender: placeholders, signal: &QMenu::aboutToShow, context: q, slot: [this]() {
559 slotPlaceholdersAboutToShow();
560 });
561 }
562
563 // Insert the selection into the edit control.
564 QAction *action = placeholders->exec(pos: backRefItem->mapToGlobal(backRefItem->rect().bottomLeft()));
565 if (action) {
566 PlaceHolderAction *placeHolderAction = static_cast<PlaceHolderAction *>(action);
567 if (placeHolderAction) {
568 QLineEdit *editor = replace->lineEdit();
569 editor->insert(QStringLiteral("\\%1").arg(a: placeHolderAction->id()));
570 }
571 }
572}
573
574void KFindDialogPrivate::slotPlaceholdersAboutToShow()
575{
576 Q_Q(KFindDialog);
577
578 placeholders->clear();
579 placeholders->addAction(action: new PlaceHolderAction(placeholders, i18n("Complete Match"), 0));
580
581 const int n = QRegularExpression(q->pattern(), QRegularExpression::UseUnicodePropertiesOption).captureCount();
582 for (int i = 1; i <= n; ++i) {
583 placeholders->addAction(action: new PlaceHolderAction(placeholders, i18n("Captured Text (%1)", i), i));
584 }
585}
586
587void KFindDialogPrivate::slotOk()
588{
589 Q_Q(KFindDialog);
590
591 // Nothing to find?
592 if (q->pattern().isEmpty()) {
593 KMessageBox::error(parent: q, i18n("You must enter some text to search for."));
594 return;
595 }
596
597 if (regExp->isChecked()) {
598 // Check for a valid regular expression.
599 if (!QRegularExpression(q->pattern(), QRegularExpression::UseUnicodePropertiesOption).isValid()) {
600 KMessageBox::error(parent: q, i18n("Invalid PCRE pattern syntax."));
601 return;
602 }
603 }
604
605 find->addToHistory(item: q->pattern());
606
607 if (q->windowModality() != Qt::NonModal) {
608 q->accept();
609 }
610 Q_EMIT q->okClicked();
611}
612
613void KFindDialogPrivate::slotReject()
614{
615 Q_Q(KFindDialog);
616
617 Q_EMIT q->cancelClicked();
618 q->reject();
619}
620
621#include "moc_kfinddialog.cpp"
622

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