File indexing completed on 2024-04-28 05:50:54

0001 /*
0002     SPDX-FileCopyrightText: 2006-2008 Robert Knight <robertknight@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 // Own
0008 #include "widgets/IncrementalSearchBar.h"
0009 
0010 // Qt
0011 #include <QApplication>
0012 #include <QHBoxLayout>
0013 #include <QKeyEvent>
0014 #include <QMenu>
0015 #include <QTimer>
0016 #include <QToolButton>
0017 
0018 // KDE
0019 #include "KonsoleSettings.h"
0020 
0021 #include <KLocalizedString>
0022 #include <KStatefulBrush>
0023 #include <QLineEdit>
0024 #include <kconfigwidgets_version.h>
0025 
0026 using namespace Konsole;
0027 
0028 IncrementalSearchBar::IncrementalSearchBar(QWidget *parent)
0029     : QWidget(parent)
0030     , _searchEdit(nullptr)
0031     , _caseSensitive(nullptr)
0032     , _regExpression(nullptr)
0033     , _highlightMatches(nullptr)
0034     , _reverseSearch(nullptr)
0035     , _findNextButton(nullptr)
0036     , _findPreviousButton(nullptr)
0037     , _searchFromButton(nullptr)
0038     , _searchTimer(nullptr)
0039 {
0040     setPalette(qApp->palette());
0041     setAutoFillBackground(true);
0042 
0043     // SubWindow flag limits tab focus switching to this widget
0044     setWindowFlags(windowFlags() | Qt::SubWindow);
0045 
0046     _searchEdit = new QLineEdit(this);
0047     _searchEdit->setClearButtonEnabled(true);
0048     _searchEdit->installEventFilter(this);
0049     _searchEdit->setPlaceholderText(i18nc("@label:textbox", "Find..."));
0050     _searchEdit->setObjectName(QStringLiteral("search-edit"));
0051     _searchEdit->setToolTip(i18nc("@info:tooltip", "Enter the text to search for here"));
0052     _searchEdit->setCursor(Qt::IBeamCursor);
0053     _searchEdit->setStyleSheet(QString());
0054     _searchEdit->setFont(QApplication::font());
0055     // When the widget focus is set, focus input box instead
0056     setFocusProxy(_searchEdit);
0057 
0058     setCursor(Qt::ArrowCursor);
0059     // text box may be a minimum of 6 characters wide and a maximum of 10 characters wide
0060     // (since the maxWidth metric is used here, more characters probably will fit in than 6
0061     //  and 10)
0062     QFontMetrics metrics(_searchEdit->font());
0063     int maxWidth = metrics.maxWidth();
0064     _searchEdit->setMinimumWidth(maxWidth * 6);
0065     _searchEdit->setMaximumWidth(maxWidth * 10);
0066 
0067     _searchTimer = new QTimer(this);
0068     _searchTimer->setInterval(250);
0069     _searchTimer->setSingleShot(true);
0070     connect(_searchTimer, &QTimer::timeout, this, &Konsole::IncrementalSearchBar::notifySearchChanged);
0071     connect(_searchEdit, &QLineEdit::textChanged, _searchTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
0072 
0073     _findNextButton = new QToolButton(this);
0074     _findNextButton->setObjectName(QStringLiteral("find-next-button"));
0075     _findNextButton->setText(i18nc("@action:button Go to the next phrase", "Next"));
0076     _findNextButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
0077     _findNextButton->setAutoRaise(true);
0078     _findNextButton->setToolTip(i18nc("@info:tooltip", "Find the next match for the current search phrase"));
0079     _findNextButton->installEventFilter(this);
0080     connect(_findNextButton, &QToolButton::clicked, this, &Konsole::IncrementalSearchBar::findNextClicked);
0081 
0082     _findPreviousButton = new QToolButton(this);
0083     _findPreviousButton->setAutoRaise(true);
0084     _findPreviousButton->setObjectName(QStringLiteral("find-previous-button"));
0085     _findPreviousButton->setText(i18nc("@action:button Go to the previous phrase", "Previous"));
0086     _findPreviousButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
0087     _findPreviousButton->setToolTip(i18nc("@info:tooltip", "Find the previous match for the current search phrase"));
0088     _findPreviousButton->installEventFilter(this);
0089     connect(_findPreviousButton, &QToolButton::clicked, this, &Konsole::IncrementalSearchBar::findPreviousClicked);
0090 
0091     _searchFromButton = new QToolButton(this);
0092     _searchFromButton->setAutoRaise(true);
0093     _searchFromButton->setObjectName(QStringLiteral("search-from-button"));
0094     _searchFromButton->installEventFilter(this);
0095     connect(_searchFromButton, &QToolButton::clicked, this, &Konsole::IncrementalSearchBar::searchFromClicked);
0096 
0097     auto optionsButton = new QToolButton(this);
0098     optionsButton->setObjectName(QStringLiteral("find-options-button"));
0099     optionsButton->setCheckable(false);
0100     optionsButton->setPopupMode(QToolButton::InstantPopup);
0101     optionsButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
0102     optionsButton->setToolTip(i18nc("@info:tooltip", "Display the options menu"));
0103     optionsButton->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
0104     optionsButton->setAutoRaise(true);
0105     optionsButton->installEventFilter(this);
0106 
0107     auto closeButton = new QToolButton(this);
0108     closeButton->setObjectName(QStringLiteral("close-button"));
0109     closeButton->setToolTip(i18nc("@info:tooltip", "Close the search bar"));
0110     closeButton->setAutoRaise(true);
0111     closeButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close")));
0112     closeButton->installEventFilter(this);
0113     connect(closeButton, &QToolButton::clicked, this, &Konsole::IncrementalSearchBar::closeClicked);
0114 
0115     // Fill the options menu
0116     auto optionsMenu = new QMenu(this);
0117     optionsButton->setMenu(optionsMenu);
0118 
0119     _caseSensitive = optionsMenu->addAction(i18nc("@item:inmenu", "Case sensitive"));
0120     _caseSensitive->setCheckable(true);
0121     _caseSensitive->setToolTip(i18nc("@info:tooltip", "Sets whether the search is case sensitive"));
0122     connect(_caseSensitive, &QAction::toggled, this, &Konsole::IncrementalSearchBar::matchCaseToggled);
0123 
0124     _regExpression = optionsMenu->addAction(i18nc("@item:inmenu", "Match regular expression"));
0125     _regExpression->setCheckable(true);
0126     connect(_regExpression, &QAction::toggled, this, &Konsole::IncrementalSearchBar::matchRegExpToggled);
0127 
0128     _highlightMatches = optionsMenu->addAction(i18nc("@item:inmenu", "Highlight all matches"));
0129     _highlightMatches->setCheckable(true);
0130     _highlightMatches->setToolTip(i18nc("@info:tooltip", "Sets whether matching text should be highlighted"));
0131     connect(_highlightMatches, &QAction::toggled, this, &Konsole::IncrementalSearchBar::highlightMatchesToggled);
0132 
0133     _reverseSearch = optionsMenu->addAction(i18nc("@item:inmenu", "Search backwards"));
0134     _reverseSearch->setCheckable(true);
0135     _reverseSearch->setToolTip(i18nc("@info:tooltip", "Sets whether search should start from the bottom"));
0136     connect(_reverseSearch, &QAction::toggled, this, &Konsole::IncrementalSearchBar::updateButtonsAccordingToReverseSearchSetting);
0137     connect(_reverseSearch, &QAction::toggled, this, &Konsole::IncrementalSearchBar::reverseSearchToggled);
0138     updateButtonsAccordingToReverseSearchSetting();
0139     setOptions();
0140 
0141     auto barLayout = new QHBoxLayout(this);
0142     barLayout->addWidget(_searchEdit);
0143     barLayout->addWidget(_findNextButton);
0144     barLayout->addWidget(_findPreviousButton);
0145     barLayout->addWidget(_searchFromButton);
0146     barLayout->addWidget(optionsButton);
0147     barLayout->addWidget(closeButton);
0148     barLayout->setContentsMargins(4, 4, 4, 4);
0149     barLayout->setSpacing(0);
0150 
0151     setLayout(barLayout);
0152     adjustSize();
0153     clearLineEdit();
0154 }
0155 
0156 void IncrementalSearchBar::notifySearchChanged()
0157 {
0158     Q_EMIT searchChanged(searchText());
0159 }
0160 
0161 void IncrementalSearchBar::updateButtonsAccordingToReverseSearchSetting()
0162 {
0163     Q_ASSERT(_reverseSearch);
0164     if (_reverseSearch->isChecked()) {
0165         _searchFromButton->setToolTip(i18nc("@info:tooltip", "Search for the current search phrase from the bottom"));
0166         _searchFromButton->setIcon(QIcon::fromTheme(QStringLiteral("go-bottom")));
0167         _findNextButton->setIcon(QIcon::fromTheme(QStringLiteral("go-up")));
0168         _findPreviousButton->setIcon(QIcon::fromTheme(QStringLiteral("go-down")));
0169     } else {
0170         _searchFromButton->setToolTip(i18nc("@info:tooltip", "Search for the current search phrase from the top"));
0171         _searchFromButton->setIcon(QIcon::fromTheme(QStringLiteral("go-top")));
0172         _findNextButton->setIcon(QIcon::fromTheme(QStringLiteral("go-down")));
0173         _findPreviousButton->setIcon(QIcon::fromTheme(QStringLiteral("go-up")));
0174     }
0175 }
0176 
0177 QString IncrementalSearchBar::searchText()
0178 {
0179     return _searchEdit->text();
0180 }
0181 
0182 void IncrementalSearchBar::setSearchText(const QString &text)
0183 {
0184     if (text != searchText()) {
0185         _searchEdit->setText(text);
0186     }
0187 }
0188 
0189 bool IncrementalSearchBar::eventFilter(QObject *watched, QEvent *event)
0190 {
0191     if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
0192         auto *keyEvent = static_cast<QKeyEvent *>(event);
0193         QToolButton *toolButton = nullptr;
0194 
0195         if (keyEvent->key() == Qt::Key_Return) {
0196             if (watched == _searchEdit && event->type() == QEvent::KeyPress) {
0197                 if (keyEvent->modifiers() == Qt::NoModifier) {
0198                     _findNextButton->click();
0199                     return true;
0200                 }
0201                 if (keyEvent->modifiers() == Qt::ShiftModifier) {
0202                     _findPreviousButton->click();
0203                     return true;
0204                 }
0205                 if (keyEvent->modifiers() == Qt::ControlModifier) {
0206                     _searchFromButton->click();
0207                     return true;
0208                 }
0209             } else if ((toolButton = qobject_cast<QToolButton *>(watched)) != nullptr) {
0210                 if (event->type() == QEvent::KeyPress && !toolButton->isDown()) {
0211                     toolButton->setDown(true);
0212                     toolButton->pressed();
0213                 } else if (toolButton->isDown()) {
0214                     toolButton->setDown(keyEvent->isAutoRepeat());
0215                     toolButton->released();
0216                     toolButton->click();
0217                 }
0218                 return true;
0219             }
0220         }
0221     }
0222     return QWidget::eventFilter(watched, event);
0223 }
0224 
0225 void IncrementalSearchBar::keyPressEvent(QKeyEvent *event)
0226 {
0227     static auto movementKeysToPassAlong = QSet<int>{Qt::Key_PageUp, Qt::Key_PageDown, Qt::Key_Up, Qt::Key_Down};
0228 
0229     if (movementKeysToPassAlong.contains(event->key()) && (event->modifiers() == Qt::ShiftModifier)) {
0230         Q_EMIT unhandledMovementKeyPressed(event);
0231     }
0232 
0233     if (event->key() == Qt::Key_Escape) {
0234         Q_EMIT closeClicked();
0235     }
0236 }
0237 
0238 void IncrementalSearchBar::setVisible(bool visible)
0239 {
0240     QWidget::setVisible(visible);
0241 
0242     if (visible) {
0243         focusLineEdit();
0244     }
0245 }
0246 
0247 void IncrementalSearchBar::setFoundMatch(bool match)
0248 {
0249     if (_searchEdit->text().isEmpty()) {
0250         clearLineEdit();
0251         return;
0252     }
0253 
0254     const auto backgroundBrush = KStatefulBrush(KColorScheme::View, match ? KColorScheme::PositiveBackground : KColorScheme::NegativeBackground);
0255 
0256     const auto matchStyleSheet = QStringLiteral("QLineEdit{ background-color:%1 }").arg(backgroundBrush.brush(_searchEdit->palette()).color().name());
0257 
0258     _searchEdit->setStyleSheet(matchStyleSheet);
0259 }
0260 
0261 void IncrementalSearchBar::clearLineEdit()
0262 {
0263     _searchEdit->setStyleSheet(QString());
0264 }
0265 
0266 void IncrementalSearchBar::focusLineEdit()
0267 {
0268     _searchEdit->setFocus(Qt::ActiveWindowFocusReason);
0269     _searchEdit->selectAll();
0270 }
0271 
0272 const QBitArray IncrementalSearchBar::optionsChecked()
0273 {
0274     QBitArray options(4, false);
0275     options.setBit(MatchCase, _caseSensitive->isChecked());
0276     options.setBit(RegExp, _regExpression->isChecked());
0277     options.setBit(HighlightMatches, _highlightMatches->isChecked());
0278     options.setBit(ReverseSearch, _reverseSearch->isChecked());
0279     return options;
0280 }
0281 
0282 void IncrementalSearchBar::setOptions()
0283 {
0284     _caseSensitive->setChecked(KonsoleSettings::searchCaseSensitive());
0285     _regExpression->setChecked(KonsoleSettings::searchRegExpression());
0286     _highlightMatches->setChecked(KonsoleSettings::searchHighlightMatches());
0287     _reverseSearch->setChecked(KonsoleSettings::searchReverseSearch());
0288 }
0289 
0290 #include "moc_IncrementalSearchBar.cpp"