diff options
Diffstat (limited to 'src')
39 files changed, 3280 insertions, 1587 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6e52772e0..4e6e10f0d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,6 +2,7 @@ include(ECMAddAppIcon) set(ADMIN_WORKER_PACKAGE_NAME "kio-admin") set(FILELIGHT_PACKAGE_NAME "filelight") +set(KFIND_PACKAGE_NAME "kfind") configure_file(config-dolphin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-dolphin.h) @@ -304,9 +305,16 @@ target_sources(dolphinstatic PRIVATE panels/folders/treeviewcontextmenu.cpp panels/folders/folderspanel.cpp panels/terminal/terminalpanel.cpp - search/dolphinfacetswidget.cpp + search/bar.cpp + search/barsecondrowflowlayout.cpp + search/chip.cpp search/dolphinquery.cpp - search/dolphinsearchbox.cpp + search/popup.cpp + search/selectors/dateselector.cpp + search/selectors/filetypeselector.cpp + search/selectors/minimumratingselector.cpp + search/selectors/tagsselector.cpp + search/widgetmenu.cpp selectionmode/actiontexthelper.cpp selectionmode/actionwithwidget.cpp selectionmode/backgroundcolorhelper.cpp @@ -365,9 +373,16 @@ target_sources(dolphinstatic PRIVATE panels/folders/treeviewcontextmenu.h panels/folders/folderspanel.h panels/terminal/terminalpanel.h - search/dolphinfacetswidget.h + search/bar.h + search/barsecondrowflowlayout.h + search/chip.h search/dolphinquery.h - search/dolphinsearchbox.h + search/popup.h + search/selectors/dateselector.h + search/selectors/filetypeselector.h + search/selectors/minimumratingselector.h + search/selectors/tagsselector.h + search/widgetmenu.h selectionmode/actiontexthelper.h selectionmode/actionwithwidget.h selectionmode/backgroundcolorhelper.h diff --git a/src/config-dolphin.h.cmake b/src/config-dolphin.h.cmake index 871adc970..b7d6c35f7 100644 --- a/src/config-dolphin.h.cmake +++ b/src/config-dolphin.h.cmake @@ -5,8 +5,13 @@ #cmakedefine01 HAVE_TERMINAL #cmakedefine01 HAVE_X11 +#define KDE_INSTALL_FULL_DATADIR "${KDE_INSTALL_FULL_DATADIR}" + /** The name of the package that needs to be installed so URLs starting with "admin:" can be opened in Dolphin. */ #cmakedefine ADMIN_WORKER_PACKAGE_NAME "@ADMIN_WORKER_PACKAGE_NAME@" /** The name of the KDE Filelight package. */ #cmakedefine FILELIGHT_PACKAGE_NAME "@FILELIGHT_PACKAGE_NAME@" + +/** The name of the KFind package. */ +#cmakedefine KFIND_PACKAGE_NAME "@KFIND_PACKAGE_NAME@" diff --git a/src/dolphinmainwindow.cpp b/src/dolphinmainwindow.cpp index 573582bdd..12b8be955 100644 --- a/src/dolphinmainwindow.cpp +++ b/src/dolphinmainwindow.cpp @@ -26,6 +26,7 @@ #include "panels/folders/folderspanel.h" #include "panels/places/placespanel.h" #include "panels/terminal/terminalpanel.h" +#include "search/dolphinquery.h" #include "selectionmode/actiontexthelper.h" #include "settings/dolphinsettingsdialog.h" #include "statusbar/dolphinstatusbar.h" @@ -404,9 +405,9 @@ void DolphinMainWindow::slotTerminalDirectoryChanged(const QUrl &url) m_tearDownFromPlacesRequested = false; } - m_activeViewContainer->setAutoGrabFocus(false); + m_activeViewContainer->setGrabFocusOnUrlChange(false); changeUrl(url); - m_activeViewContainer->setAutoGrabFocus(true); + m_activeViewContainer->setGrabFocusOnUrlChange(true); } void DolphinMainWindow::slotEditableStateChanged(bool editable) @@ -487,7 +488,7 @@ void DolphinMainWindow::addToPlaces() } if (url.isValid()) { QString icon; - if (m_activeViewContainer->isSearchModeEnabled()) { + if (isSearchUrl(url)) { icon = QStringLiteral("folder-saved-search-symbolic"); } else { icon = KIO::iconNameForUrl(url); @@ -892,13 +893,14 @@ void DolphinMainWindow::paste() void DolphinMainWindow::find() { - m_activeViewContainer->setSearchModeEnabled(true); + m_activeViewContainer->setSearchBarVisible(true); + m_activeViewContainer->setFocusToSearchBar(); } void DolphinMainWindow::updateSearchAction() { QAction *toggleSearchAction = actionCollection()->action(QStringLiteral("toggle_search")); - toggleSearchAction->setChecked(m_activeViewContainer->isSearchModeEnabled()); + toggleSearchAction->setChecked(m_activeViewContainer->isSearchBarVisible()); } void DolphinMainWindow::updatePasteAction() @@ -935,9 +937,13 @@ QAction *DolphinMainWindow::urlNavigatorHistoryAction(const KUrlNavigator *urlNa { const QUrl url = urlNavigator->locationUrl(historyIndex); - QString text = url.toDisplayString(QUrl::PreferLocalFile); + QString text; - if (!urlNavigator->showFullPath()) { + if (isSearchUrl(url)) { + text = Search::DolphinQuery(url, QUrl{}).title(); + } else if (urlNavigator->showFullPath()) { + text = url.toDisplayString(QUrl::PreferLocalFile); + } else { const KFilePlacesModel *placesModel = DolphinPlacesModelSingleton::instance().placesModel(); const QModelIndex closestIdx = placesModel->closestItem(url); @@ -1604,9 +1610,6 @@ void DolphinMainWindow::activeViewChanged(DolphinViewContainer *viewContainer) m_activeViewContainer = viewContainer; if (oldViewContainer) { - const QAction *toggleSearchAction = actionCollection()->action(QStringLiteral("toggle_search")); - toggleSearchAction->disconnect(oldViewContainer); - // Disconnect all signals between the old view container (container, // view and url navigator) and main window. oldViewContainer->disconnect(this); @@ -1861,9 +1864,7 @@ void DolphinMainWindow::setupActions() "<para>This helps you " "find files and folders by opening a <emphasis>search bar</emphasis>. " "There you can enter search terms and specify settings to find the " - "items you are looking for.</para><para>Use this help again on " - "the search bar so we can have a look at it while the settings are " - "explained.</para>")); + "items you are looking for.</para>")); // toggle_search acts as a copy of the main searchAction to be used mainly // in the toolbar, with no default shortcut attached, to avoid messing with @@ -1875,6 +1876,13 @@ void DolphinMainWindow::setupActions() toggleSearchAction->setToolTip(searchAction->toolTip()); toggleSearchAction->setWhatsThis(searchAction->whatsThis()); toggleSearchAction->setCheckable(true); + connect(toggleSearchAction, &QAction::triggered, this, [this](bool checked) { + if (checked) { + find(); + } else { + m_activeViewContainer->setSearchBarVisible(false); + } + }); QAction *toggleSelectionModeAction = actionCollection()->addAction(QStringLiteral("toggle_selection_mode")); // i18n: This action toggles a selection mode. @@ -2641,14 +2649,11 @@ void DolphinMainWindow::connectViewSignals(DolphinViewContainer *container) connect(container, &DolphinViewContainer::showFilterBarChanged, this, &DolphinMainWindow::updateFilterBarAction); connect(container, &DolphinViewContainer::writeStateChanged, this, &DolphinMainWindow::slotWriteStateChanged); slotWriteStateChanged(container->view()->isFolderWritable()); - connect(container, &DolphinViewContainer::searchModeEnabledChanged, this, &DolphinMainWindow::updateSearchAction); + connect(container, &DolphinViewContainer::searchBarVisibilityChanged, this, &DolphinMainWindow::updateSearchAction); connect(container, &DolphinViewContainer::captionChanged, this, &DolphinMainWindow::updateWindowTitle); connect(container, &DolphinViewContainer::tabRequested, this, &DolphinMainWindow::openNewTab); connect(container, &DolphinViewContainer::activeTabRequested, this, &DolphinMainWindow::openNewTabAndActivate); - const QAction *toggleSearchAction = actionCollection()->action(QStringLiteral("toggle_search")); - connect(toggleSearchAction, &QAction::triggered, container, &DolphinViewContainer::setSearchModeEnabled); - // Make the toggled state of the selection mode actions visually follow the selection mode state of the view. auto toggleSelectionModeAction = actionCollection()->action(QStringLiteral("toggle_selection_mode")); toggleSelectionModeAction->setChecked(m_activeViewContainer->isSelectionModeEnabled()); diff --git a/src/dolphinrecenttabsmenu.cpp b/src/dolphinrecenttabsmenu.cpp index 74aaf232e..d9b103c0c 100644 --- a/src/dolphinrecenttabsmenu.cpp +++ b/src/dolphinrecenttabsmenu.cpp @@ -32,12 +32,8 @@ DolphinRecentTabsMenu::DolphinRecentTabsMenu(QObject *parent) void DolphinRecentTabsMenu::rememberClosedTab(const QUrl &url, const QByteArray &state) { QAction *action = new QAction(menu()); - if (DolphinQuery::supportsScheme(url.scheme())) { - const DolphinQuery query = DolphinQuery::fromSearchUrl(url); - action->setText(i18n("Search for %1 in %2", query.text(), query.includeFolder())); - } else if (url.scheme() == QLatin1String("filenamesearch")) { - const QUrlQuery query(url); - action->setText(i18n("Search for %1 in %2", query.queryItemValue(QStringLiteral("search")), query.queryItemValue(QStringLiteral("url")))); + if (Search::isSupportedSearchScheme(url.scheme())) { + action->setText(Search::DolphinQuery{url, QUrl{}}.title()); } else { action->setText(url.path()); } diff --git a/src/dolphinviewcontainer.cpp b/src/dolphinviewcontainer.cpp index dcf159cda..6d08c47c7 100644 --- a/src/dolphinviewcontainer.cpp +++ b/src/dolphinviewcontainer.cpp @@ -18,7 +18,7 @@ #include "filterbar/filterbar.h" #include "global.h" #include "kitemviews/kitemlistcontainer.h" -#include "search/dolphinsearchbox.h" +#include "search/bar.h" #include "selectionmode/topbar.h" #include "statusbar/dolphinstatusbar.h" @@ -49,9 +49,14 @@ #include <QUrl> #include <QUrlQuery> +bool isSearchUrl(const QUrl &url) +{ + return url.scheme().contains(QLatin1String("search")); +} + // An overview of the widgets contained by this ViewContainer struct LayoutStructure { - int searchBox = 0; + int searchBar = 0; int adminBar = 1; int messageWidget = 2; int selectionModeTopBar = 3; @@ -67,7 +72,7 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent) , m_topLayout(nullptr) , m_urlNavigator{new DolphinUrlNavigator(url)} , m_urlNavigatorConnected{nullptr} - , m_searchBox(nullptr) + , m_searchBar(nullptr) , m_searchModeEnabled(false) , m_adminBar{nullptr} , m_authorizeToEnterFolderAction{nullptr} @@ -79,7 +84,7 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent) , m_statusBar(nullptr) , m_statusBarTimer(nullptr) , m_statusBarTimestamp() - , m_autoGrabFocus(true) + , m_grabFocusOnUrlChange{true} { hide(); @@ -87,26 +92,6 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent) m_topLayout->setSpacing(0); m_topLayout->setContentsMargins(0, 0, 0, 0); - m_searchBox = new DolphinSearchBox(this); - m_searchBox->setVisible(false, WithoutAnimation); - connect(m_searchBox, &DolphinSearchBox::activated, this, &DolphinViewContainer::activate); - connect(m_searchBox, &DolphinSearchBox::openRequest, this, &DolphinViewContainer::openSearchBox); - connect(m_searchBox, &DolphinSearchBox::closeRequest, this, &DolphinViewContainer::closeSearchBox); - connect(m_searchBox, &DolphinSearchBox::searchRequest, this, &DolphinViewContainer::startSearching); - connect(m_searchBox, &DolphinSearchBox::focusViewRequest, this, &DolphinViewContainer::requestFocus); - m_searchBox->setWhatsThis(xi18nc("@info:whatsthis findbar", - "<para>This helps you find files and folders. Enter a <emphasis>" - "search term</emphasis> and specify search settings with the " - "buttons at the bottom:<list><item>Filename/Content: " - "Does the item you are looking for contain the search terms " - "within its filename or its contents?<nl/>The contents of images, " - "audio files and videos will not be searched.</item><item>" - "From Here/Everywhere: Do you want to search in this " - "folder and its sub-folders or everywhere?</item><item>" - "More Options: Click this to search by media type, access " - "time or rating.</item><item>More Search Tools: Install other " - "means to find an item.</item></list></para>")); - m_messageWidget = new KMessageWidget(this); m_messageWidget->setCloseButtonVisible(true); m_messageWidget->setPosition(KMessageWidget::Header); @@ -188,7 +173,6 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent) KIO::FileUndoManager *undoManager = KIO::FileUndoManager::self(); connect(undoManager, &KIO::FileUndoManager::jobRecordingFinished, this, &DolphinViewContainer::delayedStatusBarUpdate); - m_topLayout->addWidget(m_searchBox, positionFor.searchBox, 0); m_topLayout->addWidget(m_messageWidget, positionFor.messageWidget, 0); m_topLayout->addWidget(m_view, positionFor.view, 0); m_topLayout->addWidget(m_filterBar, positionFor.filterBar, 0); @@ -211,7 +195,7 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent) }); m_statusBar->setHidden(false); - setSearchModeEnabled(isSearchUrl(url)); + setSearchBarVisible(isSearchUrl(url)); // Update view as the ContentDisplaySettings change // this happens here and not in DolphinView as DolphinviewContainer and DolphinView are not in the same build target ATM @@ -222,8 +206,6 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent) connect(placesModel, &KFilePlacesModel::rowsInserted, this, &DolphinViewContainer::slotPlacesModelChanged); connect(placesModel, &KFilePlacesModel::rowsRemoved, this, &DolphinViewContainer::slotPlacesModelChanged); - connect(this, &DolphinViewContainer::searchModeEnabledChanged, this, &DolphinViewContainer::captionChanged); - QApplication::instance()->installEventFilter(this); } @@ -243,7 +225,6 @@ KFileItem DolphinViewContainer::rootItem() const void DolphinViewContainer::setActive(bool active) { - m_searchBox->setActive(active); if (m_urlNavigatorConnected) { m_urlNavigatorConnected->setActive(active); } @@ -255,19 +236,9 @@ bool DolphinViewContainer::isActive() const return m_view->isActive(); } -void DolphinViewContainer::setAutoGrabFocus(bool grab) +void DolphinViewContainer::setGrabFocusOnUrlChange(bool grabFocus) { - m_autoGrabFocus = grab; -} - -bool DolphinViewContainer::autoGrabFocus() const -{ - return m_autoGrabFocus; -} - -QString DolphinViewContainer::currentSearchText() const -{ - return m_searchBox->text(); + m_grabFocusOnUrlChange = grabFocus; } const DolphinStatusBar *DolphinViewContainer::statusBar() const @@ -359,6 +330,79 @@ void DolphinViewContainer::disconnectUrlNavigator() m_urlNavigatorConnected = nullptr; } +void DolphinViewContainer::setSearchBarVisible(bool visible) +{ + if (!visible) { + if (isSearchBarVisible()) { + m_searchBar->setVisible(false, WithAnimation); + } + return; + } + + if (!m_searchBar) { + m_searchBar = new Search::Bar(std::make_shared<const Search::DolphinQuery>(m_urlNavigator->locationUrl(), QUrl{} /** will be set below. */), this); + connect(m_searchBar, &Search::Bar::urlChangeRequested, this, [this](const QUrl &url) { + m_view->setViewPropertiesContext(isSearchUrl(url) ? QStringLiteral("search") : QString()); + setGrabFocusOnUrlChange(false); // Prevent loss of focus while typing or refining a search. + setUrl(url); + setGrabFocusOnUrlChange(true); + }); + connect(m_searchBar, &Search::Bar::focusViewRequest, this, &DolphinViewContainer::requestFocus); + connect(m_searchBar, &Search::Bar::showMessage, this, [this](const QString &message, KMessageWidget::MessageType messageType) { + showMessage(message, messageType); + }); + connect(m_searchBar, + &Search::Bar::showInstallationProgress, + m_statusBar, + [this](const QString ¤tlyRunningTaskTitle, int installationProgressPercent) { + m_statusBar->showProgress(currentlyRunningTaskTitle, installationProgressPercent, DolphinStatusBar::CancelLoading::Disallowed); + }); + connect(m_searchBar, &Search::Bar::visibilityChanged, this, &DolphinViewContainer::searchBarVisibilityChanged); + m_topLayout->addWidget(m_searchBar, positionFor.searchBar, 0); + } + + m_searchBar->setVisible(true, WithAnimation); + + // The Search::Bar has been set visible but its state does not yet match with this view container or view. + // The view might for example already be searching because it was opened with a search URL. The Search::Bar needs to be updated to show the parameters of + // that search. And even if there is no search URL loaded in the view currently, we still need to figure out where the Search::Bar should be searching if + // the user starts a search from there. Let's figure out the search location in this method and let the DolphinQuery constructor figure out the rest from + // the current m_urlNavigator->locationUrl(). + for (int i = m_urlNavigator->historyIndex(); i < m_urlNavigator->historySize(); i++) { + QUrl url = m_urlNavigator->locationUrl(i); + if (isSearchUrl(url)) { + // The previous location was a search URL. Try to see if that search URL has a valid search path so we keep searching in the same location. + const auto searchPath = Search::DolphinQuery(url, QUrl{}).searchPath(); // DolphinQuery is great at extracting the search path from a search URL. + if (searchPath.isValid()) { + m_searchBar->updateStateToMatch(std::make_shared<const Search::DolphinQuery>(m_urlNavigator->locationUrl(), searchPath)); + return; + } + } else if (url.scheme() == QLatin1String("tags")) { + continue; // We avoid setting a tags url as the backup search path because a DolphinQuery constructed from a tags url will already search tagged + // items everywhere. + } else { + m_searchBar->updateStateToMatch(std::make_shared<const Search::DolphinQuery>(m_urlNavigator->locationUrl(), url)); + return; + } + } + // We could not find any URL fit for searching in the history. This might happen because this view only ever loaded a search which searches "Everywhere" + // and therefore there is no specific search path to choose from. But the Search::Bar *needs* to know a search path because the user might switch from + // searching "Everywhere" to "Here" and it is everybody's guess what "Here" is supposed to mean in that context… We'll simply fall back to the user's home + // path for lack of a better option. + m_searchBar->updateStateToMatch(std::make_shared<const Search::DolphinQuery>(m_urlNavigator->locationUrl(), QUrl::fromUserInput(QDir::homePath()))); +} + +bool DolphinViewContainer::isSearchBarVisible() const +{ + return m_searchBar && m_searchBar->isVisible() && m_searchBar->isEnabled(); +} + +void DolphinViewContainer::setFocusToSearchBar() +{ + Q_ASSERT(isSearchBarVisible()); + m_searchBar->selectAll(); +} + void DolphinViewContainer::setSelectionModeEnabled(bool enabled, KActionCollection *actionCollection, SelectionMode::BottomBar::Contents bottomBarContents) { const bool wasEnabled = m_view->selectionMode(); @@ -496,53 +540,12 @@ bool DolphinViewContainer::isFilterBarVisible() const return m_filterBar->isEnabled(); // Gets disabled in AnimatedHeightWidget while animating towards a hidden state. } -void DolphinViewContainer::setSearchModeEnabled(bool enabled) -{ - m_searchBox->setVisible(enabled, WithAnimation); - - if (enabled) { - const QUrl &locationUrl = m_urlNavigator->locationUrl(); - m_searchBox->fromSearchUrl(locationUrl); - } - - if (enabled == isSearchModeEnabled()) { - if (enabled && !m_searchBox->hasFocus()) { - m_searchBox->setFocus(); - m_searchBox->selectAll(); - } - return; - } - - if (!enabled) { - m_view->setViewPropertiesContext(QString()); - - // Restore the URL for the URL navigator. If Dolphin has been - // started with a search-URL, the home URL is used as fallback. - QUrl url = m_searchBox->searchPath(); - if (url.isEmpty() || !url.isValid() || isSearchUrl(url)) { - url = Dolphin::homeUrl(); - } - if (m_urlNavigatorConnected) { - m_urlNavigatorConnected->setLocationUrl(url); - } - } - - m_searchModeEnabled = enabled; - - Q_EMIT searchModeEnabledChanged(enabled); -} - -bool DolphinViewContainer::isSearchModeEnabled() const -{ - return m_searchModeEnabled; -} - QString DolphinViewContainer::placesText() const { QString text; - if (isSearchModeEnabled()) { - text = i18n("Search for %1 in %2", m_searchBox->text(), m_searchBox->searchPath().fileName()); + if (isSearchBarVisible() && m_searchBar->isSearchConfigured()) { + text = m_searchBar->queryTitle(); } else { text = url().adjusted(QUrl::StripTrailingSlash).fileName(); if (text.isEmpty()) { @@ -564,7 +567,7 @@ void DolphinViewContainer::reload() QString DolphinViewContainer::captionWindowTitle() const { - if (GeneralSettings::showFullPathInTitlebar() && !isSearchModeEnabled()) { + if (GeneralSettings::showFullPathInTitlebar() && (!isSearchBarVisible() || !m_searchBar->isSearchConfigured())) { if (!url().isLocalFile()) { return url().adjusted(QUrl::StripTrailingSlash).toString(); } @@ -579,18 +582,14 @@ QString DolphinViewContainer::caption() const // see KUrlNavigatorPrivate::firstButtonText(). if (url().path().isEmpty() || url().path() == QLatin1Char('/')) { QUrlQuery query(url()); - const QString title = query.queryItemValue(QStringLiteral("title")); + const QString title = query.queryItemValue(QStringLiteral("title"), QUrl::FullyDecoded); if (!title.isEmpty()) { return title; } } - if (isSearchModeEnabled()) { - if (currentSearchText().isEmpty()) { - return i18n("Search"); - } else { - return i18n("Search for %1", currentSearchText()); - } + if (isSearchBarVisible() && m_searchBar->isSearchConfigured()) { + return m_searchBar->queryTitle(); } KFilePlacesModel *placesModel = DolphinPlacesModelSingleton::instance().placesModel(); @@ -628,6 +627,9 @@ void DolphinViewContainer::setUrl(const QUrl &newUrl) { if (newUrl != m_urlNavigator->locationUrl()) { m_urlNavigator->setLocationUrl(newUrl); + if (m_searchBar && !Search::isSupportedSearchScheme(newUrl.scheme())) { + m_searchBar->setSearchPath(newUrl); + } } } @@ -862,13 +864,17 @@ void DolphinViewContainer::slotUrlNavigatorLocationChanged(const QUrl &url) } if (KProtocolManager::supportsListing(url)) { - const bool searchBoxInitialized = isSearchModeEnabled() && m_searchBox->text().isEmpty(); - setSearchModeEnabled(isSearchUrl(url) || searchBoxInitialized); + if (isSearchUrl(url)) { + setSearchBarVisible(true); + } else if (m_searchBar && m_searchBar->isSearchConfigured()) { + // Hide the search bar because it shows an outdated search which the user does not care about anymore. + setSearchBarVisible(false); + } m_view->setUrl(url); tryRestoreViewState(); - if (m_autoGrabFocus && isActive() && !isSearchModeEnabled()) { + if (m_grabFocusOnUrlChange && isActive()) { // When an URL has been entered, the view should get the focus. // The focus must be requested asynchronously, as changing the URL might create // a new view widget. @@ -923,10 +929,7 @@ void DolphinViewContainer::redirect(const QUrl &oldUrl, const QUrl &newUrl) // URL history. m_urlNavigator->saveLocationState(QByteArray()); m_urlNavigator->setLocationUrl(newUrl); - if (m_searchBox->isActive()) { - m_searchBox->setSearchPath(newUrl); - } - setSearchModeEnabled(isSearchUrl(newUrl)); + setSearchBarVisible(isSearchUrl(newUrl)); m_urlNavigator->blockSignals(block); } @@ -936,31 +939,6 @@ void DolphinViewContainer::requestFocus() m_view->setFocus(); } -void DolphinViewContainer::startSearching() -{ - Q_CHECK_PTR(m_urlNavigatorConnected); - const QUrl url = m_searchBox->urlForSearching(); - if (url.isValid() && !url.isEmpty()) { - m_view->setViewPropertiesContext(QStringLiteral("search")); - // If we open a new tab that has a search assigned to it, we can't - // update the urlNavigator, since there is none connected to that tab. - // See BUG:500101 - if (m_urlNavigatorConnected) { - m_urlNavigatorConnected->setLocationUrl(url); - } - } -} - -void DolphinViewContainer::openSearchBox() -{ - setSearchModeEnabled(true); -} - -void DolphinViewContainer::closeSearchBox() -{ - setSearchModeEnabled(false); -} - void DolphinViewContainer::stopDirectoryLoading() { m_view->stopLoading(); @@ -1001,7 +979,7 @@ void DolphinViewContainer::showErrorMessage(const QString &message) void DolphinViewContainer::slotPlacesModelChanged() { - if (!GeneralSettings::showFullPathInTitlebar() && !isSearchModeEnabled()) { + if (!GeneralSettings::showFullPathInTitlebar()) { Q_EMIT captionChanged(); } } @@ -1043,11 +1021,6 @@ void DolphinViewContainer::slotOpenUrlFinished(KJob *job) } } -bool DolphinViewContainer::isSearchUrl(const QUrl &url) const -{ - return url.scheme().contains(QLatin1String("search")); -} - void DolphinViewContainer::saveViewState() { QByteArray locationState; diff --git a/src/dolphinviewcontainer.h b/src/dolphinviewcontainer.h index a509bab3d..e827c0885 100644 --- a/src/dolphinviewcontainer.h +++ b/src/dolphinviewcontainer.h @@ -31,7 +31,10 @@ class FilterBar; class QAction; class QGridLayout; class QUrl; -class DolphinSearchBox; +namespace Search +{ +class Bar; +} class DolphinStatusBar; class KFileItemList; namespace SelectionMode @@ -40,6 +43,11 @@ class TopBar; } /** + * @return True if the URL protocol is a search URL (e. g. baloosearch:// or filenamesearch://). + */ +bool isSearchUrl(const QUrl &url); + +/** * @short Represents a view for the directory content * including the navigation bar, filter bar and status bar. * @@ -78,10 +86,7 @@ public: * as soon as the URL has been changed. Per default the grabbing * of the focus is enabled. */ - void setAutoGrabFocus(bool grab); - bool autoGrabFocus() const; - - QString currentSearchText() const; + void setGrabFocusOnUrlChange(bool grabFocus); const DolphinStatusBar *statusBar() const; DolphinStatusBar *statusBar(); @@ -134,6 +139,28 @@ public: void disconnectUrlNavigator(); /** + * Sets the visibility of this objects search configuration user interface. This search bar is the primary interface in Dolphin to search for files and + * folders. + * + * The signal searchBarVisibilityChanged will be emitted when the new visibility state is different from the old. + * + * Typically an animation will play when the search bar is shown or hidden, so the visibility of the bar will not necessarily match @p visible when this + * method returns. Instead use isSearchBarVisible(), which will always communicate the visibility state the search bar is heading to. + * + * @see Search::Bar. + * @see isSearchBarVisible(). + */ + void setSearchBarVisible(bool visible); + + /** @returns true if the search bar is visible while not being in the process to hide itself. */ + bool isSearchBarVisible() const; + + /** + * Moves keyboard focus to the search bar. The search term is fully selected to allow easy replacing. + */ + void setFocusToSearchBar(); + + /** * Sets a selection mode that is useful for quick and easy selecting or deselecting of files. * This method is the central authority about enabling or disabling selection mode: * All other classes that want to enable or disable selection mode should trigger a call of this method. @@ -166,9 +193,6 @@ public: * false, if it is hidden or currently animating towards a hidden state. */ bool isFilterBarVisible() const; - /** Returns true if the search mode is enabled. */ - bool isSearchModeEnabled() const; - /** * @return Text that should be used for the current URL when creating * a new place. @@ -221,12 +245,6 @@ public Q_SLOTS: */ void setFilterBarVisible(bool visible); - /** - * Enables the search mode, if \p enabled is true. In the search mode the URL navigator - * will be hidden and replaced by a line editor that allows to enter a search term. - */ - void setSearchModeEnabled(bool enabled); - /** Used to notify the m_selectionModeBottomBar that there is no other ViewContainer in the tab. */ void slotSplitTabDisabled(); @@ -236,9 +254,14 @@ Q_SIGNALS: */ void showFilterBarChanged(bool shown); /** - * Is emitted whenever the search mode has changed its state. + * Is emitted whenever a change to the search bar's visibility is invoked. The visibility change might not have actually already taken effect by the time + * this signal is emitted because typically the showing and hiding is animated. + * @param visible The visibility state the search bar is going to end up at. + * @see Search::Bar. + * @see setSearchBarVisible(). + * @see isSearchBarVisible(). */ - void searchModeEnabledChanged(bool enabled); + void searchBarVisibilityChanged(bool visible); void selectionModeChanged(bool enabled); @@ -374,13 +397,6 @@ private Q_SLOTS: void requestFocus(); /** - * Gets the search URL from the searchbox and starts searching. - */ - void startSearching(); - void openSearchBox(); - void closeSearchBox(); - - /** * Stops the loading of a directory. Is connected with the "stopPressed" signal * from the statusbar. */ @@ -412,11 +428,6 @@ private Q_SLOTS: private: /** - * @return True if the URL protocol is a search URL (e. g. baloosearch:// or filenamesearch://). - */ - bool isSearchUrl(const QUrl &url) const; - - /** * Saves the state of the current view: contents position, * root URL, ... */ @@ -463,7 +474,7 @@ private: */ QPointer<DolphinUrlNavigator> m_urlNavigatorConnected; - DolphinSearchBox *m_searchBox; + Search::Bar *m_searchBar; bool m_searchModeEnabled; /// A bar shown at the top of the view to signify that the view is currently viewed and acted on with elevated privileges. @@ -486,7 +497,7 @@ private: DolphinStatusBar *m_statusBar; QTimer *m_statusBarTimer; // Triggers a delayed update QElapsedTimer m_statusBarTimestamp; // Time in ms since last update - bool m_autoGrabFocus; + bool m_grabFocusOnUrlChange; /** * The visual state to be applied to the next UrlNavigator that gets * connected to this ViewContainer. diff --git a/src/global.h b/src/global.h index 9161ed877..81ade47fa 100644 --- a/src/global.h +++ b/src/global.h @@ -47,10 +47,10 @@ QVector<QPair<QSharedPointer<OrgKdeDolphinMainWindowInterface>, QStringList>> do QPair<QString, Qt::SortOrder> sortOrderForUrl(QUrl &url); /** - * TODO: Move this somewhere global to all KDE apps, not just Dolphin + * TODO: Use global KDE spacings instead of Dolphin-specific ones once we have them. */ constexpr int VERTICAL_SPACER_HEIGHT = 12; -constexpr int LAYOUT_SPACING_SMALL = 2; +constexpr int LAYOUT_SPACING_SMALL = 4; } enum Animated { WithAnimation, WithoutAnimation }; diff --git a/src/search/bar.cpp b/src/search/bar.cpp new file mode 100644 index 000000000..18707ef23 --- /dev/null +++ b/src/search/bar.cpp @@ -0,0 +1,356 @@ +/* + SPDX-FileCopyrightText: 2010 Peter Penz <[email protected]> + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "bar.h" +#include "global.h" + +#include "barsecondrowflowlayout.h" +#include "chip.h" +#include "dolphin_searchsettings.h" +#include "dolphinplacesmodelsingleton.h" +#include "dolphinquery.h" +#include "popup.h" +#include "widgetmenu.h" + +#include "config-dolphin.h" +#include <KLocalizedString> + +#include <QApplication> +#include <QHBoxLayout> +#include <QIcon> +#include <QKeyEvent> +#include <QLineEdit> +#include <QMenu> +#include <QScrollArea> +#include <QTimer> +#include <QToolButton> + +using namespace Search; + +namespace +{ +/** + * @see Bar::IsSearchConfigured(). + */ +bool isSearchConfigured(const std::shared_ptr<const DolphinQuery> &searchConfiguration) +{ + return !searchConfiguration->searchTerm().isEmpty() + || (searchConfiguration->searchTool() != SearchTool::Filenamesearch + && (searchConfiguration->fileType() != KFileMetaData::Type::Empty || searchConfiguration->modifiedSinceDate().isValid() + || searchConfiguration->minimumRating() > 0 || !searchConfiguration->requiredTags().isEmpty())); +}; +} + +Bar::Bar(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent) + : AnimatedHeightWidget(parent) + , UpdatableStateInterface{dolphinQuery} +{ + QWidget *contentsContainer = prepareContentsContainer(); + + // Create search box + m_searchTermEditor = new QLineEdit(contentsContainer); + m_searchTermEditor->setClearButtonEnabled(true); + connect(m_searchTermEditor, &QLineEdit::returnPressed, this, &Bar::slotReturnPressed); + connect(m_searchTermEditor, &QLineEdit::textEdited, this, &Bar::slotSearchTermEdited); + setFocusProxy(m_searchTermEditor); + + // Add "Save search" button inside search box + m_saveSearchAction = new QAction(this); + m_saveSearchAction->setIcon(QIcon::fromTheme(QStringLiteral("document-save-symbolic"))); + m_saveSearchAction->setText(i18nc("action:button", "Save this search to quickly access it again in the future")); + m_searchTermEditor->addAction(m_saveSearchAction, QLineEdit::TrailingPosition); + connect(m_saveSearchAction, &QAction::triggered, this, &Bar::slotSaveSearch); + + // Filter button + auto filterButton = new QToolButton(contentsContainer); + filterButton->setIcon(QIcon::fromTheme(QStringLiteral("view-filter"))); + filterButton->setText(i18nc("@action:button for changing search options", "Filter")); + filterButton->setAutoRaise(true); + filterButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + filterButton->setPopupMode(QToolButton::InstantPopup); + filterButton->setAttribute(Qt::WA_CustomWhatsThis); + m_popup = new Popup{m_searchConfiguration, this}; + connect(m_popup, &QMenu::aboutToShow, this, [this]() { + m_popup->updateStateToMatch(m_searchConfiguration); + }); + connect(m_popup, &Popup::configurationChanged, this, &Bar::slotConfigurationChanged); + connect(m_popup, &Popup::showMessage, this, &Bar::showMessage); + connect(m_popup, &Popup::showInstallationProgress, this, &Bar::showInstallationProgress); + filterButton->setMenu(m_popup); + + // Create close button + QToolButton *closeButton = new QToolButton(contentsContainer); + closeButton->setAutoRaise(true); + closeButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); + closeButton->setToolTip(i18nc("@info:tooltip", "Quit searching")); + connect(closeButton, &QToolButton::clicked, this, [this]() { + setVisible(false, WithAnimation); + }); + + // Apply layout for the search input row + QHBoxLayout *firstRowLayout = new QHBoxLayout{}; + firstRowLayout->setContentsMargins(0, 0, 0, 0); + firstRowLayout->addWidget(m_searchTermEditor); + firstRowLayout->addWidget(filterButton); + firstRowLayout->addWidget(closeButton); + + // Create "From Here" and "Your files" buttons + m_fromHereButton = new QToolButton(contentsContainer); + m_fromHereButton->setText(i18nc("action:button search from here", "Here")); + m_fromHereButton->setAutoRaise(true); + m_fromHereButton->setCheckable(true); + connect(m_fromHereButton, &QToolButton::clicked, this, [this]() { + if (m_searchConfiguration->searchLocations() == SearchLocations::FromHere) { + return; // Already selected. + } + SearchSettings::setLocation(QStringLiteral("FromHere")); + SearchSettings::self()->save(); + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setSearchLocations(SearchLocations::FromHere); + slotConfigurationChanged(std::move(searchConfigurationCopy)); + }); + + m_everywhereButton = new QToolButton(contentsContainer); + m_everywhereButton->setText(i18nc("action:button search everywhere", "Everywhere")); + m_everywhereButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + m_everywhereButton->setAutoRaise(true); + m_everywhereButton->setCheckable(true); + connect(m_everywhereButton, &QToolButton::clicked, this, [this]() { + if (m_searchConfiguration->searchLocations() == SearchLocations::Everywhere) { + return; // Already selected. + } + SearchSettings::setLocation(QStringLiteral("Everywhere")); + SearchSettings::self()->save(); + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setSearchLocations(SearchLocations::Everywhere); + slotConfigurationChanged(std::move(searchConfigurationCopy)); + }); + + // Apply layout for the location buttons and chips row + m_secondRowLayout = new BarSecondRowFlowLayout{nullptr}; + m_secondRowLayout->setSpacing(Dolphin::LAYOUT_SPACING_SMALL); + connect(m_secondRowLayout, &BarSecondRowFlowLayout::heightHintChanged, this, [this]() { + if (isEnabled()) { + AnimatedHeightWidget::setVisible(true, WithAnimation); + } + // If this Search::Bar is not enabled we can safely assume that this widget is currently in an animation to hide itself and we do nothing. + }); + m_secondRowLayout->addWidget(m_fromHereButton); + m_secondRowLayout->addWidget(m_everywhereButton); + + m_topLayout = new QVBoxLayout(contentsContainer); + m_topLayout->setContentsMargins(Dolphin::LAYOUT_SPACING_SMALL, Dolphin::LAYOUT_SPACING_SMALL, Dolphin::LAYOUT_SPACING_SMALL, Dolphin::LAYOUT_SPACING_SMALL); + m_topLayout->setSpacing(Dolphin::LAYOUT_SPACING_SMALL); + m_topLayout->addLayout(firstRowLayout); + m_topLayout->addLayout(m_secondRowLayout); + + setWhatsThis(xi18nc( + "@info:whatsthis search bar", + "<para>This helps you find files and folders.<list><item>Enter a <emphasis>search term</emphasis> in the input field.</item><item>Decide where to " + "search by pressing the location buttons below the search field. “Here” refers to the location that was open prior to starting a search, so navigating " + "to a different location first can narrow down the search.</item><item>Press the “%1” button to further refine the manner of searching or the " + "results.</item><item>Press the “Save” icon to add the current search configuration to the <emphasis>Places panel</emphasis>.</item></list></para>", + filterButton->text())); + + // The searching should be started automatically after the user did not change + // the text for a while + m_startSearchTimer = new QTimer(this); + m_startSearchTimer->setSingleShot(true); + m_startSearchTimer->setInterval(500); + connect(m_startSearchTimer, &QTimer::timeout, this, &Bar::commitCurrentConfiguration); + + updateStateToMatch(dolphinQuery); +} + +QString Bar::text() const +{ + return m_searchTermEditor->text(); +} + +void Bar::setSearchPath(const QUrl &url) +{ + if (url == m_searchConfiguration->searchPath()) { + return; + } + + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setSearchPath(url); + updateStateToMatch(std::make_shared<const DolphinQuery>(std::move(searchConfigurationCopy))); +} + +void Bar::selectAll() +{ + m_searchTermEditor->setFocus(); + m_searchTermEditor->selectAll(); +} + +void Bar::setVisible(bool visible, Animated animated) +{ + if (!visible) { + m_startSearchTimer->stop(); + Q_EMIT urlChangeRequested(m_searchConfiguration->searchPath()); + if (isAncestorOf(QApplication::focusWidget())) { + Q_EMIT focusViewRequest(); + } + } + AnimatedHeightWidget::setVisible(visible, animated); + Q_EMIT visibilityChanged(visible); +} + +void Bar::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) +{ + m_searchTermEditor->setText(dolphinQuery->searchTerm()); + // When the Popup is closed users might not know whether they are searching in file names or contents. This can be problematic when users do not find a + // file and then assume it doesn't exist. We consider searching for names matching the search term the default and only show a generic "Search…" text as + // the placeholder then. But when names are not searched we change the placeholder message to make this clear. + m_searchTermEditor->setPlaceholderText(dolphinQuery->searchTool() == SearchTool::Filenamesearch + && dolphinQuery->searchThrough() == SearchThrough::FileContents + ? i18nc("@info:placeholder", "Search in file contents…") + : i18n("Search…")); + m_saveSearchAction->setEnabled(::isSearchConfigured(dolphinQuery)); + m_fromHereButton->setChecked(dolphinQuery->searchLocations() == SearchLocations::FromHere); + m_everywhereButton->setChecked(dolphinQuery->searchLocations() == SearchLocations::Everywhere); + + if (m_popup && m_popup->isVisible()) { + // The user actually sees the popup, so update it now! Normally the popup is only updated when Popup::aboutToShow() is emitted. + m_popup->updateStateToMatch(dolphinQuery); + } + + /// Update tooltip + const QUrl cleanedUrl = dolphinQuery->searchPath().adjusted(QUrl::RemoveUserInfo | QUrl::StripTrailingSlash); + m_fromHereButton->setToolTip( + xi18nc("@info:tooltip", "Limit the search to <filename>%1</filename> and its subfolders.", cleanedUrl.toString(QUrl::PreferLocalFile))); + m_everywhereButton->setToolTip( + dolphinQuery->searchTool() == SearchTool::Filenamesearch + // clang-format off + // clang-format is turned off because we need to make sure the i18n call is in a single row or the i18n comment above will not be extracted. + // See https://commits.kde.org/kxmlgui/a31135046e1b3335b5d7bbbe6aa9a883ce3284c1 + // i18n: The "Everywhere" button makes Dolphin search all files in "/" recursively. "From the root up" is meant to + // communicate this colloquially while containing the technical term "root". It is fine to drop the technicalities here + // and only to communicate that everything in the file system is supposed to be searched here. + ? i18nc("@info:tooltip", "Search all directories from the root up.") + // i18n: Tooltip for "Everywhere" button as opposed to searching for files in specific folders. The search tool uses + // file indexing and will therefore only be able to search through directories which have been put into a data base. + // Please make sure your translation of the path to the Search settings page is identical to translation there. + : xi18nc("@info:tooltip", "Search all indexed locations.<nl/><nl/>Configure which locations are indexed in <interface>System Settings|Workspace|Search</interface>.")); + // clang-format on + + auto updateChip = [this, &dolphinQuery]<typename Selector>(bool shouldExist, Chip<Selector> *chip) -> Chip<Selector> * { + if (shouldExist) { + if (!chip) { + chip = new Chip<Selector>{dolphinQuery, nullptr}; + chip->hide(); + chip->setMaximumHeight(m_fromHereButton->height()); + connect(chip, &ChipBase::configurationChanged, this, &Bar::slotConfigurationChanged); + m_secondRowLayout->addWidget(chip); // Transfers ownership + chip->show(); // Only showing the chip after it was added to the correct layout avoids a bug which shows the chip at the top of the bar. + } else { + chip->updateStateToMatch(dolphinQuery); + } + return chip; + } + if (chip) { + chip->deleteLater(); + } + return nullptr; + }; + + m_fileTypeSelectorChip = updateChip.template operator()<FileTypeSelector>(dolphinQuery->searchTool() != SearchTool::Filenamesearch + && dolphinQuery->fileType() != KFileMetaData::Type::Empty, + m_fileTypeSelectorChip); + m_modifiedSinceDateSelectorChip = + updateChip.template operator()<DateSelector>(dolphinQuery->searchTool() != SearchTool::Filenamesearch && dolphinQuery->modifiedSinceDate().isValid(), + m_modifiedSinceDateSelectorChip); + m_minimumRatingSelectorChip = + updateChip.template operator()<MinimumRatingSelector>(dolphinQuery->searchTool() != SearchTool::Filenamesearch && dolphinQuery->minimumRating() > 0, + m_minimumRatingSelectorChip); + m_requiredTagsSelectorChip = + updateChip.template operator()<TagsSelector>(dolphinQuery->searchTool() != SearchTool::Filenamesearch && dolphinQuery->requiredTags().count(), + m_requiredTagsSelectorChip); +} + +void Bar::keyPressEvent(QKeyEvent *event) +{ + QWidget::keyReleaseEvent(event); + if (event->key() == Qt::Key_Escape) { + if (m_searchTermEditor->text().isEmpty()) { + setVisible(false, WithAnimation); + } else { + // Clear the text input + slotSearchTermEdited(QString()); + } + } +} + +void Bar::keyReleaseEvent(QKeyEvent *event) +{ + QWidget::keyReleaseEvent(event); + if (event->key() == Qt::Key_Down) { + Q_EMIT focusViewRequest(); + } +} + +void Bar::slotConfigurationChanged(const DolphinQuery &searchConfiguration) +{ + Q_ASSERT_X(*m_searchConfiguration != searchConfiguration, "Bar::updateState()", "Redundantly updating to a state that is identical to the previous state."); + updateStateToMatch(std::make_shared<const DolphinQuery>(searchConfiguration)); + + commitCurrentConfiguration(); +} + +void Bar::slotSearchTermEdited(const QString &text) +{ + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setSearchTerm(text); + updateStateToMatch(std::make_shared<const DolphinQuery>(searchConfigurationCopy)); + + m_startSearchTimer->start(); +} + +void Bar::slotReturnPressed() +{ + commitCurrentConfiguration(); + Q_EMIT focusViewRequest(); +} + +void Bar::commitCurrentConfiguration() +{ + m_startSearchTimer->stop(); + // We return early and avoid searching when the user has not given any information we can search for. They might for example have deleted the search term. + // In that case we want to show the files of the normal location again. + if (!isSearchConfigured()) { + Q_EMIT urlChangeRequested(m_searchConfiguration->searchPath()); + return; + } + Q_EMIT urlChangeRequested(m_searchConfiguration->toUrl()); +} + +void Bar::slotSaveSearch() +{ + Q_ASSERT_X(isSearchConfigured(), + "Search::Bar::slotSaveSearch()", + "Search::Bar::isSearchConfigured() considers this search invalid, so the user should not be able to save this search. The button to save should " + "be disabled."); + const QUrl searchUrl = m_searchConfiguration->toUrl(); + Q_ASSERT(searchUrl.isValid() && isSupportedSearchScheme(searchUrl.scheme())); + DolphinPlacesModelSingleton::instance().placesModel()->addPlace(m_searchConfiguration->title(), searchUrl, QStringLiteral("folder-saved-search-symbolic")); +} + +bool Bar::isSearchConfigured() const +{ + return ::isSearchConfigured(m_searchConfiguration); +} + +QString Bar::queryTitle() const +{ + return m_searchConfiguration->title(); +} + +int Bar::preferredHeight() const +{ + return m_secondRowLayout->geometry().y() + m_secondRowLayout->sizeHint().height() + Dolphin::LAYOUT_SPACING_SMALL; +} diff --git a/src/search/bar.h b/src/search/bar.h new file mode 100644 index 000000000..e681b8a6e --- /dev/null +++ b/src/search/bar.h @@ -0,0 +1,206 @@ +/* + SPDX-FileCopyrightText: 2010 Peter Penz <[email protected]> + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef SEARCHBAR_H +#define SEARCHBAR_H + +#include "animatedheightwidget.h" +#include "dolphinquery.h" +#include "updatablestateinterface.h" + +#include <KMessageWidget> + +#include <QUrl> + +class DolphinSearchBarTest; +class QHBoxLayout; +class QLineEdit; +class QToolButton; +class QVBoxLayout; + +namespace Search +{ +class BarSecondRowFlowLayout; +template<class Selector> +class Chip; +class DateSelector; +class FileTypeSelector; +class MinimumRatingSelector; +class Popup; +class TagsSelector; + +/** + * @brief User interface for searching files and folders. + * + * This Bar is both for configuring a new search as well as showing the search parameter of any search URL opened in Dolphin. + * There are many search parameters whose availability can depend on various conditions. Those include: + * - Where to search: Everywhere or below the current directory + * - What to search: Filenames or content + * - How to search: Which search tool to use + * - etc. + * + * The class which defines the state of this Bar and its children is DolphinQuery. + * @see DolphinQuery. + */ +class Bar : public AnimatedHeightWidget, public UpdatableStateInterface +{ + Q_OBJECT + +public: + /** + * @brief Constructs a Search::Bar with an initial state matching @p dolphinQuery and with parent @p parent. + */ + explicit Bar(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr); + + /** + * Returns the text that should be used as input + * for searching. + */ + QString text() const; + + /** + * Sets the current path that is used as root for searching files. + * If @url is the Home dir, "From Here" is selected instead. + */ + void setSearchPath(const QUrl &url); + + /** + * Selects the whole text of the search box. + */ + void selectAll(); + + /** + * All showing and hiding of this bar is supposed to go through this method. When hiding this bar, it emits all the necessary signals to restore the view + * container to a non-search URL. + * This method also aims to make sure that visibilityChanged() will be emitted no matter from where setVisible() is called. This way the "Find" action can + * be properly un/checked. + * @see AnimatedHeightWidget::setVisible(). + */ + void setVisible(bool visible, Animated animated); + + /** + * @returns false, when the search UI has not yet been changed to search for anything specific. For example when no search term has been entered yet. + * Otherwise returns true, for example when a search term has been entered or there is a search request for all files of a specific file type or + * with a specific modification date. + */ + bool isSearchConfigured() const; + + /** + * @returns the title for the search that is currently configured in this bar. + * @see DolphinQuery::title(). + */ + QString queryTitle() const; + +Q_SIGNALS: + /** + * This signals a request for the attached view container to switch to @p url. + * A URL for searching is requested when the user actively engages with this UI to trigger a search. + * A non-search URL is requested when this search UI is closed and no search results should be displayed anymore. + */ + void urlChangeRequested(const QUrl &url); + + /** + * Is emitted when the bar should receive focus. This is usually triggered by a user action that implies that this bar should no longer have focus. + */ + void focusViewRequest(); + + /** + * Requests for @p message with the given @p messageType to be shown to the user in a non-modal way. + */ + void showMessage(const QString &message, KMessageWidget::MessageType messageType); + + /** + * Requests for a progress update to be shown to the user in a non-modal way. + * @param currentlyRunningTaskTitle The task that is currently progressing. + * @param installationProgressPercent The current percentage of completion. + */ + void showInstallationProgress(const QString ¤tlyRunningTaskTitle, int installationProgressPercent); + + /** + * Is emitted when a change of the visibility of this bar is invoked in any way. This can happen from code calling from outside this class, for example + * when the user triggered a keyboard shortcut to show this bar, or from inside, for example because the close button on this bar was pressed or an Escape + * key press was received. + */ + void visibilityChanged(bool visible); + +protected: + /** Handles Escape key presses to clear the search field or close this bar. */ + void keyPressEvent(QKeyEvent *event) override; + /** Allows moving the focus to the view with the Down arrow key. */ + void keyReleaseEvent(QKeyEvent *event) override; + +private Q_SLOTS: + /** + * Is called when any component within this Bar emits a configurationChanged() signal. + * This method is then responsible to communicate the changed search configuration to every other interested party by calling + * UpdatableStateInterface::updateStateToMatch() methods and commiting the new search configuration. + * @see UpdatableStateInterface::updateStateToMatch(). + * @see commitCurrentConfiguration(). + */ + void slotConfigurationChanged(const DolphinQuery &searchConfiguration); + + /** + * Changes the m_searchConfiguration in response to the user editing the search term. If no further changes to the search term happen within a time limit, + * the new search configuration will eventually be commited. + * @see commitCurrentConfiguration. + */ + void slotSearchTermEdited(const QString &text); + + /** + * Commits the current search configuration and then requests moving focus away from this bar and to the view. + * @see commitCurrentConfiguration. + */ + void slotReturnPressed(); + + /** + * Translates the current m_searchConfiguration into URLs which are then emitted through the urlChangeRequested() signal. + * If the current m_searchConfiguration is a valid search, a searchUrl is emitted. If it is not a valid search, i.e. when isSearchConfigured() is false, + * the search path is instead emitted so the view returns to showing a normal folder instead of search results. + * @see urlChangeRequested(). + */ + void commitCurrentConfiguration(); + + /** Adds the current search as a link/favorite to the Places panel. */ + void slotSaveSearch(); + +private: + /** + * This Search::Bar always represents a search configuration. This method takes a new @p dolphinQuery i.e. search configuration and updates itself and all + * child widgets to match it. This way the user always knows which search parameters lead to the query results that appear in the view. + * @see UpdatableStateInterface::updateStateToMatch(). + */ + void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) override; + + /** @see AnimatedHeightWidget::preferredHeight() */ + int preferredHeight() const override; + +private: + QVBoxLayout *m_topLayout = nullptr; + + // The widgets below are sorted by their tab order. + + QLineEdit *m_searchTermEditor = nullptr; + QAction *m_saveSearchAction = nullptr; + /// The main popup of this bar that allows configuring most search parameters. + Popup *m_popup = nullptr; + BarSecondRowFlowLayout *m_secondRowLayout = nullptr; + QToolButton *m_fromHereButton = nullptr; + QToolButton *m_everywhereButton = nullptr; + Chip<FileTypeSelector> *m_fileTypeSelectorChip = nullptr; + Chip<DateSelector> *m_modifiedSinceDateSelectorChip = nullptr; + Chip<MinimumRatingSelector> *m_minimumRatingSelectorChip = nullptr; + Chip<TagsSelector> *m_requiredTagsSelectorChip = nullptr; + + /// Starts a new search when the user has finished typing the search term. + QTimer *m_startSearchTimer = nullptr; + + friend DolphinSearchBarTest; +}; + +} + +#endif diff --git a/src/search/barsecondrowflowlayout.cpp b/src/search/barsecondrowflowlayout.cpp new file mode 100644 index 000000000..29e3513e8 --- /dev/null +++ b/src/search/barsecondrowflowlayout.cpp @@ -0,0 +1,150 @@ +/* + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "barsecondrowflowlayout.h" + +#include <QWidget> + +#include <vector> + +using namespace Search; + +namespace +{ +constexpr int searchLocationButtonsCount = 2; +} + +BarSecondRowFlowLayout::BarSecondRowFlowLayout(QWidget *parent) + : QLayout{parent} +{ + setContentsMargins(0, 0, 0, 0); +} + +BarSecondRowFlowLayout::~BarSecondRowFlowLayout() +{ + QLayoutItem *item; + while ((item = takeAt(0))) + delete item; +} + +void BarSecondRowFlowLayout::addItem(QLayoutItem *item) +{ + itemList.append(item); +} + +Qt::Orientations BarSecondRowFlowLayout::expandingDirections() const +{ + return {}; +} + +bool BarSecondRowFlowLayout::hasHeightForWidth() const +{ + return false; +} + +int BarSecondRowFlowLayout::count() const +{ + return itemList.size(); +} + +QLayoutItem *BarSecondRowFlowLayout::itemAt(int index) const +{ + return itemList.value(index); +} + +QSize BarSecondRowFlowLayout::sizeHint() const +{ + const QRect rect = geometry(); + QSize size; + for (const QLayoutItem *item : std::as_const(itemList)) { + size = size.expandedTo(QSize{item->geometry().right() - rect.x(), item->geometry().bottom() - rect.y()}); + } + return size; +} + +void BarSecondRowFlowLayout::setGeometry(const QRect &rect) +{ + const int oldHeightHint = sizeHint().height(); + QLayout::setGeometry(rect); + const bool isLeftToRight = itemAt(0)->widget()->layoutDirection() == Qt::LeftToRight; + int x = rect.left(); + int y = rect.top(); + + /// The search location buttons are treated differently. They are meant to be in the same row and aligned the other way. + int totalLocationButtonWidth = 0; + for (int i = 0; i < searchLocationButtonsCount; i++) { + totalLocationButtonWidth += itemAt(i)->widget()->sizeHint().width(); + } + if (totalLocationButtonWidth > rect.width()) { + /// There is not enough space so we will smush all the location buttons into the first row. + for (int i = 0; i < searchLocationButtonsCount; i++) { + QWidget *widget = itemAt(i)->widget(); + const int targetWidth = qMin(widget->sizeHint().width(), rect.width() / searchLocationButtonsCount); + widget->setGeometry(isLeftToRight ? x : rect.right() - x - targetWidth, y, targetWidth, widget->sizeHint().height()); + x += widget->width(); + } + } else { + for (int i = 0; i < searchLocationButtonsCount; i++) { + QWidget *widget = itemAt(i)->widget(); + QSize preferredSize = widget->sizeHint(); + widget->setGeometry(isLeftToRight ? x : rect.right() - x - preferredSize.width(), y, preferredSize.width(), preferredSize.height()); + x += widget->width() + spacing(); + } + } + + // We want to align all further widgets the other way. We do this by first filling up the row like usual and then moving all widgets of the current row by + // the remaining space. + std::vector<QWidget *> currentRowWidgets; + for (int i = searchLocationButtonsCount; i < count(); i++) { + QWidget *widget = itemAt(i)->widget(); + const int remainingSpace = rect.right() - x + spacing(); + if (widget->sizeHint().width() < remainingSpace) { + QSize preferredSize = widget->sizeHint(); + widget->setGeometry(isLeftToRight ? x : rect.right() - x - preferredSize.width(), y, preferredSize.width(), preferredSize.height()); + x += widget->width() + spacing(); + currentRowWidgets.push_back(widget); + continue; + } + + // There is not enough space for the next widget. We need to open up a new row. + // Right align all the widgets of the previous row. + for (QWidget *widget : std::as_const(currentRowWidgets)) { + widget->setGeometry(widget->geometry().translated(isLeftToRight ? remainingSpace : -remainingSpace, 0)); + } + currentRowWidgets.clear(); + + x = 0; + y += itemAt(i - 1)->widget()->height() + spacing(); + + QSize preferredSize = widget->sizeHint(); + const int targetWidth = qMin(preferredSize.width(), rect.width()); + widget->setGeometry(isLeftToRight ? x : rect.right() - x - targetWidth, y, targetWidth, preferredSize.height()); + x += widget->width() + spacing(); + currentRowWidgets.push_back(widget); + } + + // Right align all the widgets of the previous row. + int remainingSpace = rect.right() - x + spacing(); + for (QWidget *widget : std::as_const(currentRowWidgets)) { + widget->setGeometry(widget->geometry().translated(isLeftToRight ? remainingSpace : -remainingSpace, 0)); + } + + if (sizeHint().height() != oldHeightHint) { + Q_EMIT heightHintChanged(); + } +} + +QSize BarSecondRowFlowLayout::minimumSize() const +{ + return QSize{0, sizeHint().height()}; +} + +QLayoutItem *BarSecondRowFlowLayout::takeAt(int index) +{ + if (index >= 0 && index < itemList.size()) + return itemList.takeAt(index); + return nullptr; +} diff --git a/src/search/barsecondrowflowlayout.h b/src/search/barsecondrowflowlayout.h new file mode 100644 index 000000000..3f9891e0b --- /dev/null +++ b/src/search/barsecondrowflowlayout.h @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef BARSECONDROWFLOWLAYOUT_H +#define BARSECONDROWFLOWLAYOUT_H + +#include <QLayout> + +namespace Search +{ + +/** + * @brief The layout for all Search::Bar contents which are not in the first row. + * + * For left-to-right languages the search location buttons are kept left-aligned while the chips are right-aligned. When there is not enough space for all the + * widgts in the current row, a new row is started and the Search::Bar is notified that it needs to resize itself. + */ +class BarSecondRowFlowLayout : public QLayout +{ + Q_OBJECT + +public: + explicit BarSecondRowFlowLayout(QWidget *parent); + ~BarSecondRowFlowLayout(); + + void addItem(QLayoutItem *item) override; + Qt::Orientations expandingDirections() const override; + bool hasHeightForWidth() const override; + int count() const override; + QLayoutItem *itemAt(int index) const override; + QSize minimumSize() const override; + void setGeometry(const QRect &rect) override; + QSize sizeHint() const override; + QLayoutItem *takeAt(int index) override; + +Q_SIGNALS: + void heightHintChanged(); + +private: + QList<QLayoutItem *> itemList; +}; + +} + +#endif // BARSECONDROWFLOWLAYOUT_H diff --git a/src/search/chip.cpp b/src/search/chip.cpp new file mode 100644 index 000000000..2d2914bc5 --- /dev/null +++ b/src/search/chip.cpp @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "chip.h" + +#include <KColorUtils> +#include <KLocalizedString> +#include <QPaintEvent> +#include <QStylePainter> +#include <QToolButton> + +using namespace Search; + +ChipBase::ChipBase(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent) + : QWidget{parent} + , UpdatableStateInterface{dolphinQuery} +{ + m_removeButton = new QToolButton{this}; + m_removeButton->setText(i18nc("@action:button", "Remove Filter")); + m_removeButton->setIcon(QIcon::fromTheme("list-remove")); + m_removeButton->setToolButtonStyle(Qt::ToolButtonIconOnly); + m_removeButton->setAutoRaise(true); + + auto layout = new QHBoxLayout{this}; + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); +} + +void ChipBase::paintEvent(QPaintEvent *event) +{ + QStylePainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + QColor penColor = KColorUtils::mix(palette().base().color(), palette().text().color(), 0.3); + // QPainter is bad at drawing lines that are exactly 1px. + // Using QPen::setCosmetic(true) with a 1px pen width + // doesn't look quite as good as just using 1.001px. + qreal penWidth = 1.001; + qreal penMargin = penWidth / 2; + QPen pen(penColor, penWidth); + pen.setCosmetic(true); + QRectF rect = event->rect(); + rect.adjust(penMargin, penMargin, -penMargin, -penMargin); + painter.setBrush(palette().base()); + painter.setPen(pen); + painter.drawRoundedRect(rect, 5, 5); // 5 is the current default Breeze corner radius + QWidget::paintEvent(event); +} + +#include "moc_chip.cpp" diff --git a/src/search/chip.h b/src/search/chip.h new file mode 100644 index 000000000..94e51968d --- /dev/null +++ b/src/search/chip.h @@ -0,0 +1,101 @@ +/* + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef CHIP_H +#define CHIP_H + +#include "dolphinquery.h" +#include "selectors/dateselector.h" +#include "selectors/filetypeselector.h" +#include "selectors/minimumratingselector.h" +#include "selectors/tagsselector.h" +#include "updatablestateinterface.h" + +#include <QComboBox> +#include <QLayout> +#include <QToolButton> +#include <QWidget> + +#include <type_traits> + +namespace Search +{ + +/** + * @brief The non-template base class for the template class Chip. + * + * @see Chip below. + * + * Template classes cannot have Qt signals. This class works around that by being a non-template class which the template Chip class then inherits from. + * This base class contains all non-template logic of Chip. + */ +class ChipBase : public QWidget, public UpdatableStateInterface +{ + Q_OBJECT + +public: + ChipBase(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr); + +Q_SIGNALS: + /** Is emitted whenever settings have changed and a new search might be necessary. */ + void configurationChanged(const DolphinQuery &dolphinQuery); + +protected: + void paintEvent(QPaintEvent *event) override; + +protected: + QToolButton *m_removeButton = nullptr; +}; + +/** + * @brief A button-sized UI component for modifying or removing search filters. + * + * A template widget is taken and this Chip forms a button-like outline around it. The Chip has a close button on the side to remove itself, which communicates + * to the user that the effect of the widget inside this Chip no longer applies. The functionality of the widget inside is not affected by the Chip. + * + * Most logic of this class is in the non-template ChipBase base class. + * @see ChipBase above. + */ +template<class Selector> +class Chip : public ChipBase +{ +public: + Chip(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr) + : ChipBase{dolphinQuery, parent} + , m_selector{new Selector{dolphinQuery, this}} + { + // Make the selector flat within the chip. + if constexpr (std::is_base_of<QComboBox, Selector>::value) { + m_selector->setFrame(false); + } else if constexpr (std::is_base_of<QToolButton, Selector>::value) { + m_selector->setAutoRaise(true); + } + setFocusProxy(m_selector); + setTabOrder(m_selector, m_removeButton); + + connect(m_selector, &Selector::configurationChanged, this, &ChipBase::configurationChanged); + + // The m_removeButton does not directly remove the Chip. Instead the Selector's removeRestriction() method will emit ChipBase::configurationChanged() + // with a DolphinQuery object that effectively removes the effects of the Selector. This in turn will then eventually remove this Chip when the new + // state of the Search UI components is propagated through the various UpdatableStateInterface::updateStateToMatch() methods. + connect(m_removeButton, &QAbstractButton::clicked, m_selector, &Selector::removeRestriction); + + layout()->addWidget(m_selector); + layout()->addWidget(m_removeButton); + }; + +private: + void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) override + { + m_selector->updateStateToMatch(dolphinQuery); + } + +private: + Selector *const m_selector; +}; +} + +#endif diff --git a/src/search/configurationpopup.h b/src/search/configurationpopup.h new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/search/configurationpopup.h diff --git a/src/search/dolphin_searchsettings.kcfg b/src/search/dolphin_searchsettings.kcfg index 1acebf874..59e6f58e3 100644 --- a/src/search/dolphin_searchsettings.kcfg +++ b/src/search/dolphin_searchsettings.kcfg @@ -12,7 +12,11 @@ </entry> <entry name="What" type="String"> <label>What</label> - <default>FileName</default> + <default>FileNames</default> + </entry> + <entry name="SearchTool" type="String"> + <label>SearchTool</label> + <default>Filenamesearch</default> </entry> </group> </kcfg> diff --git a/src/search/dolphinfacetswidget.cpp b/src/search/dolphinfacetswidget.cpp deleted file mode 100644 index da36caa36..000000000 --- a/src/search/dolphinfacetswidget.cpp +++ /dev/null @@ -1,312 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * SPDX-FileCopyrightText: 2019 Ismael Asensio <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "dolphinfacetswidget.h" - -#include <KLocalizedString> -#include <KProtocolInfo> - -#include <QComboBox> -#include <QDate> -#include <QEvent> -#include <QHBoxLayout> -#include <QIcon> -#include <QMenu> -#include <QToolButton> - -DolphinFacetsWidget::DolphinFacetsWidget(QWidget *parent) - : QWidget(parent) - , m_typeSelector(nullptr) - , m_dateSelector(nullptr) - , m_ratingSelector(nullptr) - , m_tagsSelector(nullptr) -{ - m_typeSelector = new QComboBox(this); - m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("none")), i18nc("@item:inlistbox", "Any Type"), QString()); - m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("inode-directory")), i18nc("@item:inlistbox", "Folders"), QStringLiteral("Folder")); - m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("text-x-generic")), i18nc("@item:inlistbox", "Documents"), QStringLiteral("Document")); - m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("image-x-generic")), i18nc("@item:inlistbox", "Images"), QStringLiteral("Image")); - m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("audio-x-generic")), i18nc("@item:inlistbox", "Audio Files"), QStringLiteral("Audio")); - m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("video-x-generic")), i18nc("@item:inlistbox", "Videos"), QStringLiteral("Video")); - initComboBox(m_typeSelector); - - const QDate currentDate = QDate::currentDate(); - - m_dateSelector = new QComboBox(this); - m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("view-calendar")), i18nc("@item:inlistbox", "Any Date"), QDate()); - m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("go-jump-today")), i18nc("@item:inlistbox", "Today"), currentDate); - m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("go-jump-today")), i18nc("@item:inlistbox", "Yesterday"), currentDate.addDays(-1)); - m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("view-calendar-week")), - i18nc("@item:inlistbox", "This Week"), - currentDate.addDays(1 - currentDate.dayOfWeek())); - m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("view-calendar-month")), - i18nc("@item:inlistbox", "This Month"), - currentDate.addDays(1 - currentDate.day())); - m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("view-calendar-year")), - i18nc("@item:inlistbox", "This Year"), - currentDate.addDays(1 - currentDate.dayOfYear())); - initComboBox(m_dateSelector); - - m_ratingSelector = new QComboBox(this); - m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("non-starred-symbolic")), i18nc("@item:inlistbox", "Any Rating"), 0); - m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "1 or more"), 1); - m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "2 or more"), 2); - m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "3 or more"), 3); - m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "4 or more"), 4); - m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "Highest Rating"), 5); - initComboBox(m_ratingSelector); - - m_clearTagsAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-all")), i18nc("@action:inmenu", "Clear Selection"), this); - connect(m_clearTagsAction, &QAction::triggered, this, [this]() { - resetSearchTags(); - Q_EMIT facetChanged(); - }); - - m_tagsSelector = new QToolButton(this); - m_tagsSelector->setIcon(QIcon::fromTheme(QStringLiteral("tag"))); - m_tagsSelector->setMenu(new QMenu(this)); - m_tagsSelector->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - m_tagsSelector->setPopupMode(QToolButton::MenuButtonPopup); - m_tagsSelector->setAutoRaise(true); - updateTagsSelector(); - - connect(m_tagsSelector, &QToolButton::clicked, m_tagsSelector, &QToolButton::showMenu); - connect(m_tagsSelector->menu(), &QMenu::aboutToShow, this, &DolphinFacetsWidget::updateTagsMenu); - connect(&m_tagsLister, &KCoreDirLister::itemsAdded, this, &DolphinFacetsWidget::updateTagsMenuItems); - updateTagsMenu(); - - QHBoxLayout *topLayout = new QHBoxLayout(this); - topLayout->setContentsMargins(0, 0, 0, 0); - topLayout->addWidget(m_typeSelector); - topLayout->addWidget(m_dateSelector); - topLayout->addWidget(m_ratingSelector); - topLayout->addWidget(m_tagsSelector); - - resetSearchTerms(); -} - -DolphinFacetsWidget::~DolphinFacetsWidget() -{ -} - -void DolphinFacetsWidget::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::EnabledChange) { - if (isEnabled()) { - updateTagsSelector(); - } else { - resetSearchTerms(); - } - } -} - -QSize DolphinFacetsWidget::minimumSizeHint() const -{ - return QSize(0, m_typeSelector->minimumHeight()); -} - -void DolphinFacetsWidget::resetSearchTerms() -{ - m_typeSelector->setCurrentIndex(0); - m_dateSelector->setCurrentIndex(0); - m_ratingSelector->setCurrentIndex(0); - - resetSearchTags(); -} - -QStringList DolphinFacetsWidget::searchTerms() const -{ - QStringList terms; - - if (m_ratingSelector->currentIndex() > 0) { - const int rating = m_ratingSelector->currentData().toInt() * 2; - terms << QStringLiteral("rating>=%1").arg(rating); - } - - if (m_dateSelector->currentIndex() > 0) { - const QDate date = m_dateSelector->currentData().toDate(); - terms << QStringLiteral("modified>=%1").arg(date.toString(Qt::ISODate)); - } - - if (!m_searchTags.isEmpty()) { - for (auto const &tag : m_searchTags) { - if (tag.contains(QLatin1Char(' '))) { - terms << QStringLiteral("tag:\"%1\"").arg(tag); - } else { - terms << QStringLiteral("tag:%1").arg(tag); - } - } - } - - return terms; -} - -QString DolphinFacetsWidget::facetType() const -{ - return m_typeSelector->currentData().toString(); -} - -bool DolphinFacetsWidget::isSearchTerm(const QString &term) const -{ - static const QLatin1String searchTokens[]{QLatin1String("modified>="), QLatin1String("rating>="), QLatin1String("tag:"), QLatin1String("tag=")}; - - for (const auto &searchToken : searchTokens) { - if (term.startsWith(searchToken)) { - return true; - } - } - return false; -} - -void DolphinFacetsWidget::setSearchTerm(const QString &term) -{ - if (term.startsWith(QLatin1String("modified>="))) { - const QString value = term.mid(10); - const QDate date = QDate::fromString(value, Qt::ISODate); - setTimespan(date); - } else if (term.startsWith(QLatin1String("rating>="))) { - const QString value = term.mid(8); - const int stars = value.toInt() / 2; - setRating(stars); - } else if (term.startsWith(QLatin1String("tag:")) || term.startsWith(QLatin1String("tag="))) { - const QString value = term.mid(4); - addSearchTag(value); - } -} - -void DolphinFacetsWidget::setFacetType(const QString &type) -{ - for (int index = 0; index <= m_typeSelector->count(); index++) { - if (type == m_typeSelector->itemData(index).toString()) { - m_typeSelector->setCurrentIndex(index); - break; - } - } -} - -void DolphinFacetsWidget::setRating(const int stars) -{ - if (stars < 0 || stars > 5) { - return; - } - m_ratingSelector->setCurrentIndex(stars); -} - -void DolphinFacetsWidget::setTimespan(const QDate &date) -{ - if (!date.isValid()) { - return; - } - m_dateSelector->setCurrentIndex(0); - for (int index = 1; index <= m_dateSelector->count(); index++) { - if (date >= m_dateSelector->itemData(index).toDate()) { - m_dateSelector->setCurrentIndex(index); - break; - } - } -} - -void DolphinFacetsWidget::addSearchTag(const QString &tag) -{ - if (tag.isEmpty() || m_searchTags.contains(tag)) { - return; - } - m_searchTags.append(tag); - m_searchTags.sort(); - updateTagsSelector(); -} - -void DolphinFacetsWidget::removeSearchTag(const QString &tag) -{ - if (tag.isEmpty() || !m_searchTags.contains(tag)) { - return; - } - m_searchTags.removeAll(tag); - updateTagsSelector(); -} - -void DolphinFacetsWidget::resetSearchTags() -{ - m_searchTags = QStringList(); - updateTagsSelector(); - updateTagsMenu(); -} - -void DolphinFacetsWidget::initComboBox(QComboBox *combo) -{ - combo->setFrame(false); - combo->setMinimumHeight(parentWidget()->height()); - combo->setCurrentIndex(0); - connect(combo, &QComboBox::activated, this, &DolphinFacetsWidget::facetChanged); -} - -void DolphinFacetsWidget::updateTagsSelector() -{ - const bool hasListedTags = !m_tagsSelector->menu()->isEmpty(); - const bool hasSelectedTags = !m_searchTags.isEmpty(); - - if (hasSelectedTags) { - const QString tagsText = m_searchTags.join(i18nc("String list separator", ", ")); - m_tagsSelector->setText(i18ncp("@action:button %2 is a list of tags", "Tag: %2", "Tags: %2", m_searchTags.count(), tagsText)); - } else { - m_tagsSelector->setText(i18nc("@action:button", "Add Tags")); - } - - m_tagsSelector->setEnabled(isEnabled() && (hasListedTags || hasSelectedTags)); - m_clearTagsAction->setEnabled(hasSelectedTags); -} - -void DolphinFacetsWidget::updateTagsMenu() -{ - updateTagsMenuItems({}, {}); - if (KProtocolInfo::isKnownProtocol(QStringLiteral("tags"))) { - m_tagsLister.openUrl(QUrl(QStringLiteral("tags:/")), KCoreDirLister::OpenUrlFlag::Reload); - } -} - -void DolphinFacetsWidget::updateTagsMenuItems(const QUrl &, const KFileItemList &items) -{ - QMenu *tagsMenu = m_tagsSelector->menu(); - tagsMenu->clear(); - - QStringList allTags = QStringList(m_searchTags); - for (const KFileItem &item : items) { - allTags.append(item.name()); - } - allTags.sort(Qt::CaseInsensitive); - allTags.removeDuplicates(); - - const bool onlyOneTag = allTags.count() == 1; - - for (const QString &tagName : std::as_const(allTags)) { - QAction *action = tagsMenu->addAction(QIcon::fromTheme(QStringLiteral("tag")), tagName); - action->setCheckable(true); - action->setChecked(m_searchTags.contains(tagName)); - - connect(action, &QAction::triggered, this, [this, tagName, onlyOneTag](bool isChecked) { - if (isChecked) { - addSearchTag(tagName); - } else { - removeSearchTag(tagName); - } - Q_EMIT facetChanged(); - - if (!onlyOneTag) { - m_tagsSelector->menu()->show(); - } - }); - } - - if (allTags.count() > 1) { - tagsMenu->addSeparator(); - tagsMenu->addAction(m_clearTagsAction); - } - - updateTagsSelector(); -} - -#include "moc_dolphinfacetswidget.cpp" diff --git a/src/search/dolphinfacetswidget.h b/src/search/dolphinfacetswidget.h deleted file mode 100644 index c7358e212..000000000 --- a/src/search/dolphinfacetswidget.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#ifndef DOLPHINFACETSWIDGET_H -#define DOLPHINFACETSWIDGET_H - -#include <KCoreDirLister> -#include <QWidget> - -class QComboBox; -class QDate; -class QEvent; -class QToolButton; - -/** - * @brief Allows to filter search-queries by facets. - * - * TODO: The current implementation is a temporary - * workaround for the 4.9 release and represents no - * real facets-implementation yet: There have been - * some Dolphin specific user-interface and interaction - * issues since 4.6 by embedding the Nepomuk facet-widget - * into a QDockWidget (this is unrelated to the - * Nepomuk facet-widget itself). Now in combination - * with the search-shortcuts in the Places Panel some - * existing issues turned into real showstoppers. - * - * So the longterm plan is to use the Nepomuk facets - * again as soon as possible. - */ -class DolphinFacetsWidget : public QWidget -{ - Q_OBJECT - -public: - explicit DolphinFacetsWidget(QWidget *parent = nullptr); - ~DolphinFacetsWidget() override; - - QStringList searchTerms() const; - QString facetType() const; - - bool isSearchTerm(const QString &term) const; - void setSearchTerm(const QString &term); - void resetSearchTerms(); - - void setFacetType(const QString &type); - - QSize minimumSizeHint() const override; - -Q_SIGNALS: - void facetChanged(); - -protected: - void changeEvent(QEvent *event) override; - -private Q_SLOTS: - void updateTagsMenu(); - void updateTagsMenuItems(const QUrl &, const KFileItemList &items); - -private: - void setRating(const int stars); - void setTimespan(const QDate &date); - void addSearchTag(const QString &tag); - void removeSearchTag(const QString &tag); - void resetSearchTags(); - - void initComboBox(QComboBox *combo); - void updateTagsSelector(); - -private: - QComboBox *m_typeSelector; - QComboBox *m_dateSelector; - QComboBox *m_ratingSelector; - QToolButton *m_tagsSelector; - - QStringList m_searchTags; - KCoreDirLister m_tagsLister; - QAction *m_clearTagsAction; -}; - -#endif diff --git a/src/search/dolphinquery.cpp b/src/search/dolphinquery.cpp index ed2a6a766..e23f1f08e 100644 --- a/src/search/dolphinquery.cpp +++ b/src/search/dolphinquery.cpp @@ -1,20 +1,75 @@ /* - * SPDX-FileCopyrightText: 2019 Ismael Asensio <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ + SPDX-FileCopyrightText: 2019 Ismael Asensio <[email protected]> + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> -#include "dolphinquery.h" + SPDX-License-Identifier: GPL-2.0-or-later +*/ -#include <QRegularExpression> +#include "dolphinquery.h" #include "config-dolphin.h" #if HAVE_BALOO +#include <Baloo/IndexerConfig> #include <Baloo/Query> #endif +#include "dolphinplacesmodelsingleton.h" + +#include <KFileMetaData/TypeInfo> +#include <KLocalizedString> + +#include <QRegularExpression> +#include <QUrlQuery> + +using namespace Search; + +bool Search::isSupportedSearchScheme(const QString &urlScheme) +{ + static const QStringList supportedSchemes = { + QStringLiteral("filenamesearch"), + QStringLiteral("baloosearch"), + QStringLiteral("tags"), + }; + + return supportedSchemes.contains(urlScheme); +} + +bool g_testMode = false; + +bool Search::isIndexingEnabledIn(QUrl directory) +{ + if (g_testMode) { + return true; // For unit-testing, let's pretend everything is indexed correctly. + } + +#if HAVE_BALOO + const Baloo::IndexerConfig searchInfo; + return searchInfo.fileIndexingEnabled() && !directory.isEmpty() && searchInfo.shouldBeIndexed(directory.toLocalFile()); +#else + Q_UNUSED(directory) + return false; +#endif +} + +bool Search::isContentIndexingEnabled() +{ + if (g_testMode) { + return true; // For unit-testing, let's pretend everything is indexed correctly. + } + +#if HAVE_BALOO + return !Baloo::IndexerConfig{}.onlyBasicIndexing(); +#else + return false; +#endif +} namespace { +/** The path to be passed so Baloo searches everywhere. */ +constexpr auto balooSearchEverywherePath = QLatin1String(""); +/** The path to be passed so Filenamesearch searches everywhere. */ +constexpr auto filenamesearchEverywherePath = QLatin1String("file:///"); + #if HAVE_BALOO /** Checks if a given term in the Baloo::Query::searchString() is a special search term * @return: the specific search token of the term, or an empty QString() if none is found @@ -23,6 +78,7 @@ QString searchTermToken(const QString &term) { static const QLatin1String searchTokens[]{QLatin1String("filename:"), QLatin1String("modified>="), + QLatin1String("modified>"), QLatin1String("rating>="), QLatin1String("tag:"), QLatin1String("tag=")}; @@ -69,110 +125,323 @@ QString trimChar(const QString &text, const QLatin1Char aChar) } } -DolphinQuery DolphinQuery::fromSearchUrl(const QUrl &searchUrl) +Search::DolphinQuery::DolphinQuery(const QUrl &url, const QUrl &backupSearchPath) { - DolphinQuery model; - model.m_searchUrl = searchUrl; + if (url.scheme() == QLatin1String("filenamesearch")) { + m_searchTool = SearchTool::Filenamesearch; + const QUrlQuery query(url); + const QString filenamesearchSearchPathString = query.queryItemValue(QStringLiteral("url"), QUrl::FullyDecoded); + const QUrl filenamesearchSearchPathUrl = QUrl::fromUserInput(filenamesearchSearchPathString, QString(), QUrl::AssumeLocalFile); + if (!filenamesearchSearchPathUrl.isValid() || filenamesearchSearchPathString == filenamesearchEverywherePath) { + // The parsed search location is either invalid or matches a string that represents searching "everywhere". + m_searchLocations = SearchLocations::Everywhere; + m_searchPath = backupSearchPath; + } else { + m_searchLocations = SearchLocations::FromHere; + m_searchPath = filenamesearchSearchPathUrl; + } + m_searchTerm = query.queryItemValue(QStringLiteral("search"), QUrl::FullyDecoded); + m_searchThrough = query.queryItemValue(QStringLiteral("checkContent")) == QLatin1String("yes") ? SearchThrough::FileContents : SearchThrough::FileNames; + return; + } + +#if HAVE_BALOO + if (url.scheme() == QLatin1String("baloosearch")) { + m_searchTool = SearchTool::Baloo; + initializeFromBalooQuery(Baloo::Query::fromSearchUrl(url), backupSearchPath); + return; + } +#endif - if (searchUrl.scheme() == QLatin1String("baloosearch")) { - model.parseBalooQuery(); - } else if (searchUrl.scheme() == QLatin1String("tags")) { + if (url.scheme() == QLatin1String("tags")) { +#if HAVE_BALOO + m_searchTool = SearchTool::Baloo; +#endif + m_searchLocations = SearchLocations::Everywhere; + m_searchPath = backupSearchPath; // tags can contain # symbols or slashes within the Url - QString tag = trimChar(searchUrl.toString(QUrl::RemoveScheme), QLatin1Char('/')); - model.m_searchTerms << QStringLiteral("tag:%1").arg(tag); + const auto tag = trimChar(url.toString(QUrl::RemoveScheme), QLatin1Char('/')); + if (!tag.isEmpty()) { + m_requiredTags.append(trimChar(url.toString(QUrl::RemoveScheme), QLatin1Char('/'))); + } + return; } - return model; + m_searchPath = url; + switchToPreferredSearchTool(); } -bool DolphinQuery::supportsScheme(const QString &urlScheme) +QUrl DolphinQuery::toUrl() const { - static const QStringList supportedSchemes = { - QStringLiteral("baloosearch"), - QStringLiteral("tags"), - }; + // The following pre-conditions are sanity checks on this DolphinQuery object. If they fail, the issue is that we ever allowed the DolphinQuery to be in an + // inconsistent state to begin with. This should be fixed by bringing this DolphinQuery object into a reasonable state at the end of the constructors or + // setter methods which caused this impossible-to-fulfill combination of conditions. + Q_ASSERT_X(m_searchLocations == SearchLocations::Everywhere || m_searchPath.isValid(), + "DolphinQuery::toUrl()", + "We are supposed to search in a specific location but we do not know where!"); +#if HAVE_BALOO + Q_ASSERT_X(m_searchLocations == SearchLocations::Everywhere || m_searchTool != SearchTool::Baloo || isIndexingEnabledIn(m_searchPath), + "DolphinQuery::toUrl()", + "We are asking Baloo to search in a location which Baloo is not supposed to have indexed!"); +#endif - return supportedSchemes.contains(urlScheme); -} + QUrl url; -void DolphinQuery::parseBalooQuery() -{ #if HAVE_BALOO - const Baloo::Query query = Baloo::Query::fromSearchUrl(m_searchUrl); + /// Create Baloo search URL + if (m_searchTool == SearchTool::Baloo) { + Baloo::Query query; + if (m_fileType != KFileMetaData::Type::Empty) { + query.addType(KFileMetaData::TypeInfo{m_fileType}.name()); + } - m_includeFolder = query.includeFolder(); + QStringList balooQueryStrings = m_unrecognizedBalooQueryStrings; - const QStringList types = query.types(); - m_fileType = types.isEmpty() ? QString() : types.first(); + if (m_searchThrough == SearchThrough::FileContents) { + balooQueryStrings << m_searchTerm; + } else if (!m_searchTerm.isEmpty()) { + balooQueryStrings << QStringLiteral("filename:\"%1\"").arg(m_searchTerm); + } - QStringList textParts; - QString fileName; + if (m_searchLocations == SearchLocations::FromHere) { + query.setIncludeFolder(m_searchPath.toLocalFile()); + } - const QStringList subTerms = splitOutsideQuotes(query.searchString()); - for (const QString &subTerm : subTerms) { - const QString token = searchTermToken(subTerm); - const QString value = stripQuotes(subTerm.mid(token.length())); + if (m_modifiedSinceDate.isValid()) { + balooQueryStrings << QStringLiteral("modified>=%1").arg(m_modifiedSinceDate.toString(Qt::ISODate)); + } - if (token == QLatin1String("filename:")) { - if (!value.isEmpty()) { - fileName = value; - m_hasFileName = true; + if (m_minimumRating >= 1) { + balooQueryStrings << QStringLiteral("rating>=%1").arg(m_minimumRating); + } + + for (const auto &tag : m_requiredTags) { + if (tag.contains(QLatin1Char(' '))) { + balooQueryStrings << QStringLiteral("tag:\"%1\"").arg(tag); + } else { + balooQueryStrings << QStringLiteral("tag:%1").arg(tag); } - continue; - } else if (!token.isEmpty()) { - m_searchTerms << token + value; - continue; - } else if (subTerm == QLatin1String("AND") && subTerm != subTerms.at(0) && subTerm != subTerms.back()) { - continue; - } else if (!value.isEmpty()) { - textParts << value; - m_hasContentSearch = true; } + + query.setSearchString(balooQueryStrings.join(QLatin1Char(' '))); + + return query.toSearchUrl(QUrl::toPercentEncoding(title())); } +#endif - if (m_hasFileName) { - if (m_hasContentSearch) { - textParts << QStringLiteral("filename:\"%1\"").arg(fileName); - } else { - textParts << fileName; - } + /// Create Filenamsearch search URL + url.setScheme(QStringLiteral("filenamesearch")); + + QUrlQuery qUrlQuery; + qUrlQuery.addQueryItem(QStringLiteral("search"), QUrl::toPercentEncoding(m_searchTerm)); + if (m_searchThrough == SearchThrough::FileContents) { + qUrlQuery.addQueryItem(QStringLiteral("checkContent"), QStringLiteral("yes")); } - m_searchText = textParts.join(QLatin1Char(' ')); -#endif + if (m_searchLocations == SearchLocations::FromHere && m_searchPath.isValid()) { + qUrlQuery.addQueryItem(QStringLiteral("url"), QUrl::toPercentEncoding(m_searchPath.url())); + } else { + // Search in root which is considered searching "everywhere". + qUrlQuery.addQueryItem(QStringLiteral("url"), QUrl::toPercentEncoding(filenamesearchEverywherePath)); + } + qUrlQuery.addQueryItem(QStringLiteral("title"), QUrl::toPercentEncoding(title())); + + url.setQuery(qUrlQuery); + return url; } -QUrl DolphinQuery::searchUrl() const +void DolphinQuery::setSearchLocations(SearchLocations searchLocations) { - return m_searchUrl; + m_searchLocations = searchLocations; + switchToPreferredSearchTool(); } -QString DolphinQuery::text() const +void DolphinQuery::setSearchPath(const QUrl &searchPath) { - return m_searchText; + m_searchPath = searchPath; + switchToPreferredSearchTool(); } -QString DolphinQuery::type() const +void DolphinQuery::setSearchThrough(SearchThrough searchThrough) { - return m_fileType; + m_searchThrough = searchThrough; + switchToPreferredSearchTool(); } -QStringList DolphinQuery::searchTerms() const +void DolphinQuery::switchToPreferredSearchTool() { - return m_searchTerms; + const bool isIndexingEnabledInCurrentSearchLocation = m_searchLocations == SearchLocations::Everywhere || isIndexingEnabledIn(m_searchPath); + const bool searchThroughFileContentsWithoutIndexing = m_searchThrough == SearchThrough::FileContents && !isContentIndexingEnabled(); + if (!isIndexingEnabledInCurrentSearchLocation || searchThroughFileContentsWithoutIndexing) { + m_searchTool = SearchTool::Filenamesearch; + return; + } +#if HAVE_BALOO + // The current search location allows searching with Baloo. We switch to Baloo if this is the saved user preference. + if (SearchSettings::searchTool() == QStringLiteral("Baloo")) { + m_searchTool = SearchTool::Baloo; + } +#endif } -QString DolphinQuery::includeFolder() const +#if HAVE_BALOO +void DolphinQuery::initializeFromBalooQuery(const Baloo::Query &balooQuery, const QUrl &backupSearchPath) { - return m_includeFolder; + const QString balooSearchPathString = balooQuery.includeFolder(); + const QUrl balooSearchPathUrl = QUrl::fromUserInput(balooSearchPathString, QString(), QUrl::AssumeLocalFile); + if (!balooSearchPathUrl.isValid() || balooSearchPathString == balooSearchEverywherePath) { + // The parsed search location is either invalid or matches a string that represents searching "everywhere" i.e. in all indexed locations. + m_searchLocations = SearchLocations::Everywhere; + m_searchPath = backupSearchPath; + } else { + m_searchLocations = SearchLocations::FromHere; + m_searchPath = balooSearchPathUrl; + } + + const QStringList types = balooQuery.types(); + // We currently only allow searching for one file type at once. (Searching for more seems out of scope for Dolphin anyway IMO.) + m_fileType = types.isEmpty() ? KFileMetaData::Type::Empty : KFileMetaData::TypeInfo::fromName(types.first()).type(); + + /// If nothing is requested, we use the default. + std::optional<SearchThrough> requestedToSearchThrough; + const QStringList subTerms = splitOutsideQuotes(balooQuery.searchString()); + for (const QString &subTerm : subTerms) { + const QString token = searchTermToken(subTerm); + const QString value = stripQuotes(subTerm.mid(token.length())); + + if (token == QLatin1String("filename:")) { + // This query is meant to not search in file contents. + if (!value.isEmpty()) { + if (m_searchTerm.isEmpty()) { // Seems like we already received a search term for the content search. We don't overwrite it because the Dolphin + // UI does not support searching for differing strings in content and file name. + m_searchTerm = value; + } + if (!requestedToSearchThrough.has_value()) { // If requested to search thorugh contents, searching file names is already implied. + requestedToSearchThrough = SearchThrough::FileNames; + } + } + continue; + } else if (token.startsWith(QLatin1String("modified>="))) { + m_modifiedSinceDate = QDate::fromString(value, Qt::ISODate); + continue; + } else if (token.startsWith(QLatin1String("modified>"))) { + m_modifiedSinceDate = QDate::fromString(value, Qt::ISODate).addDays(1); + continue; + } else if (token.startsWith(QLatin1String("rating>="))) { + m_minimumRating = value.toInt(); + continue; + } else if (token.startsWith(QLatin1String("tag"))) { + m_requiredTags.append(value); + continue; + } else if (!token.isEmpty()) { + m_unrecognizedBalooQueryStrings << token + value; + continue; + } else if (subTerm == QLatin1String("AND") && subTerm != subTerms.at(0) && subTerm != subTerms.back()) { + continue; + } else if (!value.isEmpty()) { + // An empty token means this is just blank text, which is where the generic search term is located. + if (!m_searchTerm.isEmpty()) { + // Multiple search terms are separated by spaces. + m_searchTerm.append(QLatin1Char{' '}); + } + m_searchTerm.append(value); + requestedToSearchThrough = SearchThrough::FileContents; + } + } + if (requestedToSearchThrough.has_value()) { + m_searchThrough = requestedToSearchThrough.value(); + } } +#endif // HAVE_BALOO -bool DolphinQuery::hasContentSearch() const +QString DolphinQuery::title() const { - return m_hasContentSearch; + if (m_searchLocations == SearchLocations::FromHere) { + QString prettySearchLocation; + KFilePlacesModel *placesModel = DolphinPlacesModelSingleton::instance().placesModel(); + QModelIndex url_index = placesModel->closestItem(m_searchPath); + if (url_index.isValid() && placesModel->url(url_index).matches(m_searchPath, QUrl::StripTrailingSlash)) { + prettySearchLocation = placesModel->text(url_index); + } else { + prettySearchLocation = m_searchPath.fileName(); + } + if (prettySearchLocation.isEmpty()) { + prettySearchLocation = m_searchPath.toString(QUrl::RemoveAuthority); + } + + // A great title clearly identifies a search results page among many tabs, windows, or links in the Places panel. + // Using the search term is phenomenal for this because the user has typed this very specific string with a clear intention, so we definitely want to + // reuse the search term in the title if possible. + if (!m_searchTerm.isEmpty()) { + if (m_searchThrough == SearchThrough::FileNames) { + return i18nc("@title of a search results page. %1 is the search term a user entered, %2 is a folder name", + "Search results for “%1” in %2", + m_searchTerm, + prettySearchLocation); + } + Q_ASSERT(m_searchThrough == SearchThrough::FileContents); + return i18nc("@title of a search results page. %1 is the search term a user entered, %2 is a folder name", + "Files containing “%1” in %2", + m_searchTerm, + prettySearchLocation); + } + if (!m_requiredTags.isEmpty()) { + if (m_requiredTags.count() == 1) { + return i18nc("@title of a search results page. %1 is a tag e.g. 'important'. %2 is a folder name", + "Search items tagged “%1” in %2", + m_requiredTags.constFirst(), + prettySearchLocation); + } + return i18nc("@title of a search results page. %1 and %2 are tags e.g. 'important'. %3 is a folder name", + "Search items tagged “%1” and “%2” in %3", + m_requiredTags.constFirst(), + m_requiredTags.constLast(), + prettySearchLocation); + } + if (m_fileType != KFileMetaData::Type::Empty) { + return i18nc("@title of a search results page for items of a specified type. %1 is a file type e.g. 'Document', 'Folder'. %2 is a folder name", + "%1 search results in %2", + KFileMetaData::TypeInfo{m_fileType}.displayName(), + prettySearchLocation); + } + // Everything else failed so we use a very generic title. + return i18nc("@title of a search results page with items matching pre-defined conditions. %1 is a folder name", + "Search results in %1", + prettySearchLocation); + } + + Q_ASSERT(m_searchLocations == SearchLocations::Everywhere); + // A great title clearly identifies a search results page among many tabs, windows, or links in the Places panel. + // Using the search term is phenomenal for this because the user has typed this very specific string with a clear intention, so we definitely want to reuse + // the search term in the title if possible. + if (!m_searchTerm.isEmpty()) { + if (m_searchThrough == SearchThrough::FileNames) { + return i18nc("@title of a search results page. %1 is the search term a user entered", "Search results for “%1”", m_searchTerm); + } + Q_ASSERT(m_searchThrough == SearchThrough::FileContents); + return i18nc("@title of a search results page. %1 is the search term a user entered", "Files containing “%1”", m_searchTerm); + } + if (!m_requiredTags.isEmpty()) { + if (m_requiredTags.count() == 1) { + return i18nc("@title of a search results page. %1 is a tag e.g. 'important'", "Search items tagged “%1”", m_requiredTags.constFirst()); + } + return i18nc("@title of a search results page. %1 and %2 are tags e.g. 'important'", + "Search items tagged “%1” and “%2”", + m_requiredTags.constFirst(), + m_requiredTags.constLast()); + } + if (m_fileType != KFileMetaData::Type::Empty) { + // i18n: Results page for items of a specified type. %1 is a file type e.g. 'Audio', 'Document', 'Folder', 'Archive'. 'Presentation'. + // If putting such a file type at the start does not work in your language in this context, you might want to translate this liberally with + // something along the lines of 'Search items of type “%1”'. + return i18nc("@title of a search. %1 is file type", "%1 search results", KFileMetaData::TypeInfo{m_fileType}.displayName()); + } + // Everything else failed so we use a very generic title. + return i18nc("@title of a search results page with items matching pre-defined conditions", "Search results"); } -bool DolphinQuery::hasFileName() const +/** For testing Baloo in DolphinQueryTest even if it is not indexing any locations yet. */ +void Search::setTestMode() { - return m_hasFileName; -} + g_testMode = true; +}; diff --git a/src/search/dolphinquery.h b/src/search/dolphinquery.h index 1334958f1..6893e1855 100644 --- a/src/search/dolphinquery.h +++ b/src/search/dolphinquery.h @@ -1,59 +1,300 @@ /* - * SPDX-FileCopyrightText: 2019 Ismael Asensio <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ + SPDX-FileCopyrightText: 2019 Ismael Asensio <[email protected]> + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: GPL-2.0-or-later +*/ #ifndef DOLPHINQUERY_H #define DOLPHINQUERY_H +#include "config-dolphin.h" #include "dolphin_export.h" +#include "dolphin_searchsettings.h" + +#include <KFileMetaData/Types> #include <QString> #include <QUrl> +#if HAVE_BALOO +namespace Baloo +{ +class Query; +} +#endif + +class DolphinQueryTest; + +namespace Search +{ + +/** Specifies which locations the user expects to be searched for matches. */ +enum class SearchLocations { + FromHere, /// Search in m_searchUrl and its sub-folders. + Everywhere, /// Search "Everywhere" as far as possible. +}; + +/** Specifies if items should be added to the search results when their file names or contents matches the search term. */ +enum class SearchThrough { + FileNames, + FileContents, // This option currently also includes any searches that search both through FileContents and FileNames at once because we currently provide + // no option to toggle between only searching FileContents or FileContents & FileNames for any search tool. +}; + +enum class SearchTool { + Filenamesearch, // Contrary to its name, it can actually also search in file contents. +#if HAVE_BALOO + Baloo, +#endif +}; + +/** @returns whether Baloo is configured to have indexed the @p directory. */ +bool isIndexingEnabledIn(QUrl directory); + +/** @returns whether Baloo is configured to index file contents. */ +bool isContentIndexingEnabled(); + +/** @returns whether the @p urlScheme should be considered a search scheme. */ +bool isSupportedSearchScheme(const QString &urlScheme); + /** - * @brief Simple query model that parses a Baloo search Url and extracts its - * separate components to be displayed on dolphin search box. + * @brief An object that fully specifies a search configuration. + * + * A DolphinQuery encompasses all state information to uniquely identify a search. It describes the search term, search tool, search options, and requirements + * towards results. As such it can fully contain all state information of the Search::Bar because the search bars only goal is configuring and then triggering + * a search. + * + * The @a toUrl() method constructs a search URL from the DolphinQuery which Dolphin can open to start searching. Such a search URL can also be transformed + * back into a DolphinQuery object through the DolphinQuery constructor. + * + * When a DolphinQuery object is constructed or changed with incompatible conditions, like asking to search with an index-based search tool in a search path + * which is not indexed, DolphinQuery tries to fix itself so the final search can give meaningful results. + * Another example of this would be a DolphinQuery that is restricted to only search for images, which is then changed to use a search tool which does not + * allow restricting results to images. In that case the DolphinQuery object would not necessarily need to fix itself, but the exported search URL will ignore + * the image restriction, and the search user interface will need to update itself to make clear that the image restriction is ignored. + * + * Most widgets in the search UI have a `updateState()` method that takes a DolphinQuery as an argument. These methods will update the components' state to be + * in line with the DolphinQuery object's configuration. */ class DolphinQuery { public: - /** Parses the components of @p searchUrl for the supported schemes */ - static DolphinQuery fromSearchUrl(const QUrl &searchUrl); - /** Checks whether the DolphinQuery supports the given @p urlScheme */ - static bool supportsScheme(const QString &urlScheme); + /** + * @brief Automagically constructs a DolphinQuery based on the given @p url. + * @param url In the most usual case @p url is considered the search path and the DolphinQuery object is initialized based on saved user preferences. + * However, if the @p url has query information encoded in itself, which is supposed to be the case if the QUrl::scheme() of the @p url is + * "baloosearch", "tags", or "filenamesearch", this constructor retrieves all the information from the @p url and initializes the DolphinQuery + * with it. + * @param backupSearchPath The last non-search location the user was on. + * A DolphinQuery object should always be fully constructible from the main @p url parameter. However, the data encoded in @url + * might not contain any search path, for example because the constructed DolphinQuery object is supposed to search "everywhere". + * This is fine until this DolphinQuery object is switched to search in a specific location instead. In that case, this + * @p backupSearchPath will become the new searchPath() of this DolphinQuery. + */ + explicit DolphinQuery(const QUrl &url, const QUrl &backupSearchPath); + + /** + * @returns a representation of this DolphinQuery as a QUrl. This QUrl can be opened in Dolphin to trigger a search that is identical to the conditions + * provided by this DolphinQuery object. + */ + QUrl toUrl() const; - /** @return the \a searchUrl passed to Baloo::Query::fromSearchUrl() */ - QUrl searchUrl() const; - /** @return the user text part of the query, to be shown in the searchbar */ - QString text() const; - /** @return the first of Baloo::Query::types(), or an empty string */ - QString type() const; - /** @return a list of the search terms of the Baloo::Query that act as a filter, - * such as \"rating>= <i>value<i>\" or \"modified>= <i>date<i>\"*/ - QStringList searchTerms() const; - /** @return Baloo::Query::includeFolder(), that is, the initial directory - * for the query or an empty string if its a global search" */ - QString includeFolder() const; - /** @return whether the query includes search in file content */ - bool hasContentSearch() const; - /** @return whether the query includes a filter by fileName */ - bool hasFileName() const; + void setSearchLocations(SearchLocations searchLocations); + inline SearchLocations searchLocations() const + { + return m_searchLocations; + }; + + /** + * Set this query to search in @p searchPath. However, if @a searchLocations() is set to "Everywhere", @p searchPath is effectively ignored because it is + * assumed that searching everywhere also includes @p searchPath. + */ + void setSearchPath(const QUrl &searchPath); + /** + * @returns in which specific directory this query will search if the search location is not set to "Everywhere". When searching "Everywhere" this url is + * ignored completely. + */ + inline QUrl searchPath() const + { + return m_searchPath; + }; + + /** + * Set whether search results should match the search term with their names or contain it in their file contents. + */ + void setSearchThrough(SearchThrough searchThrough); + inline SearchThrough searchThrough() const + { + return m_searchThrough; + }; + + /** + * Set the search tool or backend that will be used to @p searchTool. + */ + inline void setSearchTool(SearchTool searchTool) + { + m_searchTool = searchTool; + // We do not remove any search parameters here, even if the new search tool does not support them. This is an attempt to avoid that we unnecessarily + // throw away configuration data. Non-applicable search parameters will be lost when exporting this DolphinQuery to a URL, + // but such an export won't happen if the changed DolphinQuery is not a valid search e.g. because the searchTerm().isEmpty() and every other search + // parameter is not supported by the new search tool. + }; + /** @returns the search tool to be used for this search. */ + inline SearchTool searchTool() const + { + return m_searchTool; + }; + + /** + * Sets the search text the user entered into the search field to @p searchTerm. + */ + inline void setSearchTerm(const QString &searchTerm) + { + m_searchTerm = searchTerm; + }; + /** @return the search text the user entered into the search field. */ + inline QString searchTerm() const + { + return m_searchTerm; + }; + + /** + * Sets the type every search result should have. + */ + inline void setFileType(const KFileMetaData::Type::Type &fileType) + { + m_fileType = fileType; + }; + /** + * @return the requested file type this search will be restricted to. + */ + inline KFileMetaData::Type::Type fileType() const + { + return m_fileType; + }; + + /** + * Sets the date since when every search result needs to have been modified. + */ + inline void setModifiedSinceDate(const QDate &modifiedLaterThanDate) + { + m_modifiedSinceDate = modifiedLaterThanDate; + }; + /** + * @return the date since when every search result needs to have been modified. + */ + inline QDate modifiedSinceDate() const + { + return m_modifiedSinceDate; + }; + + /** + * @param minimumRating the minimum rating value every search result needs to at least have to be considered a valid result of this query. + * Values <= 0 mean no restriction. 1 is half a star, 2 one full star, etc. 10 is typically the maximum in KDE software. + */ + inline void setMinimumRating(int minimumRating) + { + m_minimumRating = minimumRating; + }; + /** + * @returns the minimum rating every search result is requested to have. + * @see setMinimumRating(). + */ + inline int minimumRating() const + { + return m_minimumRating; + }; + + /** + * @param requiredTags All the tags every search result is required to have. + */ + inline void setRequiredTags(const QStringList &requiredTags) + { + m_requiredTags = requiredTags; + }; + /** + * @returns all the tags every search result is required to have. + */ + inline QStringList requiredTags() const + { + return m_requiredTags; + }; + + bool operator==(const DolphinQuery &) const = default; + + /** + * @returns a title to be used in user-facing situations to represent this DolphinQuery, such as "Query Results from 'importantFile'". + */ + QString title() const; private: - /** Calls Baloo::Query::fromSearchUrl() on the current searchUrl - * and parses the result to extract its separate components */ - void parseBalooQuery(); +#if HAVE_BALOO + /** Parses a Baloo::Query to extract its separate components */ + void initializeFromBalooQuery(const Baloo::Query &balooQuery, const QUrl &backupSearchPath); +#endif + + /** + * Switches to the user's preferred search tool if this is possible. If the preferred search tool cannot perform a search within this DolphinQuery's + * conditions, a different search tool will be used instead. + */ + void switchToPreferredSearchTool(); private: - QUrl m_searchUrl; - QString m_searchText; - QString m_fileType; - QStringList m_searchTerms; - QString m_includeFolder; - bool m_hasContentSearch = false; - bool m_hasFileName = false; + /** Specifies which locations will be searched for the search terms. */ + SearchLocations m_searchLocations = SearchSettings::location() == QLatin1String("Everywhere") ? SearchLocations::Everywhere : SearchLocations::FromHere; + + /** + * Specifies where searching will begin. + * @note The value of this variable is ignored when this query is set to search "Everywhere". + */ + QUrl m_searchPath; + + /** Specifies whether file names, file contents, or both will be searched for the search terms. */ + SearchThrough m_searchThrough = SearchSettings::what() == QLatin1String("FileContents") ? SearchThrough::FileContents : SearchThrough::FileNames; + + /** Specifies which search tool will be used for the search. */ +#if HAVE_BALOO + SearchTool m_searchTool = SearchSettings::searchTool() == QLatin1String("Baloo") ? SearchTool::Baloo : SearchTool::Filenamesearch; +#else + SearchTool m_searchTool = SearchTool::Filenamesearch; +#endif + + QString m_searchTerm; + /** Specifies which file type all search results should have. "Empty" means there is no restriction on file type. */ + KFileMetaData::Type::Type m_fileType = KFileMetaData::Type::Empty; + + /** All search results are requested to be modified later than or equal to this date. Null or invalid dates mean no restriction. */ + QDate m_modifiedSinceDate; + + /** + * All search results are requested to have at least this rating. + * If the minimum rating is less than or equal to 0, this variable is ignored. + * 1 is generally considered half a star in KDE software, 2 a full star, etc. Generally 10 is considered the max rating i.e. 5/5 stars or a song marked as + * one of your favourites in a music application. Higher values are AFAIK not used in KDE applications but other software might use a different scale. + */ + int m_minimumRating = 0; + + /** All the tags every search result is required to have. */ + QStringList m_requiredTags; + + /** + * @brief Any imported Baloo search parameters (token:value pairs) which Dolphin does not understand are stored in this list unmodified. + * Dolphin only allows modifying a certain selection of search parameters, but there are more. This is a bit of an unfortunate situation because we can not + * represent every single query in the user interface without creating a mess of a UI. However, we also don't want to drop these extra parameters because + * that would unexpectedly modify the query. + * So this variable simply stores anything we don't recognize and reproduces it when exporting to a Baloo URL. + */ + QStringList m_unrecognizedBalooQueryStrings; + + friend DolphinQueryTest; }; +/** + * For testing Baloo in DolphinQueryTest even if it is not indexing any locations yet. + * The test mode makes sure that DolphinQuery can be set to use Baloo even if Baloo has not indexed any locations yet. + */ +void setTestMode(); +} + #endif //DOLPHINQUERY_H diff --git a/src/search/dolphinsearchbox.cpp b/src/search/dolphinsearchbox.cpp deleted file mode 100644 index ee425501d..000000000 --- a/src/search/dolphinsearchbox.cpp +++ /dev/null @@ -1,575 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2010 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "dolphinsearchbox.h" -#include "global.h" - -#include "dolphin_searchsettings.h" -#include "dolphinfacetswidget.h" -#include "dolphinplacesmodelsingleton.h" -#include "dolphinquery.h" - -#include "config-dolphin.h" -#include <KIO/ApplicationLauncherJob> -#include <KLocalizedString> -#include <KSeparator> -#include <KService> -#if HAVE_BALOO -#include <Baloo/IndexerConfig> -#include <Baloo/Query> -#endif - -#include <QButtonGroup> -#include <QDir> -#include <QFontDatabase> -#include <QHBoxLayout> -#include <QIcon> -#include <QKeyEvent> -#include <QLineEdit> -#include <QScrollArea> -#include <QShowEvent> -#include <QTimer> -#include <QToolButton> -#include <QUrlQuery> - -DolphinSearchBox::DolphinSearchBox(QWidget *parent) - : AnimatedHeightWidget(parent) - , m_startedSearching(false) - , m_active(true) - , m_topLayout(nullptr) - , m_searchInput(nullptr) - , m_saveSearchAction(nullptr) - , m_optionsScrollArea(nullptr) - , m_fileNameButton(nullptr) - , m_contentButton(nullptr) - , m_separator(nullptr) - , m_fromHereButton(nullptr) - , m_everywhereButton(nullptr) - , m_facetsWidget(nullptr) - , m_searchPath() - , m_startSearchTimer(nullptr) - , m_initialized(false) -{ -} - -DolphinSearchBox::~DolphinSearchBox() -{ - saveSettings(); -} - -void DolphinSearchBox::setText(const QString &text) -{ - if (m_searchInput->text() != text) { - m_searchInput->setText(text); - } -} - -QString DolphinSearchBox::text() const -{ - return m_searchInput->text(); -} - -void DolphinSearchBox::setSearchPath(const QUrl &url) -{ - if (url == m_searchPath || !m_initialized) { - return; - } - - const QUrl cleanedUrl = url.adjusted(QUrl::RemoveUserInfo | QUrl::StripTrailingSlash); - - if (cleanedUrl.path() == QDir::homePath()) { - m_fromHereButton->setChecked(false); - m_everywhereButton->setChecked(true); - if (!m_searchPath.isEmpty()) { - return; - } - } else { - m_everywhereButton->setChecked(false); - m_fromHereButton->setChecked(true); - } - - m_searchPath = url; - - QFontMetrics metrics(m_fromHereButton->font()); - const int maxWidth = metrics.height() * 8; - - QString location = cleanedUrl.fileName(); - if (location.isEmpty()) { - location = cleanedUrl.toString(QUrl::PreferLocalFile); - } - const QString elidedLocation = metrics.elidedText(location, Qt::ElideMiddle, maxWidth); - m_fromHereButton->setText(i18nc("action:button", "From Here (%1)", elidedLocation)); - m_fromHereButton->setToolTip(i18nc("action:button", "Limit search to '%1' and its subfolders", cleanedUrl.toString(QUrl::PreferLocalFile))); -} - -QUrl DolphinSearchBox::searchPath() const -{ - return m_everywhereButton->isChecked() ? QUrl::fromLocalFile(QDir::homePath()) : m_searchPath; -} - -QUrl DolphinSearchBox::urlForSearching() const -{ - QUrl url; - - if (isIndexingEnabled()) { - url = balooUrlForSearching(); - } else { - url.setScheme(QStringLiteral("filenamesearch")); - - QUrlQuery query; - query.addQueryItem(QStringLiteral("search"), QUrl::toPercentEncoding(m_searchInput->text())); - if (m_contentButton->isChecked()) { - query.addQueryItem(QStringLiteral("checkContent"), QStringLiteral("yes")); - } - - query.addQueryItem(QStringLiteral("url"), QUrl::toPercentEncoding(searchPath().url())); - query.addQueryItem(QStringLiteral("title"), QUrl::toPercentEncoding(queryTitle(m_searchInput->text()))); - - url.setQuery(query); - } - - return url; -} - -void DolphinSearchBox::fromSearchUrl(const QUrl &url) -{ - if (DolphinQuery::supportsScheme(url.scheme())) { - const DolphinQuery query = DolphinQuery::fromSearchUrl(url); - updateFromQuery(query); - } else if (url.scheme() == QLatin1String("filenamesearch")) { - const QUrlQuery query(url); - setText(query.queryItemValue(QStringLiteral("search"), QUrl::FullyDecoded)); - if (m_searchPath.scheme() != url.scheme()) { - m_searchPath = QUrl(); - } - setSearchPath(QUrl::fromUserInput(query.queryItemValue(QStringLiteral("url"), QUrl::FullyDecoded), QString(), QUrl::AssumeLocalFile)); - m_contentButton->setChecked(query.queryItemValue(QStringLiteral("checkContent")) == QLatin1String("yes")); - } else { - setText(QString()); - m_searchPath = QUrl(); - setSearchPath(url); - } - - updateFacetsVisible(); -} - -void DolphinSearchBox::selectAll() -{ - m_searchInput->selectAll(); -} - -void DolphinSearchBox::setActive(bool active) -{ - if (active != m_active) { - m_active = active; - - if (active) { - Q_EMIT activated(); - } - } -} - -bool DolphinSearchBox::isActive() const -{ - return m_active; -} - -void DolphinSearchBox::setVisible(bool visible, Animated animated) -{ - if (visible) { - init(); - } - AnimatedHeightWidget::setVisible(visible, animated); -} - -void DolphinSearchBox::showEvent(QShowEvent *event) -{ - if (!event->spontaneous()) { - m_searchInput->setFocus(); - m_startedSearching = false; - } -} - -void DolphinSearchBox::hideEvent(QHideEvent *event) -{ - Q_UNUSED(event) - m_startedSearching = false; - if (m_startSearchTimer) { - m_startSearchTimer->stop(); - } -} - -void DolphinSearchBox::keyReleaseEvent(QKeyEvent *event) -{ - QWidget::keyReleaseEvent(event); - if (event->key() == Qt::Key_Escape) { - if (m_searchInput->text().isEmpty()) { - emitCloseRequest(); - } else { - m_searchInput->clear(); - } - } else if (event->key() == Qt::Key_Down) { - Q_EMIT focusViewRequest(); - } -} - -bool DolphinSearchBox::eventFilter(QObject *obj, QEvent *event) -{ - switch (event->type()) { - case QEvent::FocusIn: - // #379135: we get the FocusIn event when we close a tab but we don't want to emit - // the activated() signal before the removeTab() call in DolphinTabWidget::closeTab() returns. - // To avoid this issue, we delay the activation of the search box. - // We also don't want to schedule the activation process if we are already active, - // otherwise we can enter in a loop of FocusIn/FocusOut events with the searchbox of another tab. - if (!isActive()) { - QTimer::singleShot(0, this, [this] { - setActive(true); - setFocus(); - }); - } - break; - - default: - break; - } - - return QObject::eventFilter(obj, event); -} - -void DolphinSearchBox::emitSearchRequest() -{ - m_startSearchTimer->stop(); - m_startedSearching = true; - m_saveSearchAction->setEnabled(true); - Q_EMIT searchRequest(); -} - -void DolphinSearchBox::emitCloseRequest() -{ - m_startSearchTimer->stop(); - m_startedSearching = false; - m_saveSearchAction->setEnabled(false); - Q_EMIT closeRequest(); -} - -void DolphinSearchBox::slotConfigurationChanged() -{ - saveSettings(); - if (m_startedSearching) { - emitSearchRequest(); - } -} - -void DolphinSearchBox::slotSearchTextChanged(const QString &text) -{ - if (text.isEmpty()) { - // Restore URL when search box is cleared by closing and reopening the box. - emitCloseRequest(); - Q_EMIT openRequest(); - } else { - m_startSearchTimer->start(); - } - Q_EMIT searchTextChanged(text); -} - -void DolphinSearchBox::slotReturnPressed() -{ - if (m_searchInput->text().isEmpty()) { - return; - } - - emitSearchRequest(); - Q_EMIT focusViewRequest(); -} - -void DolphinSearchBox::slotFacetChanged() -{ - m_startedSearching = true; - m_startSearchTimer->stop(); - Q_EMIT searchRequest(); -} - -void DolphinSearchBox::slotSearchSaved() -{ - const QUrl searchURL = urlForSearching(); - if (searchURL.isValid()) { - const QString label = i18n("Search for %1 in %2", text(), searchPath().fileName()); - DolphinPlacesModelSingleton::instance().placesModel()->addPlace(label, searchURL, QStringLiteral("folder-saved-search-symbolic")); - } -} - -void DolphinSearchBox::initButton(QToolButton *button) -{ - button->installEventFilter(this); - button->setAutoExclusive(true); - button->setAutoRaise(true); - button->setCheckable(true); - connect(button, &QToolButton::clicked, this, &DolphinSearchBox::slotConfigurationChanged); -} - -void DolphinSearchBox::loadSettings() -{ - if (SearchSettings::location() == QLatin1String("Everywhere")) { - m_everywhereButton->setChecked(true); - } else { - m_fromHereButton->setChecked(true); - } - - if (SearchSettings::what() == QLatin1String("Content")) { - m_contentButton->setChecked(true); - } else { - m_fileNameButton->setChecked(true); - } - - updateFacetsVisible(); -} - -void DolphinSearchBox::saveSettings() -{ - if (m_initialized) { - SearchSettings::setLocation(m_fromHereButton->isChecked() ? QStringLiteral("FromHere") : QStringLiteral("Everywhere")); - SearchSettings::setWhat(m_fileNameButton->isChecked() ? QStringLiteral("FileName") : QStringLiteral("Content")); - SearchSettings::self()->save(); - } -} - -void DolphinSearchBox::init() -{ - if (m_initialized) { - return; // This object is already initialised. - } - - QWidget *contentsContainer = prepareContentsContainer(); - - // Create search box - m_searchInput = new QLineEdit(contentsContainer); - m_searchInput->setPlaceholderText(i18n("Search…")); - m_searchInput->installEventFilter(this); - m_searchInput->setClearButtonEnabled(true); - m_searchInput->setFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont)); - connect(m_searchInput, &QLineEdit::returnPressed, this, &DolphinSearchBox::slotReturnPressed); - connect(m_searchInput, &QLineEdit::textChanged, this, &DolphinSearchBox::slotSearchTextChanged); - setFocusProxy(m_searchInput); - - // Add "Save search" button inside search box - m_saveSearchAction = new QAction(this); - m_saveSearchAction->setIcon(QIcon::fromTheme(QStringLiteral("document-save-symbolic"))); - m_saveSearchAction->setText(i18nc("action:button", "Save this search to quickly access it again in the future")); - m_saveSearchAction->setEnabled(false); - m_searchInput->addAction(m_saveSearchAction, QLineEdit::TrailingPosition); - connect(m_saveSearchAction, &QAction::triggered, this, &DolphinSearchBox::slotSearchSaved); - - // Create close button - QToolButton *closeButton = new QToolButton(contentsContainer); - closeButton->setAutoRaise(true); - closeButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); - closeButton->setToolTip(i18nc("@info:tooltip", "Quit searching")); - connect(closeButton, &QToolButton::clicked, this, &DolphinSearchBox::emitCloseRequest); - - // Apply layout for the search input - QHBoxLayout *searchInputLayout = new QHBoxLayout(); - searchInputLayout->setContentsMargins(0, 0, 0, 0); - searchInputLayout->addWidget(m_searchInput); - searchInputLayout->addWidget(closeButton); - - // Create "Filename" and "Content" button - m_fileNameButton = new QToolButton(contentsContainer); - m_fileNameButton->setText(i18nc("action:button", "Filename")); - initButton(m_fileNameButton); - - m_contentButton = new QToolButton(); - m_contentButton->setText(i18nc("action:button", "Content")); - initButton(m_contentButton); - - QButtonGroup *searchWhatGroup = new QButtonGroup(contentsContainer); - searchWhatGroup->addButton(m_fileNameButton); - searchWhatGroup->addButton(m_contentButton); - - m_separator = new KSeparator(Qt::Vertical, contentsContainer); - - // Create "From Here" and "Your files" buttons - m_fromHereButton = new QToolButton(contentsContainer); - m_fromHereButton->setText(i18nc("action:button", "From Here")); - initButton(m_fromHereButton); - - m_everywhereButton = new QToolButton(contentsContainer); - m_everywhereButton->setText(i18nc("action:button", "Your files")); - m_everywhereButton->setToolTip(i18nc("action:button", "Search in your home directory")); - m_everywhereButton->setIcon(QIcon::fromTheme(QStringLiteral("user-home"))); - m_everywhereButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - initButton(m_everywhereButton); - - QButtonGroup *searchLocationGroup = new QButtonGroup(contentsContainer); - searchLocationGroup->addButton(m_fromHereButton); - searchLocationGroup->addButton(m_everywhereButton); - - KService::Ptr kfind = KService::serviceByDesktopName(QStringLiteral("org.kde.kfind")); - - QToolButton *kfindToolsButton = nullptr; - if (kfind) { - kfindToolsButton = new QToolButton(contentsContainer); - kfindToolsButton->setAutoRaise(true); - kfindToolsButton->setPopupMode(QToolButton::InstantPopup); - kfindToolsButton->setIcon(QIcon::fromTheme("arrow-down-double")); - kfindToolsButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - kfindToolsButton->setText(i18n("Open %1", kfind->name())); - kfindToolsButton->setIcon(QIcon::fromTheme(kfind->icon())); - - connect(kfindToolsButton, &QToolButton::clicked, this, [this, kfind] { - auto *job = new KIO::ApplicationLauncherJob(kfind); - job->setUrls({m_searchPath}); - job->start(); - }); - } - - // Create "Facets" widget - m_facetsWidget = new DolphinFacetsWidget(contentsContainer); - m_facetsWidget->installEventFilter(this); - m_facetsWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - m_facetsWidget->layout()->setSpacing(Dolphin::LAYOUT_SPACING_SMALL); - connect(m_facetsWidget, &DolphinFacetsWidget::facetChanged, this, &DolphinSearchBox::slotFacetChanged); - - // Put the options into a QScrollArea. This prevents increasing the view width - // in case that not enough width for the options is available. - QWidget *optionsContainer = new QWidget(contentsContainer); - - // Apply layout for the options - QHBoxLayout *optionsLayout = new QHBoxLayout(optionsContainer); - optionsLayout->setContentsMargins(0, 0, 0, 0); - optionsLayout->setSpacing(Dolphin::LAYOUT_SPACING_SMALL); - optionsLayout->addWidget(m_fileNameButton); - optionsLayout->addWidget(m_contentButton); - optionsLayout->addWidget(m_separator); - optionsLayout->addWidget(m_fromHereButton); - optionsLayout->addWidget(m_everywhereButton); - optionsLayout->addWidget(new KSeparator(Qt::Vertical, contentsContainer)); - if (kfindToolsButton) { - optionsLayout->addWidget(kfindToolsButton); - } - optionsLayout->addStretch(1); - - m_optionsScrollArea = new QScrollArea(contentsContainer); - m_optionsScrollArea->setFrameShape(QFrame::NoFrame); - m_optionsScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_optionsScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_optionsScrollArea->setMaximumHeight(optionsContainer->sizeHint().height()); - m_optionsScrollArea->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - m_optionsScrollArea->setWidget(optionsContainer); - m_optionsScrollArea->setWidgetResizable(true); - - m_topLayout = new QVBoxLayout(contentsContainer); - m_topLayout->setContentsMargins(0, Dolphin::LAYOUT_SPACING_SMALL, 0, 0); - m_topLayout->setSpacing(Dolphin::LAYOUT_SPACING_SMALL); - m_topLayout->addLayout(searchInputLayout); - m_topLayout->addWidget(m_optionsScrollArea); - m_topLayout->addWidget(m_facetsWidget); - - loadSettings(); - - // The searching should be started automatically after the user did not change - // the text for a while - m_startSearchTimer = new QTimer(this); - m_startSearchTimer->setSingleShot(true); - m_startSearchTimer->setInterval(500); - connect(m_startSearchTimer, &QTimer::timeout, this, &DolphinSearchBox::emitSearchRequest); - - m_initialized = true; -} - -QString DolphinSearchBox::queryTitle(const QString &text) const -{ - return i18nc("@title UDS_DISPLAY_NAME for a KIO directory listing. %1 is the query the user entered.", "Query Results from '%1'", text); -} - -QUrl DolphinSearchBox::balooUrlForSearching() const -{ -#if HAVE_BALOO - const QString text = m_searchInput->text(); - - Baloo::Query query; - query.addType(m_facetsWidget->facetType()); - - QStringList queryStrings = m_facetsWidget->searchTerms(); - - if (m_contentButton->isChecked()) { - queryStrings << text; - } else if (!text.isEmpty()) { - queryStrings << QStringLiteral("filename:\"%1\"").arg(text); - } - - if (m_fromHereButton->isChecked()) { - query.setIncludeFolder(m_searchPath.toLocalFile()); - } - - query.setSearchString(queryStrings.join(QLatin1Char(' '))); - - return query.toSearchUrl(queryTitle(text)); -#else - return QUrl(); -#endif -} - -void DolphinSearchBox::updateFromQuery(const DolphinQuery &query) -{ - // Block all signals to avoid unnecessary "searchRequest" signals - // while we adjust the search text and the facet widget. - blockSignals(true); - - const QString customDir = query.includeFolder(); - if (!customDir.isEmpty()) { - setSearchPath(QUrl::fromLocalFile(customDir)); - } else { - setSearchPath(QUrl::fromLocalFile(QDir::homePath())); - } - - setText(query.text()); - - if (query.hasContentSearch()) { - m_contentButton->setChecked(true); - } else if (query.hasFileName()) { - m_fileNameButton->setChecked(true); - } - - m_facetsWidget->resetSearchTerms(); - m_facetsWidget->setFacetType(query.type()); - const QStringList searchTerms = query.searchTerms(); - for (const QString &searchTerm : searchTerms) { - m_facetsWidget->setSearchTerm(searchTerm); - } - - m_startSearchTimer->stop(); - blockSignals(false); -} - -void DolphinSearchBox::updateFacetsVisible() -{ - const bool indexingEnabled = isIndexingEnabled(); - m_facetsWidget->setEnabled(indexingEnabled); - m_facetsWidget->setVisible(indexingEnabled); - - // The m_facetsWidget might have changed visibility. We smoothly animate towards the updated height. - if (isVisible() && isEnabled()) { - setVisible(true, WithAnimation); - } -} - -bool DolphinSearchBox::isIndexingEnabled() const -{ -#if HAVE_BALOO - const Baloo::IndexerConfig searchInfo; - return searchInfo.fileIndexingEnabled() && !searchPath().isEmpty() && searchInfo.shouldBeIndexed(searchPath().toLocalFile()); -#else - return false; -#endif -} - -int DolphinSearchBox::preferredHeight() const -{ - return m_initialized ? m_topLayout->sizeHint().height() : 0; -} - -#include "moc_dolphinsearchbox.cpp" diff --git a/src/search/dolphinsearchbox.h b/src/search/dolphinsearchbox.h deleted file mode 100644 index 6a847ba57..000000000 --- a/src/search/dolphinsearchbox.h +++ /dev/null @@ -1,189 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2010 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#ifndef DOLPHINSEARCHBOX_H -#define DOLPHINSEARCHBOX_H - -#include "animatedheightwidget.h" - -#include <QUrl> - -class DolphinFacetsWidget; -class DolphinQuery; -class QLineEdit; -class KSeparator; -class QToolButton; -class QScrollArea; -class QLabel; -class QVBoxLayout; - -/** - * @brief Input box for searching files with or without Baloo. - * - * The widget allows to specify: - * - Where to search: Everywhere or below the current directory - * - What to search: Filenames or content - * - * If Baloo is available and the current folder is indexed, further - * options are offered. - */ -class DolphinSearchBox : public AnimatedHeightWidget -{ - Q_OBJECT - -public: - explicit DolphinSearchBox(QWidget *parent = nullptr); - ~DolphinSearchBox() override; - - /** - * Sets the text that should be used as input for - * searching. - */ - void setText(const QString &text); - - /** - * Returns the text that should be used as input - * for searching. - */ - QString text() const; - - /** - * Sets the current path that is used as root for searching files. - * If @url is the Home dir, "From Here" is selected instead. - */ - void setSearchPath(const QUrl &url); - QUrl searchPath() const; - - /** @return URL that will start the searching of files. */ - QUrl urlForSearching() const; - - /** - * Extracts information from the given search \a url to - * initialize the search box properly. - */ - void fromSearchUrl(const QUrl &url); - - /** - * Selects the whole text of the search box. - */ - void selectAll(); - - /** - * Set the search box to the active mode, if \a active - * is true. The active mode is default. The inactive mode only differs - * visually from the active mode, no change of the behavior is given. - * - * Using the search box in the inactive mode is useful when having split views, - * where the inactive view is indicated by an search box visually. - */ - void setActive(bool active); - - /** - * @return True, if the search box is in the active mode. - * @see DolphinSearchBox::setActive() - */ - bool isActive() const; - - /* - * @see AnimatedHeightWidget::setVisible() - * @see QWidget::setVisible() - */ - void setVisible(bool visible, Animated animated); - -protected: - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override; - void keyReleaseEvent(QKeyEvent *event) override; - bool eventFilter(QObject *obj, QEvent *event) override; - -Q_SIGNALS: - /** - * Is emitted when a searching should be triggered. - */ - void searchRequest(); - - /** - * Is emitted when the user has changed a character of - * the text that should be used as input for searching. - */ - void searchTextChanged(const QString &text); - - /** - * Emitted as soon as the search box should get closed. - */ - void closeRequest(); - - /** - * Is emitted when the search box should be opened. - */ - void openRequest(); - - /** - * Is emitted, if the searchbox has been activated by - * an user interaction - * @see DolphinSearchBox::setActive() - */ - void activated(); - void focusViewRequest(); - -private Q_SLOTS: - void emitSearchRequest(); - void emitCloseRequest(); - void slotConfigurationChanged(); - void slotSearchTextChanged(const QString &text); - void slotReturnPressed(); - void slotFacetChanged(); - void slotSearchSaved(); - -private: - void initButton(QToolButton *button); - void loadSettings(); - void saveSettings(); - void init(); - - /** - * @return URL that represents the Baloo query for starting the search. - */ - QUrl balooUrlForSearching() const; - - /** - * Sets the searchbox UI with the parameters established by the \a query - */ - void updateFromQuery(const DolphinQuery &query); - - void updateFacetsVisible(); - - bool isIndexingEnabled() const; - - /** @see AnimatedHeightWidget::preferredHeight() */ - int preferredHeight() const override; - -private: - QString queryTitle(const QString &text) const; - - bool m_startedSearching; - bool m_active; - - QVBoxLayout *m_topLayout; - - QLineEdit *m_searchInput; - QAction *m_saveSearchAction; - QScrollArea *m_optionsScrollArea; - QToolButton *m_fileNameButton; - QToolButton *m_contentButton; - KSeparator *m_separator; - QToolButton *m_fromHereButton; - QToolButton *m_everywhereButton; - DolphinFacetsWidget *m_facetsWidget; - - QUrl m_searchPath; - - QTimer *m_startSearchTimer; - - bool m_initialized; -}; - -#endif diff --git a/src/search/popup.cpp b/src/search/popup.cpp new file mode 100644 index 000000000..ce0fb21a2 --- /dev/null +++ b/src/search/popup.cpp @@ -0,0 +1,378 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2024 Felix Ernst <[email protected]> + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "popup.h" + +#include "config-dolphin.h" +#include "dolphinpackageinstaller.h" +#include "dolphinquery.h" +#include "global.h" +#include "selectors/dateselector.h" +#include "selectors/filetypeselector.h" +#include "selectors/minimumratingselector.h" +#include "selectors/tagsselector.h" + +#include <KContextualHelpButton> +#include <KDialogJobUiDelegate> +#include <KIO/ApplicationLauncherJob> +#include <KIO/CommandLauncherJob> +#include <KLocalizedString> +#include <KService> + +#include <QButtonGroup> +#ifdef Q_OS_WIN +#include <QDesktopServices> +#endif +#include <QHBoxLayout> +#include <QLabel> +#include <QMenu> +#include <QRadioButton> +#include <QStandardPaths> +#include <QToolButton> +#include <QVBoxLayout> + +namespace +{ +constexpr auto kFindDesktopName = "org.kde.kfind"; +} + +using namespace Search; + +QString Search::filenamesearchUiName() +{ + // i18n: Localized name for the Filenamesearch search tool for use in user interfaces. + return i18n("Simple search"); +}; + +QString Search::balooUiName() +{ + // i18n: Localized name for the Baloo search tool for use in user interfaces. + return i18n("File Indexing"); +}; + +Popup::Popup(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent) + : WidgetMenu{parent} + , UpdatableStateInterface{dolphinQuery} +{ +} + +QWidget *Popup::init() +{ + auto containerWidget = new QWidget{this}; + containerWidget->setContentsMargins(Dolphin::VERTICAL_SPACER_HEIGHT, + Dolphin::VERTICAL_SPACER_HEIGHT, + Dolphin::VERTICAL_SPACER_HEIGHT, // Using the same value for every spacing in this containerWidget looks nice. + Dolphin::VERTICAL_SPACER_HEIGHT); + auto verticalMainLayout = new QVBoxLayout{containerWidget}; + verticalMainLayout->setSpacing((2 * Dolphin::VERTICAL_SPACER_HEIGHT) / 3); // A bit less spacing between rows than when adding an explicit spacer. + + /// Add UI to switch between only searching in file names or also in contents. + auto searchInLabel = new QLabel{i18nc("@title:group", "Search in:"), containerWidget}; + searchInLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard); + verticalMainLayout->addWidget(searchInLabel); + + m_searchInFileNamesRadioButton = new QRadioButton{i18nc("@option:radio Search in:", "File names"), containerWidget}; + connect(m_searchInFileNamesRadioButton, &QAbstractButton::clicked, this, [this]() { + if (m_searchConfiguration->searchThrough() == SearchThrough::FileNames) { + return; // Already selected. + } + SearchSettings::setWhat(QStringLiteral("FileNames")); + SearchSettings::self()->save(); + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setSearchThrough(SearchThrough::FileNames); + Q_EMIT configurationChanged(std::move(searchConfigurationCopy)); + }); + verticalMainLayout->addWidget(m_searchInFileNamesRadioButton); + + m_searchInFileContentsRadioButton = new QRadioButton{containerWidget}; + connect(m_searchInFileContentsRadioButton, &QAbstractButton::clicked, this, [this]() { + if (m_searchConfiguration->searchThrough() == SearchThrough::FileContents) { + return; // Already selected. + } + SearchSettings::setWhat(QStringLiteral("FileContents")); + SearchSettings::self()->save(); + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setSearchThrough(SearchThrough::FileContents); + Q_EMIT configurationChanged(std::move(searchConfigurationCopy)); + }); + verticalMainLayout->addWidget(m_searchInFileContentsRadioButton); + + auto searchInButtonGroup = new QButtonGroup{this}; + searchInButtonGroup->addButton(m_searchInFileNamesRadioButton); + searchInButtonGroup->addButton(m_searchInFileContentsRadioButton); + + /// Add UI to switch between search tools. + // When we build without Baloo, there is only one search tool available, so we skip adding the UI to switch. +#if HAVE_BALOO + verticalMainLayout->addSpacing(Dolphin::VERTICAL_SPACER_HEIGHT); + + auto searchUsingLabel = new QLabel{i18nc("@title:group", "Search using:"), containerWidget}; + searchUsingLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard); + verticalMainLayout->addWidget(searchUsingLabel); + + /// Initialize the Filenamesearch row. + m_filenamesearchRadioButton = new QRadioButton{filenamesearchUiName(), containerWidget}; + connect(m_filenamesearchRadioButton, &QAbstractButton::clicked, this, [this]() { + if (m_searchConfiguration->searchTool() == SearchTool::Filenamesearch) { + return; // Already selected. + } + SearchSettings::setSearchTool(QStringLiteral("Filenamesearch")); + SearchSettings::self()->save(); + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setSearchTool(SearchTool::Filenamesearch); + Q_EMIT configurationChanged(std::move(searchConfigurationCopy)); + }); + + m_filenamesearchContextualHelpButton = new KContextualHelpButton( + xi18nc("@info about a search tool", + "<para>For searching in file contents <application>%1</application> attempts to use third-party search tools if they are available on this " + "system and are expected to lead to better or faster results. <application>ripgrep</application> and <application>ripgrep-all</application> " + "might improve your search experience if they are installed. <application>ripgrep-all</application> in particular enables searches in more " + "file types (e.g. pdf, docx, sqlite, jpg, movie subtitles (mkv, mp4)).</para><para>The manner in which these search tools are invoked can be " + "configured by editing a script file. Copy it from <filename>%2</filename> to <filename>%3</filename> before modifying your copy. If any " + "issues arise, delete your copy <filename>%3</filename> to revert your changes.</para>", + filenamesearchUiName(), + QStringLiteral("%1/kio_filenamesearch/kio-filenamesearch-grep").arg(KDE_INSTALL_FULL_DATADIR), + QStringLiteral("%1/kio_filenamesearch/kio-filenamesearch-grep").arg(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation))), + m_filenamesearchRadioButton, + containerWidget); + + auto filenamesearchRowLayout = new QHBoxLayout; + filenamesearchRowLayout->addWidget(m_filenamesearchRadioButton); + filenamesearchRowLayout->addWidget(m_filenamesearchContextualHelpButton); + filenamesearchRowLayout->addStretch(); // for left-alignment + verticalMainLayout->addLayout(filenamesearchRowLayout); + + /// Initialize the Baloo row. + m_balooRadioButton = new QRadioButton{balooUiName(), containerWidget}; + connect(m_balooRadioButton, &QAbstractButton::clicked, this, [this]() { + if (m_searchConfiguration->searchTool() == SearchTool::Baloo) { + return; // Already selected. + } + SearchSettings::setSearchTool(QStringLiteral("Baloo")); + SearchSettings::self()->save(); + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setSearchTool(SearchTool::Baloo); + Q_EMIT configurationChanged(std::move(searchConfigurationCopy)); + }); + + m_balooContextualHelpButton = new KContextualHelpButton(QString(), m_balooRadioButton, containerWidget); + + auto balooSettingsButton = new QToolButton{containerWidget}; + balooSettingsButton->setText(i18nc("@action:button %1 is software name", "Configure %1…", balooUiName())); + balooSettingsButton->setIcon(QIcon::fromTheme("configure")); + balooSettingsButton->setToolTip(balooSettingsButton->text()); + balooSettingsButton->setToolButtonStyle(Qt::ToolButtonIconOnly); + balooSettingsButton->setAutoRaise(true); + balooSettingsButton->setFixedHeight(m_balooRadioButton->sizeHint().height()); + connect(balooSettingsButton, &QToolButton::clicked, this, [containerWidget] { + // Code taken from KCMLauncher::openSystemSettings() in the KCMUtil KDE framework. + constexpr auto systemSettings = "systemsettings"; + KIO::CommandLauncherJob *openBalooSettingsJob; + // Open in System Settings if it's available + if (KService::serviceByDesktopName(systemSettings)) { + openBalooSettingsJob = new KIO::CommandLauncherJob(systemSettings, {"kcm_baloofile"}, containerWidget); + openBalooSettingsJob->setDesktopName(systemSettings); + } else { + openBalooSettingsJob = new KIO::CommandLauncherJob(QStringLiteral("kcmshell6"), {"kcm_baloofile"}, containerWidget); + } + openBalooSettingsJob->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, containerWidget)); + openBalooSettingsJob->start(); + }); + + auto balooRowLayout = new QHBoxLayout; + balooRowLayout->addWidget(m_balooRadioButton); + balooRowLayout->addWidget(m_balooContextualHelpButton); + balooRowLayout->addWidget(balooSettingsButton); + balooRowLayout->addStretch(); // for left-alignment + verticalMainLayout->addLayout(balooRowLayout); + + auto searchUsingButtonGroup = new QButtonGroup{this}; + searchUsingButtonGroup->addButton(m_filenamesearchRadioButton); + searchUsingButtonGroup->addButton(m_balooRadioButton); + + verticalMainLayout->addSpacing(Dolphin::VERTICAL_SPACER_HEIGHT); + + /// Add extra search filters like date, tags, rating, etc. + m_selectorsLayoutWidget = new QWidget{containerWidget}; + if (m_searchConfiguration->searchTool() == SearchTool::Filenamesearch) { + m_selectorsLayoutWidget->hide(); + } + auto selectorsLayout = new QGridLayout{m_selectorsLayoutWidget}; + selectorsLayout->setContentsMargins(0, 0, 0, 0); + selectorsLayout->setSpacing(verticalMainLayout->spacing()); + + auto typeSelectorTitle = new QLabel{i18nc("@title:group for filtering files based on their type", "File Type:"), containerWidget}; + typeSelectorTitle->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard); + selectorsLayout->addWidget(typeSelectorTitle, 1, 0); + + m_typeSelector = new FileTypeSelector{m_searchConfiguration, containerWidget}; + connect(m_typeSelector, &FileTypeSelector::configurationChanged, this, &Popup::configurationChanged); + selectorsLayout->addWidget(m_typeSelector, 2, 0); + + auto dateSelectorTitle = new QLabel{i18nc("@title:group for filtering files by modified date", "Modified since:"), containerWidget}; + dateSelectorTitle->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard); + selectorsLayout->addWidget(dateSelectorTitle, 1, 1); + + m_dateSelector = new DateSelector{m_searchConfiguration, containerWidget}; + m_dateSelector->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); // Make sure this button is as wide as the other button in this column. + connect(m_dateSelector, &DateSelector::configurationChanged, this, &Popup::configurationChanged); + selectorsLayout->addWidget(m_dateSelector, 2, 1); + + auto ratingSelectorTitle = new QLabel{i18nc("@title:group for selecting a minimum rating of search results", "Rating:"), containerWidget}; + ratingSelectorTitle->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard); + selectorsLayout->addWidget(ratingSelectorTitle, 3, 0); + + m_ratingSelector = new MinimumRatingSelector{m_searchConfiguration, containerWidget}; + connect(m_ratingSelector, &MinimumRatingSelector::configurationChanged, this, &Popup::configurationChanged); + selectorsLayout->addWidget(m_ratingSelector, 4, 0); + + auto tagsSelectorTitle = new QLabel{i18nc("@title:group for selecting required tags for search results", "Tags:"), containerWidget}; + tagsSelectorTitle->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard); + selectorsLayout->addWidget(tagsSelectorTitle, 3, 1); + + m_tagsSelector = new TagsSelector{m_searchConfiguration, containerWidget}; + m_tagsSelector->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); // Make sure this button is as wide as the other button in this column. + connect(m_tagsSelector, &TagsSelector::configurationChanged, this, &Popup::configurationChanged); + selectorsLayout->addWidget(m_tagsSelector, 4, 1); + + verticalMainLayout->addWidget(m_selectorsLayoutWidget); +#endif // HAVE_BALOO + + /** + * Dolphin cannot provide every advanced search workflow, so here at the end we need to push users to more dedicated search tools if what Dolphin provides + * turns out to be insufficient. + */ + verticalMainLayout->addSpacing(Dolphin::VERTICAL_SPACER_HEIGHT); + + auto kfindLabel = new QLabel{i18nc("@label above 'Install KFind'/'Open KFind' button", "For more advanced searches:"), containerWidget}; + kfindLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard); + verticalMainLayout->addWidget(kfindLabel); + + m_kFindButton = new QToolButton{containerWidget}; + m_kFindButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + connect(m_kFindButton, &QToolButton::clicked, this, &Popup::slotKFindButtonClicked); + verticalMainLayout->addWidget(m_kFindButton); + + return containerWidget; +} + +void Popup::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) +{ + m_searchInFileNamesRadioButton->setChecked(dolphinQuery->searchThrough() == SearchThrough::FileNames); + m_searchInFileContentsRadioButton->setChecked(dolphinQuery->searchThrough() == SearchThrough::FileContents); + + // When we build without Baloo, there is only one search tool available and no UI to switch. +#if HAVE_BALOO + m_filenamesearchRadioButton->setChecked(dolphinQuery->searchTool() == SearchTool::Filenamesearch); + m_filenamesearchContextualHelpButton->setVisible(dolphinQuery->searchThrough() == SearchThrough::FileContents); + + if (dolphinQuery->searchLocations() != SearchLocations::Everywhere && !isIndexingEnabledIn(dolphinQuery->searchPath())) { + m_balooRadioButton->setToolTip( + xi18nc("@info:tooltip", + "<para>Searching in <filename>%1</filename> using <application>%2</application> is currently not possible because " + "<application>%2</application> is configured to never create a search index of that location.</para>", + dolphinQuery->searchPath().adjusted(QUrl::RemoveUserInfo | QUrl::StripTrailingSlash).toString(QUrl::PreferLocalFile), + balooUiName())); + m_balooRadioButton->setDisabled(true); + } else if (dolphinQuery->searchThrough() == SearchThrough::FileContents && !isContentIndexingEnabled()) { + m_balooRadioButton->setToolTip(xi18nc("@info:tooltip", + "<para>Searching through file contents using <application>%1</application> is currently not possible because " + "<application>%1</application> is configured to never create a search index for file contents.</para>", + balooUiName())); + m_balooRadioButton->setDisabled(true); + } else { + m_balooRadioButton->setToolTip(QString()); + m_balooRadioButton->setEnabled(true); + } + m_balooContextualHelpButton->setContextualHelpText( + i18nc("@info make a warning paragraph bold before other paragraphs", "<b>%1</b>", m_balooRadioButton->toolTip()) + + xi18nc( + "@info about a search tool", + "<para><application>%1</application> uses a database for searching. The database is created by indexing your files in the background based on " + "how <application>%1</application> is configured.<list><item><application>%1</application> provides results extremely " + "quickly.</item><item>Allows searching for file types, dates, tags, etc.</item><item>Only searches in indexed folders. Configure which folders " + "should be indexed in <application>System Settings</application>.</item><item>When the searched locations contain links to other files or " + "folders, those will not be searched or show up in search results.</item><item>Hidden files and folders and their contents might also not be " + "searched depending on how <application>%1</application> is configured.</item></list></para>", + balooUiName())); + + m_balooRadioButton->setChecked(dolphinQuery->searchTool() == SearchTool::Baloo); + m_balooRadioButton->setChecked(false); + + if (m_balooRadioButton->isChecked()) { + m_searchInFileContentsRadioButton->setText(i18nc("@option:radio Search in:", "File names and contents")); + m_typeSelector->updateStateToMatch(dolphinQuery); + m_dateSelector->updateStateToMatch(dolphinQuery); + m_ratingSelector->updateStateToMatch(dolphinQuery); + m_tagsSelector->updateStateToMatch(dolphinQuery); + } else { +#endif // HAVE_BALOO + m_searchInFileContentsRadioButton->setText(i18nc("@option:radio Search in:", "File contents")); +#if HAVE_BALOO + } + + /// Show/Hide Baloo-specific selectors. + m_selectorsLayoutWidget->setVisible(m_balooRadioButton->isChecked()); + const int columnWidth = std::max( + {m_typeSelector->sizeHint().width(), m_dateSelector->sizeHint().width(), m_ratingSelector->sizeHint().width(), m_tagsSelector->sizeHint().width()}); + static_cast<QGridLayout *>(m_selectorsLayoutWidget->layout())->setColumnMinimumWidth(0, columnWidth); + static_cast<QGridLayout *>(m_selectorsLayoutWidget->layout())->setColumnMinimumWidth(1, columnWidth); + resizeToFitContents(); +#endif // HAVE_BALOO + + KService::Ptr kFind = KService::serviceByDesktopName(kFindDesktopName); + if (kFind) { + m_kFindButton->setText(i18nc("@action:button 1 is KFind app name", "Open %1", kFind->name())); + m_kFindButton->setIcon(QIcon::fromTheme(kFind->icon())); + } else { + m_kFindButton->setText(i18nc("@action:button", "Install KFind…")); + m_kFindButton->setIcon(QIcon::fromTheme(QStringLiteral("kfind"), QIcon::fromTheme(QStringLiteral("install")))); + } +} + +void Popup::slotKFindButtonClicked() +{ + /// Open KFind if it is installed. + KService::Ptr kFind = KService::serviceByDesktopName(kFindDesktopName); + if (kFind) { + auto *job = new KIO::ApplicationLauncherJob(kFind); + job->setUrls({m_searchConfiguration->searchPath()}); + job->start(); + return; + } + + /// Otherwise, install KFind. +#ifdef Q_OS_WIN + QDesktopServices::openUrl(QUrl("https://apps.kde.org/kfind")); +#else + auto packageInstaller = new DolphinPackageInstaller( + KFIND_PACKAGE_NAME, + QUrl("appstream://org.kde.kfind.desktop"), + []() { + return KService::serviceByDesktopName(kFindDesktopName); + }, + this); + connect(packageInstaller, &KJob::result, this, [this](KJob *job) { + Q_EMIT showInstallationProgress(QString(), 100); // Hides the progress information in the status bar. + if (job->error()) { + Q_EMIT showMessage(job->errorString(), KMessageWidget::Error); + } else { + Q_EMIT showMessage(xi18nc("@info", "<application>KFind</application> installed successfully."), KMessageWidget::Positive); + updateStateToMatch(m_searchConfiguration); // Updates m_kfindButton from an "Install KFind…" to an "Open KFind" button. + } + }); + const auto installationTaskText{i18nc("@info:status", "Installing KFind")}; + Q_EMIT showInstallationProgress(installationTaskText, -1); + connect(packageInstaller, &KJob::percentChanged, this, [this, installationTaskText](KJob * /* job */, long unsigned int percent) { + if (percent < 100) { // Ignore some weird reported values. + Q_EMIT showInstallationProgress(installationTaskText, percent); + } + }); + packageInstaller->start(); +#endif +} diff --git a/src/search/popup.h b/src/search/popup.h new file mode 100644 index 000000000..423c9dde6 --- /dev/null +++ b/src/search/popup.h @@ -0,0 +1,99 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2024 Felix Ernst <[email protected]> + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef POPUP_H +#define POPUP_H + +#include "dolphinquery.h" +#include "updatablestateinterface.h" +#include "widgetmenu.h" + +#include <KMessageWidget> + +#include <QUrl> + +class KContextualHelpButton; +class QRadioButton; +class QToolButton; + +namespace Search +{ +class DateSelector; +class FileTypeSelector; +class MinimumRatingSelector; +class TagsSelector; + +/** @returns the localized name for the Filenamesearch search tool for use in user interfaces. */ +QString filenamesearchUiName(); + +/** @returns the localized name for the Baloo search tool for use in user interfaces. */ +QString balooUiName(); + +/** + * This object contains most of the UI to set the search configuration. + */ +class Popup : public WidgetMenu, public UpdatableStateInterface +{ + Q_OBJECT + +public: + explicit Popup(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr); + +Q_SIGNALS: + /** Is emitted whenever settings have changed and a new search might be necessary. */ + void configurationChanged(const DolphinQuery &dolphinQuery); + + /** + * Requests for @p message with the given @p messageType to be shown to the user in a non-modal way. + */ + void showMessage(const QString &message, KMessageWidget::MessageType messageType); + + /** + * Requests for a progress update to be shown to the user in a non-modal way. + * @param currentlyRunningTaskTitle The task that is currently progressing. + * @param installationProgressPercent The current percentage of completion. + */ + void showInstallationProgress(const QString ¤tlyRunningTaskTitle, int installationProgressPercent); + +private: + QWidget *init() override; + + void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) override; + +private Q_SLOTS: + /** + * Opens KFind if KFind is installed. + * If KFind is not installed, this method asynchronously starts a Filelight installation using DolphinPackageInstaller. @see DolphinPackageInstaller. + * Installation success or failure is reported through showMessage(). @see Popup::showMessage(). + * Installation progress is reported through showInstallationProgress(). @see Popup::showInstallationProgress(). + */ + void slotKFindButtonClicked(); + +private: + QRadioButton *m_searchInFileNamesRadioButton = nullptr; + QRadioButton *m_searchInFileContentsRadioButton = nullptr; + QRadioButton *m_filenamesearchRadioButton = nullptr; + KContextualHelpButton *m_filenamesearchContextualHelpButton = nullptr; + QRadioButton *m_balooRadioButton = nullptr; + KContextualHelpButton *m_balooContextualHelpButton = nullptr; + /** A container widget for easy showing/hiding of all selectors. */ + QWidget *m_selectorsLayoutWidget = nullptr; + /** Allows to set the file type each search result is expected to have. */ + FileTypeSelector *m_typeSelector = nullptr; + /** Allows to set a date since when each search result needs to have been modified. */ + DateSelector *m_dateSelector = nullptr; + /** Allows selecting the minimum rating search results are expected to have. */ + MinimumRatingSelector *m_ratingSelector = nullptr; + /** Allows to set tags which each search result is required to have. */ + TagsSelector *m_tagsSelector = nullptr; + /** A button that allows installing or opening KFind. */ + QToolButton *m_kFindButton = nullptr; +}; + +} + +#endif // POPUP_H diff --git a/src/search/selectors/dateselector.cpp b/src/search/selectors/dateselector.cpp new file mode 100644 index 000000000..2951b1dc4 --- /dev/null +++ b/src/search/selectors/dateselector.cpp @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "dateselector.h" + +#include "../dolphinquery.h" + +#include <KDatePicker> +#include <KDatePickerPopup> +#include <KFormat> +#include <KLocalizedString> + +using namespace Search; + +Search::DateSelector::DateSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent) + : QToolButton{parent} + , UpdatableStateInterface{dolphinQuery} + , m_datePickerPopup{ + new KDatePickerPopup{KDatePickerPopup::NoDate | KDatePickerPopup::DatePicker | KDatePickerPopup::Words, dolphinQuery->modifiedSinceDate(), this}} +{ + setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + setPopupMode(QToolButton::InstantPopup); + + m_datePickerPopup->setDateRange(QDate{}, QDate::currentDate()); + connect(m_datePickerPopup, &KDatePickerPopup::dateChanged, this, [this](const QDate &activatedDate) { + if (activatedDate == m_searchConfiguration->modifiedSinceDate()) { + return; // Already selected. + } + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setModifiedSinceDate(activatedDate); + Q_EMIT configurationChanged(std::move(searchConfigurationCopy)); + }); + setMenu(m_datePickerPopup); + + updateStateToMatch(std::move(dolphinQuery)); +} + +void DateSelector::removeRestriction() +{ + Q_ASSERT(m_searchConfiguration->modifiedSinceDate().isValid()); + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setModifiedSinceDate(QDate{}); + Q_EMIT configurationChanged(std::move(searchConfigurationCopy)); +} + +void DateSelector::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) +{ + m_datePickerPopup->setDate(dolphinQuery->modifiedSinceDate()); + if (!dolphinQuery->modifiedSinceDate().isValid()) { + setIcon(QIcon{}); // No icon for the empty state + setText(i18nc("@item:inlistbox", "Any Date")); + return; + } + setIcon(QIcon::fromTheme(QStringLiteral("view-calendar"))); + QLocale local; + KFormat formatter(local); + setText(formatter.formatRelativeDate(dolphinQuery->modifiedSinceDate(), QLocale::ShortFormat)); +} diff --git a/src/search/selectors/dateselector.h b/src/search/selectors/dateselector.h new file mode 100644 index 000000000..99cecec06 --- /dev/null +++ b/src/search/selectors/dateselector.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef DATESELECTOR_H +#define DATESELECTOR_H + +#include "../updatablestateinterface.h" + +#include <QToolButton> + +class KDatePickerPopup; + +namespace Search +{ + +class DateSelector : public QToolButton, public UpdatableStateInterface +{ + Q_OBJECT + +public: + explicit DateSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr); + + /** Causes configurationChanged() to be emitted with a DolphinQuery object that does not contain any restriction settable by this class. */ + void removeRestriction(); + +Q_SIGNALS: + /** Is emitted whenever settings have changed and a new search might be necessary. */ + void configurationChanged(const DolphinQuery &dolphinQuery); + +private: + void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) override; + +private: + KDatePickerPopup *m_datePickerPopup = nullptr; +}; + +} + +#endif // DATESELECTOR_H diff --git a/src/search/selectors/filetypeselector.cpp b/src/search/selectors/filetypeselector.cpp new file mode 100644 index 000000000..7852aced2 --- /dev/null +++ b/src/search/selectors/filetypeselector.cpp @@ -0,0 +1,81 @@ +/* + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "filetypeselector.h" + +#include "../dolphinquery.h" + +#include <KFileMetaData/TypeInfo> +#include <KLocalizedString> + +using namespace Search; + +FileTypeSelector::FileTypeSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent) + : QComboBox{parent} + , UpdatableStateInterface{dolphinQuery} +{ + for (KFileMetaData::Type::Type type = KFileMetaData::Type::FirstType; type <= KFileMetaData::Type::LastType; type = KFileMetaData::Type::Type(type + 1)) { + switch (type) { + case KFileMetaData::Type::Empty: + addItem(/** No icon for the empty state */ i18nc("@item:inlistbox", "Any Type"), type); + continue; + case KFileMetaData::Type::Archive: + addItem(QIcon::fromTheme(QStringLiteral("package-x-generic")), KFileMetaData::TypeInfo{type}.displayName(), type); + continue; + case KFileMetaData::Type::Audio: + addItem(QIcon::fromTheme(QStringLiteral("audio-x-generic")), KFileMetaData::TypeInfo{type}.displayName(), type); + continue; + case KFileMetaData::Type::Video: + addItem(QIcon::fromTheme(QStringLiteral("video-x-generic")), KFileMetaData::TypeInfo{type}.displayName(), type); + continue; + case KFileMetaData::Type::Image: + addItem(QIcon::fromTheme(QStringLiteral("image-x-generic")), KFileMetaData::TypeInfo{type}.displayName(), type); + continue; + case KFileMetaData::Type::Document: + addItem(QIcon::fromTheme(QStringLiteral("text-x-generic")), KFileMetaData::TypeInfo{type}.displayName(), type); + continue; + case KFileMetaData::Type::Spreadsheet: + addItem(QIcon::fromTheme(QStringLiteral("table")), KFileMetaData::TypeInfo{type}.displayName(), type); + continue; + case KFileMetaData::Type::Presentation: + addItem(QIcon::fromTheme(QStringLiteral("view-presentation")), KFileMetaData::TypeInfo{type}.displayName(), type); + continue; + case KFileMetaData::Type::Text: + addItem(QIcon::fromTheme(QStringLiteral("text-x-generic")), KFileMetaData::TypeInfo{type}.displayName(), type); + continue; + case KFileMetaData::Type::Folder: + addItem(QIcon::fromTheme(QStringLiteral("inode-directory")), KFileMetaData::TypeInfo{type}.displayName(), type); + continue; + default: + addItem(QIcon(), KFileMetaData::TypeInfo{type}.displayName(), type); + } + } + + connect(this, &QComboBox::activated, this, [this](int activatedIndex) { + auto activatedType = itemData(activatedIndex).value<KFileMetaData::Type::Type>(); + if (activatedType == m_searchConfiguration->fileType()) { + return; // Already selected. + } + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setFileType(activatedType); + Q_EMIT configurationChanged(std::move(searchConfigurationCopy)); + }); + + updateStateToMatch(std::move(dolphinQuery)); +} + +void Search::FileTypeSelector::removeRestriction() +{ + Q_ASSERT(m_searchConfiguration->fileType() != KFileMetaData::Type::Empty); + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setFileType(KFileMetaData::Type::Empty); + Q_EMIT configurationChanged(std::move(searchConfigurationCopy)); +} + +void FileTypeSelector::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) +{ + setCurrentIndex(findData(dolphinQuery->fileType())); +} diff --git a/src/search/selectors/filetypeselector.h b/src/search/selectors/filetypeselector.h new file mode 100644 index 000000000..bfc827344 --- /dev/null +++ b/src/search/selectors/filetypeselector.h @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef FILETYPESELECTOR_H +#define FILETYPESELECTOR_H + +#include "../updatablestateinterface.h" + +#include <QComboBox> + +namespace Search +{ + +class FileTypeSelector : public QComboBox, public UpdatableStateInterface +{ + Q_OBJECT + +public: + explicit FileTypeSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr); + + /** Causes configurationChanged() to be emitted with a DolphinQuery object that does not contain any restriction settable by this class. */ + void removeRestriction(); + +Q_SIGNALS: + /** Is emitted whenever settings have changed and a new search might be necessary. */ + void configurationChanged(const DolphinQuery &dolphinQuery); + +private: + void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) override; +}; +} + +#endif // FILETYPESELECTOR_H diff --git a/src/search/selectors/minimumratingselector.cpp b/src/search/selectors/minimumratingselector.cpp new file mode 100644 index 000000000..e46dfd4e0 --- /dev/null +++ b/src/search/selectors/minimumratingselector.cpp @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "minimumratingselector.h" + +#include "../dolphinquery.h" + +#include <KLocalizedString> + +using namespace Search; + +MinimumRatingSelector::MinimumRatingSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent) + : QComboBox{parent} + , UpdatableStateInterface{dolphinQuery} +{ + addItem(/** No icon for the empty state */ i18nc("@item:inlistbox", "Any Rating"), 0); + addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "1 or more"), 2); + addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "2 or more"), 4); + addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "3 or more"), 6); + addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "4 or more"), 8); + addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox 5 star rating, has a star icon in front", "5"), 10); + + connect(this, &QComboBox::activated, this, [this](int activatedIndex) { + auto activatedMinimumRating = itemData(activatedIndex).value<int>(); + if (activatedMinimumRating == m_searchConfiguration->minimumRating()) { + return; // Already selected. + } + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setMinimumRating(activatedMinimumRating); + Q_EMIT configurationChanged(std::move(searchConfigurationCopy)); + }); + + updateStateToMatch(std::move(dolphinQuery)); +} + +void MinimumRatingSelector::removeRestriction() +{ + Q_ASSERT(m_searchConfiguration->minimumRating() > 0); + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setMinimumRating(0); + Q_EMIT configurationChanged(std::move(searchConfigurationCopy)); +} + +void MinimumRatingSelector::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) +{ + setCurrentIndex(findData(dolphinQuery->minimumRating())); +} diff --git a/src/search/selectors/minimumratingselector.h b/src/search/selectors/minimumratingselector.h new file mode 100644 index 000000000..02364cd1a --- /dev/null +++ b/src/search/selectors/minimumratingselector.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef MINIMUMRATINGSELECTOR_H +#define MINIMUMRATINGSELECTOR_H + +#include "../updatablestateinterface.h" + +#include <QComboBox> + +namespace Search +{ + +/** + * @brief Select the minimum rating search results should have. + * Values <= 0 mean no restriction. 1 is half a star, 2 one full star, etc. 10 is typically the maximum in KDE software. + * Since this box only allows selecting full star ratings, the possible values are 0, 2, 4, 6, 8, 10. + */ +class MinimumRatingSelector : public QComboBox, public UpdatableStateInterface +{ + Q_OBJECT + +public: + explicit MinimumRatingSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr); + + /** Causes configurationChanged() to be emitted with a DolphinQuery object that does not contain any restriction settable by this class. */ + void removeRestriction(); + +Q_SIGNALS: + /** Is emitted whenever settings have changed and a new search might be necessary. */ + void configurationChanged(const DolphinQuery &dolphinQuery); + +private: + void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) override; +}; + +} + +#endif // MINIMUMRATINGSELECTOR_H diff --git a/src/search/selectors/tagsselector.cpp b/src/search/selectors/tagsselector.cpp new file mode 100644 index 000000000..95d7ff52a --- /dev/null +++ b/src/search/selectors/tagsselector.cpp @@ -0,0 +1,189 @@ +/* + SPDX-FileCopyrightText: 2019 Ismael Asensio <[email protected]> + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "tagsselector.h" + +#include "../dolphinquery.h" + +#include <KCoreDirLister> +#include <KLocalizedString> +#include <KProtocolInfo> + +#include <QMenu> +#include <QStringList> + +using namespace Search; + +namespace +{ +/** + * @brief Provides the list of tags to all TagsSelectors. + * + * This QStringList of tags populates itself. Additional tags the user is actively searching for can be added with addTag() even though we assume that no file + * with such a tag exists if we did not find it automatically. + * @note Use the tagsList() function below instead of constructing TagsList objects yourself. + */ +class TagsList : public QStringList, public QObject +{ +public: + TagsList() + : QStringList{} + { + m_tagsLister->openUrl(QUrl(QStringLiteral("tags:/")), KCoreDirLister::OpenUrlFlag::Reload); + connect(m_tagsLister.get(), &KCoreDirLister::itemsAdded, this, [this](const QUrl &, const KFileItemList &items) { + for (const KFileItem &item : items) { + append(item.text()); + } + removeDuplicates(); + sort(Qt::CaseInsensitive); + }); + }; + + virtual ~TagsList() = default; + + void addTag(const QString &tag) + { + if (contains(tag)) { + return; + } + append(tag); + sort(Qt::CaseInsensitive); + }; + + /** Used to access to the itemsAdded signal so outside users of this class know when items were added. */ + KCoreDirLister *tagsLister() const + { + return m_tagsLister.get(); + }; + +private: + std::unique_ptr<KCoreDirLister> m_tagsLister = std::make_unique<KCoreDirLister>(); +}; + +/** + * @returns a list of all tags found since the construction of the first TagsSelector object. + * @note Use this function instead of constructing additional TagsList objects. + */ +TagsList *tagsList() +{ + static TagsList *g_tagsList = new TagsList; + return g_tagsList; +} +} + +Search::TagsSelector::TagsSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent) + : QToolButton{parent} + , UpdatableStateInterface{dolphinQuery} +{ + setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + setPopupMode(QToolButton::InstantPopup); + + auto menu = new QMenu{this}; + setMenu(menu); + connect(menu, &QMenu::aboutToShow, this, [this]() { + TagsList *tags = tagsList(); + // The TagsList might not have been updated for a while and new tags might be available. We update now, but this is unfortunately not instant. + // However this selector is connected to the itemsAdded() signal, so we will add any new tags eventually. + tags->tagsLister()->updateDirectory(tags->tagsLister()->url()); + updateMenu(m_searchConfiguration); + }); + + TagsList *tags = tagsList(); + if (tags->isEmpty()) { + // Either there really are no tags or the TagsList has not loaded the tags yet. It only begins loading the first time tagsList() is globally called. + setEnabled(false); + connect( + tags->tagsLister(), + &KCoreDirLister::itemsAdded, + this, + [this]() { + setEnabled(true); + }, + Qt::SingleShotConnection); + } + + connect(tags->tagsLister(), &KCoreDirLister::itemsAdded, this, [this, menu]() { + if (menu->isVisible()) { + updateMenu(m_searchConfiguration); + } + }); + + updateStateToMatch(std::move(dolphinQuery)); +} + +void TagsSelector::removeRestriction() +{ + Q_ASSERT(!m_searchConfiguration->requiredTags().isEmpty()); + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setRequiredTags({}); + Q_EMIT configurationChanged(std::move(searchConfigurationCopy)); +} + +void TagsSelector::updateMenu(const std::shared_ptr<const DolphinQuery> &dolphinQuery) +{ + if (!KProtocolInfo::isKnownProtocol(QStringLiteral("tags"))) { + return; + } + const bool menuWasVisible = menu()->isVisible(); + if (menuWasVisible) { + menu()->hide(); // The menu needs to be hidden now, then updated, and then shown again. + } + // Delete all existing actions in the menu + for (QAction *action : menu()->actions()) { + action->deleteLater(); + } + menu()->clear(); + // Populate the menu + const TagsList *tags = tagsList(); + const bool onlyOneTagExists = tags->count() == 1; + + for (const QString &tag : *tags) { + QAction *tagAction = new QAction{QIcon::fromTheme(QStringLiteral("tag")), tag, menu()}; + tagAction->setCheckable(true); + tagAction->setChecked(dolphinQuery->requiredTags().contains(tag)); + connect(tagAction, &QAction::triggered, this, [this, tag, onlyOneTagExists](bool checked) { + QStringList requiredTags = m_searchConfiguration->requiredTags(); + if (checked == requiredTags.contains(tag)) { + return; // Already selected. + } + if (checked) { + requiredTags.append(tag); + } else { + requiredTags.removeOne(tag); + } + DolphinQuery searchConfigurationCopy = *m_searchConfiguration; + searchConfigurationCopy.setRequiredTags(requiredTags); + Q_EMIT configurationChanged(std::move(searchConfigurationCopy)); + + if (!onlyOneTagExists) { + // Keep the menu open to allow easier tag multi-selection. + menu()->show(); + } + }); + menu()->addAction(tagAction); + } + if (menuWasVisible) { + menu()->show(); + } +} + +void TagsSelector::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) +{ + if (dolphinQuery->requiredTags().count()) { + setIcon(QIcon::fromTheme(QStringLiteral("tag"))); + setText(dolphinQuery->requiredTags().join(i18nc("list separator for file tags e.g. all images tagged 'family & party & 2025'", " && "))); + } else { + setIcon(QIcon{}); // No icon for the empty state + setText(i18nc("@action:button Required tags for search results: None", "None")); + } + for (const auto &tag : dolphinQuery->requiredTags()) { + tagsList()->addTag(tag); // We add it just in case this tag is not (or no longer) available on the system. This way the UI always works as expected. + } + if (menu()->isVisible()) { + updateMenu(dolphinQuery); + } +} diff --git a/src/search/selectors/tagsselector.h b/src/search/selectors/tagsselector.h new file mode 100644 index 000000000..386cbb924 --- /dev/null +++ b/src/search/selectors/tagsselector.h @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2019 Ismael Asensio <[email protected]> + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef TAGSSELECTOR_H +#define TAGSSELECTOR_H + +#include "../updatablestateinterface.h" + +#include <QToolButton> + +namespace Search +{ + +class TagsSelector : public QToolButton, public UpdatableStateInterface +{ + Q_OBJECT + +public: + explicit TagsSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr); + + /** Causes configurationChanged() to be emitted with a DolphinQuery object that does not contain any restriction settable by this class. */ + void removeRestriction(); + +Q_SIGNALS: + /** Is emitted whenever settings have changed and a new search might be necessary. */ + void configurationChanged(const DolphinQuery &dolphinQuery); + +private: + /** + * Updates the menu items for the various tags based on @p dolphinQuery and the available tags. + * This method should only be called when the menu is QMenu::aboutToShow() or the menu is currently visible already while this selector's state changes. + * If the menu is open when this method is called, the menu will automatically be reopened to reflect the updated contents. + */ + void updateMenu(const std::shared_ptr<const DolphinQuery> &dolphinQuery); + + void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) override; +}; + +} + +#endif // TAGSSELECTOR_H diff --git a/src/search/updatablestateinterface.h b/src/search/updatablestateinterface.h new file mode 100644 index 000000000..79bc25ff6 --- /dev/null +++ b/src/search/updatablestateinterface.h @@ -0,0 +1,56 @@ +/* + SPDX-FileCopyrightText: 2024 Felix Ernst <[email protected]> + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef UPDATABLESTATEINTERFACE_H +#define UPDATABLESTATEINTERFACE_H + +#include <QtAssert> + +#include <memory> + +namespace Search +{ +class DolphinQuery; + +class UpdatableStateInterface +{ +public: + inline explicit UpdatableStateInterface(std::shared_ptr<const DolphinQuery> dolphinQuery) + : m_searchConfiguration{std::move(dolphinQuery)} {}; + + virtual ~UpdatableStateInterface(){}; + + /** + * Updates this object and its child widgets so their states are correctly described by the @p dolphinQuery. + * This method is always initially called on the Search::Bar which in turn calls this method on its child widgets. That is because the Search::Bar is the + * ancestor widget of all classes implementing UpdatableStateInterface, and from Search::Bar::updateStateToMatch() the changed state represented by the + * @p dolphinQuery is propagated to all other UpdatableStateInterfaces through UpdatableStateInterface::updateState() calls. + */ + inline void updateStateToMatch(std::shared_ptr<const DolphinQuery> dolphinQuery) + { + Q_ASSERT_X(m_searchConfiguration, "UpdatableStateInterface::updateStateToMatch()", "An UpdatableStateInterface should always have a consistent state."); + updateState(dolphinQuery); + m_searchConfiguration = std::move(dolphinQuery); + Q_ASSERT_X(m_searchConfiguration, "UpdatableStateInterface::updateStateToMatch()", "An UpdatableStateInterface should always have a consistent state."); + }; + +private: + /** + * Implementations of this method initialize the state of this object and its child widgets to represent the state of the @p dolphinQuery. + * This method is only ever called from UpdatableStateInterface::updateStateToMatch(). + */ + virtual void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) = 0; + +protected: + /** + * The DolphinQuery that was used to initialize this object's state. + */ + std::shared_ptr<const DolphinQuery> m_searchConfiguration; +}; + +} + +#endif // UPDATABLESTATEINTERFACE_H diff --git a/src/search/widgetmenu.cpp b/src/search/widgetmenu.cpp new file mode 100644 index 000000000..701a0ce9c --- /dev/null +++ b/src/search/widgetmenu.cpp @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "widgetmenu.h" + +#include <QApplication> +#include <QShowEvent> +#include <QWidgetAction> + +using namespace Search; + +Search::WidgetMenu::WidgetMenu(QWidget *parent) + : QMenu{parent} +{ + connect( + this, + &QMenu::aboutToShow, + this, + [this]() { + auto widgetAction = new QWidgetAction{this}; + auto widget = init(); + Q_CHECK_PTR(widget); + widgetAction->setDefaultWidget(widget); // Transfers ownership to the widgetAction. + addAction(widgetAction); + }, + Qt::SingleShotConnection); +} + +bool WidgetMenu::focusNextPrevChild(bool next) +{ + return QWidget::focusNextPrevChild(next); +} + +void WidgetMenu::mouseReleaseEvent(QMouseEvent *event) +{ + return QWidget::mouseReleaseEvent(event); +} + +void WidgetMenu::resizeToFitContents() +{ + auto *widgetAction = static_cast<QWidgetAction *>(actions().first()); + auto focusedChildWidget = QApplication::focusWidget(); + if (!widgetAction->defaultWidget()->isAncestorOf(focusedChildWidget)) { + focusedChildWidget = nullptr; + } + + // Removing and readding the widget triggers the resize. + removeAction(widgetAction); + addAction(widgetAction); + + // The previous removing and readding removed the focus from any child widgets. We return the focus to where it was. + if (focusedChildWidget) { + focusedChildWidget->setFocus(); + } +} + +void WidgetMenu::showEvent(QShowEvent *event) +{ + if (!event->spontaneous()) { + auto widgetAction = static_cast<QWidgetAction *>(actions().first()); + widgetAction->defaultWidget()->setFocus(); + } + QMenu::showEvent(event); +} diff --git a/src/search/widgetmenu.h b/src/search/widgetmenu.h new file mode 100644 index 000000000..def75354f --- /dev/null +++ b/src/search/widgetmenu.h @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef WIDGETMENU_H +#define WIDGETMENU_H + +#include <QMenu> + +class QMouseEvent; +class QShowEvent; + +namespace Search +{ + +/** + * @brief A QMenu that contains nothing but a lazily constructed widget. + * + * Usually QMenus contain a list of actions. WidgetMenu allows showing any QWidget instead. This is useful to show popups, random text, or full user interfaces + * when a button is pressed or a menu action in a QMenu is hovered. + * + * This class also encapsulates lazy construction of the widget within. It will only be created when this menu is actually being opened. + */ +class WidgetMenu : public QMenu +{ +public: + explicit WidgetMenu(QWidget *parent = nullptr); + +protected: + /** + * Overrides the weird QMenu Tab key handling with the usual QWidget one. + */ + bool focusNextPrevChild(bool next) override; + + /** + * Overrides the QMenu behaviour of closing itself when clicked with the non-closing QWidget one. + */ + void mouseReleaseEvent(QMouseEvent *event) override; + + /** + * This unfortuantely needs to be explicitly called to resize the WidgetMenu because the size of a QMenu will not automatically change to fit the QWidgets + * within. + */ + void resizeToFitContents(); + + /** + * Move focus to the widget when this WidgetMenu is shown. + */ + void showEvent(QShowEvent *event) override; + +private: + /** + * @return the widget which is contained in this WidgetMenu. This method is at most called once per WidgetMenu object when the WidgetMenu is about to be + * shown for the first time. The ownership of the widget will be transfered to an internal QWidgetAction. + */ + virtual QWidget *init() = 0; +}; + +} + +#endif // WIDGETMENU_H diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 1ef82e3d9..e77d7d7bf 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -42,10 +42,10 @@ target_link_libraries(kfileitemmodelbenchmark dolphinprivate Qt6::Test) # KItemListKeyboardSearchManagerTest ecm_add_test(kitemlistkeyboardsearchmanagertest.cpp LINK_LIBRARIES dolphinprivate Qt6::Test) -# DolphinSearchBox +# DolphinSearchBar if (KF6Baloo_FOUND) - ecm_add_test(dolphinsearchboxtest.cpp - TEST_NAME dolphinsearchboxtest + ecm_add_test(dolphinsearchbartest.cpp + TEST_NAME dolphinsearchbartest LINK_LIBRARIES dolphinprivate dolphinstatic Qt6::Test) endif() diff --git a/src/tests/dolphinmainwindowtest.cpp b/src/tests/dolphinmainwindowtest.cpp index c7d2f4b6d..4ae36e7e6 100644 --- a/src/tests/dolphinmainwindowtest.cpp +++ b/src/tests/dolphinmainwindowtest.cpp @@ -204,7 +204,7 @@ void DolphinMainWindowTest::testClosingTabsWithSearchBoxVisible() QVERIFY(tabWidget); // Show search box on first tab. - tabWidget->currentTabPage()->activeViewContainer()->setSearchModeEnabled(true); + tabWidget->currentTabPage()->activeViewContainer()->setSearchBarVisible(true); tabWidget->openNewActivatedTab(QUrl::fromLocalFile(QDir::homePath())); QCOMPARE(tabWidget->count(), 2); diff --git a/src/tests/dolphinquerytest.cpp b/src/tests/dolphinquerytest.cpp index b6feab686..76cea999f 100644 --- a/src/tests/dolphinquerytest.cpp +++ b/src/tests/dolphinquerytest.cpp @@ -1,13 +1,15 @@ /* - * SPDX-FileCopyrightText: 2019 Ismael Asensio <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ + SPDX-FileCopyrightText: 2019 Ismael Asensio <[email protected]> + SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]> + + SPDX-License-Identifier: GPL-2.0-or-later +*/ #include "search/dolphinquery.h" #include <QTest> +#include <QDate> #include <QJsonDocument> #include <QJsonObject> #include <QStandardPaths> @@ -23,6 +25,7 @@ private Q_SLOTS: void initTestCase(); void testBalooSearchParsing_data(); void testBalooSearchParsing(); + void testExportImport(); }; /** @@ -48,6 +51,7 @@ QUrl balooQueryUrl(const QString &searchString) void DolphinQueryTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); + Search::setTestMode(); } /** @@ -56,8 +60,10 @@ void DolphinQueryTest::initTestCase() void DolphinQueryTest::testBalooSearchParsing_data() { QTest::addColumn<QUrl>("searchUrl"); - QTest::addColumn<QString>("expectedText"); - QTest::addColumn<QStringList>("expectedTerms"); + QTest::addColumn<QString>("expectedSearchTerm"); + QTest::addColumn<QDate>("expectedModifiedSinceDate"); + QTest::addColumn<int>("expectedMinimumRating"); + QTest::addColumn<QStringList>("expectedTags"); QTest::addColumn<bool>("hasContent"); QTest::addColumn<bool>("hasFileName"); @@ -73,78 +79,80 @@ void DolphinQueryTest::testBalooSearchParsing_data() const QString rating = QStringLiteral("rating>=2"); const QString modified = QStringLiteral("modified>=2019-08-07"); + QDate modifiedDate; + modifiedDate.setDate(2019, 8, 7); const QString tag = QStringLiteral("tag:tagA"); const QString tagS = QStringLiteral("tag:\"tagB with spaces\""); // in search url const QString tagR = QStringLiteral("tag:tagB with spaces"); // in result term + const QLatin1String tagA{"tagA"}; + const QLatin1String tagBWithSpaces{"tagB with spaces"}; + // Test for "Content" - QTest::newRow("content") << balooQueryUrl(text) << text << QStringList() << true << false; - QTest::newRow("content/space") << balooQueryUrl(textS) << textS << QStringList() << true << false; - QTest::newRow("content/quoted") << balooQueryUrl(textQ) << textS << QStringList() << true << false; - QTest::newRow("content/empty") << balooQueryUrl("") << "" << QStringList() << false << false; - QTest::newRow("content/single_quote") << balooQueryUrl("\"") << "\"" << QStringList() << true << false; - QTest::newRow("content/double_quote") << balooQueryUrl("\"\"") << "" << QStringList() << false << false; + QTest::newRow("content") << balooQueryUrl(text) << text << QDate{} << 0 << QStringList() << true << true; + QTest::newRow("content/space") << balooQueryUrl(textS) << textS << QDate{} << 0 << QStringList() << true << true; + QTest::newRow("content/quoted") << balooQueryUrl(textQ) << textS << QDate{} << 0 << QStringList() << true << true; + QTest::newRow("content/empty") << balooQueryUrl("") << "" << QDate{} << 0 << QStringList() << false << false; + QTest::newRow("content/single_quote") << balooQueryUrl("\"") << "\"" << QDate{} << 0 << QStringList() << true << true; + QTest::newRow("content/double_quote") << balooQueryUrl("\"\"") << "" << QDate{} << 0 << QStringList() << false << false; // Test for "FileName" - QTest::newRow("filename") << balooQueryUrl(filename) << text << QStringList() << false << true; - QTest::newRow("filename/space") << balooQueryUrl(filenameS) << textS << QStringList() << false << true; - QTest::newRow("filename/quoted") << balooQueryUrl(filenameQ) << textQ << QStringList() << false << true; - QTest::newRow("filename/mixed") << balooQueryUrl(filenameM) << textM << QStringList() << false << true; - QTest::newRow("filename/empty") << balooQueryUrl("filename:") << "" << QStringList() << false << false; - QTest::newRow("filename/single_quote") << balooQueryUrl("filename:\"") << "\"" << QStringList() << false << true; - QTest::newRow("filename/double_quote") << balooQueryUrl("filename:\"\"") << "" << QStringList() << false << false; + QTest::newRow("filename") << balooQueryUrl(filename) << text << QDate{} << 0 << QStringList() << false << true; + QTest::newRow("filename/space") << balooQueryUrl(filenameS) << textS << QDate{} << 0 << QStringList() << false << true; + QTest::newRow("filename/quoted") << balooQueryUrl(filenameQ) << textQ << QDate{} << 0 << QStringList() << false << true; + QTest::newRow("filename/mixed") << balooQueryUrl(filenameM) << textM << QDate{} << 0 << QStringList() << false << true; + QTest::newRow("filename/empty") << balooQueryUrl("filename:") << "" << QDate{} << 0 << QStringList() << false << false; + QTest::newRow("filename/single_quote") << balooQueryUrl("filename:\"") << "\"" << QDate{} << 0 << QStringList() << false << true; + QTest::newRow("filename/double_quote") << balooQueryUrl("filename:\"\"") << "" << QDate{} << 0 << QStringList() << false << false; // Combined content and filename search - QTest::newRow("content+filename") << balooQueryUrl(text + " " + filename) << text + " " + filename << QStringList() << true << true; + QTest::newRow("content+filename") << balooQueryUrl(text + " " + filename) << text << QDate{} << 0 << QStringList() << true << true; - QTest::newRow("content+filename/quoted") << balooQueryUrl(textQ + " " + filenameQ) << textS + " " + filenameQ << QStringList() << true << true; + QTest::newRow("content+filename/quoted") << balooQueryUrl(textQ + " " + filenameQ) << textS << QDate{} << 0 << QStringList() << true << true; // Test for rating - QTest::newRow("rating") << balooQueryUrl(rating) << "" << QStringList({rating}) << false << false; - QTest::newRow("rating+content") << balooQueryUrl(rating + " " + text) << text << QStringList({rating}) << true << false; - QTest::newRow("rating+filename") << balooQueryUrl(rating + " " + filename) << text << QStringList({rating}) << false << true; + QTest::newRow("rating") << balooQueryUrl(rating) << "" << QDate{} << 2 << QStringList() << false << false; + QTest::newRow("rating+content") << balooQueryUrl(rating + " " + text) << text << QDate{} << 2 << QStringList() << true << true; + QTest::newRow("rating+filename") << balooQueryUrl(rating + " " + filename) << text << QDate{} << 2 << QStringList() << false << true; // Test for modified date - QTest::newRow("modified") << balooQueryUrl(modified) << "" << QStringList({modified}) << false << false; - QTest::newRow("modified+content") << balooQueryUrl(modified + " " + text) << text << QStringList({modified}) << true << false; - QTest::newRow("modified+filename") << balooQueryUrl(modified + " " + filename) << text << QStringList({modified}) << false << true; + QTest::newRow("modified") << balooQueryUrl(modified) << "" << modifiedDate << 0 << QStringList() << false << false; + QTest::newRow("modified+content") << balooQueryUrl(modified + " " + text) << text << modifiedDate << 0 << QStringList() << true << true; + QTest::newRow("modified+filename") << balooQueryUrl(modified + " " + filename) << text << modifiedDate << 0 << QStringList() << false << true; // Test for tags - QTest::newRow("tag") << balooQueryUrl(tag) << "" << QStringList({tag}) << false << false; - QTest::newRow("tag/space") << balooQueryUrl(tagS) << "" << QStringList({tagR}) << false << false; - QTest::newRow("tag/double") << balooQueryUrl(tag + " " + tagS) << "" << QStringList({tag, tagR}) << false << false; - QTest::newRow("tag+content") << balooQueryUrl(tag + " " + text) << text << QStringList({tag}) << true << false; - QTest::newRow("tag+filename") << balooQueryUrl(tag + " " + filename) << text << QStringList({tag}) << false << true; + QTest::newRow("tag") << balooQueryUrl(tag) << "" << QDate{} << 0 << QStringList{tagA} << false << false; + QTest::newRow("tag/space") << balooQueryUrl(tagS) << "" << QDate{} << 0 << QStringList{tagBWithSpaces} << false << false; + QTest::newRow("tag/double") << balooQueryUrl(tag + " " + tagS) << "" << QDate{} << 0 << QStringList{tagA, tagBWithSpaces} << false << false; + QTest::newRow("tag+content") << balooQueryUrl(tag + " " + text) << text << QDate{} << 0 << QStringList{tagA} << true << true; + QTest::newRow("tag+filename") << balooQueryUrl(tag + " " + filename) << text << QDate{} << 0 << QStringList{tagA} << false << true; // Combined search terms - QTest::newRow("searchTerms") << balooQueryUrl(rating + " AND " + modified + " AND " + tag + " AND " + tagS) << "" - << QStringList({modified, rating, tag, tagR}) << false << false; + QTest::newRow("searchTerms") << balooQueryUrl(rating + " AND " + modified + " AND " + tag + " AND " + tagS) << "" << modifiedDate << 2 + << QStringList{tagA, tagBWithSpaces} << false << false; - QTest::newRow("searchTerms+content") << balooQueryUrl(rating + " AND " + modified + " " + text + " " + tag + " AND " + tagS) << text - << QStringList({modified, rating, tag, tagR}) << true << false; + QTest::newRow("searchTerms+content") << balooQueryUrl(rating + " AND " + modified + " " + text + " " + tag + " AND " + tagS) << text << modifiedDate << 2 + << QStringList{tagA, tagBWithSpaces} << true << true; - QTest::newRow("searchTerms+filename") << balooQueryUrl(rating + " AND " + modified + " " + filename + " " + tag + " AND " + tagS) << text - << QStringList({modified, rating, tag, tagR}) << false << true; + QTest::newRow("searchTerms+filename") << balooQueryUrl(rating + " AND " + modified + " " + filename + " " + tag + " AND " + tagS) << text << modifiedDate + << 2 << QStringList{tagA, tagBWithSpaces} << false << true; - QTest::newRow("allTerms") << balooQueryUrl(text + " " + filename + " " + rating + " AND " + modified + " AND " + tag) << text + " " + filename - << QStringList({modified, rating, tag}) << true << true; + QTest::newRow("allTerms") << balooQueryUrl(text + " " + filename + " " + rating + " AND " + modified + " AND " + tag) << text << modifiedDate << 2 + << QStringList{tagA} << true << true; - QTest::newRow("allTerms/space") << balooQueryUrl(textS + " " + filenameS + " " + rating + " AND " + modified + " AND " + tagS) << textS + " " + filenameS - << QStringList({modified, rating, tagR}) << true << true; + QTest::newRow("allTerms/space") << balooQueryUrl(textS + " " + filenameS + " " + rating + " AND " + modified + " AND " + tagS) << textS << modifiedDate << 2 + << QStringList{tagBWithSpaces} << true << true; // Test tags:/ URL scheme const auto tagUrl = [](const QString &tag) { return QUrl(QStringLiteral("tags:/%1/").arg(tag)); }; - const auto tagTerms = [](const QString &tag) { - return QStringList{QStringLiteral("tag:%1").arg(tag)}; - }; - QTest::newRow("tagsUrl") << tagUrl("tagA") << "" << tagTerms("tagA") << false << false; - QTest::newRow("tagsUrl/space") << tagUrl("tagB with spaces") << "" << tagTerms("tagB with spaces") << false << false; - QTest::newRow("tagsUrl/hash") << tagUrl("tagC#hash") << "" << tagTerms("tagC#hash") << false << false; - QTest::newRow("tagsUrl/slash") << tagUrl("tagD/with/slash") << "" << tagTerms("tagD/with/slash") << false << false; + QTest::newRow("tagsUrl") << tagUrl(tagA) << "" << QDate{} << 0 << QStringList{tagA} << false << false; + QTest::newRow("tagsUrl/space") << tagUrl(tagBWithSpaces) << "" << QDate{} << 0 << QStringList{tagBWithSpaces} << false << false; + QTest::newRow("tagsUrl/hash") << tagUrl("tagC#hash") << "" << QDate{} << 0 << QStringList{QStringLiteral("tagC#hash")} << false << false; + QTest::newRow("tagsUrl/slash") << tagUrl("tagD/with/slash") << "" << QDate{} << 0 << QStringList{QStringLiteral("tagD/with/slash")} << false << false; } /** @@ -155,31 +163,173 @@ void DolphinQueryTest::testBalooSearchParsing_data() void DolphinQueryTest::testBalooSearchParsing() { QFETCH(QUrl, searchUrl); - QFETCH(QString, expectedText); - QFETCH(QStringList, expectedTerms); + QFETCH(QString, expectedSearchTerm); + QFETCH(QDate, expectedModifiedSinceDate); + QFETCH(int, expectedMinimumRating); + QFETCH(QStringList, expectedTags); QFETCH(bool, hasContent); QFETCH(bool, hasFileName); - const DolphinQuery query = DolphinQuery::fromSearchUrl(searchUrl); + const Search::DolphinQuery query = Search::DolphinQuery{searchUrl, /** No backupSearchPath should be needed because searchUrl should be valid. */ QUrl{}}; // Checkt that the URL is supported - QVERIFY(DolphinQuery::supportsScheme(searchUrl.scheme())); + QVERIFY(Search::isSupportedSearchScheme(searchUrl.scheme())); // Check for parsed text (would be displayed on the input search bar) - QCOMPARE(query.text(), expectedText); + QCOMPARE(query.searchTerm(), expectedSearchTerm); + + QCOMPARE(query.modifiedSinceDate(), expectedModifiedSinceDate); + + QCOMPARE(query.minimumRating(), expectedMinimumRating); + + QCOMPARE(query.requiredTags(), expectedTags); + + // Check that there were no unrecognized baloo query parameters in the above strings. + Q_ASSERT(query.m_unrecognizedBalooQueryStrings.isEmpty()); + + // Check if a search term is looked up in the file names or contents + QCOMPARE(query.searchThrough() == Search::SearchThrough::FileContents && !query.searchTerm().isEmpty(), hasContent); + QCOMPARE(!query.searchTerm().isEmpty(), hasFileName); // The file names are always also searched even when searching through file contents. +} + +/** + * Tests whether exporting a DolphinQuery object to a URL and then constructing a DolphinQuery object from that URL recreates the same DolphinQuery. + */ +void DolphinQueryTest::testExportImport() +{ + /// Initialize the DolphinQuery with some standard settings. + const QUrl searchPath1{"file:///someNonExistentUrl"}; + Search::DolphinQuery query{searchPath1, searchPath1}; + query.setSearchLocations(Search::SearchLocations::FromHere); + QVERIFY(query.searchLocations() == Search::SearchLocations::FromHere); + query.setSearchThrough(Search::SearchThrough::FileNames); + QVERIFY(query.searchThrough() == Search::SearchThrough::FileNames); + query.setSearchTool(Search::SearchTool::Filenamesearch); + QVERIFY(query.searchTool() == Search::SearchTool::Filenamesearch); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath1)); // Export then import + + /// Test that exporting and importing works as expected no matter which aspect we change. + query.setSearchThrough(Search::SearchThrough::FileContents); + QVERIFY(query.searchThrough() == Search::SearchThrough::FileContents); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath1)); // Export then import + + constexpr QLatin1String searchTerm1{"abc"}; + query.setSearchTerm(searchTerm1); + QVERIFY(query.searchTerm() == searchTerm1); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath1)); // Export then import + + query.setSearchThrough(Search::SearchThrough::FileNames); + QVERIFY(query.searchThrough() == Search::SearchThrough::FileNames); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath1)); // Export then import + + QVERIFY(query.searchPath() == searchPath1); + const QUrl searchPath2{"file:///someNonExistentUrl2"}; + query.setSearchPath(searchPath2); + QVERIFY(query.searchPath() == searchPath2); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because otherUrl is imported. + + query.setSearchLocations(Search::SearchLocations::Everywhere); + QVERIFY(query.searchLocations() == Search::SearchLocations::Everywhere); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath2)); // Export then import. searchPath2 is required to match as the fallback. + + QVERIFY(query.searchTerm() == searchTerm1); + constexpr QLatin1String searchTerm2{"xyz"}; + query.setSearchTerm(searchTerm2); + QVERIFY(query.searchTerm() == searchTerm2); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath2)); // Export then import + + QVERIFY(query.searchLocations() == Search::SearchLocations::Everywhere); + query.setSearchLocations(Search::SearchLocations::FromHere); + QVERIFY(query.searchLocations() == Search::SearchLocations::FromHere); + QVERIFY(query.searchPath() == searchPath2); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported. + +#if HAVE_BALOO + /// Test Baloo search queries + query.setSearchTool(Search::SearchTool::Baloo); + QVERIFY(query.searchTool() == Search::SearchTool::Baloo); + QVERIFY(query.searchTerm() == searchTerm2); + QVERIFY(query.searchLocations() == Search::SearchLocations::FromHere); + QVERIFY(query.searchPath() == searchPath2); + QVERIFY(query.searchThrough() == Search::SearchThrough::FileNames); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported. + + /// Test that exporting and importing works as expected no matter which aspect we change. + query.setSearchThrough(Search::SearchThrough::FileContents); + QVERIFY(query.searchThrough() == Search::SearchThrough::FileContents); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported. + + query.setSearchTerm(searchTerm1); + QVERIFY(query.searchTerm() == searchTerm1); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported. + + query.setSearchThrough(Search::SearchThrough::FileNames); + QVERIFY(query.searchThrough() == Search::SearchThrough::FileNames); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported. + + QVERIFY(query.searchPath() == searchPath2); + query.setSearchPath(searchPath1); + QVERIFY(query.searchPath() == searchPath1); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported. + + query.setSearchLocations(Search::SearchLocations::Everywhere); + QVERIFY(query.searchLocations() == Search::SearchLocations::Everywhere); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath1)); // Export then import. searchPath1 is required to match as the fallback. + + QVERIFY(query.searchTerm() == searchTerm1); + query.setSearchTerm(searchTerm2); + QVERIFY(query.searchTerm() == searchTerm2); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath1)); // Export then import + + QVERIFY(query.searchLocations() == Search::SearchLocations::Everywhere); + query.setSearchLocations(Search::SearchLocations::FromHere); + QVERIFY(query.searchLocations() == Search::SearchLocations::FromHere); + QVERIFY(query.searchPath() == searchPath1); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported. + + QVERIFY(query.fileType() == KFileMetaData::Type::Empty); + query.setFileType(KFileMetaData::Type::Archive); + QVERIFY(query.fileType() == KFileMetaData::Type::Archive); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported. + + QVERIFY(!query.modifiedSinceDate().isValid()); + QDate modifiedDate; + modifiedDate.setDate(2018, 6, 3); // World Bicycle Day + query.setModifiedSinceDate(modifiedDate); + QVERIFY(query.modifiedSinceDate() == modifiedDate); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported. + + QVERIFY(query.minimumRating() == 0); + query.setMinimumRating(4); + QVERIFY(query.minimumRating() == 4); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported. - // Check for parsed search terms (would be displayed by the facetsWidget) - QStringList searchTerms = query.searchTerms(); - searchTerms.sort(); + QVERIFY(query.requiredTags().isEmpty()); + query.setRequiredTags({searchTerm1, searchTerm2}); + QVERIFY(query.requiredTags().contains(searchTerm1)); + QVERIFY(query.requiredTags().contains(searchTerm2)); + QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported. - QCOMPARE(searchTerms.count(), expectedTerms.count()); - for (int i = 0; i < expectedTerms.count(); i++) { - QCOMPARE(searchTerms.at(i), expectedTerms.at(i)); - } + QVERIFY(query.searchTool() == Search::SearchTool::Baloo); + QVERIFY(query.searchThrough() == Search::SearchThrough::FileNames); + QVERIFY(query.searchPath() == searchPath1); + QVERIFY(query.searchTerm() == searchTerm2); + QVERIFY(query.searchLocations() == Search::SearchLocations::FromHere); + QVERIFY(query.fileType() == KFileMetaData::Type::Archive); + QVERIFY(query.modifiedSinceDate() == modifiedDate); + QVERIFY(query.minimumRating() == 4); - // Check for filename and content detection - QCOMPARE(query.hasContentSearch(), hasContent); - QCOMPARE(query.hasFileName(), hasFileName); + /// Changing the search tool should not immediately drop all the extra information even if the search tool might not support searching for them. + /// This is mostly an attempt to not drop properties set by the user earlier than we have to. + query.setSearchTool(Search::SearchTool::Filenamesearch); + QVERIFY(query.searchThrough() == Search::SearchThrough::FileNames); + QVERIFY(query.searchPath() == searchPath1); + QVERIFY(query.searchTerm() == searchTerm2); + QVERIFY(query.searchLocations() == Search::SearchLocations::FromHere); + QVERIFY(query.fileType() == KFileMetaData::Type::Archive); + QVERIFY(query.modifiedSinceDate() == modifiedDate); + QVERIFY(query.minimumRating() == 4); +#endif } QTEST_MAIN(DolphinQueryTest) diff --git a/src/tests/dolphinsearchbartest.cpp b/src/tests/dolphinsearchbartest.cpp new file mode 100644 index 000000000..0efb05dc8 --- /dev/null +++ b/src/tests/dolphinsearchbartest.cpp @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2011 Peter Penz <[email protected]> + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "search/bar.h" +#include "search/popup.h" + +#include <QLineEdit> +#include <QSignalSpy> +#include <QStandardPaths> +#include <QTest> +#include <QToolButton> + +class DolphinSearchBarTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testPopupLazyLoading(); + void testTextClearing(); + void testUrlChangeSignals(); + +private: + Search::Bar *m_searchBar; +}; + +void DolphinSearchBarTest::initTestCase() +{ + QStandardPaths::setTestModeEnabled(true); +} + +void DolphinSearchBarTest::init() +{ + const auto homeUrl{QUrl::fromUserInput(QDir::homePath())}; + m_searchBar = new Search::Bar(std::make_shared<const Search::DolphinQuery>(homeUrl, homeUrl)); +} + +void DolphinSearchBarTest::cleanup() +{ + delete m_searchBar; +} + +void DolphinSearchBarTest::testPopupLazyLoading() +{ + m_searchBar->setVisible(true, WithoutAnimation); + QVERIFY2(m_searchBar->m_popup->isEmpty(), "The popup should only be populated or updated when it was opened at least once by the user."); +} + +/** + * The test verifies whether the automatic clearing of the text works correctly. + * The text may not get cleared when the search bar gets visible or invisible, + * as this would clear the text when switching between tabs. + */ +void DolphinSearchBarTest::testTextClearing() +{ + m_searchBar->setVisible(true, WithoutAnimation); + QVERIFY(m_searchBar->text().isEmpty()); + QVERIFY(!m_searchBar->isSearchConfigured()); + + m_searchBar->updateStateToMatch(std::make_shared<const Search::DolphinQuery>(QUrl::fromUserInput("filenamesearch:?search=xyz"), QUrl{})); + m_searchBar->setVisible(false, WithoutAnimation); + m_searchBar->setVisible(true, WithoutAnimation); + QCOMPARE(m_searchBar->text(), QStringLiteral("xyz")); + QVERIFY(m_searchBar->isSearchConfigured()); + QVERIFY(!m_searchBar->queryTitle().isEmpty()); + + QTest::keyClick(m_searchBar, Qt::Key_Escape); + QVERIFY(m_searchBar->text().isEmpty()); + QVERIFY(!m_searchBar->isSearchConfigured()); +} + +void DolphinSearchBarTest::testUrlChangeSignals() +{ + QSignalSpy spyUrlChangeRequested(m_searchBar, &Search::Bar::urlChangeRequested); + m_searchBar->setVisible(true, WithoutAnimation); + QVERIFY2(spyUrlChangeRequested.isEmpty(), "Opening the Search::Bar should not trigger anything."); + + m_searchBar->m_everywhereButton->click(); + m_searchBar->m_fromHereButton->click(); + QVERIFY(!m_searchBar->isSearchConfigured()); + + while (!spyUrlChangeRequested.isEmpty()) { + const QUrl requestedUrl = qvariant_cast<QUrl>(spyUrlChangeRequested.takeFirst().at(0)); + QVERIFY2(!Search::isSupportedSearchScheme(requestedUrl.scheme()) && requestedUrl == m_searchBar->m_searchConfiguration->searchPath(), + "The search is still not in a state to search for anything specific, so any requested URLs would be identical to the current search path of " + "the Search::Bar."); + } + + Search::DolphinQuery searchConfiguration = *m_searchBar->m_searchConfiguration; + searchConfiguration.setSearchTerm(QStringLiteral("searchTerm")); + m_searchBar->updateStateToMatch(std::make_shared<const Search::DolphinQuery>(searchConfiguration)); + QVERIFY2(m_searchBar->isSearchConfigured(), "The search bar now has enough information to trigger a meaningful search."); + QVERIFY2(spyUrlChangeRequested.isEmpty(), "The visual state was updated to match a new search configuration, but the user never triggered a search."); + + m_searchBar->commitCurrentConfiguration(); // We pretend to be a user interaction that would trigger a search to happen. + const QUrl requestedUrl = qvariant_cast<QUrl>(spyUrlChangeRequested.takeFirst().at(0)); + QVERIFY2(Search::isSupportedSearchScheme(requestedUrl.scheme()), "The search bar requested to open a search url and therefore start searching."); + QVERIFY2(spyUrlChangeRequested.isEmpty(), "The search URL was requested exactly once, so no additional urlChangeRequested signals should exist."); + + Search::DolphinQuery searchConfiguration2 = *m_searchBar->m_searchConfiguration; + searchConfiguration2.setSearchTerm(QString("")); + m_searchBar->updateStateToMatch(std::make_shared<const Search::DolphinQuery>(searchConfiguration2)); + QVERIFY2(!m_searchBar->isSearchConfigured(), "The search bar does not have enough information anymore to trigger a meaningful search."); + QVERIFY2(spyUrlChangeRequested.isEmpty(), "The visual state was updated to match a new search configuration, but the user never triggered a search."); + + m_searchBar->commitCurrentConfiguration(); // We pretend to be a user interaction that would trigger a search to happen. + const QUrl requestedUrl2 = qvariant_cast<QUrl>(spyUrlChangeRequested.takeFirst().at(0)); + QVERIFY2(!Search::isSupportedSearchScheme(requestedUrl2.scheme()) && requestedUrl2 == m_searchBar->m_searchConfiguration->searchPath(), + "The Search::Bar is not in a state to search for anything specific, so the search bar requests to show the previously visited location normally " + "again instead of any previous search URL."); + QVERIFY2(spyUrlChangeRequested.isEmpty(), "The non-search URL was requested exactly once, so no additional urlChangeRequested signals should exist."); + + QVERIFY2(m_searchBar->m_popup->isEmpty(), "Through all of this, the popup should still be empty because it was never opened by the user."); +} + +QTEST_MAIN(DolphinSearchBarTest) + +#include "dolphinsearchbartest.moc" diff --git a/src/tests/dolphinsearchboxtest.cpp b/src/tests/dolphinsearchboxtest.cpp deleted file mode 100644 index bda60909d..000000000 --- a/src/tests/dolphinsearchboxtest.cpp +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2011 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "search/dolphinsearchbox.h" - -#include <QStandardPaths> -#include <QTest> - -class DolphinSearchBoxTest : public QObject -{ - Q_OBJECT - -private Q_SLOTS: - void initTestCase(); - void init(); - void cleanup(); - - void testTextClearing(); - -private: - DolphinSearchBox *m_searchBox; -}; - -void DolphinSearchBoxTest::initTestCase() -{ - QStandardPaths::setTestModeEnabled(true); -} - -void DolphinSearchBoxTest::init() -{ - m_searchBox = new DolphinSearchBox(); -} - -void DolphinSearchBoxTest::cleanup() -{ - delete m_searchBox; -} - -/** - * The test verifies whether the automatic clearing of the text works correctly. - * The text may not get cleared when the searchbox gets visible or invisible, - * as this would clear the text when switching between tabs. - */ -void DolphinSearchBoxTest::testTextClearing() -{ - m_searchBox->setVisible(true, WithoutAnimation); - QVERIFY(m_searchBox->text().isEmpty()); - - m_searchBox->setText("xyz"); - m_searchBox->setVisible(false, WithoutAnimation); - m_searchBox->setVisible(true, WithoutAnimation); - QCOMPARE(m_searchBox->text(), QStringLiteral("xyz")); - - QTest::keyClick(m_searchBox, Qt::Key_Escape); - QVERIFY(m_searchBox->text().isEmpty()); -} - -QTEST_MAIN(DolphinSearchBoxTest) - -#include "dolphinsearchboxtest.moc" |
