File indexing completed on 2024-04-28 17:06:21

0001 /*
0002     SPDX-FileCopyrightText: 2010 Jan Lepper <dehtris@yahoo.de>
0003     SPDX-FileCopyrightText: 2010-2022 Krusader Krew <https://krusader.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "krsearchbar.h"
0009 
0010 #include "../FileSystem/dirlisterinterface.h"
0011 #include "../defaults.h"
0012 #include "../icon.h"
0013 #include "../krglobal.h"
0014 #include "PanelView/krview.h"
0015 #include "PanelView/krviewitem.h"
0016 
0017 #include <QDebug>
0018 #include <QGuiApplication>
0019 #include <QHBoxLayout>
0020 #include <QKeyEvent>
0021 #include <QLineEdit>
0022 #include <QToolButton>
0023 #include <QToolTip>
0024 
0025 #include <KConfigCore/KSharedConfig>
0026 #include <KI18n/KLocalizedString>
0027 
0028 KrSearchBar::KrSearchBar(KrView *view, QWidget *parent)
0029     : QWidget(parent)
0030     , _view(nullptr)
0031     , _rightArrowEntersDirFlag(true)
0032 {
0033     // close button
0034     auto *closeButton = new QToolButton(this);
0035     closeButton->setAutoRaise(true);
0036     closeButton->setIcon(Icon(QStringLiteral("dialog-close")));
0037     closeButton->setToolTip(i18n("Close the search bar"));
0038     connect(closeButton, &QToolButton::clicked, this, &KrSearchBar::hideBar);
0039 
0040     // combo box for changing search mode
0041     _modeBox = new QComboBox(this);
0042     _modeBox->addItems(QStringList() << i18n("Search") << i18n("Select") << i18n("Filter"));
0043     _modeBox->setToolTip(i18n("Change the search mode"));
0044     connect(_modeBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &KrSearchBar::onModeChange);
0045 
0046     _currentMode = static_cast<SearchMode>(_modeBox->currentIndex());
0047 
0048     // combo box for entering search string
0049     _textBox = new KComboBox(this);
0050     _textBox->setEditable(true);
0051     _modeBox->setToolTip(i18n("Enter or select search string"));
0052     QStringList savedSearches = KConfigGroup(krConfig, "Private").readEntry("Predefined Selections", QStringList());
0053     if (savedSearches.count() > 0)
0054         _textBox->addItems(savedSearches);
0055     _textBox->setCurrentText("");
0056     _textBox->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred));
0057     connect(_textBox, &KComboBox::currentTextChanged, this, &KrSearchBar::onSearchChange);
0058 
0059     auto *saveSearchBtn = new QToolButton(this);
0060     saveSearchBtn->setIcon(Icon("document-save"));
0061     saveSearchBtn->setFixedSize(20, 20);
0062     saveSearchBtn->setToolTip(i18n("Save the current search string"));
0063     connect(saveSearchBtn, &QToolButton::clicked, this, &KrSearchBar::saveSearchString);
0064 
0065     _openSelectDialogBtn = new QToolButton(this);
0066     _openSelectDialogBtn->setIcon(Icon("configure"));
0067     _openSelectDialogBtn->setFixedSize(20, 20);
0068     _openSelectDialogBtn->setToolTip(i18n("Open selection dialog"));
0069 
0070     auto *layout = new QHBoxLayout(this);
0071     layout->setContentsMargins(0, 0, 0, 0);
0072     layout->addWidget(closeButton);
0073     layout->addWidget(_modeBox);
0074     layout->addWidget(_textBox);
0075     layout->addWidget(saveSearchBtn);
0076     layout->addWidget(_openSelectDialogBtn);
0077 
0078     _textBox->installEventFilter(this);
0079 
0080     setView(view);
0081 }
0082 
0083 void KrSearchBar::setView(KrView *view)
0084 {
0085     if (_view) {
0086         _view->widget()->removeEventFilter(this);
0087         disconnect(_openSelectDialogBtn, nullptr, nullptr, nullptr);
0088     }
0089 
0090     _view = view;
0091 
0092     connect(_openSelectDialogBtn, &QToolButton::clicked, this, [this]() {
0093         _view->customSelection(true);
0094     });
0095     _view->widget()->installEventFilter(this);
0096 }
0097 
0098 void KrSearchBar::hideBarIfSearching()
0099 {
0100     if (_currentMode == MODE_SEARCH)
0101         hideBar();
0102 }
0103 
0104 // #### public slots
0105 
0106 void KrSearchBar::showBar(SearchMode mode)
0107 {
0108     int index =
0109         mode == MODE_DEFAULT ? KConfigGroup(krConfig, "Look&Feel").readEntry("Default Search Mode", QString::number(KrSearchBar::MODE_SEARCH)).toInt() : mode;
0110     _modeBox->setCurrentIndex(index);
0111 
0112     show();
0113     _textBox->setFocus();
0114     _rightArrowEntersDirFlag = true;
0115 }
0116 
0117 void KrSearchBar::hideBar()
0118 {
0119     resetSearch();
0120     if (_textBox->hasFocus())
0121         _view->widget()->setFocus();
0122     hide();
0123 }
0124 
0125 void KrSearchBar::resetSearch()
0126 {
0127     _textBox->clearEditText();
0128     indicateMatch(true);
0129 }
0130 
0131 // #### protected slots
0132 
0133 void KrSearchBar::onModeChange()
0134 {
0135     if (_currentMode == MODE_FILTER) {
0136         _view->op()->filterSearch(QString(), true); // reset filter
0137     }
0138     _currentMode = static_cast<SearchMode>(_modeBox->currentIndex());
0139 
0140     onSearchChange();
0141 }
0142 
0143 void KrSearchBar::onSearchChange()
0144 {
0145     const QString text = _textBox->currentText();
0146 
0147     switch (_currentMode) {
0148     case MODE_SEARCH: {
0149         const bool anyMatch = _view->op()->searchItem(text, caseSensitive());
0150         indicateMatch(anyMatch);
0151         break;
0152     }
0153     case MODE_SELECT: {
0154         _view->unselectAll();
0155         if (!text.isEmpty()) {
0156             const bool anyMatch = _view->changeSelection(KrQuery(text, caseSensitive()), true);
0157             indicateMatch(anyMatch);
0158         }
0159         break;
0160     }
0161     case MODE_FILTER: {
0162         const bool anyMatch = _view->op()->filterSearch(text, caseSensitive());
0163         indicateMatch(anyMatch);
0164         break;
0165     }
0166     default:
0167         qWarning() << "unexpected search mode: " << _currentMode;
0168     }
0169 
0170     _textBox->setFocus();
0171 }
0172 
0173 void KrSearchBar::saveSearchString()
0174 {
0175     KConfigGroup group(krConfig, "Private");
0176     QStringList lst = group.readEntry("Predefined Selections", QStringList());
0177     QString searchString = _textBox->currentText();
0178     if (lst.indexOf(searchString) != -1) {
0179         // already saved
0180         return;
0181     }
0182 
0183     lst.append(searchString);
0184     group.writeEntry("Predefined Selections", lst);
0185 
0186     _textBox->addItem(searchString);
0187     QToolTip::showText(QCursor::pos(), i18n("Saved search text to history"));
0188 }
0189 
0190 // #### protected
0191 
0192 void KrSearchBar::keyPressEvent(QKeyEvent *event)
0193 {
0194     const bool handled = handleKeyPressEvent(static_cast<QKeyEvent *>(event));
0195     if (handled) {
0196         return;
0197     }
0198 
0199     QWidget::keyPressEvent(event);
0200 }
0201 
0202 bool KrSearchBar::eventFilter(QObject *watched, QEvent *event)
0203 {
0204     // only handle KeyPress events in this method
0205     if (event->type() != QEvent::KeyPress) {
0206         return false;
0207     }
0208 
0209     qDebug() << "key press event=" << event;
0210 
0211     auto *ke = static_cast<QKeyEvent *>(event);
0212     auto modifiers = ke->modifiers();
0213 
0214     if (watched == _view->widget()) {
0215         KConfigGroup grpSv(krConfig, "Look&Feel");
0216         const bool autoShow = grpSv.readEntry("New Style Quicksearch", _NewStyleQuicksearch);
0217 
0218         if (isHidden() && !autoShow) {
0219             return false;
0220         }
0221 
0222         if (!isHidden()) {
0223             // view widget has focus but search bar is open and may wants to steal key events
0224             const bool handled = handleKeyPressEvent(ke);
0225             if (handled) {
0226                 return true;
0227             }
0228         }
0229 
0230         if (isHidden() ||
0231             // view can handle its own event if user does not want to remove text or...
0232             !((ke->key() == Qt::Key_Backspace && !_textBox->currentText().isEmpty()) ||
0233               // ...insert space in search bar (even if not focused)
0234               (ke->key() == Qt::Key_Space && _currentMode == KrSearchBar::MODE_SEARCH))) {
0235             const bool handled = _view->handleKeyEvent(ke);
0236             if (handled) {
0237                 return true;
0238             }
0239         }
0240 
0241         if (ke->text().isEmpty() || (modifiers != Qt::NoModifier && modifiers != Qt::ShiftModifier && modifiers != Qt::KeypadModifier)) {
0242             return false;
0243         }
0244 
0245         // start searching if bar is hidden?
0246         if (isHidden()) {
0247             if (autoShow) {
0248                 showBar();
0249             } else {
0250                 return false;
0251             }
0252         }
0253 
0254         // bar is visible and gets the key input
0255         _textBox->setFocus();
0256         if (ke->key() == Qt::Key_Backspace) {
0257             _textBox->lineEdit()->backspace();
0258         } else {
0259             _textBox->setEditText(_textBox->currentText().append(ke->text()));
0260         }
0261         return true;
0262     } else if (watched == _textBox) {
0263         const bool handled = handleKeyPressEvent(ke);
0264         if (handled) {
0265             _view->widget()->setFocus();
0266             return true;
0267         }
0268         // allow the view to handle (most) key events from the text box
0269         if ((modifiers == Qt::NoModifier || modifiers == Qt::KeypadModifier) && ke->key() != Qt::Key_Space && ke->key() != Qt::Key_Backspace
0270             && ke->key() != Qt::Key_Left && ke->key() != Qt::Key_Right) {
0271             const bool handled = _view->handleKeyEvent(ke);
0272             if (handled) {
0273                 _view->widget()->setFocus();
0274                 return true;
0275             }
0276         }
0277     }
0278     return false;
0279 }
0280 
0281 // #### private
0282 
0283 bool KrSearchBar::handleKeyPressEvent(QKeyEvent *ke)
0284 {
0285     auto modifiers = ke->modifiers();
0286     if (!(modifiers == Qt::NoModifier || modifiers == Qt::KeypadModifier)) {
0287         return false;
0288     }
0289 
0290     switch (ke->key()) {
0291     case Qt::Key_Escape: {
0292         hideBar();
0293         return true;
0294     }
0295 
0296     case Qt::Key_Enter:
0297     case Qt::Key_Return: {
0298         hideBarIfSearching();
0299         return false;
0300     }
0301 
0302     case Qt::Key_Up:
0303         return handleUpDownKeyPress(true);
0304     case Qt::Key_Down:
0305         return handleUpDownKeyPress(false);
0306     case Qt::Key_Left:
0307     case Qt::Key_Right:
0308         return handleLeftRightKeyPress(ke);
0309     case Qt::Key_Insert: {
0310         // select current item and jump to next search result
0311         KrViewItem *item = _view->getCurrentKrViewItem();
0312         if (item) {
0313             item->setSelected(!item->isSelected());
0314             _view->op()->searchItem(_textBox->currentText(), caseSensitive(), 1);
0315         }
0316         return true;
0317     }
0318     case Qt::Key_Home: {
0319         // jump to first search result
0320         KrViewItem *item = _view->getLast();
0321         if (item) {
0322             _view->setCurrentKrViewItem(_view->getLast());
0323             _view->op()->searchItem(_textBox->currentText(), caseSensitive(), 1);
0324         }
0325         return true;
0326     }
0327     case Qt::Key_End: {
0328         // jump to last search result
0329         KrViewItem *item = _view->getFirst();
0330         if (item) {
0331             _view->setCurrentKrViewItem(_view->getFirst());
0332             _view->op()->searchItem(_textBox->currentText(), caseSensitive(), -1);
0333         }
0334         return true;
0335     }
0336     }
0337     return false;
0338 }
0339 
0340 bool KrSearchBar::handleUpDownKeyPress(bool up)
0341 {
0342     if (_currentMode != MODE_SEARCH) {
0343         return false;
0344     }
0345 
0346     const bool updownCancel = KConfigGroup(krConfig, "Look&Feel").readEntry("Up/Down Cancels Quicksearch", false);
0347     if (updownCancel) {
0348         hideBar();
0349         return false;
0350     }
0351 
0352     const bool anyMatch = _view->op()->searchItem(_textBox->currentText(), caseSensitive(), up ? -1 : 1);
0353     indicateMatch(anyMatch);
0354     return true;
0355 }
0356 
0357 bool KrSearchBar::handleLeftRightKeyPress(QKeyEvent *ke)
0358 {
0359     const bool useQuickDirectoryNavigation = KConfigGroup(krConfig, "Look&Feel").readEntry("Navigation with Right Arrow Quicksearch", true);
0360     if (!useQuickDirectoryNavigation)
0361         return false;
0362 
0363     const bool isRight = ke->key() == Qt::Key_Right;
0364 
0365     if (isRight && _rightArrowEntersDirFlag) {
0366         // in case the Right Arrow has been pressed when cursor is in the end of the line
0367         if (_textBox->cursorPosition() == _textBox->currentText().length())
0368             // we let the view enter the directory if it's selected
0369             return _view->handleKeyEvent(ke);
0370     } else {
0371         _rightArrowEntersDirFlag = false;
0372     }
0373 
0374     return false;
0375 }
0376 
0377 void KrSearchBar::indicateMatch(bool anyMatch)
0378 {
0379     KConfigGroup gc(krConfig, "Colors");
0380     QPalette p = QGuiApplication::palette();
0381     QString foreground, background;
0382     if (anyMatch) {
0383         foreground = "Quicksearch Match Foreground";
0384         background = "Quicksearch Match Background";
0385     } else {
0386         foreground = "Quicksearch Non-match Foreground";
0387         background = "Quicksearch Non-match Background";
0388     }
0389 
0390     QColor fore = Qt::black;
0391     QString foreSetting = gc.readEntry(foreground, QString());
0392     if (foreSetting == "KDE default") {
0393         fore = p.color(QPalette::Active, QPalette::Text);
0394     } else if (!foreSetting.isEmpty()) {
0395         fore = gc.readEntry(foreground, fore);
0396     }
0397 
0398     QColor back = anyMatch ? QColor(192, 255, 192) : QColor(255, 192, 192);
0399     QString backSetting = gc.readEntry(background, QString());
0400     if (backSetting == "KDE default") {
0401         back = p.color(QPalette::Active, QPalette::Base);
0402     } else if (!backSetting.isEmpty()) {
0403         back = gc.readEntry(background, back);
0404     }
0405 
0406     QPalette pal = palette();
0407     pal.setColor(QPalette::Base, back);
0408     pal.setColor(QPalette::Text, fore);
0409     _textBox->lineEdit()->setPalette(pal);
0410 }
0411 
0412 bool KrSearchBar::caseSensitive()
0413 {
0414     KConfigGroup grpSvr(krConfig, "Look&Feel");
0415     return grpSvr.readEntry("Case Sensitive Quicksearch", _CaseSensitiveQuicksearch);
0416 }