File indexing completed on 2024-05-19 05:35:56
0001 /* 0002 SPDX-FileCopyrightText: 2012-2016 Eike Hein <hein@kde.org> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "backend.h" 0008 0009 #include "log_settings.h" 0010 #include <KConfigGroup> 0011 #include <KDesktopFile> 0012 #include <KFileItem> 0013 #include <KFilePlacesModel> 0014 #include <KLocalizedString> 0015 #include <KNotificationJobUiDelegate> 0016 #include <KProtocolInfo> 0017 #include <KService> 0018 #include <KServiceAction> 0019 #include <KWindowEffects> 0020 #include <KWindowSystem> 0021 0022 #include <KApplicationTrader> 0023 #include <KIO/ApplicationLauncherJob> 0024 0025 #include <QAction> 0026 #include <QActionGroup> 0027 #include <QApplication> 0028 #include <QDBusConnection> 0029 #include <QDBusConnectionInterface> 0030 #include <QDBusMessage> 0031 #include <QDBusMetaType> 0032 #include <QDBusPendingCall> 0033 #include <QDBusReply> 0034 #include <QDBusServiceWatcher> 0035 #include <QJsonArray> 0036 #include <QMenu> 0037 #include <QQuickItem> 0038 #include <QQuickWindow> 0039 #include <QStandardPaths> 0040 #include <QTimer> 0041 #include <QVersionNumber> 0042 0043 #include <PlasmaActivities/Consumer> 0044 #include <PlasmaActivities/Stats/Cleaning> 0045 #include <PlasmaActivities/Stats/ResultSet> 0046 #include <PlasmaActivities/Stats/Terms> 0047 0048 #include <processcore/process.h> 0049 #include <processcore/processes.h> 0050 0051 namespace KAStats = KActivities::Stats; 0052 0053 using namespace KAStats; 0054 using namespace KAStats::Terms; 0055 0056 static const QString highlightWindowName = QStringLiteral("org.kde.KWin.HighlightWindow"); 0057 static const QString highlightWindowPath = QStringLiteral("/org/kde/KWin/HighlightWindow"); 0058 static const QString &highlightWindowInterface = highlightWindowName; 0059 0060 static const QString appViewName = QStringLiteral("org.kde.KWin.Effect.WindowView1"); 0061 static const QString appViewPath = QStringLiteral("/org/kde/KWin/Effect/WindowView1"); 0062 static const QString &appViewInterface = appViewName; 0063 0064 Backend::Backend(QObject *parent) 0065 : QObject(parent) 0066 , m_highlightWindows(false) 0067 , m_actionGroup(new QActionGroup(this)) 0068 { 0069 m_windowViewAvailable = QDBusConnection::sessionBus().interface()->isServiceRegistered(appViewName); 0070 auto watcher = new QDBusServiceWatcher(appViewName, 0071 QDBusConnection::sessionBus(), 0072 QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, 0073 this); 0074 connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, [this] { 0075 m_windowViewAvailable = true; 0076 Q_EMIT windowViewAvailableChanged(); 0077 }); 0078 connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, [this] { 0079 m_windowViewAvailable = false; 0080 Q_EMIT windowViewAvailableChanged(); 0081 }); 0082 } 0083 0084 Backend::~Backend() 0085 { 0086 } 0087 0088 bool Backend::highlightWindows() const 0089 { 0090 return m_highlightWindows; 0091 } 0092 0093 void Backend::setHighlightWindows(bool highlight) 0094 { 0095 if (highlight != m_highlightWindows) { 0096 m_highlightWindows = highlight; 0097 0098 updateWindowHighlight(); 0099 0100 Q_EMIT highlightWindowsChanged(); 0101 } 0102 } 0103 0104 QUrl Backend::tryDecodeApplicationsUrl(const QUrl &launcherUrl) 0105 { 0106 if (launcherUrl.isValid() && launcherUrl.scheme() == QLatin1String("applications")) { 0107 const KService::Ptr service = KService::serviceByMenuId(launcherUrl.path()); 0108 0109 if (service) { 0110 return QUrl::fromLocalFile(service->entryPath()); 0111 } 0112 } 0113 0114 return launcherUrl; 0115 } 0116 0117 QStringList Backend::applicationCategories(const QUrl &launcherUrl) 0118 { 0119 const QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl); 0120 0121 if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) { 0122 return QStringList(); 0123 } 0124 0125 KDesktopFile desktopFile(desktopEntryUrl.toLocalFile()); 0126 0127 // Since we can't have dynamic jump list actions, at least add the user's "Places" for file managers. 0128 return desktopFile.desktopGroup().readXdgListEntry(QStringLiteral("Categories")); 0129 } 0130 0131 QVariantList Backend::jumpListActions(const QUrl &launcherUrl, QObject *parent) 0132 { 0133 QVariantList actions; 0134 0135 if (!parent) { 0136 return actions; 0137 } 0138 0139 QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl); 0140 0141 if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) { 0142 return actions; 0143 } 0144 0145 const KService::Ptr service = KService::serviceByDesktopPath(desktopEntryUrl.toLocalFile()); 0146 if (!service) { 0147 return actions; 0148 } 0149 0150 if (service->storageId() == QLatin1String("systemsettings.desktop")) { 0151 actions = systemSettingsActions(parent); 0152 if (!actions.isEmpty()) { 0153 return actions; 0154 } 0155 } 0156 0157 const auto jumpListActions = service->actions(); 0158 0159 for (const KServiceAction &serviceAction : jumpListActions) { 0160 if (serviceAction.noDisplay()) { 0161 continue; 0162 } 0163 0164 QAction *action = new QAction(parent); 0165 action->setText(serviceAction.text()); 0166 action->setIcon(QIcon::fromTheme(serviceAction.icon())); 0167 if (serviceAction.isSeparator()) { 0168 action->setSeparator(true); 0169 } 0170 0171 connect(action, &QAction::triggered, this, [serviceAction]() { 0172 auto *job = new KIO::ApplicationLauncherJob(serviceAction); 0173 auto *delegate = new KNotificationJobUiDelegate; 0174 delegate->setAutoErrorHandlingEnabled(true); 0175 job->setUiDelegate(delegate); 0176 job->start(); 0177 }); 0178 0179 actions << QVariant::fromValue<QAction *>(action); 0180 } 0181 0182 return actions; 0183 } 0184 0185 QVariantList Backend::systemSettingsActions(QObject *parent) const 0186 { 0187 QVariantList actions; 0188 0189 auto query = AllResources | Agent(QStringLiteral("org.kde.systemsettings")) | HighScoredFirst | Limit(5); 0190 0191 ResultSet results(query); 0192 0193 QStringList ids; 0194 for (const ResultSet::Result &result : results) { 0195 ids << QUrl(result.resource()).path(); 0196 } 0197 0198 if (ids.count() < 5) { 0199 // We'll load the default set of settings from its jump list actions. 0200 return actions; 0201 } 0202 0203 for (const QString &id : std::as_const(ids)) { 0204 KService::Ptr service = KService::serviceByStorageId(id); 0205 if (!service || !service->isValid()) { 0206 continue; 0207 } 0208 0209 QAction *action = new QAction(parent); 0210 action->setText(service->name()); 0211 action->setIcon(QIcon::fromTheme(service->icon())); 0212 0213 connect(action, &QAction::triggered, this, [service]() { 0214 auto *job = new KIO::ApplicationLauncherJob(service); 0215 auto *delegate = new KNotificationJobUiDelegate; 0216 delegate->setAutoErrorHandlingEnabled(true); 0217 job->setUiDelegate(delegate); 0218 job->start(); 0219 }); 0220 0221 actions << QVariant::fromValue<QAction *>(action); 0222 } 0223 return actions; 0224 } 0225 0226 QVariantList Backend::placesActions(const QUrl &launcherUrl, bool showAllPlaces, QObject *parent) 0227 { 0228 if (!parent) { 0229 return QVariantList(); 0230 } 0231 0232 QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl); 0233 0234 if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) { 0235 return QVariantList(); 0236 } 0237 0238 QVariantList actions; 0239 0240 // Since we can't have dynamic jump list actions, at least add the user's "Places" for file managers. 0241 if (!applicationCategories(launcherUrl).contains(QLatin1String("FileManager"))) { 0242 return actions; 0243 } 0244 0245 QString previousGroup; 0246 QMenu *subMenu = nullptr; 0247 0248 std::unique_ptr<KFilePlacesModel> placesModel(new KFilePlacesModel()); 0249 for (int i = 0; i < placesModel->rowCount(); ++i) { 0250 QModelIndex idx = placesModel->index(i, 0); 0251 0252 if (placesModel->isHidden(idx)) { 0253 continue; 0254 } 0255 0256 const QString &title = idx.data(Qt::DisplayRole).toString(); 0257 const QIcon &icon = idx.data(Qt::DecorationRole).value<QIcon>(); 0258 const QUrl &url = idx.data(KFilePlacesModel::UrlRole).toUrl(); 0259 0260 QAction *placeAction = new QAction(icon, title, parent); 0261 0262 connect(placeAction, &QAction::triggered, this, [url, desktopEntryUrl] { 0263 KService::Ptr service = KService::serviceByDesktopPath(desktopEntryUrl.toLocalFile()); 0264 if (!service) { 0265 return; 0266 } 0267 0268 auto *job = new KIO::ApplicationLauncherJob(service); 0269 auto *delegate = new KNotificationJobUiDelegate; 0270 delegate->setAutoErrorHandlingEnabled(true); 0271 job->setUiDelegate(delegate); 0272 0273 job->setUrls({url}); 0274 job->start(); 0275 }); 0276 0277 const QString &groupName = idx.data(KFilePlacesModel::GroupRole).toString(); 0278 if (previousGroup.isEmpty()) { // Skip first group heading. 0279 previousGroup = groupName; 0280 } 0281 0282 // Put all subsequent categories into a submenu. 0283 if (previousGroup != groupName) { 0284 QAction *subMenuAction = new QAction(groupName, parent); 0285 subMenu = new QMenu(); 0286 // Cannot parent a QMenu to a QAction, need to delete it manually. 0287 connect(parent, &QObject::destroyed, subMenu, &QObject::deleteLater); 0288 subMenuAction->setMenu(subMenu); 0289 0290 actions << QVariant::fromValue(subMenuAction); 0291 0292 previousGroup = groupName; 0293 } 0294 0295 if (subMenu) { 0296 subMenu->addAction(placeAction); 0297 } else { 0298 actions << QVariant::fromValue(placeAction); 0299 } 0300 } 0301 0302 // There is nothing more frustrating than having a "More" entry that ends up showing just one or two 0303 // additional entries. Therefore we truncate to max. 5 entries only if there are more than 7 in total. 0304 if (!showAllPlaces && actions.count() > 7) { 0305 const int totalActionCount = actions.count(); 0306 0307 while (actions.count() > 5) { 0308 actions.removeLast(); 0309 } 0310 0311 QAction *action = new QAction(parent); 0312 action->setText(i18ncp("Show all user Places", "%1 more Place", "%1 more Places", totalActionCount - actions.count())); 0313 connect(action, &QAction::triggered, this, &Backend::showAllPlaces); 0314 actions << QVariant::fromValue(action); 0315 } 0316 0317 return actions; 0318 } 0319 0320 QVariantList Backend::recentDocumentActions(const QUrl &launcherUrl, QObject *parent) 0321 { 0322 QVariantList actions; 0323 if (!parent) { 0324 return actions; 0325 } 0326 0327 QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl); 0328 0329 if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) { 0330 return QVariantList(); 0331 } 0332 0333 QString desktopName = desktopEntryUrl.fileName(); 0334 QString storageId = desktopName; 0335 0336 if (storageId.endsWith(QLatin1String(".desktop"))) { 0337 storageId = storageId.left(storageId.length() - 8); 0338 } 0339 0340 auto query = UsedResources | RecentlyUsedFirst | Agent(storageId) | Type::any() | Activity::current(); 0341 0342 ResultSet results(query); 0343 0344 ResultSet::const_iterator resultIt = results.begin(); 0345 0346 int actionCount = 0; 0347 0348 bool allFolders = true; 0349 bool allDownloads = true; 0350 bool allRemoteWithoutFileName = true; 0351 const QString downloadsPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); 0352 0353 while (actionCount < 5 && resultIt != results.end()) { 0354 const QString resource = (*resultIt).resource(); 0355 const QString mimetype = (*resultIt).mimetype(); 0356 const QUrl url = (*resultIt).url(); 0357 ++resultIt; 0358 0359 if (!url.isValid()) { 0360 continue; 0361 } 0362 0363 allFolders = allFolders && mimetype == QLatin1String("inode/directory"); 0364 allDownloads = allDownloads && url.toLocalFile().startsWith(downloadsPath); 0365 allRemoteWithoutFileName = allRemoteWithoutFileName && !url.isLocalFile() && url.fileName().isEmpty(); 0366 0367 QString name = url.fileName(); 0368 if (name.isEmpty()) { 0369 name = url.toDisplayString(); 0370 } 0371 0372 QString iconName; 0373 0374 const QString protocol = url.scheme(); 0375 if (!KProtocolInfo::isKnownProtocol(protocol) || KProtocolInfo::isHelperProtocol(protocol)) { 0376 const KService::Ptr service = KApplicationTrader::preferredService(QLatin1String("x-scheme-handler/") + protocol); 0377 if (service) { 0378 iconName = service->icon(); 0379 } else if (KProtocolInfo::isKnownProtocol(protocol)) { 0380 Q_ASSERT(KProtocolInfo::isHelperProtocol(protocol)); 0381 iconName = KProtocolInfo::icon(protocol); 0382 } else { 0383 // Should not happen? 0384 continue; 0385 } 0386 } else { 0387 const KFileItem fileItem(url, mimetype); 0388 iconName = fileItem.iconName(); 0389 } 0390 0391 QAction *action = new QAction(parent); 0392 action->setText(name); 0393 action->setIcon(QIcon::fromTheme(iconName, QIcon::fromTheme(QStringLiteral("unknown")))); 0394 action->setProperty("agent", storageId); 0395 action->setProperty("entryPath", desktopEntryUrl); 0396 action->setProperty("mimeType", mimetype); 0397 action->setData(url); 0398 connect(action, &QAction::triggered, this, &Backend::handleRecentDocumentAction); 0399 0400 actions << QVariant::fromValue<QAction *>(action); 0401 0402 ++actionCount; 0403 } 0404 0405 if (actionCount > 0) { 0406 // Overrides section heading on QML side 0407 if (allDownloads) { 0408 actions.prepend(i18n("Recent Downloads")); 0409 } else if (allRemoteWithoutFileName) { 0410 actions.prepend(i18n("Recent Connections")); 0411 } else if (allFolders) { 0412 actions.prepend(i18n("Recent Places")); 0413 } 0414 0415 QAction *separatorAction = new QAction(parent); 0416 separatorAction->setSeparator(true); 0417 actions << QVariant::fromValue<QAction *>(separatorAction); 0418 0419 QAction *action = new QAction(parent); 0420 if (allDownloads) { 0421 action->setText(i18nc("@action:inmenu", "Forget Recent Downloads")); 0422 } else if (allRemoteWithoutFileName) { 0423 action->setText(i18nc("@action:inmenu", "Forget Recent Connections")); 0424 } else if (allFolders) { 0425 action->setText(i18nc("@action:inmenu", "Forget Recent Places")); 0426 } else { 0427 action->setText(i18nc("@action:inmenu", "Forget Recent Files")); 0428 } 0429 action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); 0430 action->setProperty("agent", storageId); 0431 connect(action, &QAction::triggered, this, &Backend::handleRecentDocumentAction); 0432 actions << QVariant::fromValue<QAction *>(action); 0433 } 0434 0435 return actions; 0436 } 0437 0438 void Backend::handleRecentDocumentAction() const 0439 { 0440 const QAction *action = qobject_cast<QAction *>(sender()); 0441 0442 if (!action) { 0443 return; 0444 } 0445 0446 const QString agent = action->property("agent").toString(); 0447 0448 if (agent.isEmpty()) { 0449 return; 0450 } 0451 0452 const QString desktopPath = action->property("entryPath").toUrl().toLocalFile(); 0453 const QUrl url = action->data().toUrl(); 0454 0455 if (desktopPath.isEmpty() || url.isEmpty()) { 0456 auto query = UsedResources | Agent(agent) | Type::any() | Activity::current() | Url::file(); 0457 0458 KAStats::forgetResources(query); 0459 0460 return; 0461 } 0462 0463 KService::Ptr service = KService::serviceByDesktopPath(desktopPath); 0464 0465 if (!service) { 0466 return; 0467 } 0468 0469 // prevents using a service file that does not support opening a mime type for a file it created 0470 // for instance spectacle 0471 const auto mimetype = action->property("mimeType").toString(); 0472 if (!mimetype.isEmpty()) { 0473 if (!service->hasMimeType(mimetype)) { 0474 // needs to find the application that supports this mimetype 0475 service = KApplicationTrader::preferredService(mimetype); 0476 0477 if (!service) { 0478 // no service found to handle the mimetype 0479 return; 0480 } else { 0481 qCWarning(TASKMANAGER_DEBUG) << "Preventing the file to open with " << service->desktopEntryName() << "no alternative found"; 0482 } 0483 } 0484 } 0485 0486 auto *job = new KIO::ApplicationLauncherJob(service); 0487 auto *delegate = new KNotificationJobUiDelegate; 0488 delegate->setAutoErrorHandlingEnabled(true); 0489 job->setUiDelegate(delegate); 0490 job->setUrls({url}); 0491 job->start(); 0492 } 0493 0494 void Backend::setActionGroup(QAction *action) const 0495 { 0496 if (action) { 0497 action->setActionGroup(m_actionGroup); 0498 } 0499 } 0500 0501 QRect Backend::globalRect(QQuickItem *item) const 0502 { 0503 if (!item || !item->window()) { 0504 return QRect(); 0505 } 0506 0507 QRect iconRect(item->x(), item->y(), item->width(), item->height()); 0508 iconRect.moveTopLeft(item->parentItem()->mapToScene(iconRect.topLeft()).toPoint()); 0509 iconRect.moveTopLeft(item->window()->mapToGlobal(iconRect.topLeft())); 0510 0511 return iconRect; 0512 } 0513 0514 bool Backend::windowViewAvailable() const 0515 { 0516 return m_windowViewAvailable; 0517 } 0518 0519 void Backend::activateWindowView(const QVariant &_winIds) 0520 { 0521 if (m_windowsToHighlight.count()) { 0522 m_windowsToHighlight.clear(); 0523 updateWindowHighlight(); 0524 } 0525 0526 auto message = QDBusMessage::createMethodCall(appViewName, appViewPath, appViewInterface, QStringLiteral("activate")); 0527 message << _winIds.toStringList(); 0528 QDBusConnection::sessionBus().asyncCall(message); 0529 } 0530 0531 bool Backend::isApplication(const QUrl &url) const 0532 { 0533 if (!url.isValid() || !url.isLocalFile()) { 0534 return false; 0535 } 0536 0537 const QString &localPath = url.toLocalFile(); 0538 0539 if (!KDesktopFile::isDesktopFile(localPath)) { 0540 return false; 0541 } 0542 0543 KDesktopFile desktopFile(localPath); 0544 return desktopFile.hasApplicationType(); 0545 } 0546 0547 void Backend::cancelHighlightWindows() 0548 { 0549 m_windowsToHighlight.clear(); 0550 updateWindowHighlight(); 0551 } 0552 0553 qint64 Backend::parentPid(qint64 pid) const 0554 { 0555 KSysGuard::Processes procs; 0556 procs.updateOrAddProcess(pid); 0557 0558 KSysGuard::Process *proc = procs.getProcess(pid); 0559 if (!proc) { 0560 return -1; 0561 } 0562 0563 int parentPid = proc->parentPid(); 0564 if (parentPid != -1) { 0565 procs.updateOrAddProcess(parentPid); 0566 0567 KSysGuard::Process *parentProc = procs.getProcess(parentPid); 0568 if (!parentProc) { 0569 return -1; 0570 } 0571 0572 if (!proc->cGroup().isEmpty() && parentProc->cGroup() == proc->cGroup()) { 0573 return parentProc->pid(); 0574 } 0575 } 0576 0577 return -1; 0578 } 0579 0580 void Backend::windowsHovered(const QVariant &_winIds, bool hovered) 0581 { 0582 m_windowsToHighlight.clear(); 0583 0584 if (hovered) { 0585 m_windowsToHighlight = _winIds.toStringList(); 0586 } 0587 0588 // Avoid flickering when scrolling in the tooltip 0589 QTimer::singleShot(0, this, &Backend::updateWindowHighlight); 0590 } 0591 0592 void Backend::updateWindowHighlight() 0593 { 0594 if (!m_highlightWindows) { 0595 return; 0596 } 0597 auto message = QDBusMessage::createMethodCall(highlightWindowName, highlightWindowPath, highlightWindowInterface, QStringLiteral("highlightWindows")); 0598 message << m_windowsToHighlight; 0599 QDBusConnection::sessionBus().asyncCall(message); 0600 } 0601 0602 #include "moc_backend.cpp"