Plasma-workspace

tasktools.cpp
1/*
2 SPDX-FileCopyrightText: 2016 Eike Hein <hein@kde.org>
3 SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@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 "tasktools.h"
9#include "abstracttasksmodel.h"
10
11#include <ranges>
12
13#include <KApplicationTrader>
14#include <KConfigGroup>
15#include <KDesktopFile>
16#include <KFileItem>
17#include <KNotificationJobUiDelegate>
18#include <KProcessList>
19#include <KWindowSystem>
20#include <PlasmaActivities/ResourceInstance>
21#include <kemailsettings.h>
22
23#include <KIO/ApplicationLauncherJob>
24#include <KIO/OpenUrlJob>
25
26#include <QDir>
27#include <QGuiApplication>
28#include <QRegularExpression>
29#include <QScreen>
30#include <QUrlQuery>
31#include <qnamespace.h>
32
33#include <defaultservice.h>
34
35using namespace Qt::StringLiterals;
36
37namespace TaskManager
38{
39AppData appDataFromUrl(const QUrl &url, const QIcon &fallbackIcon)
40{
41 AppData data;
42 data.url = url;
43
44 if (url.hasQuery()) {
45 QUrlQuery uQuery(url);
46
47 if (uQuery.hasQueryItem(QLatin1String("iconData"))) {
48 QString iconData(uQuery.queryItemValue(QLatin1String("iconData")));
49 QPixmap pixmap;
51 pixmap.loadFromData(bytes);
52 data.icon.addPixmap(pixmap);
53 }
54
55 if (uQuery.hasQueryItem(QLatin1String("skipTaskbar"))) {
56 QString skipTaskbar(uQuery.queryItemValue(QLatin1String("skipTaskbar")));
57 data.skipTaskbar = (skipTaskbar == QLatin1String("true"));
58 }
59 }
60
61 // applications: URLs are used to refer to applications by their KService::menuId
62 // (i.e. .desktop file name) rather than the absolute path to a .desktop file.
63 if (url.scheme() == QLatin1String("applications")) {
64 const KService::Ptr service = KService::serviceByMenuId(url.path());
65
66 if (service && url.path() == service->menuId()) {
67 data.name = service->name();
68 data.genericName = service->genericName();
69 data.id = service->storageId();
70
71 if (data.icon.isNull()) {
72 data.icon = QIcon::fromTheme(service->icon());
73 }
74 }
75 }
76
77 if (url.isLocalFile()) {
80
81 // Resolve to non-absolute menuId-based URL if possible.
82 if (service) {
83 const QString &menuId = service->menuId();
84
85 if (!menuId.isEmpty()) {
86 data.url = QUrl(QLatin1String("applications:") + menuId);
87 }
88 }
89
90 if (service && QUrl::fromLocalFile(service->entryPath()) == url) {
91 data.name = service->name();
92 data.genericName = service->genericName();
93 data.id = service->storageId();
94
95 if (data.icon.isNull()) {
96 data.icon = QIcon::fromTheme(service->icon());
97 }
98 } else {
100 if (f.tryExec()) {
101 data.name = f.readName();
102 data.genericName = f.readGenericName();
103 data.id = QUrl::fromLocalFile(f.fileName()).fileName();
104
105 if (data.icon.isNull()) {
106 const QString iconValue = f.readIcon();
107 if (QIcon::hasThemeIcon(iconValue)) {
108 data.icon = QIcon::fromTheme(iconValue);
109 } else if (!iconValue.startsWith(QDir::separator())) {
110 const int lastIndexOfPeriod = iconValue.lastIndexOf(QLatin1Char('.'));
111 const QString iconValueWithoutSuffix = lastIndexOfPeriod < 0 ? iconValue : iconValue.left(lastIndexOfPeriod);
112 // Find an icon in the same folder
113 const QDir sameDir = QFileInfo(url.toLocalFile()).absoluteDir();
114 const auto iconList = sameDir.entryInfoList(
115 {
116 QStringLiteral("*.png").arg(iconValueWithoutSuffix),
117 QStringLiteral("*.svg").arg(iconValueWithoutSuffix),
118 },
120 if (!iconList.empty()) {
121 data.icon = QIcon(iconList[0].absoluteFilePath());
122 }
123 } else {
124 data.icon = QIcon(iconValue);
125 }
126 }
127 }
128 }
129
130 if (data.id.endsWith(u".desktop")) {
131 data.id = data.id.left(data.id.length() - 8);
132 }
133 } else {
134 data.id = url.fileName();
135 }
136
137 } else if (url.scheme() == QLatin1String("preferred")) {
138 data.id = defaultApplication(url);
139
140 const KService::Ptr service = KService::serviceByStorageId(data.id);
141
142 if (service) {
143 const QString &menuId = service->menuId();
144 const QString &desktopFile = service->entryPath();
145
146 data.name = service->name();
147 data.genericName = service->genericName();
148 data.id = service->storageId();
149
150 if (data.icon.isNull()) {
151 data.icon = QIcon::fromTheme(service->icon());
152 }
153
154 // Update with resolved URL.
155 if (!menuId.isEmpty()) {
156 data.url = QUrl(QLatin1String("applications:") + menuId);
157 } else {
158 data.url = QUrl::fromLocalFile(desktopFile);
159 }
160 }
161 }
162
163 if (data.name.isEmpty()) {
164 data.name = url.fileName();
165 }
166
167 if (data.icon.isNull()) {
168 data.icon = fallbackIcon;
169 }
170
171 return data;
172}
173
174QUrl windowUrlFromMetadata(const QString &appId, quint32 pid, const KSharedConfig::Ptr &rulesConfig, const QString &xWindowsWMClassName)
175{
176 static_assert(!std::is_trivially_copy_assignable_v<KSharedConfig::Ptr>);
177 if (!rulesConfig) {
178 return QUrl();
179 }
180
181 QUrl url;
182 KService::List services;
183 bool triedPid = false;
184
185 // The code below this function goes on a hunt for services based on the metadata
186 // that has been passed in. Occasionally, it will find more than one matching
187 // service. In some scenarios (e.g. multiple identically-named .desktop files)
188 // there's a need to pick the most useful one. The function below promises to "sort"
189 // a list of services by how closely their KService::menuId() relates to the key that
190 // has been passed in. The current naive implementation simply looks for a menuId
191 // that starts with the key, prepends it to the list and returns it. In practice,
192 // that means a KService with a menuId matching the appId will win over one with a
193 // menuId that encodes a subfolder hierarchy.
194 // A concrete example: Valve's Steam client is sometimes installed two times, once
195 // natively as a Linux application, once via Wine. Both have .desktop files named
196 // (S|)steam.desktop. The Linux native version is located in the menu by means of
197 // categorization ("Games") and just has a menuId() matching the .desktop file name,
198 // but the Wine version is placed in a folder hierarchy by Wine and gets a menuId()
199 // of wine-Programs-Steam-Steam.desktop. The weighing done by this function makes
200 // sure the Linux native version gets mapped to the former, while other heuristics
201 // map the Wine version reliably to the latter.
202 // In lieu of this weighing we just used whatever KApplicationTrader returned first,
203 // so what we do here can be no worse.
204 auto sortServicesByMenuId = [](KService::List &services, const QString &key) {
205 if (services.count() == 1) {
206 return;
207 }
208
209 for (const auto &service : services) {
210 if (service->menuId().startsWith(key, Qt::CaseInsensitive)) {
211 services.prepend(service);
212 return;
213 }
214 }
215 };
216
217 if (!(appId.isEmpty() && xWindowsWMClassName.isEmpty())) {
218 // Check to see if this wmClass matched a saved one ...
219 KConfigGroup grp(rulesConfig, u"Mapping"_s);
220 KConfigGroup set(rulesConfig, u"Settings"_s);
221
222 // Evaluate MatchCommandLineFirst directives from config first.
223 // Some apps have different launchers depending upon command line ...
224 QStringList matchCommandLineFirst = set.readEntry("MatchCommandLineFirst", QStringList());
225
226 if (!appId.isEmpty() && matchCommandLineFirst.contains(appId)) {
227 triedPid = true;
228 services = servicesFromPid(pid, rulesConfig);
229 }
230
231 // Try to match using xWindowsWMClassName also.
232 if (!xWindowsWMClassName.isEmpty() && matchCommandLineFirst.contains(u"::" + xWindowsWMClassName)) {
233 triedPid = true;
234 services = servicesFromPid(pid, rulesConfig);
235 }
236
237 if (!appId.isEmpty()) {
238 // Evaluate any mapping rules that map to a specific .desktop file.
239 QString mapped(grp.readEntry(appId + u"::" + xWindowsWMClassName, QString()));
240
241 if (mapped.endsWith(QLatin1String(".desktop"))) {
242 url = QUrl(mapped);
243 return url;
244 }
245
246 if (mapped.isEmpty()) {
247 mapped = grp.readEntry(appId, QString());
248
249 if (mapped.endsWith(QLatin1String(".desktop"))) {
250 url = QUrl(mapped);
251 return url;
252 }
253 }
254
255 // Some apps, such as Wine, cannot use xWindowsWMClassName to map to launcher name - as Wine itself is not a GUI app
256 // So, Settings/ManualOnly lists window classes where the user will always have to manualy set the launcher ...
257 QStringList manualOnly = set.readEntry("ManualOnly", QStringList());
258
259 if (!appId.isEmpty() && manualOnly.contains(appId)) {
260 return url;
261 }
262
263 // Try matching both appId and xWindowsWMClassName against StartupWMClass.
264 // We do this before evaluating the mapping rules further, because StartupWMClass
265 // is essentially a mapping rule, and we expect it to be set deliberately and
266 // sensibly to instruct us what to do. Also, mapping rules
267 //
268 // StartupWMClass=STRING
269 //
270 // If true, it is KNOWN that the application will map at least one
271 // window with the given string as its WM class or WM name hint.
272 //
273 // Source: https://specifications.freedesktop.org/startup-notification-spec/startup-notification-0.1.txt
274 if (services.isEmpty() && !xWindowsWMClassName.isEmpty()) {
275 services = KApplicationTrader::query([&xWindowsWMClassName](const KService::Ptr &service) {
276 return service->property<QString>(QStringLiteral("StartupWMClass")).compare(xWindowsWMClassName, Qt::CaseInsensitive) == 0;
277 });
278 sortServicesByMenuId(services, xWindowsWMClassName);
279 }
280
281 if (services.isEmpty()) {
282 services = KApplicationTrader::query([&appId](const KService::Ptr &service) {
283 return service->property<QString>(QStringLiteral("StartupWMClass")).compare(appId, Qt::CaseInsensitive) == 0;
284 });
285 sortServicesByMenuId(services, appId);
286 }
287
288 // Evaluate rewrite rules from config.
289 if (services.isEmpty()) {
290 KConfigGroup rewriteRulesGroup(rulesConfig, QStringLiteral("Rewrite Rules"));
291 if (rewriteRulesGroup.hasGroup(appId)) {
292 KConfigGroup rewriteGroup(&rewriteRulesGroup, appId);
293
294 const QStringList &rules = rewriteGroup.groupList();
295 for (const QString &rule : rules) {
296 KConfigGroup ruleGroup(&rewriteGroup, rule);
297
298 const QString propertyConfig = ruleGroup.readEntry(QStringLiteral("Property"), QString());
299
300 QString matchProperty;
301 if (propertyConfig == QLatin1String("ClassClass")) {
302 matchProperty = appId;
303 } else if (propertyConfig == QLatin1String("ClassName")) {
304 matchProperty = xWindowsWMClassName;
305 }
306
307 if (matchProperty.isEmpty()) {
308 continue;
309 }
310
311 const QString serviceSearchIdentifier = ruleGroup.readEntry(QStringLiteral("Identifier"), QString());
312 if (serviceSearchIdentifier.isEmpty()) {
313 continue;
314 }
315
316 QRegularExpression regExp(ruleGroup.readEntry(QStringLiteral("Match")));
317 const auto match = regExp.match(matchProperty);
318
319 if (match.hasMatch()) {
320 const QString actualMatch = match.captured(QStringLiteral("match"));
321 if (actualMatch.isEmpty()) {
322 continue;
323 }
324
325 QString rewrittenString = ruleGroup.readEntry(QStringLiteral("Target")).arg(actualMatch);
326 // If no "Target" is provided, instead assume the matched property (appId/xWindowsWMClassName).
327 if (rewrittenString.isEmpty()) {
328 rewrittenString = matchProperty;
329 }
330
331 services = KApplicationTrader::query([&rewrittenString, &serviceSearchIdentifier](const KService::Ptr &service) {
332 return service->property<QString>(serviceSearchIdentifier).compare(rewrittenString, Qt::CaseInsensitive) == 0;
333 });
334 sortServicesByMenuId(services, serviceSearchIdentifier);
335
336 if (!services.isEmpty()) {
337 break;
338 }
339 }
340 }
341 }
342 }
343
344 // The appId looks like a path.
345 if (services.isEmpty() && appId.startsWith(QLatin1String("/"))) {
346 // Check if it's a path to a .desktop file.
347 if (KDesktopFile::isDesktopFile(appId) && QFile::exists(appId)) {
348 return QUrl::fromLocalFile(appId);
349 }
350
351 // Check if the appId passes as a .desktop file path if we add the extension.
352 const QString appIdPlusExtension(appId + QStringLiteral(".desktop"));
353
354 if (KDesktopFile::isDesktopFile(appIdPlusExtension) && QFile::exists(appIdPlusExtension)) {
355 return QUrl::fromLocalFile(appIdPlusExtension);
356 }
357 }
358
359 // Try matching mapped name against DesktopEntryName.
360 if (!mapped.isEmpty() && services.isEmpty()) {
361 services = KApplicationTrader::query([&mapped](const KService::Ptr &service) {
362 return !service->noDisplay() && service->desktopEntryName().compare(mapped, Qt::CaseInsensitive) == 0;
363 });
364 sortServicesByMenuId(services, mapped);
365 }
366
367 // Try matching mapped name against 'Name'.
368 if (!mapped.isEmpty() && services.isEmpty()) {
369 services = KApplicationTrader::query([&mapped](const KService::Ptr &service) {
370 return !service->noDisplay() && service->name().compare(mapped, Qt::CaseInsensitive) == 0;
371 });
372 sortServicesByMenuId(services, mapped);
373 }
374
375 // Try matching appId against DesktopEntryName.
376 if (services.isEmpty()) {
377 services = KApplicationTrader::query([&appId](const KService::Ptr &service) {
378 return service->desktopEntryName().compare(appId, Qt::CaseInsensitive) == 0;
379 });
380 sortServicesByMenuId(services, appId);
381 }
382
383 // Try matching appId against 'Name'.
384 // This has a shaky chance of success as appId is untranslated, but 'Name' may be localized.
385 if (services.isEmpty()) {
386 services = KApplicationTrader::query([&appId](const KService::Ptr &service) {
387 return !service->noDisplay() && service->name().compare(appId, Qt::CaseInsensitive) == 0;
388 });
389 sortServicesByMenuId(services, appId);
390 }
391
392 // Check rules configuration for whether we want to hide this task.
393 // Some window tasks update from bogus to useful metadata early during startup.
394 // This config key allows listing the bogus metadata, and the matching window
395 // tasks are hidden until they perform a metadate update that stops them from
396 // matching.
397 QStringList skipTaskbar = set.readEntry("SkipTaskbar", QStringList());
398
399 if (skipTaskbar.contains(appId)) {
400 QUrlQuery query(url);
401 query.addQueryItem(QStringLiteral("skipTaskbar"), QStringLiteral("true"));
402 url.setQuery(query);
403 } else if (skipTaskbar.contains(mapped)) {
404 QUrlQuery query(url);
405 query.addQueryItem(QStringLiteral("skipTaskbar"), QStringLiteral("true"));
406 url.setQuery(query);
407 }
408 }
409
410 // Ok, absolute *last* chance, try matching via pid (but only if we have not already tried this!) ...
411 if (services.isEmpty() && !triedPid) {
412 services = servicesFromPid(pid, rulesConfig);
413 }
414 }
415
416 // Try to improve on a possible from-binary fallback.
417 // If no services were found or we got a fake-service back from getServicesViaPid()
418 // we attempt to improve on this by adding a loosely matched reverse-domain-name
419 // DesktopEntryName. Namely anything that is '*.appId.desktop' would qualify here.
420 //
421 // Illustrative example of a case where the above heuristics would fail to produce
422 // a reasonable result:
423 // - org.kde.dragonplayer.desktop
424 // - binary is 'dragon'
425 // - qapp appname and thus appId is 'dragonplayer'
426 // - appId cannot directly match the desktop file because of RDN
427 // - appId also cannot match the binary because of name mismatch
428 // - in the following code *.appId can match org.kde.dragonplayer though
429 if (!appId.isEmpty() /* BUG 472576 */ && (services.isEmpty() || services.at(0)->desktopEntryName().isEmpty())) {
430 auto matchingServices = KApplicationTrader::query([&appId](const KService::Ptr &service) {
431 return !service->noDisplay() && service->desktopEntryName().contains(appId, Qt::CaseInsensitive);
432 });
433
434 QMutableListIterator<KService::Ptr> it(matchingServices);
435 while (it.hasNext()) {
436 auto service = it.next();
437 if (!service->desktopEntryName().endsWith(u'.' + appId)) {
438 it.remove();
439 }
440 }
441 // Exactly one match is expected, otherwise we discard the results as to reduce
442 // the likelihood of false-positive mappings. Since we essentially eliminate the
443 // uniqueness that RDN is meant to bring to the table we could potentially end
444 // up with more than one match here.
445 if (matchingServices.length() == 1) {
446 services = matchingServices;
447 }
448 }
449
450 if (!services.isEmpty()) {
451 const QString &menuId = services.at(0)->menuId();
452
453 // applications: URLs are used to refer to applications by their KService::menuId
454 // (i.e. .desktop file name) rather than the absolute path to a .desktop file.
455 if (!menuId.isEmpty()) {
456 url.setUrl(QString(u"applications:" + menuId));
457 return url;
458 }
459
460 QString path = services.at(0)->entryPath();
461
462 if (path.isEmpty()) {
463 path = services.at(0)->exec();
464 }
465
466 if (!path.isEmpty()) {
467 QString query = url.query();
468 url = QUrl::fromLocalFile(path);
469 url.setQuery(query);
470 return url;
471 }
472 }
473
474 return url;
475}
476
477KService::List servicesFromPid(quint32 pid, const KSharedConfig::Ptr &rulesConfig)
478{
479 if (pid == 0) {
480 return KService::List();
481 }
482
483 if (!rulesConfig) {
484 return KService::List();
485 }
486
487 // Read the BAMF_DESKTOP_FILE_HINT environment variable which contains the actual desktop file path for Snaps.
488 QFile environFile(QStringLiteral("/proc/%1/environ").arg(QString::number(pid)));
489 if (environFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
490 constexpr QByteArrayView bamfDesktopFileHint{"BAMF_DESKTOP_FILE_HINT"};
491 constexpr QByteArrayView appDir{"APPDIR"};
492 const QByteArray environ = environFile.readAll();
493#if (defined(__GNUC__) && __GNUC__ >= 12) || !defined(__GNUC__)
494 for (const QByteArrayView line : QByteArrayView(environ) | std::views::split('\0')) {
495#else
496 for (const QByteArrayView line : environ.split('\0')) {
497#endif
498 const auto equalsIdx = line.indexOf('=');
499 if (equalsIdx == -1) {
500 continue;
501 }
502
503 const QByteArrayView key = line.sliced(0, equalsIdx);
504 if (key == bamfDesktopFileHint) {
505 KService::Ptr service = KService::serviceByDesktopPath(QString::fromUtf8(line.sliced(equalsIdx + 1)));
506 if (service) {
507 return {service};
508 }
509 break;
510 } else if (key == appDir) {
511 // For AppImage
512 const QByteArrayView value = line.sliced(equalsIdx + 1);
513 const auto desktopFileList = QDir(QString::fromUtf8(value)).entryInfoList(QStringList{QStringLiteral("*.desktop")}, QDir::Files);
514 if (!desktopFileList.empty()) {
515 return {QExplicitlySharedDataPointer<KService>(new KService(desktopFileList[0].absoluteFilePath()))};
516 }
517 break;
518 }
519 }
520 }
521
522 auto proc = KProcessList::processInfo(pid);
523 if (!proc.isValid()) {
524 return KService::List();
525 }
526
527 const QString cmdLine = proc.command();
528
529 if (cmdLine.isEmpty()) {
530 return KService::List();
531 }
532
533 return servicesFromCmdLine(cmdLine, proc.name(), rulesConfig);
534}
535
536KService::List servicesFromCmdLine(const QString &_cmdLine, const QString &processName, const KSharedConfig::Ptr &rulesConfig)
537{
538 QString cmdLine = _cmdLine;
539 KService::List services;
540
541 if (!rulesConfig) {
542 return services;
543 }
544
545 const int firstSpace = cmdLine.indexOf(u' ');
546 int slash = 0;
547
548 services = KApplicationTrader::query([&cmdLine](const KService::Ptr &service) {
549 return service->exec() == cmdLine;
550 });
551
552 if (services.isEmpty()) {
553 // Could not find with complete command line, so strip out the path part ...
554 slash = cmdLine.lastIndexOf(u'/', firstSpace);
555
556 if (slash > 0) {
557 const QStringView midCmd = QStringView(cmdLine).mid(slash + 1);
558 services = services = KApplicationTrader::query([&midCmd](const KService::Ptr &service) {
559 return service->exec() == midCmd;
560 });
561 }
562 }
563
564 if (services.isEmpty() && firstSpace > 0) {
565 // Could not find with arguments, so try without ...
566 cmdLine.truncate(firstSpace);
567
568 services = KApplicationTrader::query([&cmdLine](const KService::Ptr &service) {
569 return service->exec() == cmdLine;
570 });
571
572 if (services.isEmpty()) {
573 slash = cmdLine.lastIndexOf(u'/');
574
575 if (slash > 0) {
576 const QStringView midCmd = QStringView(cmdLine).mid(slash + 1);
577 services = KApplicationTrader::query([&midCmd](const KService::Ptr &service) {
578 return service->exec() == midCmd;
579 });
580 }
581 }
582 }
583
584 if (services.isEmpty()) {
585 KConfigGroup set(rulesConfig, u"Settings"_s);
586 const QStringList &runtimes = set.readEntry("TryIgnoreRuntimes", QStringList());
587
588 bool ignore = runtimes.contains(cmdLine);
589
590 if (!ignore && slash > 0) {
591 ignore = runtimes.contains(cmdLine.mid(slash + 1));
592 }
593
594 if (ignore) {
595 return servicesFromCmdLine(_cmdLine.mid(firstSpace + 1), processName, rulesConfig);
596 }
597 }
598
599 if (services.isEmpty() && !processName.isEmpty() && !QStandardPaths::findExecutable(cmdLine).isEmpty()) {
600 // cmdLine now exists without arguments if there were any.
601 services << QExplicitlySharedDataPointer<KService>(new KService(processName, cmdLine, QString()));
602 }
603
604 return services;
605}
606
607QString defaultApplication(const QUrl &url)
608{
609 if (url.scheme() != QLatin1String("preferred")) {
610 return QString();
611 }
612
613 const QString &application = url.host();
614
615 if (application.isEmpty()) {
616 return QString();
617 }
618
619 if (application.compare(QLatin1String("mailer"), Qt::CaseInsensitive) == 0) {
620 KEMailSettings settings;
621
622 // In KToolInvocation, the default is kmail; but let's be friendlier.
623 QString command = settings.getSetting(KEMailSettings::ClientProgram);
624
625 if (command.isEmpty()) {
626 if (KService::Ptr kontact = KService::serviceByStorageId(QStringLiteral("kontact"))) {
627 return kontact->storageId();
628 } else if (KService::Ptr kmail = KService::serviceByStorageId(QStringLiteral("kmail"))) {
629 return kmail->storageId();
630 }
631 }
632
633 if (!command.isEmpty()) {
634 if (settings.getSetting(KEMailSettings::ClientTerminal) == QLatin1String("true")) {
635 KConfigGroup confGroup(KSharedConfig::openConfig(), u"General"_s);
636 const QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole"));
637 command = preferredTerminal + QLatin1String(" -e ") + command;
638 }
639
640 return command;
641 }
642 } else if (application.compare(QLatin1String("browser"), Qt::CaseInsensitive) == 0) {
643 const auto service = DefaultService::browser();
644 return service ? service->storageId() : DefaultService::legacyBrowserExec();
645 } else if (application.compare(QLatin1String("terminal"), Qt::CaseInsensitive) == 0) {
646 KConfigGroup confGroup(KSharedConfig::openConfig(), u"General"_s);
647
648 return confGroup.readPathEntry("TerminalApplication", KService::serviceByStorageId(QStringLiteral("konsole")) ? QStringLiteral("konsole") : QString());
649 } else if (application.compare(QLatin1String("filemanager"), Qt::CaseInsensitive) == 0) {
650 KService::Ptr service = KApplicationTrader::preferredService(QStringLiteral("inode/directory"));
651
652 if (service) {
653 return service->storageId();
654 }
655 } else if (KService::Ptr service = KApplicationTrader::preferredService(application)) {
656 return service->storageId();
657 }
658
659 return QLatin1String("");
660}
661
662bool launcherUrlsMatch(const QUrl &a, const QUrl &b, UrlComparisonMode mode)
663{
664 QUrl sanitizedA = a;
665 QUrl sanitizedB = b;
666
667 if (mode == IgnoreQueryItems) {
668 sanitizedA = a.adjusted(QUrl::RemoveQuery);
669 sanitizedB = b.adjusted(QUrl::RemoveQuery);
670 }
671
672 auto tryResolveToApplicationsUrl = [](const QUrl &url) -> QUrl {
673 QUrl resolvedUrl = url;
674
676 KDesktopFile f(url.toLocalFile());
677
678 const KService::Ptr service = KService::serviceByStorageId(f.fileName());
679
680 // Resolve to non-absolute menuId-based URL if possible.
681 if (service) {
682 const QString &menuId = service->menuId();
683
684 if (!menuId.isEmpty()) {
685 resolvedUrl = QUrl(QLatin1String("applications:") + menuId);
686 resolvedUrl.setQuery(url.query());
687 }
688 }
689 }
690
691 return resolvedUrl;
692 };
693
694 sanitizedA = tryResolveToApplicationsUrl(sanitizedA);
695 sanitizedB = tryResolveToApplicationsUrl(sanitizedB);
696
697 return (sanitizedA == sanitizedB);
698}
699
700bool appsMatch(const QModelIndex &a, const QModelIndex &b)
701{
702 const QString &aAppId = a.data(AbstractTasksModel::AppId).toString();
703 const QString &bAppId = b.data(AbstractTasksModel::AppId).toString();
704
705 if (!aAppId.isEmpty() && aAppId == bAppId) {
706 return true;
707 }
708
711
712 if (aUrl.isValid() && aUrl == bUrl) {
713 return true;
714 }
715
716 return false;
717}
718
719QRect screenGeometry(const QPoint &pos)
720{
721 if (pos.isNull()) {
722 return QRect();
723 }
724
726 QRect screenGeometry;
727 int shortestDistance = INT_MAX;
728
729 for (int i = 0; i < screens.count(); ++i) {
730 const QRect &geometry = screens.at(i)->geometry();
731
732 if (geometry.contains(pos)) {
733 return geometry;
734 }
735
736 int distance = QPoint(geometry.topLeft() - pos).manhattanLength();
737 distance = qMin(distance, QPoint(geometry.topRight() - pos).manhattanLength());
738 distance = qMin(distance, QPoint(geometry.bottomRight() - pos).manhattanLength());
739 distance = qMin(distance, QPoint(geometry.bottomLeft() - pos).manhattanLength());
740
741 if (distance < shortestDistance) {
742 shortestDistance = distance;
743 screenGeometry = geometry;
744 }
745 }
746
747 return screenGeometry;
748}
749
750void runApp(const AppData &appData, const QList<QUrl> &urls)
751{
752 if (appData.url.isValid()) {
753 KService::Ptr service;
754
755 // applications: URLs are used to refer to applications by their KService::menuId
756 // (i.e. .desktop file name) rather than the absolute path to a .desktop file.
757 if (appData.url.scheme() == QLatin1String("applications")) {
758 service = KService::serviceByMenuId(appData.url.path());
759 } else if (appData.url.scheme() == QLatin1String("preferred")) {
760 service = KService::serviceByStorageId(defaultApplication(appData.url));
761 } else {
762 service = KService::serviceByDesktopPath(appData.url.toLocalFile());
763 }
764
765 if (service && service->isApplication()) {
766 auto *job = new KIO::ApplicationLauncherJob(service);
768 job->setUrls(urls);
769 job->start();
770
771 KActivities::ResourceInstance::notifyAccessed(QUrl(QString(u"applications:" + service->storageId())), QStringLiteral("org.kde.libtaskmanager"));
772 } else {
773 auto *job = new KIO::OpenUrlJob(appData.url);
775 job->setRunExecutables(true);
776 job->start();
777
778 if (!appData.id.isEmpty()) {
779 KActivities::ResourceInstance::notifyAccessed(QUrl(QString(u"applications:" + appData.id)), QStringLiteral("org.kde.libtaskmanager"));
780 }
781 }
782 }
783}
784
785bool canLauchNewInstance(const AppData &appData)
786{
787 if (appData.url.isEmpty()) {
788 return false;
789 }
790
791 QString desktopEntry = appData.id;
792
793 // Remove suffix if necessary
794 if (desktopEntry.endsWith(QLatin1String(".desktop"))) {
795 desktopEntry.chop(8);
796 }
797
798 const KService::Ptr service = KService::serviceByDesktopName(desktopEntry);
799
800 if (service) {
801 if (service->noDisplay()) {
802 return false;
803 }
804
805 if (service->property<bool>(QStringLiteral("SingleMainWindow"))) {
806 return false;
807 }
808
809 // GNOME-specific key, for backwards compatibility with apps that haven't
810 // started using the XDG "SingleMainWindow" key yet
811 if (service->property<bool>(QStringLiteral("X-GNOME-SingleWindow"))) {
812 return false;
813 }
814
815 // Hide our own action if there's already a "New Window" action
816 const auto actions = service->actions();
817 for (const KServiceAction &action : actions) {
818 if (action.name().startsWith(u"new", Qt::CaseInsensitive) && action.name().endsWith(u"window", Qt::CaseInsensitive)) {
819 return false;
820 }
821
822 if (action.name() == QLatin1String("WindowNew")) {
823 return false;
824 }
825 }
826 }
827
828 return true;
829}
830}
static bool isDesktopFile(const QString &path)
QString getSetting(KEMailSettings::Setting s) const
static Ptr serviceByStorageId(const QString &_storageId)
static Ptr serviceByDesktopName(const QString &_name)
QList< Ptr > List
static Ptr serviceByMenuId(const QString &_menuId)
static Ptr serviceByDesktopPath(const QString &_path)
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
@ LauncherUrlWithoutIcon
Special path to get a launcher URL while skipping fallback icon encoding.
@ AppId
KService storage id (.desktop name sans extension).
std::optional< QSqlQuery > query(const QString &queryStatement)
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
KSERVICE_EXPORT KService::Ptr preferredService(const QString &mimeType)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QString path(const QString &relativePath)
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
QByteArray fromBase64(const QByteArray &base64, Base64Options options)
QList< QByteArray > split(char sep) const const
QByteArrayView sliced(qsizetype pos) const const
QFileInfoList entryInfoList(Filters filters, SortFlags sort) const const
QChar separator()
bool exists() const const
QDir absoluteDir() const const
QList< QScreen * > screens()
QIcon fromTheme(const QString &name)
bool hasThemeIcon(const QString &name)
const_reference at(qsizetype i) const const
qsizetype count() const const
bool isEmpty() const const
QVariant data(int role) const const
bool loadFromData(const QByteArray &data, const char *format, Qt::ImageConversionFlags flags)
bool isNull() const const
int manhattanLength() const const
QPoint bottomLeft() const const
QPoint bottomRight() const const
bool contains(const QPoint &point, bool proper) const const
QPoint topLeft() const const
QPoint topRight() const const
QString findExecutable(const QString &executableName, const QStringList &paths)
QString arg(Args &&... args) const const
const QChar at(qsizetype position) const const
void chop(qsizetype n)
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromUtf8(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
QString left(qsizetype n) const const
QString mid(qsizetype position, qsizetype n) const const
QString number(double n, char format, int precision)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
void truncate(qsizetype position)
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
QStringView mid(qsizetype start, qsizetype length) const const
CaseInsensitive
QFuture< QtPrivate::MapResultType< Iterator, MapFunctor > > mapped(Iterator begin, Iterator end, MapFunctor &&function)
QUrl adjusted(FormattingOptions options) const const
QString fileName(ComponentFormattingOptions options) const const
QUrl fromLocalFile(const QString &localFile)
bool hasQuery() const const
QString host(ComponentFormattingOptions options) const const
bool isLocalFile() const const
bool isValid() const const
QString path(ComponentFormattingOptions options) const const
QString query(ComponentFormattingOptions options) const const
QString scheme() const const
void setQuery(const QString &query, ParsingMode mode)
void setUrl(const QString &url, ParsingMode parsingMode)
QString toLocalFile() const const
QString toString() const const
QUrl toUrl() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 16:58:38 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.