/* * MIT License * * Copyright (c) 2014-2025 Patrizio Bekerle -- * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include "qplaintexteditsearchwidget.h" #include #include #include #include "ui_qplaintexteditsearchwidget.h" QPlainTextEditSearchWidget::QPlainTextEditSearchWidget(QPlainTextEdit *parent) : QWidget(parent), ui(new Ui::QPlainTextEditSearchWidget), selectionColor(0, 180, 0, 100) { ui->setupUi(this); _textEdit = parent; _darkMode = false; hide(); ui->searchCountLabel->setStyleSheet(QStringLiteral("* {color: grey}")); // hiding will leave a open space in the horizontal layout ui->searchCountLabel->setEnabled(false); _currentSearchResult = 0; _searchResultCount = 0; connect(ui->closeButton, &QPushButton::clicked, this, &QPlainTextEditSearchWidget::deactivate); connect(ui->searchLineEdit, &QLineEdit::textChanged, this, &QPlainTextEditSearchWidget::searchLineEditTextChanged); connect(ui->searchDownButton, &QPushButton::clicked, this, &QPlainTextEditSearchWidget::doSearchDown); connect(ui->searchUpButton, &QPushButton::clicked, this, &QPlainTextEditSearchWidget::doSearchUp); connect(ui->replaceToggleButton, &QPushButton::toggled, this, &QPlainTextEditSearchWidget::setReplaceMode); connect(ui->replaceButton, &QPushButton::clicked, this, &QPlainTextEditSearchWidget::doReplace); connect(ui->replaceAllButton, &QPushButton::clicked, this, &QPlainTextEditSearchWidget::doReplaceAll); connect(&_debounceTimer, &QTimer::timeout, this, &QPlainTextEditSearchWidget::performSearch); installEventFilter(this); ui->searchLineEdit->installEventFilter(this); ui->replaceLineEdit->installEventFilter(this); #ifdef Q_OS_MAC // set the spacing to 8 for OS X layout()->setSpacing(8); ui->buttonFrame->layout()->setSpacing(9); // set the margin to 0 for the top buttons for OS X QString buttonStyle = QStringLiteral("QPushButton {margin: 0}"); ui->closeButton->setStyleSheet(buttonStyle); ui->searchDownButton->setStyleSheet(buttonStyle); ui->searchUpButton->setStyleSheet(buttonStyle); ui->replaceToggleButton->setStyleSheet(buttonStyle); ui->matchCaseSensitiveButton->setStyleSheet(buttonStyle); #endif } QPlainTextEditSearchWidget::~QPlainTextEditSearchWidget() { delete ui; } void QPlainTextEditSearchWidget::activate() { activate(true); } void QPlainTextEditSearchWidget::activateReplace() { // replacing is prohibited if the text edit is readonly if (_textEdit->isReadOnly()) { return; } ui->searchLineEdit->setText(_textEdit->textCursor().selectedText()); ui->searchLineEdit->selectAll(); activate(); setReplaceMode(true); } void QPlainTextEditSearchWidget::deactivate() { stopDebounce(); hide(); // Clear the search extra selections when closing the search bar clearSearchExtraSelections(); _textEdit->setFocus(); } void QPlainTextEditSearchWidget::setReplaceMode(bool enabled) { ui->replaceToggleButton->setChecked(enabled); ui->replaceLabel->setVisible(enabled); ui->replaceLineEdit->setVisible(enabled); ui->modeLabel->setVisible(enabled); ui->buttonFrame->setVisible(enabled); ui->matchCaseSensitiveButton->setVisible(enabled); } bool QPlainTextEditSearchWidget::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { auto *keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Escape) { deactivate(); return true; } else if ((!_debounceTimer.isActive() && keyEvent->modifiers().testFlag(Qt::ShiftModifier) && (keyEvent->key() == Qt::Key_Return)) || (keyEvent->key() == Qt::Key_Up)) { doSearchUp(); return true; } else if (!_debounceTimer.isActive() && ((keyEvent->key() == Qt::Key_Return) || (keyEvent->key() == Qt::Key_Down))) { doSearchDown(); return true; } else if (!_debounceTimer.isActive() && keyEvent->key() == Qt::Key_F3) { doSearch(!keyEvent->modifiers().testFlag(Qt::ShiftModifier)); return true; } // if ((obj == ui->replaceLineEdit) && (keyEvent->key() == // Qt::Key_Tab) // && ui->replaceToggleButton->isChecked()) { // ui->replaceLineEdit->setFocus(); // } return false; } return QWidget::eventFilter(obj, event); } void QPlainTextEditSearchWidget::searchLineEditTextChanged( const QString &arg1) { _searchTerm = arg1; if (_debounceTimer.interval() != 0 && !_searchTerm.isEmpty()) { _debounceTimer.start(); ui->searchDownButton->setEnabled(false); ui->searchUpButton->setEnabled(false); } else { performSearch(); } } void QPlainTextEditSearchWidget::performSearch() { doSearchCount(); updateSearchExtraSelections(); doSearchDown(); } void QPlainTextEditSearchWidget::clearSearchExtraSelections() { _searchExtraSelections.clear(); setSearchExtraSelections(); } void QPlainTextEditSearchWidget::updateSearchExtraSelections() { _searchExtraSelections.clear(); const auto textCursor = _textEdit->textCursor(); _textEdit->moveCursor(QTextCursor::Start); const QColor color = selectionColor; QTextCharFormat extraFmt; extraFmt.setBackground(color); int findCounter = 0; const int searchMode = ui->modeComboBox->currentIndex(); while (doSearch(true, false, false)) { findCounter++; // prevent infinite loops from regular expression searches like "$", "^" // or "\b" if (searchMode == RegularExpressionMode && findCounter >= 10000) { break; } QTextEdit::ExtraSelection extra = QTextEdit::ExtraSelection(); extra.format = extraFmt; extra.cursor = _textEdit->textCursor(); _searchExtraSelections.append(extra); } _textEdit->setTextCursor(textCursor); this->setSearchExtraSelections(); } void QPlainTextEditSearchWidget::setSearchExtraSelections() const { this->_textEdit->setExtraSelections(this->_searchExtraSelections); } void QPlainTextEditSearchWidget::stopDebounce() { _debounceTimer.stop(); ui->searchDownButton->setEnabled(true); ui->searchUpButton->setEnabled(true); } void QPlainTextEditSearchWidget::doSearchUp() { doSearch(false); } void QPlainTextEditSearchWidget::doSearchDown() { doSearch(true); } bool QPlainTextEditSearchWidget::doReplace(bool forAll) { if (_textEdit->isReadOnly()) { return false; } QTextCursor cursor = _textEdit->textCursor(); if (!forAll && cursor.selectedText().isEmpty()) { return false; } const int searchMode = ui->modeComboBox->currentIndex(); if (searchMode == RegularExpressionMode) { QString text = cursor.selectedText(); text.replace(QRegularExpression(ui->searchLineEdit->text()), ui->replaceLineEdit->text()); cursor.insertText(text); } else { cursor.insertText(ui->replaceLineEdit->text()); } if (!forAll) { const int position = cursor.position(); if (!doSearch(true)) { // restore the last cursor position if text wasn't found any more cursor.setPosition(position); _textEdit->setTextCursor(cursor); } } return true; } void QPlainTextEditSearchWidget::doReplaceAll() { if (_textEdit->isReadOnly()) { return; } // start at the top _textEdit->moveCursor(QTextCursor::Start); // replace until everything to the bottom is replaced while (doSearch(true, false) && doReplace(true)) { } } /** * @brief Searches for text in the text edit * @returns true if found */ bool QPlainTextEditSearchWidget::doSearch(bool searchDown, bool allowRestartAtTop, bool updateUI) { if (_debounceTimer.isActive()) { stopDebounce(); } const QString text = ui->searchLineEdit->text(); if (text.isEmpty()) { if (updateUI) { ui->searchLineEdit->setStyleSheet(QLatin1String("")); } return false; } const int searchMode = ui->modeComboBox->currentIndex(); const bool caseSensitive = ui->matchCaseSensitiveButton->isChecked(); QFlags options = searchDown ? QTextDocument::FindFlag(0) : QTextDocument::FindBackward; if (searchMode == WholeWordsMode) { options |= QTextDocument::FindWholeWords; } if (caseSensitive) { options |= QTextDocument::FindCaseSensitively; } // block signal to reduce too many signals being fired and too many updates _textEdit->blockSignals(true); bool found = searchMode == RegularExpressionMode ? #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) _textEdit->find( QRegularExpression( text, caseSensitive ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption), options) : #else _textEdit->find(QRegExp(text, caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive), options) : #endif _textEdit->find(text, options); _textEdit->blockSignals(false); if (found) { const int result = searchDown ? ++_currentSearchResult : --_currentSearchResult; _currentSearchResult = std::min(result, _searchResultCount); updateSearchCountLabelText(); } // start at the top (or bottom) if not found if (!found && allowRestartAtTop) { _textEdit->moveCursor(searchDown ? QTextCursor::Start : QTextCursor::End); found = searchMode == RegularExpressionMode ? #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) _textEdit->find( QRegularExpression( text, caseSensitive ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption), options) : #else _textEdit->find( QRegExp(text, caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive), options) : #endif _textEdit->find(text, options); if (found && updateUI) { _currentSearchResult = searchDown ? 1 : _searchResultCount; updateSearchCountLabelText(); } } if (updateUI) { const QRect rect = _textEdit->cursorRect(); QMargins margins = _textEdit->layout()->contentsMargins(); const int searchWidgetHotArea = _textEdit->height() - this->height(); const int marginBottom = (rect.y() > searchWidgetHotArea) ? (this->height() + 10) : 0; // move the search box a bit up if we would block the search result if (margins.bottom() != marginBottom) { margins.setBottom(marginBottom); _textEdit->layout()->setContentsMargins(margins); } // add a background color according if we found the text or not const QString bgColorCode = _darkMode ? (found ? QStringLiteral("#135a13") : QStringLiteral("#8d2b36")) : found ? QStringLiteral("#D5FAE2") : QStringLiteral("#FAE9EB"); const QString fgColorCode = _darkMode ? QStringLiteral("#cccccc") : QStringLiteral("#404040"); ui->searchLineEdit->setStyleSheet( QStringLiteral("* { background: ") + bgColorCode + QStringLiteral("; color: ") + fgColorCode + QStringLiteral("; }")); // restore the search extra selections after the find command this->setSearchExtraSelections(); } return found; } /** * @brief Counts the search results */ void QPlainTextEditSearchWidget::doSearchCount() { // Note that we are moving the anchor, so the search will start from the top // again! Alternative: Restore cursor position afterward, but then we will // not know // at what _currentSearchResult we currently are _textEdit->moveCursor(QTextCursor::Start, QTextCursor::MoveAnchor); bool found; _searchResultCount = 0; _currentSearchResult = 0; const int searchMode = ui->modeComboBox->currentIndex(); do { found = doSearch(true, false, false); if (found) { _searchResultCount++; } // prevent infinite loops from regular expression searches like "$", "^" // or "\b" if (searchMode == RegularExpressionMode && _searchResultCount >= 10000) { break; } } while (found); updateSearchCountLabelText(); } void QPlainTextEditSearchWidget::setDarkMode(bool enabled) { _darkMode = enabled; } void QPlainTextEditSearchWidget::setSearchText(const QString &searchText) { ui->searchLineEdit->setText(searchText); } void QPlainTextEditSearchWidget::setSearchMode(SearchMode searchMode) { ui->modeComboBox->setCurrentIndex(searchMode); } void QPlainTextEditSearchWidget::setDebounceDelay(uint debounceDelay) { _debounceTimer.setInterval(static_cast(debounceDelay)); } void QPlainTextEditSearchWidget::activate(bool focus) { setReplaceMode(ui->modeComboBox->currentIndex() != SearchMode::PlainTextMode); show(); // preset the selected text as search text if there is any and there is no // other search text const QString selectedText = _textEdit->textCursor().selectedText(); if (!selectedText.isEmpty() && ui->searchLineEdit->text().isEmpty()) { ui->searchLineEdit->setText(selectedText); } if (focus) { ui->searchLineEdit->setFocus(); } ui->searchLineEdit->selectAll(); updateSearchExtraSelections(); doSearchDown(); } void QPlainTextEditSearchWidget::reset() { ui->searchLineEdit->clear(); setSearchMode(SearchMode::PlainTextMode); setReplaceMode(false); ui->searchCountLabel->setEnabled(false); } void QPlainTextEditSearchWidget::updateSearchCountLabelText() { ui->searchCountLabel->setEnabled(true); ui->searchCountLabel->setText(QStringLiteral("%1/%2").arg( _currentSearchResult == 0 ? QChar('-') : QString::number(_currentSearchResult), _searchResultCount == 0 ? QChar('-') : QString::number(_searchResultCount))); } void QPlainTextEditSearchWidget::setSearchSelectionColor(const QColor &color) { selectionColor = color; } void QPlainTextEditSearchWidget::on_modeComboBox_currentIndexChanged( int index) { Q_UNUSED(index) doSearchCount(); doSearchDown(); } void QPlainTextEditSearchWidget::on_matchCaseSensitiveButton_toggled( bool checked) { Q_UNUSED(checked) doSearchCount(); doSearchDown(); }