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"