File indexing completed on 2024-06-09 05:30:58
0001 /* 0002 SPDX-FileCopyrightText: 2014-2015 Eike Hein <hein@kde.org> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "recentusagemodel.h" 0008 #include "actionlist.h" 0009 #include "appentry.h" 0010 #include "appsmodel.h" 0011 #include "debug.h" 0012 #include "kastatsfavoritesmodel.h" 0013 #include <kio_version.h> 0014 0015 #include <QApplication> 0016 #include <QDir> 0017 #include <QMimeDatabase> 0018 #include <QQmlEngine> 0019 #include <QTimer> 0020 0021 #include <KApplicationTrader> 0022 #include <KFileItem> 0023 #include <KIO/ApplicationLauncherJob> 0024 #include <KIO/JobUiDelegate> 0025 #include <KIO/JobUiDelegateFactory> 0026 #include <KIO/OpenFileManagerWindowJob> 0027 #include <KIO/OpenUrlJob> 0028 #include <KLocalizedString> 0029 #include <KNotificationJobUiDelegate> 0030 #include <KService> 0031 #include <PlasmaActivities/ResourceInstance> 0032 0033 #include <KWindowSystem> 0034 #include <PlasmaActivities/Stats/Cleaning> 0035 #include <PlasmaActivities/Stats/Terms> 0036 0037 namespace KAStats = KActivities::Stats; 0038 0039 using namespace KAStats; 0040 using namespace KAStats::Terms; 0041 0042 GroupSortProxy::GroupSortProxy(AbstractModel *parentModel, QAbstractItemModel *sourceModel) 0043 : QSortFilterProxyModel(parentModel) 0044 { 0045 sourceModel->setParent(this); 0046 setSourceModel(sourceModel); 0047 sort(0); 0048 } 0049 0050 GroupSortProxy::~GroupSortProxy() 0051 { 0052 } 0053 0054 InvalidAppsFilterProxy::InvalidAppsFilterProxy(AbstractModel *parentModel, QAbstractItemModel *sourceModel) 0055 : QSortFilterProxyModel(parentModel) 0056 , m_parentModel(parentModel) 0057 { 0058 connect(parentModel, &AbstractModel::favoritesModelChanged, this, &InvalidAppsFilterProxy::connectNewFavoritesModel); 0059 connectNewFavoritesModel(); 0060 0061 sourceModel->setParent(this); 0062 setSourceModel(sourceModel); 0063 } 0064 0065 InvalidAppsFilterProxy::~InvalidAppsFilterProxy() 0066 { 0067 } 0068 0069 void InvalidAppsFilterProxy::connectNewFavoritesModel() 0070 { 0071 KAStatsFavoritesModel *favoritesModel = static_cast<KAStatsFavoritesModel *>(m_parentModel->favoritesModel()); 0072 if (favoritesModel) { 0073 connect(favoritesModel, &KAStatsFavoritesModel::favoritesChanged, this, &QSortFilterProxyModel::invalidate); 0074 } 0075 0076 invalidate(); 0077 } 0078 0079 bool InvalidAppsFilterProxy::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const 0080 { 0081 Q_UNUSED(source_parent); 0082 0083 const QString resource = sourceModel()->index(source_row, 0).data(ResultModel::ResourceRole).toString(); 0084 0085 if (resource.startsWith(QLatin1String("applications:"))) { 0086 KService::Ptr service = KService::serviceByStorageId(resource.section(QLatin1Char(':'), 1)); 0087 0088 KAStatsFavoritesModel *favoritesModel = m_parentModel ? static_cast<KAStatsFavoritesModel *>(m_parentModel->favoritesModel()) : nullptr; 0089 0090 return (service && (!favoritesModel || !favoritesModel->isFavorite(service->storageId()))); 0091 } 0092 0093 return true; 0094 } 0095 0096 bool InvalidAppsFilterProxy::lessThan(const QModelIndex &left, const QModelIndex &right) const 0097 { 0098 return (left.row() < right.row()); 0099 } 0100 0101 bool GroupSortProxy::lessThan(const QModelIndex &left, const QModelIndex &right) const 0102 { 0103 const QString &lResource = sourceModel()->data(left, ResultModel::ResourceRole).toString(); 0104 const QString &rResource = sourceModel()->data(right, ResultModel::ResourceRole).toString(); 0105 0106 if (lResource.startsWith(QLatin1String("applications:")) && !rResource.startsWith(QLatin1String("applications:"))) { 0107 return true; 0108 } else if (!lResource.startsWith(QLatin1String("applications:")) && rResource.startsWith(QLatin1String("applications:"))) { 0109 return false; 0110 } 0111 0112 return (left.row() < right.row()); 0113 } 0114 0115 RecentUsageModel::RecentUsageModel(QObject *parent, IncludeUsage usage, int ordering) 0116 : ForwardingModel(parent) 0117 , m_usage(usage) 0118 , m_ordering((Ordering)ordering) 0119 , m_complete(false) 0120 , m_placesModel(new KFilePlacesModel(this)) 0121 { 0122 refresh(); 0123 } 0124 0125 RecentUsageModel::~RecentUsageModel() 0126 { 0127 } 0128 0129 void RecentUsageModel::setShownItems(IncludeUsage usage) 0130 { 0131 if (m_usage == usage) { 0132 return; 0133 } 0134 0135 m_usage = usage; 0136 0137 Q_EMIT shownItemsChanged(); 0138 refresh(); 0139 } 0140 0141 RecentUsageModel::IncludeUsage RecentUsageModel::shownItems() const 0142 { 0143 return m_usage; 0144 } 0145 0146 QString RecentUsageModel::description() const 0147 { 0148 switch (m_usage) { 0149 case AppsAndDocs: 0150 return i18n("Recently Used"); 0151 case OnlyApps: 0152 return i18n("Applications"); 0153 case OnlyDocs: 0154 default: 0155 return i18n("Files"); 0156 } 0157 } 0158 0159 QString RecentUsageModel::resourceAt(int row) const 0160 { 0161 return rowValueAt(row, ResultModel::ResourceRole).toString(); 0162 } 0163 0164 QVariant RecentUsageModel::rowValueAt(int row, ResultModel::Roles role) const 0165 { 0166 QSortFilterProxyModel *sourceProxy = qobject_cast<QSortFilterProxyModel *>(sourceModel()); 0167 0168 if (sourceProxy) { 0169 return sourceProxy->sourceModel()->data(sourceProxy->mapToSource(sourceProxy->index(row, 0)), role).toString(); 0170 } 0171 0172 return sourceModel()->data(index(row, 0), role); 0173 } 0174 0175 QVariant RecentUsageModel::data(const QModelIndex &index, int role) const 0176 { 0177 if (!index.isValid()) { 0178 return QVariant(); 0179 } 0180 0181 const QString &resource = resourceAt(index.row()); 0182 0183 if (resource.startsWith(QLatin1String("applications:"))) { 0184 return appData(resource, role); 0185 } else { 0186 const QString &mimeType = rowValueAt(index.row(), ResultModel::MimeType).toString(); 0187 return docData(resource, role, mimeType); 0188 } 0189 } 0190 0191 QVariant RecentUsageModel::appData(const QString &resource, int role) const 0192 { 0193 const QString storageId = resource.section(QLatin1Char(':'), 1); 0194 KService::Ptr service = KService::serviceByStorageId(storageId); 0195 0196 QStringList allowedTypes({QLatin1String("Service"), QLatin1String("Application")}); 0197 0198 if (!service || !allowedTypes.contains(service->property<QString>(QLatin1String("Type"))) || service->exec().isEmpty()) { 0199 return QVariant(); 0200 } 0201 0202 if (role == Qt::DisplayRole) { 0203 AppsModel *parentModel = qobject_cast<AppsModel *>(QObject::parent()); 0204 0205 if (parentModel) { 0206 return AppEntry::nameFromService(service, (AppEntry::NameFormat)qobject_cast<AppsModel *>(QObject::parent())->appNameFormat()); 0207 } else { 0208 return AppEntry::nameFromService(service, AppEntry::NameOnly); 0209 } 0210 } else if (role == Qt::DecorationRole) { 0211 return service->icon(); 0212 } else if (role == Kicker::DescriptionRole) { 0213 return service->comment(); 0214 } else if (role == Kicker::GroupRole) { 0215 return i18n("Applications"); 0216 } else if (role == Kicker::FavoriteIdRole) { 0217 return service->storageId(); 0218 } else if (role == Kicker::HasActionListRole) { 0219 return true; 0220 } else if (role == Kicker::ActionListRole) { 0221 QVariantList actionList; 0222 0223 const QVariantList &jumpList = Kicker::jumpListActions(service); 0224 if (!jumpList.isEmpty()) { 0225 actionList << jumpList; 0226 } 0227 0228 const QVariantList &recentDocuments = Kicker::recentDocumentActions(service); 0229 if (!recentDocuments.isEmpty()) { 0230 actionList << recentDocuments; 0231 } 0232 0233 if (!actionList.isEmpty()) { 0234 actionList << Kicker::createSeparatorActionItem(); 0235 } 0236 0237 const QVariantMap &forgetAction = Kicker::createActionItem(i18n("Forget Application"), QStringLiteral("edit-clear-history"), QStringLiteral("forget")); 0238 actionList << forgetAction; 0239 0240 const QVariantMap &forgetAllAction = Kicker::createActionItem(forgetAllActionName(), QStringLiteral("edit-clear-history"), QStringLiteral("forgetAll")); 0241 actionList << forgetAllAction; 0242 0243 return actionList; 0244 } 0245 0246 return QVariant(); 0247 } 0248 0249 QModelIndex RecentUsageModel::findPlaceForKFileItem(const KFileItem &fileItem) const 0250 { 0251 const auto index = m_placesModel->closestItem(fileItem.url()); 0252 if (index.isValid()) { 0253 const auto parentUrl = m_placesModel->url(index); 0254 if (parentUrl == fileItem.url()) { 0255 return index; 0256 } 0257 } 0258 return QModelIndex(); 0259 } 0260 0261 QVariant RecentUsageModel::docData(const QString &resource, int role, const QString &mimeType) const 0262 { 0263 QUrl url(resource); 0264 0265 if (url.scheme().isEmpty()) { 0266 url.setScheme(QStringLiteral("file")); 0267 } 0268 0269 auto getFileItem = [=]() { 0270 // Avoid calling QT_LSTAT and accessing recent documents 0271 if (mimeType.simplified().isEmpty()) { 0272 return KFileItem(url, KFileItem::SkipMimeTypeFromContent); 0273 } else { 0274 return KFileItem(url, mimeType); 0275 } 0276 }; 0277 0278 if (!url.isValid()) { 0279 return QVariant(); 0280 } 0281 0282 if (role == Qt::DisplayRole) { 0283 auto fileItem = getFileItem(); 0284 const auto index = findPlaceForKFileItem(fileItem); 0285 if (index.isValid()) { 0286 return m_placesModel->text(index); 0287 } 0288 return fileItem.text(); 0289 } else if (role == Qt::DecorationRole) { 0290 auto fileItem = getFileItem(); 0291 const auto index = findPlaceForKFileItem(fileItem); 0292 if (index.isValid()) { 0293 return m_placesModel->icon(index); 0294 } 0295 return fileItem.iconName(); 0296 } else if (role == Kicker::GroupRole) { 0297 return i18n("Files"); 0298 } else if (role == Kicker::FavoriteIdRole || role == Kicker::UrlRole) { 0299 return url.toString(); 0300 } else if (role == Kicker::DescriptionRole) { 0301 auto fileItem = getFileItem(); 0302 QString desc = fileItem.localPath(); 0303 0304 const auto index = m_placesModel->closestItem(fileItem.url()); 0305 if (index.isValid()) { 0306 // the current file has a parent in placesModel 0307 const auto parentUrl = m_placesModel->url(index); 0308 if (parentUrl == fileItem.url()) { 0309 // if the current item is a place 0310 return QString(); 0311 } 0312 desc.truncate(desc.lastIndexOf(QLatin1Char('/'))); 0313 const auto text = m_placesModel->text(index); 0314 desc.replace(0, parentUrl.path().length(), text); 0315 } else { 0316 // remove filename 0317 desc.truncate(desc.lastIndexOf(QLatin1Char('/'))); 0318 } 0319 return desc; 0320 } else if (role == Kicker::UrlRole) { 0321 return url; 0322 } else if (role == Kicker::HasActionListRole) { 0323 return true; 0324 } else if (role == Kicker::ActionListRole) { 0325 auto fileItem = getFileItem(); 0326 QVariantList actionList = Kicker::createActionListForFileItem(fileItem); 0327 0328 actionList << Kicker::createSeparatorActionItem(); 0329 0330 QVariantMap openParentFolder = 0331 Kicker::createActionItem(i18n("Open Containing Folder"), QStringLiteral("folder-open"), QStringLiteral("openParentFolder")); 0332 actionList << openParentFolder; 0333 0334 QVariantMap forgetAction = Kicker::createActionItem(i18n("Forget File"), QStringLiteral("edit-clear-history"), QStringLiteral("forget")); 0335 actionList << forgetAction; 0336 0337 QVariantMap forgetAllAction = Kicker::createActionItem(forgetAllActionName(), QStringLiteral("edit-clear-history"), QStringLiteral("forgetAll")); 0338 actionList << forgetAllAction; 0339 0340 return actionList; 0341 } 0342 0343 return QVariant(); 0344 } 0345 0346 bool RecentUsageModel::trigger(int row, const QString &actionId, const QVariant &argument) 0347 { 0348 Q_UNUSED(argument) 0349 0350 bool withinBounds = row >= 0 && row < rowCount(); 0351 0352 if (actionId.isEmpty() && withinBounds) { 0353 const QString &resource = resourceAt(row); 0354 const QString &mimeType = rowValueAt(row, ResultModel::MimeType).toString(); 0355 0356 if (!resource.startsWith(QLatin1String("applications:"))) { 0357 const QUrl resourceUrl = docData(resource, Kicker::UrlRole, mimeType).toUrl(); 0358 0359 auto job = new KIO::OpenUrlJob(resourceUrl); 0360 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, QApplication::activeWindow())); 0361 job->setShowOpenOrExecuteDialog(true); 0362 job->start(); 0363 0364 return true; 0365 } 0366 0367 const QString storageId = resource.section(QLatin1Char(':'), 1); 0368 KService::Ptr service = KService::serviceByStorageId(storageId); 0369 0370 if (!service) { 0371 return false; 0372 } 0373 0374 // prevents using a service file that does not support opening a mime type for a file it created 0375 // for instance a screenshot tool 0376 if (!mimeType.simplified().isEmpty()) { 0377 if (!service->hasMimeType(mimeType)) { 0378 // needs to find the application that supports this mimetype 0379 service = KApplicationTrader::preferredService(mimeType); 0380 0381 if (!service) { 0382 // no service found to handle the mimetype 0383 return false; 0384 } else { 0385 qCWarning(KICKER_DEBUG) << "Preventing the file to open with " << service->desktopEntryName() << "no alternative found"; 0386 } 0387 } 0388 } 0389 0390 auto *job = new KIO::ApplicationLauncherJob(service); 0391 job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); 0392 job->start(); 0393 0394 KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + storageId), QStringLiteral("org.kde.plasma.kicker")); 0395 0396 return true; 0397 } else if (actionId == QLatin1String("forget") && withinBounds) { 0398 if (m_activitiesModel) { 0399 QModelIndex idx = sourceModel()->index(row, 0); 0400 QSortFilterProxyModel *sourceProxy = qobject_cast<QSortFilterProxyModel *>(sourceModel()); 0401 0402 while (sourceProxy) { 0403 idx = sourceProxy->mapToSource(idx); 0404 sourceProxy = qobject_cast<QSortFilterProxyModel *>(sourceProxy->sourceModel()); 0405 } 0406 0407 static_cast<ResultModel *>(m_activitiesModel.data())->forgetResource(idx.row()); 0408 } 0409 0410 return false; 0411 } else if (actionId == QLatin1String("openParentFolder") && withinBounds) { 0412 const auto url = QUrl::fromUserInput(resourceAt(row)); 0413 KIO::highlightInFileManager({url}); 0414 } else if (actionId == QLatin1String("forgetAll")) { 0415 if (m_activitiesModel) { 0416 static_cast<ResultModel *>(m_activitiesModel.data())->forgetAllResources(); 0417 } 0418 0419 return false; 0420 } else if (actionId == QLatin1String("_kicker_jumpListAction")) { 0421 KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(argument.value<KServiceAction>()); 0422 job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); 0423 job->start(); 0424 return true; 0425 } else if (withinBounds) { 0426 const QString &resource = resourceAt(row); 0427 0428 if (resource.startsWith(QLatin1String("applications:"))) { 0429 const QString storageId = sourceModel()->data(sourceModel()->index(row, 0), ResultModel::ResourceRole).toString().section(QLatin1Char(':'), 1); 0430 KService::Ptr service = KService::serviceByStorageId(storageId); 0431 0432 if (service) { 0433 return Kicker::handleRecentDocumentAction(service, actionId, argument); 0434 } 0435 } else { 0436 bool close = false; 0437 0438 QUrl url(sourceModel()->data(sourceModel()->index(row, 0), ResultModel::ResourceRole).toString()); 0439 0440 KFileItem item(url); 0441 0442 if (Kicker::handleFileItemAction(item, actionId, argument, &close)) { 0443 return close; 0444 } 0445 } 0446 } 0447 0448 return false; 0449 } 0450 0451 bool RecentUsageModel::hasActions() const 0452 { 0453 return rowCount(); 0454 } 0455 0456 QVariantList RecentUsageModel::actions() const 0457 { 0458 QVariantList actionList; 0459 0460 if (rowCount()) { 0461 actionList << Kicker::createActionItem(forgetAllActionName(), QStringLiteral("edit-clear-history"), QStringLiteral("forgetAll")); 0462 } 0463 0464 return actionList; 0465 } 0466 0467 QString RecentUsageModel::forgetAllActionName() const 0468 { 0469 switch (m_usage) { 0470 case AppsAndDocs: 0471 return i18n("Forget All"); 0472 case OnlyApps: 0473 return i18n("Forget All Applications"); 0474 case OnlyDocs: 0475 default: 0476 return i18n("Forget All Files"); 0477 } 0478 } 0479 0480 void RecentUsageModel::setOrdering(int ordering) 0481 { 0482 if (ordering == m_ordering) 0483 return; 0484 0485 m_ordering = (Ordering)ordering; 0486 refresh(); 0487 0488 Q_EMIT orderingChanged(ordering); 0489 } 0490 0491 int RecentUsageModel::ordering() const 0492 { 0493 return m_ordering; 0494 } 0495 0496 void RecentUsageModel::classBegin() 0497 { 0498 } 0499 0500 void RecentUsageModel::componentComplete() 0501 { 0502 m_complete = true; 0503 0504 refresh(); 0505 } 0506 0507 void RecentUsageModel::refresh() 0508 { 0509 if (qmlEngine(this) && !m_complete) { 0510 return; 0511 } 0512 0513 QAbstractItemModel *oldModel = sourceModel(); 0514 disconnectSignals(); 0515 setSourceModel(nullptr); 0516 delete oldModel; 0517 0518 // clang-format off 0519 auto query = UsedResources 0520 | (m_ordering == Recent ? RecentlyUsedFirst : HighScoredFirst) 0521 | Agent::any() 0522 | (m_usage == OnlyDocs ? Type::files() : Type::any()) 0523 | Activity::current(); 0524 // clang-format on 0525 0526 switch (m_usage) { 0527 case AppsAndDocs: { 0528 query = query | Url::startsWith(QStringLiteral("applications:")) | Url::file() | Limit(30); 0529 break; 0530 } 0531 case OnlyApps: { 0532 query = query | Url::startsWith(QStringLiteral("applications:")) | Limit(15); 0533 break; 0534 } 0535 case OnlyDocs: 0536 default: { 0537 query = query | Url::file() | Limit(15); 0538 } 0539 } 0540 0541 m_activitiesModel = new ResultModel(query); 0542 QAbstractItemModel *model = m_activitiesModel; 0543 0544 QModelIndex index; 0545 0546 if (model->canFetchMore(index)) { 0547 model->fetchMore(index); 0548 } 0549 0550 if (m_usage != OnlyDocs) { 0551 model = new InvalidAppsFilterProxy(this, model); 0552 } 0553 0554 if (m_usage == AppsAndDocs) { 0555 model = new GroupSortProxy(this, model); 0556 } 0557 0558 setSourceModel(model); 0559 }