]> cloud.milkyroute.net Git - dolphin.git/blob - src/search/popup.cpp
DolphinStatusbar: Fix background and margins for non-Breeze styles
[dolphin.git] / src / search / popup.cpp
1 /*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6 */
7
8 #include "popup.h"
9
10 #include "config-dolphin.h"
11 #include "dolphinpackageinstaller.h"
12 #include "dolphinquery.h"
13 #include "global.h"
14 #include "selectors/dateselector.h"
15 #include "selectors/filetypeselector.h"
16 #include "selectors/minimumratingselector.h"
17 #include "selectors/tagsselector.h"
18
19 #include <KContextualHelpButton>
20 #include <KDialogJobUiDelegate>
21 #include <KIO/ApplicationLauncherJob>
22 #include <KIO/CommandLauncherJob>
23 #include <KLocalizedString>
24 #include <KService>
25
26 #include <QButtonGroup>
27 #ifdef Q_OS_WIN
28 #include <QDesktopServices>
29 #endif
30 #include <QHBoxLayout>
31 #include <QLabel>
32 #include <QMenu>
33 #include <QRadioButton>
34 #include <QStandardPaths>
35 #include <QToolButton>
36 #include <QVBoxLayout>
37
38 namespace
39 {
40 constexpr auto kFindDesktopName = "org.kde.kfind";
41 }
42
43 using namespace Search;
44
45 QString Search::filenamesearchUiName()
46 {
47 // i18n: Localized name for the Filenamesearch search tool for use in user interfaces.
48 return i18n("Simple search");
49 };
50
51 QString Search::balooUiName()
52 {
53 // i18n: Localized name for the Baloo search tool for use in user interfaces.
54 return i18n("File Indexing");
55 };
56
57 Popup::Popup(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent)
58 : WidgetMenu{parent}
59 , UpdatableStateInterface{dolphinQuery}
60 {
61 }
62
63 QWidget *Popup::init()
64 {
65 auto containerWidget = new QWidget{this};
66 containerWidget->setContentsMargins(Dolphin::VERTICAL_SPACER_HEIGHT,
67 Dolphin::VERTICAL_SPACER_HEIGHT,
68 Dolphin::VERTICAL_SPACER_HEIGHT, // Using the same value for every spacing in this containerWidget looks nice.
69 Dolphin::VERTICAL_SPACER_HEIGHT);
70 auto verticalMainLayout = new QVBoxLayout{containerWidget};
71 verticalMainLayout->setSpacing((2 * Dolphin::VERTICAL_SPACER_HEIGHT) / 3); // A bit less spacing between rows than when adding an explicit spacer.
72
73 /// Add UI to switch between only searching in file names or also in contents.
74 auto searchInLabel = new QLabel{i18nc("@title:group", "Search in:"), containerWidget};
75 searchInLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
76 verticalMainLayout->addWidget(searchInLabel);
77
78 m_searchInFileNamesRadioButton = new QRadioButton{i18nc("@option:radio Search in:", "File names"), containerWidget};
79 connect(m_searchInFileNamesRadioButton, &QAbstractButton::clicked, this, [this]() {
80 if (m_searchConfiguration->searchThrough() == SearchThrough::FileNames) {
81 return; // Already selected.
82 }
83 SearchSettings::setWhat(QStringLiteral("FileNames"));
84 SearchSettings::self()->save();
85 DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
86 searchConfigurationCopy.setSearchThrough(SearchThrough::FileNames);
87 Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
88 });
89 verticalMainLayout->addWidget(m_searchInFileNamesRadioButton);
90
91 m_searchInFileContentsRadioButton = new QRadioButton{containerWidget};
92 connect(m_searchInFileContentsRadioButton, &QAbstractButton::clicked, this, [this]() {
93 if (m_searchConfiguration->searchThrough() == SearchThrough::FileContents) {
94 return; // Already selected.
95 }
96 SearchSettings::setWhat(QStringLiteral("FileContents"));
97 SearchSettings::self()->save();
98 DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
99 searchConfigurationCopy.setSearchThrough(SearchThrough::FileContents);
100 Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
101 });
102 verticalMainLayout->addWidget(m_searchInFileContentsRadioButton);
103
104 auto searchInButtonGroup = new QButtonGroup{this};
105 searchInButtonGroup->addButton(m_searchInFileNamesRadioButton);
106 searchInButtonGroup->addButton(m_searchInFileContentsRadioButton);
107
108 /// Add UI to switch between search tools.
109 // When we build without Baloo, there is only one search tool available, so we skip adding the UI to switch.
110 #if HAVE_BALOO
111 verticalMainLayout->addSpacing(Dolphin::VERTICAL_SPACER_HEIGHT);
112
113 auto searchUsingLabel = new QLabel{i18nc("@title:group", "Search using:"), containerWidget};
114 searchUsingLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
115 verticalMainLayout->addWidget(searchUsingLabel);
116
117 /// Initialize the Filenamesearch row.
118 m_filenamesearchRadioButton = new QRadioButton{filenamesearchUiName(), containerWidget};
119 connect(m_filenamesearchRadioButton, &QAbstractButton::clicked, this, [this]() {
120 if (m_searchConfiguration->searchTool() == SearchTool::Filenamesearch) {
121 return; // Already selected.
122 }
123 SearchSettings::setSearchTool(QStringLiteral("Filenamesearch"));
124 SearchSettings::self()->save();
125 DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
126 searchConfigurationCopy.setSearchTool(SearchTool::Filenamesearch);
127 Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
128 });
129
130 m_filenamesearchContextualHelpButton = new KContextualHelpButton(
131 xi18nc("@info about a search tool",
132 "<para>For searching in file contents <application>%1</application> attempts to use third-party search tools if they are available on this "
133 "system and are expected to lead to better or faster results. <application>ripgrep</application> and <application>ripgrep-all</application> "
134 "might improve your search experience if they are installed. <application>ripgrep-all</application> in particular enables searches in more "
135 "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 "
136 "configured by editing a script file. Copy it from <filename>%2</filename> to <filename>%3</filename> before modifying your copy. If any "
137 "issues arise, delete your copy <filename>%3</filename> to revert your changes.</para>",
138 filenamesearchUiName(),
139 QStringLiteral("%1/kio_filenamesearch/kio-filenamesearch-grep").arg(KDE_INSTALL_FULL_DATADIR),
140 QStringLiteral("%1/kio_filenamesearch/kio-filenamesearch-grep").arg(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation))),
141 m_filenamesearchRadioButton,
142 containerWidget);
143
144 auto filenamesearchRowLayout = new QHBoxLayout;
145 filenamesearchRowLayout->addWidget(m_filenamesearchRadioButton);
146 filenamesearchRowLayout->addWidget(m_filenamesearchContextualHelpButton);
147 filenamesearchRowLayout->addStretch(); // for left-alignment
148 verticalMainLayout->addLayout(filenamesearchRowLayout);
149
150 /// Initialize the Baloo row.
151 m_balooRadioButton = new QRadioButton{balooUiName(), containerWidget};
152 connect(m_balooRadioButton, &QAbstractButton::clicked, this, [this]() {
153 if (m_searchConfiguration->searchTool() == SearchTool::Baloo) {
154 return; // Already selected.
155 }
156 SearchSettings::setSearchTool(QStringLiteral("Baloo"));
157 SearchSettings::self()->save();
158 DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
159 searchConfigurationCopy.setSearchTool(SearchTool::Baloo);
160 Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
161 });
162
163 m_balooContextualHelpButton = new KContextualHelpButton(QString(), m_balooRadioButton, containerWidget);
164
165 auto balooSettingsButton = new QToolButton{containerWidget};
166 balooSettingsButton->setText(i18nc("@action:button %1 is software name", "Configure %1…", balooUiName()));
167 balooSettingsButton->setIcon(QIcon::fromTheme("configure"));
168 balooSettingsButton->setToolTip(balooSettingsButton->text());
169 balooSettingsButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
170 balooSettingsButton->setAutoRaise(true);
171 balooSettingsButton->setFixedHeight(m_balooRadioButton->sizeHint().height());
172 connect(balooSettingsButton, &QToolButton::clicked, this, [containerWidget] {
173 // Code taken from KCMLauncher::openSystemSettings() in the KCMUtil KDE framework.
174 constexpr auto systemSettings = "systemsettings";
175 KIO::CommandLauncherJob *openBalooSettingsJob;
176 // Open in System Settings if it's available
177 if (KService::serviceByDesktopName(systemSettings)) {
178 openBalooSettingsJob = new KIO::CommandLauncherJob(systemSettings, {"kcm_baloofile"}, containerWidget);
179 openBalooSettingsJob->setDesktopName(systemSettings);
180 } else {
181 openBalooSettingsJob = new KIO::CommandLauncherJob(QStringLiteral("kcmshell6"), {"kcm_baloofile"}, containerWidget);
182 }
183 openBalooSettingsJob->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, containerWidget));
184 openBalooSettingsJob->start();
185 });
186
187 auto balooRowLayout = new QHBoxLayout;
188 balooRowLayout->addWidget(m_balooRadioButton);
189 balooRowLayout->addWidget(m_balooContextualHelpButton);
190 balooRowLayout->addWidget(balooSettingsButton);
191 balooRowLayout->addStretch(); // for left-alignment
192 verticalMainLayout->addLayout(balooRowLayout);
193
194 auto searchUsingButtonGroup = new QButtonGroup{this};
195 searchUsingButtonGroup->addButton(m_filenamesearchRadioButton);
196 searchUsingButtonGroup->addButton(m_balooRadioButton);
197
198 verticalMainLayout->addSpacing(Dolphin::VERTICAL_SPACER_HEIGHT);
199
200 /// Add extra search filters like date, tags, rating, etc.
201 m_selectorsLayoutWidget = new QWidget{containerWidget};
202 if (m_searchConfiguration->searchTool() == SearchTool::Filenamesearch) {
203 m_selectorsLayoutWidget->hide();
204 }
205 auto selectorsLayout = new QGridLayout{m_selectorsLayoutWidget};
206 selectorsLayout->setContentsMargins(0, 0, 0, 0);
207 selectorsLayout->setSpacing(verticalMainLayout->spacing());
208
209 auto typeSelectorTitle = new QLabel{i18nc("@title:group for filtering files based on their type", "File Type:"), containerWidget};
210 typeSelectorTitle->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
211 selectorsLayout->addWidget(typeSelectorTitle, 1, 0);
212
213 m_typeSelector = new FileTypeSelector{m_searchConfiguration, containerWidget};
214 connect(m_typeSelector, &FileTypeSelector::configurationChanged, this, &Popup::configurationChanged);
215 selectorsLayout->addWidget(m_typeSelector, 2, 0);
216
217 auto dateSelectorTitle = new QLabel{i18nc("@title:group for filtering files by modified date", "Modified since:"), containerWidget};
218 dateSelectorTitle->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
219 selectorsLayout->addWidget(dateSelectorTitle, 1, 1);
220
221 m_dateSelector = new DateSelector{m_searchConfiguration, containerWidget};
222 m_dateSelector->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); // Make sure this button is as wide as the other button in this column.
223 connect(m_dateSelector, &DateSelector::configurationChanged, this, &Popup::configurationChanged);
224 selectorsLayout->addWidget(m_dateSelector, 2, 1);
225
226 auto ratingSelectorTitle = new QLabel{i18nc("@title:group for selecting a minimum rating of search results", "Rating:"), containerWidget};
227 ratingSelectorTitle->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
228 selectorsLayout->addWidget(ratingSelectorTitle, 3, 0);
229
230 m_ratingSelector = new MinimumRatingSelector{m_searchConfiguration, containerWidget};
231 connect(m_ratingSelector, &MinimumRatingSelector::configurationChanged, this, &Popup::configurationChanged);
232 selectorsLayout->addWidget(m_ratingSelector, 4, 0);
233
234 auto tagsSelectorTitle = new QLabel{i18nc("@title:group for selecting required tags for search results", "Tags:"), containerWidget};
235 tagsSelectorTitle->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
236 selectorsLayout->addWidget(tagsSelectorTitle, 3, 1);
237
238 m_tagsSelector = new TagsSelector{m_searchConfiguration, containerWidget};
239 m_tagsSelector->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); // Make sure this button is as wide as the other button in this column.
240 connect(m_tagsSelector, &TagsSelector::configurationChanged, this, &Popup::configurationChanged);
241 selectorsLayout->addWidget(m_tagsSelector, 4, 1);
242
243 verticalMainLayout->addWidget(m_selectorsLayoutWidget);
244 #endif // HAVE_BALOO
245
246 /**
247 * 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
248 * turns out to be insufficient.
249 */
250 verticalMainLayout->addSpacing(Dolphin::VERTICAL_SPACER_HEIGHT);
251
252 auto kfindLabel = new QLabel{i18nc("@label above 'Install KFind'/'Open KFind' button", "For more advanced searches:"), containerWidget};
253 kfindLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
254 verticalMainLayout->addWidget(kfindLabel);
255
256 m_kFindButton = new QToolButton{containerWidget};
257 m_kFindButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
258 connect(m_kFindButton, &QToolButton::clicked, this, &Popup::slotKFindButtonClicked);
259 verticalMainLayout->addWidget(m_kFindButton);
260
261 return containerWidget;
262 }
263
264 void Popup::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery)
265 {
266 m_searchInFileNamesRadioButton->setChecked(dolphinQuery->searchThrough() == SearchThrough::FileNames);
267 m_searchInFileContentsRadioButton->setChecked(dolphinQuery->searchThrough() == SearchThrough::FileContents);
268
269 // When we build without Baloo, there is only one search tool available and no UI to switch.
270 #if HAVE_BALOO
271 m_filenamesearchRadioButton->setChecked(dolphinQuery->searchTool() == SearchTool::Filenamesearch);
272 m_filenamesearchContextualHelpButton->setVisible(dolphinQuery->searchThrough() == SearchThrough::FileContents);
273
274 if (dolphinQuery->searchLocations() != SearchLocations::Everywhere && !isIndexingEnabledIn(dolphinQuery->searchPath())) {
275 m_balooRadioButton->setToolTip(
276 xi18nc("@info:tooltip",
277 "<para>Searching in <filename>%1</filename> using <application>%2</application> is currently not possible because "
278 "<application>%2</application> is configured to never create a search index of that location.</para>",
279 dolphinQuery->searchPath().adjusted(QUrl::RemoveUserInfo | QUrl::StripTrailingSlash).toString(QUrl::PreferLocalFile),
280 balooUiName()));
281 m_balooRadioButton->setDisabled(true);
282 } else if (dolphinQuery->searchThrough() == SearchThrough::FileContents && !isContentIndexingEnabled()) {
283 m_balooRadioButton->setToolTip(xi18nc("@info:tooltip",
284 "<para>Searching through file contents using <application>%1</application> is currently not possible because "
285 "<application>%1</application> is configured to never create a search index for file contents.</para>",
286 balooUiName()));
287 m_balooRadioButton->setDisabled(true);
288 } else {
289 m_balooRadioButton->setToolTip(QString());
290 m_balooRadioButton->setEnabled(true);
291 }
292 m_balooContextualHelpButton->setContextualHelpText(
293 i18nc("@info make a warning paragraph bold before other paragraphs", "<b>%1</b>", m_balooRadioButton->toolTip())
294 + xi18nc(
295 "@info about a search tool",
296 "<para><application>%1</application> uses a database for searching. The database is created by indexing your files in the background based on "
297 "how <application>%1</application> is configured.<list><item><application>%1</application> provides results extremely "
298 "quickly.</item><item>Allows searching for file types, dates, tags, etc.</item><item>Only searches in indexed folders. Configure which folders "
299 "should be indexed in <application>System Settings</application>.</item><item>When the searched locations contain links to other files or "
300 "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 "
301 "searched depending on how <application>%1</application> is configured.</item></list></para>",
302 balooUiName()));
303
304 m_balooRadioButton->setChecked(dolphinQuery->searchTool() == SearchTool::Baloo);
305 m_balooRadioButton->setChecked(false);
306
307 if (m_balooRadioButton->isChecked()) {
308 m_searchInFileContentsRadioButton->setText(i18nc("@option:radio Search in:", "File names and contents"));
309 m_typeSelector->updateStateToMatch(dolphinQuery);
310 m_dateSelector->updateStateToMatch(dolphinQuery);
311 m_ratingSelector->updateStateToMatch(dolphinQuery);
312 m_tagsSelector->updateStateToMatch(dolphinQuery);
313 } else {
314 #endif // HAVE_BALOO
315 m_searchInFileContentsRadioButton->setText(i18nc("@option:radio Search in:", "File contents"));
316 #if HAVE_BALOO
317 }
318
319 /// Show/Hide Baloo-specific selectors.
320 m_selectorsLayoutWidget->setVisible(m_balooRadioButton->isChecked());
321 const int columnWidth = std::max(
322 {m_typeSelector->sizeHint().width(), m_dateSelector->sizeHint().width(), m_ratingSelector->sizeHint().width(), m_tagsSelector->sizeHint().width()});
323 static_cast<QGridLayout *>(m_selectorsLayoutWidget->layout())->setColumnMinimumWidth(0, columnWidth);
324 static_cast<QGridLayout *>(m_selectorsLayoutWidget->layout())->setColumnMinimumWidth(1, columnWidth);
325 resizeToFitContents();
326 #endif // HAVE_BALOO
327
328 KService::Ptr kFind = KService::serviceByDesktopName(kFindDesktopName);
329 if (kFind) {
330 m_kFindButton->setText(i18nc("@action:button 1 is KFind app name", "Open %1", kFind->name()));
331 m_kFindButton->setIcon(QIcon::fromTheme(kFind->icon()));
332 } else {
333 m_kFindButton->setText(i18nc("@action:button", "Install KFind…"));
334 m_kFindButton->setIcon(QIcon::fromTheme(QStringLiteral("kfind"), QIcon::fromTheme(QStringLiteral("install"))));
335 }
336 }
337
338 void Popup::slotKFindButtonClicked()
339 {
340 /// Open KFind if it is installed.
341 KService::Ptr kFind = KService::serviceByDesktopName(kFindDesktopName);
342 if (kFind) {
343 auto *job = new KIO::ApplicationLauncherJob(kFind);
344 job->setUrls({m_searchConfiguration->searchPath()});
345 job->start();
346 return;
347 }
348
349 /// Otherwise, install KFind.
350 #ifdef Q_OS_WIN
351 QDesktopServices::openUrl(QUrl("https://apps.kde.org/kfind"));
352 #else
353 auto packageInstaller = new DolphinPackageInstaller(
354 KFIND_PACKAGE_NAME,
355 QUrl("appstream://org.kde.kfind.desktop"),
356 []() {
357 return KService::serviceByDesktopName(kFindDesktopName);
358 },
359 this);
360 connect(packageInstaller, &KJob::result, this, [this](KJob *job) {
361 Q_EMIT showInstallationProgress(QString(), 100); // Hides the progress information in the status bar.
362 if (job->error()) {
363 Q_EMIT showMessage(job->errorString(), KMessageWidget::Error);
364 } else {
365 Q_EMIT showMessage(xi18nc("@info", "<application>KFind</application> installed successfully."), KMessageWidget::Positive);
366 updateStateToMatch(m_searchConfiguration); // Updates m_kfindButton from an "Install KFind…" to an "Open KFind" button.
367 }
368 });
369 const auto installationTaskText{i18nc("@info:status", "Installing KFind")};
370 Q_EMIT showInstallationProgress(installationTaskText, -1);
371 connect(packageInstaller, &KJob::percentChanged, this, [this, installationTaskText](KJob * /* job */, long unsigned int percent) {
372 if (percent < 100) { // Ignore some weird reported values.
373 Q_EMIT showInstallationProgress(installationTaskText, percent);
374 }
375 });
376 packageInstaller->start();
377 #endif
378 }