File indexing completed on 2024-11-24 05:01:59

0001 /*
0002     SPDX-FileCopyrightText: 2007 Ivan Cukic <ivan.cukic+kde@gmail.com>
0003     SPDX-FileCopyrightText: 2009 Ana Cecília Martins <anaceciliamb@gmail.com>
0004     SPDX-FileCopyrightText: 2013 Sebastian Kügler <sebas@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "widgetexplorer.h"
0010 
0011 #include <QDebug>
0012 #include <QFileDialog>
0013 #include <QQmlContext>
0014 #include <QQmlEngine>
0015 #include <QQmlExpression>
0016 #include <QQmlProperty>
0017 
0018 #include <KAuthorized>
0019 #include <KLazyLocalizedString>
0020 #include <KLocalizedString>
0021 #include <KMessageBox>
0022 #include <KNSWidgets/Dialog>
0023 
0024 #include <Plasma/Applet>
0025 #include <Plasma/Containment>
0026 #include <Plasma/Corona>
0027 #include <Plasma/PluginLoader>
0028 #include <QStandardPaths>
0029 
0030 #include <PlasmaActivities/Consumer>
0031 
0032 #include <KPackage/Package>
0033 #include <KPackage/PackageJob>
0034 #include <KPackage/PackageLoader>
0035 #include <KPackage/PackageStructure>
0036 
0037 #include "config-workspace.h"
0038 #include "kcategorizeditemsviewmodels_p.h"
0039 
0040 using namespace KActivities;
0041 using namespace KCategorizedItemsViewModels;
0042 using namespace Plasma;
0043 
0044 WidgetAction::WidgetAction(QObject *parent)
0045     : QAction(parent)
0046 {
0047 }
0048 
0049 WidgetAction::WidgetAction(const QIcon &icon, const QString &text, QObject *parent)
0050     : QAction(icon, text, parent)
0051 {
0052 }
0053 
0054 class WidgetExplorerPrivate
0055 {
0056 public:
0057     WidgetExplorerPrivate(WidgetExplorer *w)
0058         : q(w)
0059         , containment(nullptr)
0060         , itemModel(w)
0061         , filterModel(w)
0062         , activitiesConsumer(new KActivities::Consumer())
0063     {
0064         QObject::connect(activitiesConsumer.get(), &Consumer::currentActivityChanged, q, [this] {
0065             initRunningApplets();
0066         });
0067     }
0068 
0069     void initFilters();
0070     void initRunningApplets();
0071     void screenAdded(int screen);
0072     void screenRemoved(int screen);
0073     void containmentDestroyed();
0074 
0075     void addContainment(Containment *containment);
0076     void removeContainment(Containment *containment);
0077 
0078     /**
0079      * Tracks a new running applet
0080      */
0081     void appletAdded(Plasma::Applet *applet);
0082 
0083     /**
0084      * A running applet is no more
0085      */
0086     void appletRemoved(Plasma::Applet *applet);
0087 
0088     WidgetExplorer *q;
0089     QString application;
0090     Plasma::Containment *containment;
0091 
0092     QHash<QString, int> runningApplets; // applet name => count
0093     // extra hash so we can look up the names of deleted applets
0094     QHash<Plasma::Applet *, QString> appletNames;
0095     KPackage::Package *package;
0096 
0097     PlasmaAppletItemModel itemModel;
0098     KCategorizedItemsViewModels::DefaultFilterModel filterModel;
0099     bool showSpecialFilters = true;
0100     DefaultItemFilterProxyModel filterItemModel;
0101     static QPointer<KNSWidgets::Dialog> newStuffDialog;
0102 
0103     std::unique_ptr<KActivities::Consumer> activitiesConsumer;
0104 };
0105 
0106 QPointer<KNSWidgets::Dialog> WidgetExplorerPrivate::newStuffDialog;
0107 
0108 QString readTranslatedCategory(const QString &category, const QString &plugin)
0109 {
0110     static const QList<KLazyLocalizedString> possibleTranslatslations{
0111         kli18nc("applet category", "Accessibility"),
0112         kli18nc("applet category", "Application Launchers"),
0113         kli18nc("applet category", "Astronomy"),
0114         kli18nc("applet category", "Date and Time"),
0115         kli18nc("applet category", "Development Tools"),
0116         kli18nc("applet category", "Education"),
0117         kli18nc("applet category", "Environment and Weather"),
0118         kli18nc("applet category", "Examples"),
0119         kli18nc("applet category", "File System"),
0120         kli18nc("applet category", "Fun and Games"),
0121         kli18nc("applet category", "Graphics"),
0122         kli18nc("applet category", "Language"),
0123         kli18nc("applet category", "Mapping"),
0124         kli18nc("applet category", "Miscellaneous"),
0125         kli18nc("applet category", "Multimedia"),
0126         kli18nc("applet category", "Online Services"),
0127         kli18nc("applet category", "Productivity"),
0128         kli18nc("applet category", "System Information"),
0129         kli18nc("applet category", "Utilities"),
0130         kli18nc("applet category", "Windows and Tasks"),
0131         kli18nc("applet category", "Clipboard"),
0132         kli18nc("applet category", "Tasks"),
0133     };
0134     const auto it = std::find_if(possibleTranslatslations.begin(), possibleTranslatslations.end(), [&category](const KLazyLocalizedString &str) {
0135         return category == str.untranslatedText();
0136     });
0137     if (it == possibleTranslatslations.cend()) {
0138         qDebug() << category << "from" << plugin << "is not a known category that can be translated ";
0139         return category;
0140     }
0141     return it->toString();
0142 }
0143 
0144 void WidgetExplorerPrivate::initFilters()
0145 {
0146     filterModel.clear();
0147 
0148     filterModel.addFilter(i18n("All Widgets"), KCategorizedItemsViewModels::Filter(), QIcon::fromTheme(QStringLiteral("plasma")));
0149 
0150     if (showSpecialFilters) {
0151         // Filters: Special
0152         filterModel.addFilter(i18n("Running"),
0153                               KCategorizedItemsViewModels::Filter(QStringLiteral("running"), true),
0154                               QIcon::fromTheme(QStringLiteral("dialog-ok")));
0155 
0156         filterModel.addFilter(i18nc("@item:inmenu used in the widget filter. Filter widgets that can be un-installed from the system, which are usually "
0157                                     "installed by the user to a local place.",
0158                                     "Uninstallable"),
0159                               KCategorizedItemsViewModels::Filter(QStringLiteral("local"), true),
0160                               QIcon::fromTheme(QStringLiteral("edit-delete")));
0161 
0162         filterModel.addSeparator(i18n("Categories:"));
0163     }
0164 
0165     struct CategoryInfo {
0166         QString untranslated;
0167         QString translated;
0168     };
0169     std::vector<CategoryInfo> categories;
0170     categories.reserve(itemModel.rowCount());
0171     for (int i = 0; i < itemModel.rowCount(); ++i) {
0172         if (PlasmaAppletItem *p = dynamic_cast<PlasmaAppletItem *>(itemModel.item(i))) {
0173             const QString translated = readTranslatedCategory(p->category(), p->pluginName());
0174             if (!translated.isEmpty()) {
0175                 categories.push_back({p->category(), translated});
0176             }
0177         }
0178     }
0179     std::sort(categories.begin(), categories.end(), [](const CategoryInfo &left, const CategoryInfo &right) {
0180         return left.translated < right.translated;
0181     });
0182     auto end = std::unique(categories.begin(), categories.end(), [](const CategoryInfo left, const CategoryInfo right) {
0183         return left.translated == right.translated;
0184     });
0185     std::for_each(categories.begin(), end, [this](const CategoryInfo &category) {
0186         filterModel.addFilter(category.translated, KCategorizedItemsViewModels::Filter(QStringLiteral("category"), category.untranslated.toLower()));
0187     });
0188 }
0189 
0190 void WidgetExplorer::classBegin()
0191 {
0192 }
0193 
0194 void WidgetExplorer::componentComplete()
0195 {
0196     d->itemModel.setStartupCompleted(true);
0197     setApplication();
0198     d->initRunningApplets();
0199 }
0200 
0201 QObject *WidgetExplorer::widgetsModel() const
0202 {
0203     return &d->filterItemModel;
0204 }
0205 
0206 QObject *WidgetExplorer::filterModel() const
0207 {
0208     return &d->filterModel;
0209 }
0210 
0211 bool WidgetExplorer::showSpecialFilters() const
0212 {
0213     return d->showSpecialFilters;
0214 }
0215 
0216 void WidgetExplorer::setShowSpecialFilters(bool show)
0217 {
0218     if (d->showSpecialFilters != show) {
0219         d->showSpecialFilters = show;
0220         d->initFilters();
0221         Q_EMIT showSpecialFiltersChanged();
0222     }
0223 }
0224 
0225 QList<QObject *> WidgetExplorer::widgetsMenuActions()
0226 {
0227     QList<QObject *> actionList;
0228 
0229     WidgetAction *action = nullptr;
0230 
0231     if (KAuthorized::authorize(KAuthorized::GHNS)) {
0232         action = new WidgetAction(QIcon::fromTheme(QStringLiteral("internet-services")), i18n("Download New Plasma Widgets"), this);
0233         connect(action, &QAction::triggered, this, &WidgetExplorer::downloadWidgets);
0234         actionList << action;
0235     }
0236 
0237     action = new WidgetAction(this);
0238     action->setSeparator(true);
0239     actionList << action;
0240 
0241     action = new WidgetAction(QIcon::fromTheme(QStringLiteral("package-x-generic")), i18n("Install Widget From Local File…"), this);
0242     QObject::connect(action, &QAction::triggered, this, &WidgetExplorer::openWidgetFile);
0243     actionList << action;
0244 
0245     return actionList;
0246 }
0247 
0248 void WidgetExplorerPrivate::initRunningApplets()
0249 {
0250     // get applets from corona, count them, send results to model
0251     if (!containment) {
0252         return;
0253     }
0254 
0255     Plasma::Corona *c = containment->corona();
0256 
0257     // we've tried our best to get a corona
0258     // we don't want just one containment, we want them all
0259     if (!c) {
0260         qWarning() << "WidgetExplorer failed to find corona";
0261         return;
0262     }
0263     appletNames.clear();
0264     runningApplets.clear();
0265 
0266     QObject::connect(c, &Plasma::Corona::screenAdded, q, [this](int screen) {
0267         screenAdded(screen);
0268     });
0269     QObject::connect(c, &Plasma::Corona::screenRemoved, q, [this](int screen) {
0270         screenRemoved(screen);
0271     });
0272 
0273     const QList<Containment *> containments = c->containments();
0274     for (Containment *containment : containments) {
0275         if (containment->containmentType() == Plasma::Containment::Desktop && containment->activity() != activitiesConsumer->currentActivity()) {
0276             continue;
0277         }
0278         if (containment->screen() != -1) {
0279             addContainment(containment);
0280         }
0281     }
0282 
0283     // qDebug() << runningApplets;
0284     itemModel.setRunningApplets(runningApplets);
0285 }
0286 
0287 void WidgetExplorerPrivate::screenAdded(int screen)
0288 {
0289     const QList<Containment *> containments = containment->corona()->containments();
0290     for (auto c : containments) {
0291         if (c->screen() == screen) {
0292             addContainment(c);
0293         }
0294     }
0295     itemModel.setRunningApplets(runningApplets);
0296 }
0297 
0298 void WidgetExplorerPrivate::screenRemoved(int screen)
0299 {
0300     const QList<Containment *> containments = containment->corona()->containments();
0301     for (auto c : containments) {
0302         if (c->lastScreen() == screen) {
0303             removeContainment(c);
0304         }
0305     }
0306     itemModel.setRunningApplets(runningApplets);
0307 }
0308 
0309 void WidgetExplorerPrivate::addContainment(Containment *containment)
0310 {
0311     QObject::connect(containment, &Plasma::Containment::appletAdded, q, [this](Plasma::Applet *a, const QRectF &) {
0312         appletAdded(a);
0313     });
0314     QObject::connect(containment, &Plasma::Containment::appletRemoved, q, [this](Plasma::Applet *a) {
0315         appletRemoved(a);
0316     });
0317 
0318     const QList<Applet *> applets = containment->applets();
0319     for (auto applet : applets) {
0320         if (applet->pluginMetaData().isValid()) {
0321             runningApplets[applet->pluginMetaData().pluginId()]++;
0322         } else {
0323             qDebug() << "Invalid plugin metadata. :(";
0324         }
0325     }
0326 }
0327 
0328 void WidgetExplorerPrivate::removeContainment(Plasma::Containment *containment)
0329 {
0330     containment->disconnect(q);
0331     const QList<Applet *> applets = containment->applets();
0332     for (auto applet : applets) {
0333         if (applet->pluginMetaData().isValid()) {
0334             Containment *childContainment = applet->property("containment").value<Containment *>();
0335             if (childContainment) {
0336                 removeContainment(childContainment);
0337             }
0338             runningApplets[applet->pluginMetaData().pluginId()]--;
0339         }
0340     }
0341 }
0342 
0343 void WidgetExplorerPrivate::containmentDestroyed()
0344 {
0345     containment = nullptr;
0346 }
0347 
0348 void WidgetExplorerPrivate::appletAdded(Plasma::Applet *applet)
0349 {
0350     if (!applet->pluginMetaData().isValid()) {
0351         return;
0352     }
0353     QString name = applet->pluginMetaData().pluginId();
0354 
0355     runningApplets[name]++;
0356     appletNames.insert(applet, name);
0357     itemModel.setRunningApplets(name, runningApplets[name]);
0358 }
0359 
0360 void WidgetExplorerPrivate::appletRemoved(Plasma::Applet *applet)
0361 {
0362     QString name = appletNames.take(applet);
0363 
0364     int count = 0;
0365     if (runningApplets.contains(name)) {
0366         count = runningApplets[name] - 1;
0367 
0368         if (count < 1) {
0369             runningApplets.remove(name);
0370         } else {
0371             runningApplets[name] = count;
0372         }
0373     }
0374 
0375     itemModel.setRunningApplets(name, count);
0376 }
0377 
0378 // WidgetExplorer
0379 
0380 WidgetExplorer::WidgetExplorer(QObject *parent)
0381     : QObject(parent)
0382     , d(new WidgetExplorerPrivate(this))
0383 {
0384     d->filterItemModel.setSortCaseSensitivity(Qt::CaseInsensitive);
0385     d->filterItemModel.setDynamicSortFilter(true);
0386     d->filterItemModel.setSourceModel(&d->itemModel);
0387     d->filterItemModel.sort(0);
0388 }
0389 
0390 WidgetExplorer::~WidgetExplorer()
0391 {
0392     delete d;
0393 }
0394 
0395 void WidgetExplorer::setApplication(const QString &app)
0396 {
0397     if (d->application == app && !app.isEmpty()) {
0398         return;
0399     }
0400 
0401     d->application = app;
0402     d->itemModel.setApplication(app);
0403     d->initFilters();
0404 
0405     d->itemModel.setRunningApplets(d->runningApplets);
0406     Q_EMIT applicationChanged();
0407 }
0408 
0409 QString WidgetExplorer::application()
0410 {
0411     return d->application;
0412 }
0413 
0414 QStringList WidgetExplorer::provides() const
0415 {
0416     return d->itemModel.provides();
0417 }
0418 
0419 void WidgetExplorer::setProvides(const QStringList &provides)
0420 {
0421     if (d->itemModel.provides() == provides) {
0422         return;
0423     }
0424 
0425     d->itemModel.setProvides(provides);
0426     Q_EMIT providesChanged();
0427 }
0428 
0429 void WidgetExplorer::setContainment(Plasma::Containment *containment)
0430 {
0431     if (d->containment != containment) {
0432         if (d->containment) {
0433             d->containment->disconnect(this);
0434         }
0435 
0436         d->containment = containment;
0437 
0438         if (d->containment) {
0439             connect(d->containment, SIGNAL(destroyed(QObject *)), this, SLOT(containmentDestroyed()));
0440             connect(d->containment, &Applet::immutabilityChanged, this, &WidgetExplorer::immutabilityChanged);
0441         }
0442 
0443         d->initRunningApplets();
0444         Q_EMIT containmentChanged();
0445     }
0446 }
0447 
0448 Containment *WidgetExplorer::containment() const
0449 {
0450     return d->containment;
0451 }
0452 
0453 Plasma::Corona *WidgetExplorer::corona() const
0454 {
0455     if (d->containment) {
0456         return d->containment->corona();
0457     }
0458 
0459     return nullptr;
0460 }
0461 
0462 void WidgetExplorer::addApplet(const QString &pluginName)
0463 {
0464     const QString p = PLASMA_RELATIVE_DATA_INSTALL_DIR "/plasmoids/" + pluginName;
0465     qWarning() << "-------->  load applet: " << pluginName << " relpath: " << p;
0466 
0467     QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, p, QStandardPaths::LocateDirectory);
0468 
0469     qDebug() << " .. pathes: " << dirs;
0470     if (!dirs.count()) {
0471         qWarning() << "Failed to find plasmoid path for " << pluginName;
0472         return;
0473     }
0474 
0475     if (d->containment) {
0476         d->containment->createApplet(dirs.first());
0477     }
0478 }
0479 
0480 void WidgetExplorer::immutabilityChanged(Plasma::Types::ImmutabilityType type)
0481 {
0482     if (type != Plasma::Types::Mutable) {
0483         Q_EMIT shouldClose();
0484     }
0485 }
0486 
0487 void WidgetExplorer::downloadWidgets()
0488 {
0489     if (WidgetExplorerPrivate::newStuffDialog.isNull()) {
0490         WidgetExplorerPrivate::newStuffDialog = new KNSWidgets::Dialog(QStringLiteral("plasmoids.knsrc"));
0491         connect(WidgetExplorerPrivate::newStuffDialog, &KNSWidgets::Dialog::finished, WidgetExplorerPrivate::newStuffDialog, &QObject::deleteLater);
0492 
0493         WidgetExplorerPrivate::newStuffDialog->open();
0494     }
0495 
0496     Q_EMIT shouldClose();
0497 }
0498 
0499 void WidgetExplorer::openWidgetFile()
0500 {
0501     QFileDialog *dialog = new QFileDialog;
0502     dialog->setMimeTypeFilters({"application/x-plasma"});
0503     dialog->setWindowTitle(i18n("Select Plasmoid File"));
0504     dialog->setFileMode(QFileDialog::ExistingFile);
0505     dialog->setAttribute(Qt::WA_DeleteOnClose, true);
0506 
0507     connect(dialog, &QFileDialog::fileSelected, [](const QString &packageFilePath) {
0508         if (packageFilePath.isEmpty()) {
0509             // TODO: user visible error handling
0510             qDebug() << "hm. no file path?";
0511             return;
0512         }
0513 
0514         auto job = KPackage::PackageJob::install(QStringLiteral("Plasma/Applet"), packageFilePath);
0515         connect(job, &KJob::result, [packageFilePath](KJob *job) {
0516             if (job->error()) {
0517                 KMessageBox::error(nullptr, i18n("Installing the package %1 failed.", packageFilePath), i18n("Installation Failure"));
0518             }
0519         });
0520     });
0521 
0522     dialog->show();
0523 
0524     Q_EMIT shouldClose();
0525 }
0526 
0527 void WidgetExplorer::uninstall(const QString &pluginName)
0528 {
0529     static const QString packageRoot =
0530         QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + PLASMA_RELATIVE_DATA_INSTALL_DIR "/plasmoids/";
0531     KPackage::PackageJob *job = KPackage::PackageJob::uninstall(QStringLiteral("Plasma/Applet"), pluginName, packageRoot);
0532     // This removes folders of packages that are *not* valid so the package job can't uninstall them
0533     // This is a bit dangerous so eventually we can drop this when we drop support for uninstalling plasma5 plasmoids
0534     if (!job->package().isValid() && !pluginName.isEmpty()) {
0535         QDir dir(packageRoot + "/" + pluginName);
0536         if (dir.exists()) {
0537             dir.removeRecursively();
0538         }
0539     }
0540 
0541     // FIXME: moreefficient way rather a linear scan?
0542     for (int i = 0; i < d->itemModel.rowCount(); ++i) {
0543         QStandardItem *item = d->itemModel.item(i);
0544         if (item->data(PlasmaAppletItemModel::PluginNameRole).toString() == pluginName) {
0545             d->itemModel.takeRow(i);
0546             break;
0547         }
0548     }
0549 
0550     // now remove all instances of that applet
0551     if (corona()) {
0552         const auto &containments = corona()->containments();
0553 
0554         for (Containment *c : containments) {
0555             const auto &applets = c->applets();
0556 
0557             for (Applet *applet : applets) {
0558                 const auto &appletInfo = applet->pluginMetaData();
0559 
0560                 if (appletInfo.isValid() && appletInfo.pluginId() == pluginName) {
0561                     applet->destroy();
0562                 }
0563             }
0564         }
0565     }
0566 }
0567 
0568 #include "moc_widgetexplorer.cpp"