2 SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz19@gmail.com>
3 SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
5 SPDX-License-Identifier: GPL-2.0-or-later
11 #include "barsecondrowflowlayout.h"
13 #include "dolphin_searchsettings.h"
14 #include "dolphinplacesmodelsingleton.h"
15 #include "dolphinquery.h"
17 #include "widgetmenu.h"
19 #include "config-dolphin.h"
20 #include <KLocalizedString>
22 #include <QApplication>
23 #include <QHBoxLayout>
28 #include <QScrollArea>
30 #include <QToolButton>
32 using namespace Search
;
37 * @see Bar::IsSearchConfigured().
39 bool isSearchConfigured(const std::shared_ptr
<const DolphinQuery
> &searchConfiguration
)
41 return !searchConfiguration
->searchTerm().isEmpty()
42 || (searchConfiguration
->searchTool() != SearchTool::Filenamesearch
43 && (searchConfiguration
->fileType() != KFileMetaData::Type::Empty
|| searchConfiguration
->modifiedSinceDate().isValid()
44 || searchConfiguration
->minimumRating() > 0 || !searchConfiguration
->requiredTags().isEmpty()));
48 Bar::Bar(std::shared_ptr
<const DolphinQuery
> dolphinQuery
, QWidget
*parent
)
49 : AnimatedHeightWidget(parent
)
50 , UpdatableStateInterface
{dolphinQuery
}
52 QWidget
*contentsContainer
= prepareContentsContainer();
55 m_searchTermEditor
= new QLineEdit(contentsContainer
);
56 m_searchTermEditor
->setClearButtonEnabled(true);
57 connect(m_searchTermEditor
, &QLineEdit::returnPressed
, this, &Bar::slotReturnPressed
);
58 connect(m_searchTermEditor
, &QLineEdit::textEdited
, this, &Bar::slotSearchTermEdited
);
59 setFocusProxy(m_searchTermEditor
);
61 // Add "Save search" button inside search box
62 m_saveSearchAction
= new QAction(this);
63 m_saveSearchAction
->setIcon(QIcon::fromTheme(QStringLiteral("document-save-symbolic")));
64 m_saveSearchAction
->setText(i18nc("action:button", "Save this search to quickly access it again in the future"));
65 m_searchTermEditor
->addAction(m_saveSearchAction
, QLineEdit::TrailingPosition
);
66 connect(m_saveSearchAction
, &QAction::triggered
, this, &Bar::slotSaveSearch
);
69 auto filterButton
= new QToolButton(contentsContainer
);
70 filterButton
->setIcon(QIcon::fromTheme(QStringLiteral("view-filter")));
71 filterButton
->setText(i18nc("@action:button for changing search options", "Filter"));
72 filterButton
->setAutoRaise(true);
73 filterButton
->setToolButtonStyle(Qt::ToolButtonTextBesideIcon
);
74 filterButton
->setPopupMode(QToolButton::InstantPopup
);
75 filterButton
->setAttribute(Qt::WA_CustomWhatsThis
);
76 m_popup
= new Popup
{m_searchConfiguration
, this};
77 connect(m_popup
, &QMenu::aboutToShow
, this, [this]() {
78 m_popup
->updateStateToMatch(m_searchConfiguration
);
80 connect(m_popup
, &Popup::configurationChanged
, this, &Bar::slotConfigurationChanged
);
81 connect(m_popup
, &Popup::showMessage
, this, &Bar::showMessage
);
82 connect(m_popup
, &Popup::showInstallationProgress
, this, &Bar::showInstallationProgress
);
83 filterButton
->setMenu(m_popup
);
85 // Create close button
86 QToolButton
*closeButton
= new QToolButton(contentsContainer
);
87 closeButton
->setAutoRaise(true);
88 closeButton
->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close")));
89 closeButton
->setToolTip(i18nc("@info:tooltip", "Quit searching"));
90 connect(closeButton
, &QToolButton::clicked
, this, [this]() {
91 setVisible(false, WithAnimation
);
94 // Apply layout for the search input row
95 QHBoxLayout
*firstRowLayout
= new QHBoxLayout
{};
96 firstRowLayout
->setContentsMargins(0, 0, 0, 0);
97 firstRowLayout
->addWidget(m_searchTermEditor
);
98 firstRowLayout
->addWidget(filterButton
);
99 firstRowLayout
->addWidget(closeButton
);
101 // Create "From Here" and "Your files" buttons
102 m_fromHereButton
= new QToolButton(contentsContainer
);
103 m_fromHereButton
->setText(i18nc("action:button search from here", "Here"));
104 m_fromHereButton
->setAutoRaise(true);
105 m_fromHereButton
->setCheckable(true);
106 connect(m_fromHereButton
, &QToolButton::clicked
, this, [this]() {
107 if (m_searchConfiguration
->searchLocations() == SearchLocations::FromHere
) {
108 return; // Already selected.
110 SearchSettings::setLocation(QStringLiteral("FromHere"));
111 SearchSettings::self()->save();
112 DolphinQuery searchConfigurationCopy
= *m_searchConfiguration
;
113 searchConfigurationCopy
.setSearchLocations(SearchLocations::FromHere
);
114 slotConfigurationChanged(std::move(searchConfigurationCopy
));
117 m_everywhereButton
= new QToolButton(contentsContainer
);
118 m_everywhereButton
->setText(i18nc("action:button search everywhere", "Everywhere"));
119 m_everywhereButton
->setToolButtonStyle(Qt::ToolButtonTextBesideIcon
);
120 m_everywhereButton
->setAutoRaise(true);
121 m_everywhereButton
->setCheckable(true);
122 connect(m_everywhereButton
, &QToolButton::clicked
, this, [this]() {
123 if (m_searchConfiguration
->searchLocations() == SearchLocations::Everywhere
) {
124 return; // Already selected.
126 SearchSettings::setLocation(QStringLiteral("Everywhere"));
127 SearchSettings::self()->save();
128 DolphinQuery searchConfigurationCopy
= *m_searchConfiguration
;
129 searchConfigurationCopy
.setSearchLocations(SearchLocations::Everywhere
);
130 slotConfigurationChanged(std::move(searchConfigurationCopy
));
133 // Apply layout for the location buttons and chips row
134 m_secondRowLayout
= new BarSecondRowFlowLayout
{nullptr};
135 m_secondRowLayout
->setSpacing(Dolphin::LAYOUT_SPACING_SMALL
);
136 connect(m_secondRowLayout
, &BarSecondRowFlowLayout::heightHintChanged
, this, [this]() {
138 AnimatedHeightWidget::setVisible(true, WithAnimation
);
140 // 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.
142 m_secondRowLayout
->addWidget(m_fromHereButton
);
143 m_secondRowLayout
->addWidget(m_everywhereButton
);
145 m_topLayout
= new QVBoxLayout(contentsContainer
);
146 m_topLayout
->setContentsMargins(Dolphin::LAYOUT_SPACING_SMALL
, Dolphin::LAYOUT_SPACING_SMALL
, Dolphin::LAYOUT_SPACING_SMALL
, Dolphin::LAYOUT_SPACING_SMALL
);
147 m_topLayout
->setSpacing(Dolphin::LAYOUT_SPACING_SMALL
);
148 m_topLayout
->addLayout(firstRowLayout
);
149 m_topLayout
->addLayout(m_secondRowLayout
);
152 "@info:whatsthis search bar",
153 "<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 "
154 "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 "
155 "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 "
156 "results.</item><item>Press the “Save” icon to add the current search configuration to the <emphasis>Places panel</emphasis>.</item></list></para>",
157 filterButton
->text()));
159 // The searching should be started automatically after the user did not change
160 // the text for a while
161 m_startSearchTimer
= new QTimer(this);
162 m_startSearchTimer
->setSingleShot(true);
163 m_startSearchTimer
->setInterval(500);
164 connect(m_startSearchTimer
, &QTimer::timeout
, this, &Bar::commitCurrentConfiguration
);
166 updateStateToMatch(dolphinQuery
);
169 QString
Bar::text() const
171 return m_searchTermEditor
->text();
174 void Bar::setSearchPath(const QUrl
&url
)
176 if (url
== m_searchConfiguration
->searchPath()) {
180 DolphinQuery searchConfigurationCopy
= *m_searchConfiguration
;
181 searchConfigurationCopy
.setSearchPath(url
);
182 updateStateToMatch(std::make_shared
<const DolphinQuery
>(std::move(searchConfigurationCopy
)));
185 void Bar::selectAll()
187 m_searchTermEditor
->setFocus();
188 m_searchTermEditor
->selectAll();
191 void Bar::setVisible(bool visible
, Animated animated
)
194 m_startSearchTimer
->stop();
195 Q_EMIT
urlChangeRequested(m_searchConfiguration
->searchPath());
196 if (isAncestorOf(QApplication::focusWidget())) {
197 Q_EMIT
focusViewRequest();
200 AnimatedHeightWidget::setVisible(visible
, animated
);
201 Q_EMIT
visibilityChanged(visible
);
204 void Bar::updateState(const std::shared_ptr
<const DolphinQuery
> &dolphinQuery
)
206 m_searchTermEditor
->setText(dolphinQuery
->searchTerm());
207 // 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
208 // 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
209 // the placeholder then. But when names are not searched we change the placeholder message to make this clear.
210 m_searchTermEditor
->setPlaceholderText(dolphinQuery
->searchTool() == SearchTool::Filenamesearch
211 && dolphinQuery
->searchThrough() == SearchThrough::FileContents
212 ? i18nc("@info:placeholder", "Search in file contents…")
214 m_saveSearchAction
->setEnabled(::isSearchConfigured(dolphinQuery
));
215 m_fromHereButton
->setChecked(dolphinQuery
->searchLocations() == SearchLocations::FromHere
);
216 m_everywhereButton
->setChecked(dolphinQuery
->searchLocations() == SearchLocations::Everywhere
);
218 if (m_popup
&& m_popup
->isVisible()) {
219 // The user actually sees the popup, so update it now! Normally the popup is only updated when Popup::aboutToShow() is emitted.
220 m_popup
->updateStateToMatch(dolphinQuery
);
224 const QUrl cleanedUrl
= dolphinQuery
->searchPath().adjusted(QUrl::RemoveUserInfo
| QUrl::StripTrailingSlash
);
225 m_fromHereButton
->setToolTip(
226 xi18nc("@info:tooltip", "Limit the search to <filename>%1</filename> and its subfolders.", cleanedUrl
.toString(QUrl::PreferLocalFile
)));
227 m_everywhereButton
->setToolTip(
228 dolphinQuery
->searchTool() == SearchTool::Filenamesearch
230 // 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.
231 // See https://commits.kde.org/kxmlgui/a31135046e1b3335b5d7bbbe6aa9a883ce3284c1
232 // i18n: The "Everywhere" button makes Dolphin search all files in "/" recursively. "From the root up" is meant to
233 // communicate this colloquially while containing the technical term "root". It is fine to drop the technicalities here
234 // and only to communicate that everything in the file system is supposed to be searched here.
235 ? i18nc("@info:tooltip", "Search all directories from the root up.")
236 // i18n: Tooltip for "Everywhere" button as opposed to searching for files in specific folders. The search tool uses
237 // file indexing and will therefore only be able to search through directories which have been put into a data base.
238 // Please make sure your translation of the path to the Search settings page is identical to translation there.
239 : xi18nc("@info:tooltip", "Search all indexed locations.<nl/><nl/>Configure which locations are indexed in <interface>System Settings|Workspace|Search</interface>."));
242 auto updateChip
= [this, &dolphinQuery
]<typename Selector
>(bool shouldExist
, Chip
<Selector
> *chip
) -> Chip
<Selector
> * {
245 chip
= new Chip
<Selector
>{dolphinQuery
, nullptr};
247 chip
->setMaximumHeight(m_fromHereButton
->height());
248 connect(chip
, &ChipBase::configurationChanged
, this, &Bar::slotConfigurationChanged
);
249 m_secondRowLayout
->addWidget(chip
); // Transfers ownership
250 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.
252 chip
->updateStateToMatch(dolphinQuery
);
262 m_fileTypeSelectorChip
= updateChip
.template operator()<FileTypeSelector
>(dolphinQuery
->searchTool() != SearchTool::Filenamesearch
263 && dolphinQuery
->fileType() != KFileMetaData::Type::Empty
,
264 m_fileTypeSelectorChip
);
265 m_modifiedSinceDateSelectorChip
=
266 updateChip
.template operator()<DateSelector
>(dolphinQuery
->searchTool() != SearchTool::Filenamesearch
&& dolphinQuery
->modifiedSinceDate().isValid(),
267 m_modifiedSinceDateSelectorChip
);
268 m_minimumRatingSelectorChip
=
269 updateChip
.template operator()<MinimumRatingSelector
>(dolphinQuery
->searchTool() != SearchTool::Filenamesearch
&& dolphinQuery
->minimumRating() > 0,
270 m_minimumRatingSelectorChip
);
271 m_requiredTagsSelectorChip
=
272 updateChip
.template operator()<TagsSelector
>(dolphinQuery
->searchTool() != SearchTool::Filenamesearch
&& dolphinQuery
->requiredTags().count(),
273 m_requiredTagsSelectorChip
);
276 void Bar::keyPressEvent(QKeyEvent
*event
)
278 QWidget::keyReleaseEvent(event
);
279 if (event
->key() == Qt::Key_Escape
) {
280 if (m_searchTermEditor
->text().isEmpty()) {
281 setVisible(false, WithAnimation
);
283 // Clear the text input
284 slotSearchTermEdited(QString());
289 void Bar::keyReleaseEvent(QKeyEvent
*event
)
291 QWidget::keyReleaseEvent(event
);
292 if (event
->key() == Qt::Key_Down
) {
293 Q_EMIT
focusViewRequest();
297 void Bar::slotConfigurationChanged(const DolphinQuery
&searchConfiguration
)
299 Q_ASSERT_X(*m_searchConfiguration
!= searchConfiguration
, "Bar::updateState()", "Redundantly updating to a state that is identical to the previous state.");
300 updateStateToMatch(std::make_shared
<const DolphinQuery
>(searchConfiguration
));
302 commitCurrentConfiguration();
305 void Bar::slotSearchTermEdited(const QString
&text
)
307 DolphinQuery searchConfigurationCopy
= *m_searchConfiguration
;
308 searchConfigurationCopy
.setSearchTerm(text
);
309 updateStateToMatch(std::make_shared
<const DolphinQuery
>(searchConfigurationCopy
));
311 m_startSearchTimer
->start();
314 void Bar::slotReturnPressed()
316 commitCurrentConfiguration();
317 Q_EMIT
focusViewRequest();
320 void Bar::commitCurrentConfiguration()
322 m_startSearchTimer
->stop();
323 // 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.
324 // In that case we want to show the files of the normal location again.
325 if (!isSearchConfigured()) {
326 Q_EMIT
urlChangeRequested(m_searchConfiguration
->searchPath());
329 Q_EMIT
urlChangeRequested(m_searchConfiguration
->toUrl());
332 void Bar::slotSaveSearch()
334 Q_ASSERT_X(isSearchConfigured(),
335 "Search::Bar::slotSaveSearch()",
336 "Search::Bar::isSearchConfigured() considers this search invalid, so the user should not be able to save this search. The button to save should "
338 const QUrl searchUrl
= m_searchConfiguration
->toUrl();
339 Q_ASSERT(searchUrl
.isValid() && isSupportedSearchScheme(searchUrl
.scheme()));
340 DolphinPlacesModelSingleton::instance().placesModel()->addPlace(m_searchConfiguration
->title(), searchUrl
, QStringLiteral("folder-saved-search-symbolic"));
343 bool Bar::isSearchConfigured() const
345 return ::isSearchConfigured(m_searchConfiguration
);
348 QString
Bar::queryTitle() const
350 return m_searchConfiguration
->title();
353 int Bar::preferredHeight() const
355 return m_secondRowLayout
->geometry().y() + m_secondRowLayout
->sizeHint().height() + Dolphin::LAYOUT_SPACING_SMALL
;