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)
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
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
#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@"
#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"
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)
}
if (url.isValid()) {
QString icon;
- if (m_activeViewContainer->isSearchModeEnabled()) {
+ if (isSearchUrl(url)) {
icon = QStringLiteral("folder-saved-search-symbolic");
} else {
icon = KIO::iconNameForUrl(url);
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()
{
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);
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);
"<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
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.
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());
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());
}
#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"
#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;
, 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}
, m_statusBar(nullptr)
, m_statusBarTimer(nullptr)
, m_statusBarTimestamp()
- , m_autoGrabFocus(true)
+ , m_grabFocusOnUrlChange{true}
{
hide();
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);
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);
});
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
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);
}
void DolphinViewContainer::setActive(bool active)
{
- m_searchBox->setActive(active);
if (m_urlNavigatorConnected) {
m_urlNavigatorConnected->setActive(active);
}
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
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();
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()) {
QString DolphinViewContainer::captionWindowTitle() const
{
- if (GeneralSettings::showFullPathInTitlebar() && !isSearchModeEnabled()) {
+ if (GeneralSettings::showFullPathInTitlebar() && (!isSearchBarVisible() || !m_searchBar->isSearchConfigured())) {
if (!url().isLocalFile()) {
return url().adjusted(QUrl::StripTrailingSlash).toString();
}
// 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();
{
if (newUrl != m_urlNavigator->locationUrl()) {
m_urlNavigator->setLocationUrl(newUrl);
+ if (m_searchBar && !Search::isSupportedSearchScheme(newUrl.scheme())) {
+ m_searchBar->setSearchPath(newUrl);
+ }
}
}
}
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.
// 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);
}
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();
void DolphinViewContainer::slotPlacesModelChanged()
{
- if (!GeneralSettings::showFullPathInTitlebar() && !isSearchModeEnabled()) {
+ if (!GeneralSettings::showFullPathInTitlebar()) {
Q_EMIT captionChanged();
}
}
}
}
-bool DolphinViewContainer::isSearchUrl(const QUrl &url) const
-{
- return url.scheme().contains(QLatin1String("search"));
-}
-
void DolphinViewContainer::saveViewState()
{
QByteArray locationState;
class QAction;
class QGridLayout;
class QUrl;
-class DolphinSearchBox;
+namespace Search
+{
+class Bar;
+}
class DolphinStatusBar;
class KFileItemList;
namespace SelectionMode
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.
* 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();
*/
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:
* 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.
*/
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();
*/
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);
/** Requests the focus for the view \a m_view. */
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.
void slotOpenUrlFinished(KJob *job);
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, ...
*/
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.
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.
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 };
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz19@gmail.com>
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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;
+}
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz19@gmail.com>
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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;
+}
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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"
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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
</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>
+++ /dev/null
-/*
- * SPDX-FileCopyrightText: 2012 Peter Penz <peter.penz19@gmail.com>
- * SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
- *
- * 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"
+++ /dev/null
-/*
- * SPDX-FileCopyrightText: 2012 Peter Penz <peter.penz19@gmail.com>
- *
- * 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
/*
- * SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
- *
- * SPDX-License-Identifier: GPL-2.0-or-later
- */
+ SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
-#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
{
static const QLatin1String searchTokens[]{QLatin1String("filename:"),
QLatin1String("modified>="),
+ QLatin1String("modified>"),
QLatin1String("rating>="),
QLatin1String("tag:"),
QLatin1String("tag=")};
}
}
-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;
+};
/*
- * SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
- *
- * SPDX-License-Identifier: GPL-2.0-or-later
- */
+ SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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);
-
- /** @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;
+ /**
+ * @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;
+
+ 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
+++ /dev/null
-/*
- * SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz19@gmail.com>
- *
- * 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"
+++ /dev/null
-/*
- * SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz19@gmail.com>
- *
- * 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
--- /dev/null
+/*
+ This file is part of the KDE project
+ SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
+
+ 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
+}
--- /dev/null
+/*
+ This file is part of the KDE project
+ SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
+
+ 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
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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));
+}
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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()));
+}
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2012 Peter Penz <peter.penz19@gmail.com>
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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()));
+}
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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);
+ }
+}
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
+
+ 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
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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);
+}
--- /dev/null
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ 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
# 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()
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);
/*
- * SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@gmail.com>
- *
- * SPDX-License-Identifier: GPL-2.0-or-later
- */
+ SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
+ SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
#include "search/dolphinquery.h"
#include <QTest>
+#include <QDate>
#include <QJsonDocument>
#include <QJsonObject>
#include <QStandardPaths>
void initTestCase();
void testBalooSearchParsing_data();
void testBalooSearchParsing();
+ void testExportImport();
};
/**
void DolphinQueryTest::initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
+ Search::setTestMode();
}
/**
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");
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;
}
/**
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);
- // Check for parsed search terms (would be displayed by the facetsWidget)
- QStringList searchTerms = query.searchTerms();
- searchTerms.sort();
+ QCOMPARE(query.requiredTags(), expectedTags);
- QCOMPARE(searchTerms.count(), expectedTerms.count());
- for (int i = 0; i < expectedTerms.count(); i++) {
- QCOMPARE(searchTerms.at(i), expectedTerms.at(i));
- }
+ // Check that there were no unrecognized baloo query parameters in the above strings.
+ Q_ASSERT(query.m_unrecognizedBalooQueryStrings.isEmpty());
- // Check for filename and content detection
- QCOMPARE(query.hasContentSearch(), hasContent);
- QCOMPARE(query.hasFileName(), hasFileName);
+ // 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.
+
+ 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.
+
+ 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);
+
+ /// 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)
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
+ *
+ * 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"
+++ /dev/null
-/*
- * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
- *
- * 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"