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