1 /*
2     SPDX-FileCopyrightText: 2005 Aaron Seigo <aseigo@kde.org>
3     SPDX-FileCopyrightText: 2007 Riccardo Iaconelli <riccardo@kde.org>
4     SPDX-FileCopyrightText: 2008 Ménard Alexis <darktears31@gmail.com>
5     SPDX-FileCopyrightText: 2009 Chani Armitage <chani@kde.org>
6     SPDX-FileCopyrightText: 2012 Marco Martin <mart@kde.org>
7 
8     SPDX-License-Identifier: LGPL-2.0-or-later
9 */
10 
11 #include "private/applet_p.h"
12 
13 #include <config-plasma.h>
14 
15 #include <QDebug>
16 #include <QDir>
17 #include <QFile>
18 #include <QFileInfo>
19 #include <QJsonArray>
20 #include <QMessageBox>
21 #include <QStandardPaths>
22 #include <QTimer>
23 
24 #include <KConfigLoader>
25 #include <KGlobalAccel>
26 #include <KKeySequenceWidget>
27 #include <KLocalizedString>
28 #include <kpackage/packageloader.h>
29 
30 #include "containment.h"
31 #include "corona.h"
32 #include "debug_p.h"
33 #include "pluginloader.h"
34 #include "private/containment_p.h"
35 #if PLASMA_BUILD_DEPRECATED_SINCE(5, 83)
36 #include "private/package_p.h"
37 #endif
38 #include "scripting/appletscript.h"
39 #include "scripting/scriptengine.h"
40 #include "timetracker.h"
41 
42 namespace Plasma
43 {
AppletPrivate(const KPluginMetaData & info,int uniqueID,Applet * applet)44 AppletPrivate::AppletPrivate(const KPluginMetaData &info, int uniqueID, Applet *applet)
45     : appletId(uniqueID)
46     , q(applet)
47     , immutability(Types::Mutable)
48     , oldImmutability(Types::Mutable)
49     , appletDescription(info)
50     , icon(appletDescription.iconName())
51     , mainConfig(nullptr)
52     , pendingConstraints(Types::NoConstraint)
53     , script(nullptr)
54     , package(nullptr)
55     , configLoader(nullptr)
56     , actions(AppletPrivate::defaultActions(applet))
57     , activationAction(nullptr)
58     , itemStatus(Types::UnknownStatus)
59     , modificationsTimer(nullptr)
60     , deleteNotificationTimer(nullptr)
61     , hasConfigurationInterface(false)
62     , failed(false)
63     , transient(false)
64     , needsConfig(false)
65     , started(false)
66     , globalShortcutEnabled(false)
67     , userConfiguring(false)
68     , busy(false)
69 {
70     if (appletId == 0) {
71         appletId = ++s_maxAppletId;
72     } else if (appletId > s_maxAppletId) {
73         s_maxAppletId = appletId;
74     }
75     QObject::connect(actions->action(QStringLiteral("configure")), SIGNAL(triggered()), q, SLOT(requestConfiguration()));
76 #ifndef NDEBUG
77     if (qEnvironmentVariableIsSet("PLASMA_TRACK_STARTUP")) {
78         new TimeTracker(q);
79     }
80 #endif
81 }
82 
~AppletPrivate()83 AppletPrivate::~AppletPrivate()
84 {
85     if (deleteNotification) {
86         deleteNotification->close();
87     }
88 
89     delete script;
90     script = nullptr;
91     delete configLoader;
92     configLoader = nullptr;
93     delete mainConfig;
94     mainConfig = nullptr;
95     delete modificationsTimer;
96 }
97 
init(const QString & _packagePath,const QVariantList & args)98 void AppletPrivate::init(const QString &_packagePath, const QVariantList &args)
99 {
100     // WARNING: do not access config() OR globalConfig() in this method!
101     //          that requires a Corona, which is not available at this point
102     q->setHasConfigurationInterface(true);
103 
104     QAction *closeApplet = actions->action(QStringLiteral("remove"));
105     if (closeApplet) {
106         closeApplet->setText(i18nc("%1 is the name of the applet", "Remove %1", q->title()));
107     }
108 
109     QAction *configAction = actions->action(QStringLiteral("configure"));
110     if (configAction) {
111         configAction->setText(i18nc("%1 is the name of the applet", "Configure %1...", q->title().replace(QLatin1Char('&'), QStringLiteral("&&"))));
112     }
113 
114     if (!appletDescription.isValid()) {
115 #ifndef NDEBUG
116         // qCDebug(LOG_PLASMA) << "Check your constructor! "
117         //         << "You probably want to be passing in a Service::Ptr "
118         //         << "or a QVariantList with a valid storageid as arg[0].";
119 #endif
120         return;
121     }
122 
123     const QString api = appletDescription.value(QStringLiteral("X-Plasma-API"));
124 
125     if (api.isEmpty()) {
126         q->setLaunchErrorMessage(i18n("The %1 widget did not define which ScriptEngine to use.", appletDescription.name()));
127         return;
128     }
129 
130     // A constructor may have set a valid package already
131     if (!package.isValid()) {
132         const QString packagePath = _packagePath.isEmpty() && !appletDescription.metaDataFileName().isEmpty()
133             ? QFileInfo(appletDescription.metaDataFileName()).dir().path()
134             : _packagePath;
135         QString path = appletDescription.value(QStringLiteral("X-Plasma-RootPath"));
136         if (path.isEmpty()) {
137             path = packagePath.isEmpty() ? appletDescription.pluginId() : packagePath;
138         }
139 
140         package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"));
141         package.setPath(path);
142 
143         if (!package.isValid()) {
144             q->setLaunchErrorMessage(i18nc("Package file, name of the widget",
145                                            "Could not open the %1 package required for the %2 widget.",
146                                            appletDescription.pluginId(),
147                                            appletDescription.name()));
148             return;
149         }
150     }
151 
152     // now we try and set up the script engine.
153     // it will be parented to this applet and so will get
154     // deleted when the applet does
155     script = Plasma::loadScriptEngine(api, q, args);
156 
157     // It's valid, let's try to load the icon from within the package
158     if (script) {
159         // use the absolute path of the in-package icon as icon name
160         if (appletDescription.iconName().startsWith(QLatin1Char('/'))) {
161             icon = package.filePath({}, appletDescription.iconName());
162         }
163         // package not valid, get rid of it
164     } else {
165         q->setLaunchErrorMessage(i18nc("API or programming language the widget was written in, name of the widget",
166                                        "Could not create a %1 ScriptEngine for the %2 widget.",
167                                        api,
168                                        appletDescription.name()));
169     }
170 
171     if (!q->isContainment()) {
172         QAction *a = new QAction(QIcon::fromTheme(QStringLiteral("widget-alternatives")), i18n("Show Alternatives..."), q);
173         a->setVisible(false);
174         q->actions()->addAction(QStringLiteral("alternatives"), a);
175         QObject::connect(a, &QAction::triggered, q, [this] {
176             if (q->containment()) {
177                 Q_EMIT q->containment()->appletAlternativesRequested(q);
178             }
179         });
180 
181         QObject::connect(q, &Applet::contextualActionsAboutToShow, a, [=]() {
182             bool hasAlternatives = false;
183 
184             const QStringList provides = q->pluginMetaData().value(QStringLiteral("X-Plasma-Provides"), QStringList());
185             if (!provides.isEmpty() && q->immutability() == Types::Mutable) {
186                 auto filter = [&provides](const KPluginMetaData &md) -> bool {
187                     const QStringList provided = md.value(QStringLiteral("X-Plasma-Provides"), QStringList());
188                     for (const QString &p : provides) {
189                         if (provided.contains(p)) {
190                             return true;
191                         }
192                     }
193                     return false;
194                 };
195                 QList<KPluginMetaData> applets = KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/Applet"), QString(), filter);
196 
197                 if (applets.count() > 1) {
198                     hasAlternatives = true;
199                 }
200             }
201             a->setVisible(hasAlternatives);
202         });
203     }
204 }
205 
cleanUpAndDelete()206 void AppletPrivate::cleanUpAndDelete()
207 {
208     // reimplemented in the UI specific library
209     if (configLoader) {
210         configLoader->clearItems();
211     }
212 
213     resetConfigurationObject();
214 
215     if (activationAction && globalShortcutEnabled) {
216         // qCDebug(LOG_PLASMA) << "resetting global action for" << q->title() << activationAction->objectName();
217         KGlobalAccel::self()->removeAllShortcuts(activationAction);
218     }
219 
220     if (q->isContainment()) {
221         // prematurely emit our destruction if we are a Containment,
222         // giving Corona a chance to remove this Containment from its collection
223         Q_EMIT q->QObject::destroyed(q);
224     }
225 
226     q->deleteLater();
227 }
228 
setDestroyed(bool destroyed)229 void AppletPrivate::setDestroyed(bool destroyed)
230 {
231     transient = destroyed;
232     Q_EMIT q->destroyedChanged(destroyed);
233     // when an applet gets transient, it's "systemimmutable"
234     Q_EMIT q->immutabilityChanged(q->immutability());
235 
236     Plasma::Containment *asContainment = qobject_cast<Plasma::Containment *>(q);
237     if (asContainment) {
238         const auto lstApplets = asContainment->applets();
239         for (Applet *a : lstApplets) {
240             a->d->setDestroyed(destroyed);
241         }
242     }
243 }
244 
askDestroy()245 void AppletPrivate::askDestroy()
246 {
247     if (q->immutability() != Types::Mutable || !started) {
248         return; // don't double delete
249     }
250 
251     if (transient) {
252         cleanUpAndDelete();
253     } else {
254         // There is no confirmation anymore for panels removal:
255         // this needs users feedback
256         setDestroyed(true);
257         // no parent, but it won't leak, since it will be closed both in case of timeout
258         // or direct action
259         deleteNotification = new KNotification(QStringLiteral("plasmoidDeleted"));
260         deleteNotification->setFlags(KNotification::Persistent | KNotification::SkipGrouping);
261 
262         deleteNotification->setComponentName(QStringLiteral("plasma_workspace"));
263         QStringList actions;
264         deleteNotification->setIconName(q->icon());
265         Plasma::Containment *asContainment = qobject_cast<Plasma::Containment *>(q);
266 
267         if (!q->isContainment()) {
268             deleteNotification->setTitle(i18n("Widget Removed"));
269             deleteNotification->setText(i18n("The widget \"%1\" has been removed.", q->title().toHtmlEscaped()));
270         } else if (asContainment
271                    && (asContainment->containmentType() == Types::PanelContainment //
272                        || asContainment->containmentType() == Types::CustomPanelContainment)) {
273             deleteNotification->setTitle(i18n("Panel Removed"));
274             deleteNotification->setText(i18n("A panel has been removed."));
275             // This will never happen with our current shell, but could with a custom one
276         } else {
277             deleteNotification->setTitle(i18n("Desktop Removed"));
278             deleteNotification->setText(i18n("A desktop has been removed."));
279         }
280 
281         actions.append(i18n("Undo"));
282         deleteNotification->setActions(actions);
283         QObject::connect(deleteNotification.data(), &KNotification::action1Activated, q, [=]() {
284             setDestroyed(false);
285             if (!q->isContainment() && q->containment()) {
286                 Plasma::Applet *containmentApplet = static_cast<Plasma::Applet *>(q->containment());
287                 if (containmentApplet && containmentApplet->d->deleteNotificationTimer) {
288                     Q_EMIT containmentApplet->destroyedChanged(false);
289                     // when an applet gets transient, it's "systemimmutable"
290                     Q_EMIT q->immutabilityChanged(q->immutability());
291                     delete containmentApplet->d->deleteNotificationTimer;
292                     containmentApplet->d->deleteNotificationTimer = nullptr;
293                 }
294 
295                 // make sure the applets are sorted by id
296                 auto position =
297                     std::lower_bound(q->containment()->d->applets.begin(), q->containment()->d->applets.end(), q, [](Plasma::Applet *a1, Plasma::Applet *a2) {
298                         return a1->id() < a2->id();
299                     });
300                 q->containment()->d->applets.insert(position, q);
301                 Q_EMIT q->containment()->appletAdded(q);
302             }
303             if (deleteNotification) {
304                 deleteNotification->close();
305             } else if (deleteNotificationTimer) {
306                 deleteNotificationTimer->stop();
307                 deleteNotificationTimer->deleteLater();
308                 deleteNotificationTimer = nullptr;
309             }
310         });
311         QObject::connect(deleteNotification.data(), &KNotification::closed, q, [=]() {
312             // If the timer still exists, it means the undo action was NOT triggered
313             if (transient) {
314                 cleanUpAndDelete();
315             }
316             if (deleteNotificationTimer) {
317                 deleteNotificationTimer->stop();
318                 deleteNotificationTimer->deleteLater();
319                 deleteNotificationTimer = nullptr;
320             }
321         });
322 
323         deleteNotification->sendEvent();
324         if (!deleteNotificationTimer) {
325             deleteNotificationTimer = new QTimer(q);
326             // really delete after a minute
327             deleteNotificationTimer->setInterval(60 * 1000);
328             deleteNotificationTimer->setSingleShot(true);
329             QObject::connect(deleteNotificationTimer, &QTimer::timeout, q, [=]() {
330                 transient = true;
331                 if (deleteNotification) {
332                     deleteNotification->close();
333                 } else {
334                     Q_EMIT q->destroyedChanged(true);
335                     cleanUpAndDelete();
336                 }
337             });
338             deleteNotificationTimer->start();
339         }
340         if (!q->isContainment() && q->containment()) {
341             q->containment()->d->applets.removeAll(q);
342             Q_EMIT q->containment()->appletRemoved(q);
343         }
344     }
345 }
346 
globalShortcutChanged()347 void AppletPrivate::globalShortcutChanged()
348 {
349     if (!activationAction) {
350         return;
351     }
352     KConfigGroup shortcutConfig(mainConfigGroup(), "Shortcuts");
353     QString newShortCut = activationAction->shortcut().toString();
354     QString oldShortCut = shortcutConfig.readEntry("global", QString());
355     if (newShortCut != oldShortCut) {
356         shortcutConfig.writeEntry("global", newShortCut);
357         scheduleModificationNotification();
358     }
359     // qCDebug(LOG_PLASMA) << "after" << shortcut.primary() << d->activationAction->globalShortcut().primary();
360 }
361 
defaultActions(QObject * parent)362 KActionCollection *AppletPrivate::defaultActions(QObject *parent)
363 {
364     KActionCollection *actions = new KActionCollection(parent);
365     actions->setConfigGroup(QStringLiteral("Shortcuts-Applet"));
366 
367     QAction *configAction = actions->add<QAction>(QStringLiteral("configure"));
368     configAction->setAutoRepeat(false);
369     configAction->setText(i18n("Widget Settings"));
370     configAction->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
371     configAction->setShortcut(QKeySequence(QStringLiteral("alt+d, s")));
372     configAction->setData(Plasma::Types::ConfigureAction);
373 
374     QAction *closeApplet = actions->add<QAction>(QStringLiteral("remove"));
375     closeApplet->setAutoRepeat(false);
376     closeApplet->setText(i18n("Remove this Widget"));
377     closeApplet->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
378     closeApplet->setShortcut(QKeySequence(QStringLiteral("alt+d, r")));
379     closeApplet->setData(Plasma::Types::DestructiveAction);
380 
381     QAction *runAssociatedApplication = actions->add<QAction>(QStringLiteral("run associated application"));
382     runAssociatedApplication->setAutoRepeat(false);
383     runAssociatedApplication->setText(i18n("Run the Associated Application"));
384     runAssociatedApplication->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
385     runAssociatedApplication->setShortcut(QKeySequence(QStringLiteral("alt+d, t")));
386     runAssociatedApplication->setVisible(false);
387     runAssociatedApplication->setEnabled(false);
388     runAssociatedApplication->setData(Plasma::Types::ControlAction);
389 
390     return actions;
391 }
392 
requestConfiguration()393 void AppletPrivate::requestConfiguration()
394 {
395     if (q->containment()) {
396         Q_EMIT q->containment()->configureRequested(q);
397     }
398 }
399 
updateShortcuts()400 void AppletPrivate::updateShortcuts()
401 {
402     if (q->isContainment()) {
403         // a horrible hack to avoid clobbering corona settings
404         // we pull them out, then read, then put them back
405         QList<QAction *> qactions;
406         const QList<QString> names = {QStringLiteral("add sibling containment"), QStringLiteral("configure shortcuts"), QStringLiteral("lock widgets")};
407         for (const QString &name : names) {
408             QAction *a = actions->action(name);
409             actions->takeAction(a); // FIXME this is stupid, KActionCollection needs a takeAction(QString) method
410             qactions << a;
411         }
412 
413         actions->readSettings();
414 
415         for (int i = 0; i < names.size(); ++i) {
416             QAction *a = qactions.at(i);
417             if (a) {
418                 actions->addAction(names.at(i), a);
419             }
420         }
421     } else {
422         actions->readSettings();
423     }
424 }
425 
propagateConfigChanged()426 void AppletPrivate::propagateConfigChanged()
427 {
428     Containment *c = qobject_cast<Containment *>(q);
429     if (c) {
430         c->d->configChanged();
431     }
432     q->configChanged();
433 }
434 
setUiReady()435 void AppletPrivate::setUiReady()
436 {
437     // am i the containment?
438     Containment *c = qobject_cast<Containment *>(q);
439     if (c && c->isContainment()) {
440         c->d->setUiReady();
441     } else if (Containment *cc = q->containment()) {
442         cc->d->appletLoaded(q);
443     }
444 }
445 
446 // put all setup routines for script here. at this point we can assume that
447 // package exists and that we have a script engine
setupPackage()448 void AppletPrivate::setupPackage()
449 {
450     if (!package.isValid()) {
451         return;
452     }
453 
454 #ifndef NDEBUG
455     // qCDebug(LOG_PLASMA) << "setting up script support, package is in" << package->path()
456     //         << ", main script is" << package->filePath("mainscript");
457 #endif
458 
459     // FIXME: Replace with ki18n functionality once semantics is clear.
460     // const QString translationsPath = package->filePath("translations");
461     // if (!translationsPath.isEmpty()) {
462     //     KGlobal::dirs()->addResourceDir("locale", translationsPath);
463     // }
464 
465     if (!package.filePath("mainconfigui").isEmpty()) {
466         q->setHasConfigurationInterface(true);
467     }
468 }
469 
setupScripting()470 void AppletPrivate::setupScripting()
471 {
472     if (script) {
473         if (!script->init() && !failed) {
474             q->setLaunchErrorMessage(i18n("Script initialization failed"));
475         }
476     }
477 }
478 
globalName() const479 QString AppletPrivate::globalName() const
480 {
481     if (!appletDescription.isValid()) {
482         return QString();
483     }
484 
485     return appletDescription.pluginId();
486 }
487 
scheduleConstraintsUpdate(Plasma::Types::Constraints c)488 void AppletPrivate::scheduleConstraintsUpdate(Plasma::Types::Constraints c)
489 {
490     // Don't start up a timer if we're just starting up
491     // flushPendingConstraints will be called by Corona
492     if (started && !constraintsTimer.isActive() && !(c & Plasma::Types::StartupCompletedConstraint)) {
493         constraintsTimer.start(0, q);
494     }
495 
496     if (c & Plasma::Types::StartupCompletedConstraint) {
497         started = true;
498         if (q->isContainment()) {
499             qobject_cast<Containment *>(q)->d->setStarted();
500         }
501     }
502 
503     pendingConstraints |= c;
504 }
505 
scheduleModificationNotification()506 void AppletPrivate::scheduleModificationNotification()
507 {
508     // modificationsTimer is not allocated until we get our notice of being started
509     if (modificationsTimer) {
510         // schedule a save
511         modificationsTimer->start(1000, q);
512     }
513 }
514 
mainConfigGroup()515 KConfigGroup *AppletPrivate::mainConfigGroup()
516 {
517     if (mainConfig) {
518         return mainConfig;
519     }
520 
521     Containment *c = q->containment();
522     Plasma::Applet *parentApplet = nullptr;
523     if (c) {
524         parentApplet = qobject_cast<Plasma::Applet *>(c->parent());
525     }
526 
527     if (q->isContainment()) {
528         Corona *corona = static_cast<Containment *>(q)->corona();
529         KConfigGroup containmentConfig;
530         // qCDebug(LOG_PLASMA) << "got a corona, baby?" << (QObject*)corona << (QObject*)q;
531 
532         if (parentApplet) {
533             containmentConfig = parentApplet->config();
534             containmentConfig = KConfigGroup(&containmentConfig, "Containments");
535         } else if (corona) {
536             containmentConfig = KConfigGroup(corona->config(), "Containments");
537         } else {
538             containmentConfig = KConfigGroup(KSharedConfig::openConfig(), "Containments");
539         }
540 
541         mainConfig = new KConfigGroup(&containmentConfig, QString::number(appletId));
542     } else {
543         KConfigGroup appletConfig;
544 
545         if (c) {
546             // applet directly in a Containment, as usual
547             appletConfig = c->config();
548             appletConfig = KConfigGroup(&appletConfig, "Applets");
549         } else {
550             qCWarning(LOG_PLASMA) << "requesting config for" << q->title() << "without a containment!";
551             appletConfig = KConfigGroup(KSharedConfig::openConfig(), "Applets");
552         }
553 
554         mainConfig = new KConfigGroup(&appletConfig, QString::number(appletId));
555     }
556 
557     if (configLoader) {
558         configLoader->setSharedConfig(KSharedConfig::openConfig(mainConfig->config()->name()));
559         configLoader->load();
560     }
561 
562     return mainConfig;
563 }
564 
resetConfigurationObject()565 void AppletPrivate::resetConfigurationObject()
566 {
567     // make sure mainConfigGroup exists in all cases
568     mainConfigGroup();
569     mainConfig->deleteEntry("plugin");
570     mainConfig->deleteEntry("formfactor");
571     mainConfig->deleteEntry("immutability");
572     mainConfig->deleteEntry("location");
573     // if it's not a containment, deleting the non existing activityId entry does nothing
574     mainConfig->deleteEntry("activityId");
575     mainConfig->deleteGroup();
576     delete mainConfig;
577     mainConfig = nullptr;
578 
579     Containment *cont = qobject_cast<Containment *>(q);
580 
581     if (cont && cont->corona()) {
582         cont->corona()->requireConfigSync();
583     } else {
584         if (!q->containment()) {
585             return;
586         }
587         Corona *corona = q->containment()->corona();
588         if (corona) {
589             corona->requireConfigSync();
590         }
591     }
592 }
593 
594 uint AppletPrivate::s_maxAppletId = 0;
595 
596 } // namespace Plasma
597