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 }