]> cloud.milkyroute.net Git - dolphin.git/blob - src/search/bar.cpp
18707ef2363f3f15257a76101cb157770c092024
[dolphin.git] / src / search / bar.cpp
1 /*
2 SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz19@gmail.com>
3 SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8 #include "bar.h"
9 #include "global.h"
10
11 #include "barsecondrowflowlayout.h"
12 #include "chip.h"
13 #include "dolphin_searchsettings.h"
14 #include "dolphinplacesmodelsingleton.h"
15 #include "dolphinquery.h"
16 #include "popup.h"
17 #include "widgetmenu.h"
18
19 #include "config-dolphin.h"
20 #include <KLocalizedString>
21
22 #include <QApplication>
23 #include <QHBoxLayout>
24 #include <QIcon>
25 #include <QKeyEvent>
26 #include <QLineEdit>
27 #include <QMenu>
28 #include <QScrollArea>
29 #include <QTimer>
30 #include <QToolButton>
31
32 using namespace Search;
33
34 namespace
35 {
36 /**
37 * @see Bar::IsSearchConfigured().
38 */
39 bool isSearchConfigured(const std::shared_ptr<const DolphinQuery> &searchConfiguration)
40 {
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()));
45 };
46 }
47
48 Bar::Bar(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent)
49 : AnimatedHeightWidget(parent)
50 , UpdatableStateInterface{dolphinQuery}
51 {
52 QWidget *contentsContainer = prepareContentsContainer();
53
54 // Create search box
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);
60
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);
67
68 // Filter button
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);
79 });
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);
84
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);
92 });
93
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);
100
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.
109 }
110 SearchSettings::setLocation(QStringLiteral("FromHere"));
111 SearchSettings::self()->save();
112 DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
113 searchConfigurationCopy.setSearchLocations(SearchLocations::FromHere);
114 slotConfigurationChanged(std::move(searchConfigurationCopy));
115 });
116
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.
125 }
126 SearchSettings::setLocation(QStringLiteral("Everywhere"));
127 SearchSettings::self()->save();
128 DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
129 searchConfigurationCopy.setSearchLocations(SearchLocations::Everywhere);
130 slotConfigurationChanged(std::move(searchConfigurationCopy));
131 });
132
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]() {
137 if (isEnabled()) {
138 AnimatedHeightWidget::setVisible(true, WithAnimation);
139 }
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.
141 });
142 m_secondRowLayout->addWidget(m_fromHereButton);
143 m_secondRowLayout->addWidget(m_everywhereButton);
144
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);
150
151 setWhatsThis(xi18nc(
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()));
158
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);
165
166 updateStateToMatch(dolphinQuery);
167 }
168
169 QString Bar::text() const
170 {
171 return m_searchTermEditor->text();
172 }
173
174 void Bar::setSearchPath(const QUrl &url)
175 {
176 if (url == m_searchConfiguration->searchPath()) {
177 return;
178 }
179
180 DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
181 searchConfigurationCopy.setSearchPath(url);
182 updateStateToMatch(std::make_shared<const DolphinQuery>(std::move(searchConfigurationCopy)));
183 }
184
185 void Bar::selectAll()
186 {
187 m_searchTermEditor->setFocus();
188 m_searchTermEditor->selectAll();
189 }
190
191 void Bar::setVisible(bool visible, Animated animated)
192 {
193 if (!visible) {
194 m_startSearchTimer->stop();
195 Q_EMIT urlChangeRequested(m_searchConfiguration->searchPath());
196 if (isAncestorOf(QApplication::focusWidget())) {
197 Q_EMIT focusViewRequest();
198 }
199 }
200 AnimatedHeightWidget::setVisible(visible, animated);
201 Q_EMIT visibilityChanged(visible);
202 }
203
204 void Bar::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery)
205 {
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…")
213 : i18n("Search…"));
214 m_saveSearchAction->setEnabled(::isSearchConfigured(dolphinQuery));
215 m_fromHereButton->setChecked(dolphinQuery->searchLocations() == SearchLocations::FromHere);
216 m_everywhereButton->setChecked(dolphinQuery->searchLocations() == SearchLocations::Everywhere);
217
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);
221 }
222
223 /// Update tooltip
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
229 // clang-format off
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>."));
240 // clang-format on
241
242 auto updateChip = [this, &dolphinQuery]<typename Selector>(bool shouldExist, Chip<Selector> *chip) -> Chip<Selector> * {
243 if (shouldExist) {
244 if (!chip) {
245 chip = new Chip<Selector>{dolphinQuery, nullptr};
246 chip->hide();
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.
251 } else {
252 chip->updateStateToMatch(dolphinQuery);
253 }
254 return chip;
255 }
256 if (chip) {
257 chip->deleteLater();
258 }
259 return nullptr;
260 };
261
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);
274 }
275
276 void Bar::keyPressEvent(QKeyEvent *event)
277 {
278 QWidget::keyReleaseEvent(event);
279 if (event->key() == Qt::Key_Escape) {
280 if (m_searchTermEditor->text().isEmpty()) {
281 setVisible(false, WithAnimation);
282 } else {
283 // Clear the text input
284 slotSearchTermEdited(QString());
285 }
286 }
287 }
288
289 void Bar::keyReleaseEvent(QKeyEvent *event)
290 {
291 QWidget::keyReleaseEvent(event);
292 if (event->key() == Qt::Key_Down) {
293 Q_EMIT focusViewRequest();
294 }
295 }
296
297 void Bar::slotConfigurationChanged(const DolphinQuery &searchConfiguration)
298 {
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));
301
302 commitCurrentConfiguration();
303 }
304
305 void Bar::slotSearchTermEdited(const QString &text)
306 {
307 DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
308 searchConfigurationCopy.setSearchTerm(text);
309 updateStateToMatch(std::make_shared<const DolphinQuery>(searchConfigurationCopy));
310
311 m_startSearchTimer->start();
312 }
313
314 void Bar::slotReturnPressed()
315 {
316 commitCurrentConfiguration();
317 Q_EMIT focusViewRequest();
318 }
319
320 void Bar::commitCurrentConfiguration()
321 {
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());
327 return;
328 }
329 Q_EMIT urlChangeRequested(m_searchConfiguration->toUrl());
330 }
331
332 void Bar::slotSaveSearch()
333 {
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 "
337 "be disabled.");
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"));
341 }
342
343 bool Bar::isSearchConfigured() const
344 {
345 return ::isSearchConfigured(m_searchConfiguration);
346 }
347
348 QString Bar::queryTitle() const
349 {
350 return m_searchConfiguration->title();
351 }
352
353 int Bar::preferredHeight() const
354 {
355 return m_secondRowLayout->geometry().y() + m_secondRowLayout->sizeHint().height() + Dolphin::LAYOUT_SPACING_SMALL;
356 }