2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
5 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
10 #include "config-dolphin.h"
11 #include "dolphinpackageinstaller.h"
12 #include "dolphinquery.h"
14 #include "selectors/dateselector.h"
15 #include "selectors/filetypeselector.h"
16 #include "selectors/minimumratingselector.h"
17 #include "selectors/tagsselector.h"
19 #include <KContextualHelpButton>
20 #include <KDialogJobUiDelegate>
21 #include <KIO/ApplicationLauncherJob>
22 #include <KIO/CommandLauncherJob>
23 #include <KLocalizedString>
26 #include <QButtonGroup>
28 #include <QDesktopServices>
30 #include <QHBoxLayout>
33 #include <QRadioButton>
34 #include <QStandardPaths>
35 #include <QToolButton>
36 #include <QVBoxLayout>
40 constexpr auto kFindDesktopName
= "org.kde.kfind";
43 using namespace Search
;
45 QString
Search::filenamesearchUiName()
47 // i18n: Localized name for the Filenamesearch search tool for use in user interfaces.
48 return i18n("Simple search");
51 QString
Search::balooUiName()
53 // i18n: Localized name for the Baloo search tool for use in user interfaces.
54 return i18n("File Indexing");
57 Popup::Popup(std::shared_ptr
<const DolphinQuery
> dolphinQuery
, QWidget
*parent
)
59 , UpdatableStateInterface
{dolphinQuery
}
63 QWidget
*Popup::init()
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.
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
);
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.
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
));
89 verticalMainLayout
->addWidget(m_searchInFileNamesRadioButton
);
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.
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
));
102 verticalMainLayout
->addWidget(m_searchInFileContentsRadioButton
);
104 auto searchInButtonGroup
= new QButtonGroup
{this};
105 searchInButtonGroup
->addButton(m_searchInFileNamesRadioButton
);
106 searchInButtonGroup
->addButton(m_searchInFileContentsRadioButton
);
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.
111 verticalMainLayout
->addSpacing(Dolphin::VERTICAL_SPACER_HEIGHT
);
113 auto searchUsingLabel
= new QLabel
{i18nc("@title:group", "Search using:"), containerWidget
};
114 searchUsingLabel
->setTextInteractionFlags(Qt::TextSelectableByMouse
| Qt::TextSelectableByKeyboard
| Qt::LinksAccessibleByKeyboard
);
115 verticalMainLayout
->addWidget(searchUsingLabel
);
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.
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
));
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
,
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
);
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.
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
));
163 m_balooContextualHelpButton
= new KContextualHelpButton(QString(), m_balooRadioButton
, containerWidget
);
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
);
181 openBalooSettingsJob
= new KIO::CommandLauncherJob(QStringLiteral("kcmshell6"), {"kcm_baloofile"}, containerWidget
);
183 openBalooSettingsJob
->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled
, containerWidget
));
184 openBalooSettingsJob
->start();
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
);
194 auto searchUsingButtonGroup
= new QButtonGroup
{this};
195 searchUsingButtonGroup
->addButton(m_filenamesearchRadioButton
);
196 searchUsingButtonGroup
->addButton(m_balooRadioButton
);
198 verticalMainLayout
->addSpacing(Dolphin::VERTICAL_SPACER_HEIGHT
);
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();
205 auto selectorsLayout
= new QGridLayout
{m_selectorsLayoutWidget
};
206 selectorsLayout
->setContentsMargins(0, 0, 0, 0);
207 selectorsLayout
->setSpacing(verticalMainLayout
->spacing());
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);
213 m_typeSelector
= new FileTypeSelector
{m_searchConfiguration
, containerWidget
};
214 connect(m_typeSelector
, &FileTypeSelector::configurationChanged
, this, &Popup::configurationChanged
);
215 selectorsLayout
->addWidget(m_typeSelector
, 2, 0);
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);
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);
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);
230 m_ratingSelector
= new MinimumRatingSelector
{m_searchConfiguration
, containerWidget
};
231 connect(m_ratingSelector
, &MinimumRatingSelector::configurationChanged
, this, &Popup::configurationChanged
);
232 selectorsLayout
->addWidget(m_ratingSelector
, 4, 0);
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);
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);
243 verticalMainLayout
->addWidget(m_selectorsLayoutWidget
);
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.
250 verticalMainLayout
->addSpacing(Dolphin::VERTICAL_SPACER_HEIGHT
);
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
);
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
);
261 return containerWidget
;
264 void Popup::updateState(const std::shared_ptr
<const DolphinQuery
> &dolphinQuery
)
266 m_searchInFileNamesRadioButton
->setChecked(dolphinQuery
->searchThrough() == SearchThrough::FileNames
);
267 m_searchInFileContentsRadioButton
->setChecked(dolphinQuery
->searchThrough() == SearchThrough::FileContents
);
269 // When we build without Baloo, there is only one search tool available and no UI to switch.
271 m_filenamesearchRadioButton
->setChecked(dolphinQuery
->searchTool() == SearchTool::Filenamesearch
);
272 m_filenamesearchContextualHelpButton
->setVisible(dolphinQuery
->searchThrough() == SearchThrough::FileContents
);
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
),
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>",
287 m_balooRadioButton
->setDisabled(true);
289 m_balooRadioButton
->setToolTip(QString());
290 m_balooRadioButton
->setEnabled(true);
292 m_balooContextualHelpButton
->setContextualHelpText(
293 i18nc("@info make a warning paragraph bold before other paragraphs", "<b>%1</b>", m_balooRadioButton
->toolTip())
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>",
304 m_balooRadioButton
->setChecked(dolphinQuery
->searchTool() == SearchTool::Baloo
);
305 m_balooRadioButton
->setChecked(false);
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
);
315 m_searchInFileContentsRadioButton
->setText(i18nc("@option:radio Search in:", "File contents"));
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();
328 KService::Ptr kFind
= KService::serviceByDesktopName(kFindDesktopName
);
330 m_kFindButton
->setText(i18nc("@action:button 1 is KFind app name", "Open %1", kFind
->name()));
331 m_kFindButton
->setIcon(QIcon::fromTheme(kFind
->icon()));
333 m_kFindButton
->setText(i18nc("@action:button", "Install KFind…"));
334 m_kFindButton
->setIcon(QIcon::fromTheme(QStringLiteral("kfind"), QIcon::fromTheme(QStringLiteral("install"))));
338 void Popup::slotKFindButtonClicked()
340 /// Open KFind if it is installed.
341 KService::Ptr kFind
= KService::serviceByDesktopName(kFindDesktopName
);
343 auto *job
= new KIO::ApplicationLauncherJob(kFind
);
344 job
->setUrls({m_searchConfiguration
->searchPath()});
349 /// Otherwise, install KFind.
351 QDesktopServices::openUrl(QUrl("https://apps.kde.org/kfind"));
353 auto packageInstaller
= new DolphinPackageInstaller(
355 QUrl("appstream://org.kde.kfind.desktop"),
357 return KService::serviceByDesktopName(kFindDesktopName
);
360 connect(packageInstaller
, &KJob::result
, this, [this](KJob
*job
) {
361 Q_EMIT
showInstallationProgress(QString(), 100); // Hides the progress information in the status bar.
363 Q_EMIT
showMessage(job
->errorString(), KMessageWidget::Error
);
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.
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
);
376 packageInstaller
->start();