1 /*
2     SPDX-FileCopyrightText: 2008 Chani Armitage <chani@kde.org>
3     SPDX-FileCopyrightText: 2008, 2009 Aaron Seigo <aseigo@kde.org>
4     SPDX-FileCopyrightText: 2010 Marco Martin <mart@kde.org>
5 
6     SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 #include "containmentinterface.h"
10 #include "dropmenu.h"
11 #include "wallpaperinterface.h"
12 #include <kdeclarative/qmlobject.h>
13 
14 #include <QApplication>
15 #include <QClipboard>
16 #include <QMimeData>
17 #include <QQmlExpression>
18 #include <QQmlProperty>
19 #include <QScreen>
20 #include <QVersionNumber>
21 
22 #include <KAcceleratorManager>
23 #include <KActionCollection>
24 #include <KAuthorized>
25 #include <KLocalizedString>
26 #include <KNotification>
27 #include <KUrlMimeData>
28 #include <QDebug>
29 #include <QMimeDatabase>
30 
31 #include <KIO/DropJob>
32 #include <KIO/MimetypeJob>
33 #include <KIO/TransferJob>
34 
35 #include <Plasma/ContainmentActions>
36 #include <Plasma/Corona>
37 #include <Plasma/PluginLoader>
38 #include <plasma.h>
39 
40 #include <KPackage/Package>
41 #include <KPackage/PackageLoader>
42 
43 #include <kactivities/info.h>
44 
45 #include <packageurlinterceptor.h>
46 
ContainmentInterface(DeclarativeAppletScript * parent,const QVariantList & args)47 ContainmentInterface::ContainmentInterface(DeclarativeAppletScript *parent, const QVariantList &args)
48     : AppletInterface(parent, args)
49     , m_wallpaperInterface(nullptr)
50     , m_activityInfo(nullptr)
51     , m_wheelDelta(0)
52 {
53     m_containment = static_cast<Plasma::Containment *>(appletScript()->applet()->containment());
54 
55     setAcceptedMouseButtons(Qt::AllButtons);
56 
57     connect(m_containment.data(), &Plasma::Containment::appletRemoved, this, &ContainmentInterface::appletRemovedForward);
58     connect(m_containment.data(), &Plasma::Containment::appletAdded, this, &ContainmentInterface::appletAddedForward);
59 
60     connect(m_containment->corona(), &Plasma::Corona::editModeChanged, this, &ContainmentInterface::editModeChanged);
61 
62     if (!m_appletInterfaces.isEmpty()) {
63         Q_EMIT appletsChanged();
64     }
65 }
66 
init()67 void ContainmentInterface::init()
68 {
69     if (qmlObject()->rootObject()) {
70         return;
71     }
72 
73     m_activityInfo = new KActivities::Info(m_containment->activity(), this);
74     connect(m_activityInfo, &KActivities::Info::nameChanged, this, &ContainmentInterface::activityNameChanged);
75     Q_EMIT activityNameChanged();
76 
77     if (!m_containment->wallpaper().isEmpty()) {
78         loadWallpaper();
79     }
80 
81     AppletInterface::init();
82 
83     // Create the ToolBox
84     if (m_containment) {
85         KConfigGroup defaults;
86         if (m_containment->containmentType() == Plasma::Types::DesktopContainment) {
87             defaults = KConfigGroup(KSharedConfig::openConfig(m_containment->corona()->kPackage().filePath("defaults")), "Desktop");
88         } else if (m_containment->containmentType() == Plasma::Types::PanelContainment) {
89             defaults = KConfigGroup(KSharedConfig::openConfig(m_containment->corona()->kPackage().filePath("defaults")), "Panel");
90         }
91 
92         if (defaults.isValid()) {
93             KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("KPackage/GenericQML"));
94             pkg.setDefaultPackageRoot(QStringLiteral("plasma/packages"));
95 
96             if (defaults.isValid()) {
97                 pkg.setPath(defaults.readEntry("ToolBox", "org.kde.desktoptoolbox"));
98             } else {
99                 pkg.setPath(QStringLiteral("org.kde.desktoptoolbox"));
100             }
101 
102             PlasmaQuick::PackageUrlInterceptor *interceptor = dynamic_cast<PlasmaQuick::PackageUrlInterceptor *>(qmlObject()->engine()->urlInterceptor());
103             if (interceptor) {
104                 interceptor->addAllowedPath(pkg.path());
105             }
106 
107             if (pkg.metadata().isValid() && !pkg.metadata().isHidden()) {
108                 if (pkg.isValid()) {
109                     QObject *containmentGraphicObject = qmlObject()->rootObject();
110 
111                     QVariantHash toolboxProperties;
112                     toolboxProperties[QStringLiteral("parent")] = QVariant::fromValue(this);
113                     QObject *toolBoxObject = qmlObject()->createObjectFromSource(pkg.fileUrl("mainscript"), nullptr, toolboxProperties);
114                     if (toolBoxObject && containmentGraphicObject) {
115                         containmentGraphicObject->setProperty("toolBox", QVariant::fromValue(toolBoxObject));
116                     }
117                 } else {
118                     qWarning() << "Could not load toolbox package." << pkg.path();
119                 }
120             } else {
121                 qWarning() << "Toolbox not loading, toolbox package is either invalid or disabled.";
122             }
123         }
124     }
125 
126     // set parent, both as object hierarchically and visually
127     // do this only for containments, applets will do it in compactrepresentationcheck
128     if (qmlObject()->rootObject()) {
129         qmlObject()->rootObject()->setProperty("parent", QVariant::fromValue(this));
130 
131         // set anchors
132         QQmlExpression expr(qmlObject()->engine()->rootContext(), qmlObject()->rootObject(), QStringLiteral("parent"));
133         QQmlProperty prop(qmlObject()->rootObject(), QStringLiteral("anchors.fill"));
134         prop.write(expr.evaluate());
135     }
136 
137     connect(m_containment.data(), &Plasma::Containment::activityChanged, this, &ContainmentInterface::activityChanged);
138     connect(m_containment.data(), &Plasma::Containment::activityChanged, this, [=]() {
139         delete m_activityInfo;
140         m_activityInfo = new KActivities::Info(m_containment->activity(), this);
141         connect(m_activityInfo, &KActivities::Info::nameChanged, this, &ContainmentInterface::activityNameChanged);
142         Q_EMIT activityNameChanged();
143     });
144     connect(m_containment.data(), &Plasma::Containment::wallpaperChanged, this, &ContainmentInterface::loadWallpaper);
145     connect(m_containment.data(), &Plasma::Containment::containmentTypeChanged, this, &ContainmentInterface::containmentTypeChanged);
146 
147     connect(m_containment.data()->actions(), &KActionCollection::changed, this, &ContainmentInterface::actionsChanged);
148 }
149 
applets()150 QList<QObject *> ContainmentInterface::applets()
151 {
152     return m_appletInterfaces;
153 }
154 
containmentType() const155 Plasma::Types::ContainmentType ContainmentInterface::containmentType() const
156 {
157     return appletScript()->containmentType();
158 }
159 
setContainmentType(Plasma::Types::ContainmentType type)160 void ContainmentInterface::setContainmentType(Plasma::Types::ContainmentType type)
161 {
162     appletScript()->setContainmentType(type);
163 }
164 
createApplet(const QString & plugin,const QVariantList & args,const QPoint & pos)165 Plasma::Applet *ContainmentInterface::createApplet(const QString &plugin, const QVariantList &args, const QPoint &pos)
166 {
167     return createApplet(plugin, args, QRectF(pos, QSize()));
168 }
169 
createApplet(const QString & plugin,const QVariantList & args,const QRectF & geom)170 Plasma::Applet *ContainmentInterface::createApplet(const QString &plugin, const QVariantList &args, const QRectF &geom)
171 {
172     // HACK
173     // This is necessary to delay the appletAdded signal (of containmentInterface) AFTER the applet graphics object has been created
174     blockSignals(true);
175     Plasma::Applet *applet = m_containment->createApplet(plugin, args);
176 
177     if (applet) {
178         QQuickItem *appletGraphicObject = applet->property("_plasma_graphicObject").value<QQuickItem *>();
179         // invalid applet?
180         if (!appletGraphicObject) {
181             blockSignals(false);
182             return applet;
183         }
184         if (geom.width() > 0 && geom.height() > 0) {
185             appletGraphicObject->setSize(geom.size());
186         }
187 
188         blockSignals(false);
189 
190         Q_EMIT appletAdded(appletGraphicObject, geom.x(), geom.y());
191         Q_EMIT appletsChanged();
192     } else {
193         blockSignals(false);
194     }
195     return applet;
196 }
197 
setAppletArgs(Plasma::Applet * applet,const QString & mimetype,const QString & data)198 void ContainmentInterface::setAppletArgs(Plasma::Applet *applet, const QString &mimetype, const QString &data)
199 {
200     if (!applet) {
201         return;
202     }
203 
204     AppletInterface *appletInterface = applet->property("_plasma_graphicObject").value<AppletInterface *>();
205     if (appletInterface) {
206         Q_EMIT appletInterface->externalData(mimetype, data);
207     }
208 }
209 
containmentAt(int x,int y)210 QObject *ContainmentInterface::containmentAt(int x, int y)
211 {
212     QObject *desktop = nullptr;
213     const auto lst = m_containment->corona()->containments();
214     for (Plasma::Containment *c : lst) {
215         ContainmentInterface *contInterface = c->property("_plasma_graphicObject").value<ContainmentInterface *>();
216 
217         if (contInterface && contInterface->isVisible()) {
218             QWindow *w = contInterface->window();
219             if (w && w->geometry().contains(QPoint(window()->x(), window()->y()) + QPoint(x, y))) {
220                 if (c->containmentType() == Plasma::Types::CustomEmbeddedContainment) {
221                     continue;
222                 }
223                 if (c->containmentType() == Plasma::Types::DesktopContainment) {
224                     desktop = contInterface;
225                 } else {
226                     return contInterface;
227                 }
228             }
229         }
230     }
231     return desktop;
232 }
233 
addApplet(AppletInterface * applet,int x,int y)234 void ContainmentInterface::addApplet(AppletInterface *applet, int x, int y)
235 {
236     if (!applet || applet->applet()->containment() == m_containment) {
237         return;
238     }
239 
240     blockSignals(true);
241     m_containment->addApplet(applet->applet());
242     blockSignals(false);
243     Q_EMIT appletAdded(applet, x, y);
244 }
245 
mapFromApplet(AppletInterface * applet,int x,int y)246 QPointF ContainmentInterface::mapFromApplet(AppletInterface *applet, int x, int y)
247 {
248     if (!applet->window() || !window()) {
249         return QPointF();
250     }
251 
252     // x,y in absolute screen coordinates of current view
253     QPointF pos = applet->mapToScene(QPointF(x, y));
254     pos = QPointF(pos + applet->window()->geometry().topLeft());
255     // return the coordinate in the relative view's coords
256     return pos - window()->geometry().topLeft();
257 }
258 
mapToApplet(AppletInterface * applet,int x,int y)259 QPointF ContainmentInterface::mapToApplet(AppletInterface *applet, int x, int y)
260 {
261     if (!applet->window() || !window()) {
262         return QPointF();
263     }
264 
265     // x,y in absolute screen coordinates of current view
266     QPointF pos(x, y);
267     pos = QPointF(pos + window()->geometry().topLeft());
268     // the coordinate in the relative view's coords
269     pos = pos - applet->window()->geometry().topLeft();
270     // make it relative to applet coords
271     return pos - applet->mapToScene(QPointF(0, 0));
272 }
273 
adjustToAvailableScreenRegion(int x,int y,int w,int h) const274 QPointF ContainmentInterface::adjustToAvailableScreenRegion(int x, int y, int w, int h) const
275 {
276     QRegion reg;
277     int screenId = screen();
278     if (screenId > -1 && m_containment->corona()) {
279         reg = m_containment->corona()->availableScreenRegion(screenId);
280     }
281 
282     if (!reg.isEmpty()) {
283         // make it relative
284         QRect geometry = m_containment->corona()->screenGeometry(screenId);
285         reg.translate(-geometry.topLeft());
286     } else {
287         reg = QRect(0, 0, width(), height());
288     }
289 
290     const QRect rect(qBound(reg.boundingRect().left(), x, reg.boundingRect().right() + 1 - w),
291                      qBound(reg.boundingRect().top(), y, reg.boundingRect().bottom() + 1 - h),
292                      w,
293                      h);
294     const QRectF ar = availableScreenRect();
295     QRect tempRect(rect);
296 
297     // in the case we are in the topleft quadrant
298     // * see if the passed rect is completely in the region, if yes, return
299     // * otherwise, try to move it horizontally to the screenrect x
300     // * if now fits, return
301     // * if fail, move vertically
302     // * as last resort, move horizontally and vertically
303 
304     // top left corner
305     if (rect.center().x() <= ar.center().x() && rect.center().y() <= ar.center().y()) {
306         // QRegion::contains doesn't do what it would suggest, so do reg.intersected(rect) != rect instead
307         if (reg.intersected(rect) != rect) {
308             tempRect = QRect(qMax(rect.left(), (int)ar.left()), rect.top(), w, h);
309             if (reg.intersected(tempRect) == tempRect) {
310                 return tempRect.topLeft();
311             }
312 
313             tempRect = QRect(rect.left(), qMax(rect.top(), (int)ar.top()), w, h);
314             if (reg.intersected(tempRect) == tempRect) {
315                 return tempRect.topLeft();
316             }
317 
318             tempRect = QRect(qMax(rect.left(), (int)ar.left()), qMax(rect.top(), (int)ar.top()), w, h);
319             return tempRect.topLeft();
320         } else {
321             return rect.topLeft();
322         }
323 
324         // bottom left corner
325     } else if (rect.center().x() <= ar.center().x() && rect.center().y() > ar.center().y()) {
326         if (reg.intersected(rect) != rect) {
327             tempRect = QRect(qMax(rect.left(), (int)ar.left()), rect.top(), w, h);
328             if (reg.intersected(tempRect) == tempRect) {
329                 return tempRect.topLeft();
330             }
331 
332             tempRect = QRect(rect.left(), qMin(rect.top(), (int)(ar.bottom() + 1 - h)), w, h);
333             if (reg.intersected(tempRect) == tempRect) {
334                 return tempRect.topLeft();
335             }
336 
337             tempRect = QRect(qMax(rect.left(), (int)ar.left()), qMin(rect.top(), (int)(ar.bottom() + 1 - h)), w, h);
338             return tempRect.topLeft();
339         } else {
340             return rect.topLeft();
341         }
342 
343         // top right corner
344     } else if (rect.center().x() > ar.center().x() && rect.center().y() <= ar.center().y()) {
345         if (reg.intersected(rect) != rect) {
346             tempRect = QRect(qMin(rect.left(), (int)(ar.right() + 1 - w)), rect.top(), w, h);
347             if (reg.intersected(tempRect) == tempRect) {
348                 return tempRect.topLeft();
349             }
350 
351             tempRect = QRect(rect.left(), qMax(rect.top(), (int)ar.top()), w, h);
352             if (reg.intersected(tempRect) == tempRect) {
353                 return tempRect.topLeft();
354             }
355 
356             tempRect = QRect(qMin(rect.left(), (int)(ar.right() + 1 - w)), qMax(rect.top(), (int)ar.top()), w, h);
357             return tempRect.topLeft();
358         } else {
359             return rect.topLeft();
360         }
361 
362         // bottom right corner
363     } else if (rect.center().x() > ar.center().x() && rect.center().y() > ar.center().y()) {
364         if (reg.intersected(rect) != rect) {
365             tempRect = QRect(qMin(rect.left(), (int)(ar.right() + 1 - w)), rect.top(), w, h);
366             if (reg.intersected(tempRect) == tempRect) {
367                 return tempRect.topLeft();
368             }
369 
370             tempRect = QRect(rect.left(), qMin(rect.top(), (int)(ar.bottom() + 1 - h)), w, h);
371             if (reg.intersected(tempRect) == tempRect) {
372                 return tempRect.topLeft();
373             }
374 
375             tempRect = QRect(qMin(rect.left(), (int)(ar.right() + 1 - w)), qMin(rect.top(), (int)(ar.bottom() + 1 - h)), w, h);
376             return tempRect.topLeft();
377         } else {
378             return rect.topLeft();
379         }
380     }
381 
382     return rect.topLeft();
383 }
384 
globalAction(QString name) const385 QAction *ContainmentInterface::globalAction(QString name) const
386 {
387     return m_containment->corona()->actions()->action(name);
388 }
389 
isEditMode() const390 bool ContainmentInterface::isEditMode() const
391 {
392     return m_containment->corona()->isEditMode();
393 }
394 
setEditMode(bool edit)395 void ContainmentInterface::setEditMode(bool edit)
396 {
397     m_containment->corona()->setEditMode(edit);
398 }
399 
processMimeData(QObject * mimeDataProxy,int x,int y,KIO::DropJob * dropJob)400 void ContainmentInterface::processMimeData(QObject *mimeDataProxy, int x, int y, KIO::DropJob *dropJob)
401 {
402     QMimeData *mime = qobject_cast<QMimeData *>(mimeDataProxy);
403     if (mime) {
404         processMimeData(mime, x, y, dropJob);
405     } else {
406         processMimeData(mimeDataProxy->property("mimeData").value<QMimeData *>(), x, y, dropJob);
407     }
408 }
409 
processMimeData(QMimeData * mimeData,int x,int y,KIO::DropJob * dropJob)410 void ContainmentInterface::processMimeData(QMimeData *mimeData, int x, int y, KIO::DropJob *dropJob)
411 {
412     if (!mimeData) {
413         return;
414     }
415 
416     if (m_dropMenu) {
417         if (dropJob) {
418             dropJob->kill();
419         }
420         return;
421     }
422     m_dropMenu = QPointer<DropMenu>(new DropMenu(dropJob, mapToGlobal(QPoint(x, y)).toPoint(), this));
423     if (dropJob) {
424         dropJob->setParent(m_dropMenu);
425     }
426 
427     // const QMimeData *mimeData = data;
428 
429     qDebug() << "Arrived mimeData" << mimeData->urls() << mimeData->formats() << "at" << x << ", " << y;
430 
431     // Catch drops from a Task Manager and convert to usable URL.
432     if (!mimeData->hasUrls() && mimeData->hasFormat(QStringLiteral("text/x-orgkdeplasmataskmanager_taskurl"))) {
433         QList<QUrl> urls = {QUrl(QString::fromUtf8(mimeData->data(QStringLiteral("text/x-orgkdeplasmataskmanager_taskurl"))))};
434         mimeData->setUrls(urls);
435     }
436 
437     if (mimeData->hasFormat(QStringLiteral("text/x-plasmoidservicename"))) {
438         QString data = QString::fromUtf8(mimeData->data(QStringLiteral("text/x-plasmoidservicename")));
439         const QStringList appletNames = data.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
440         for (const QString &appletName : appletNames) {
441             qDebug() << "adding" << appletName;
442 
443             metaObject()->invokeMethod(this,
444                                        "createApplet",
445                                        Qt::QueuedConnection,
446                                        Q_ARG(QString, appletName),
447                                        Q_ARG(QVariantList, QVariantList()),
448                                        Q_ARG(QRectF, QRectF(x, y, -1, -1)));
449         }
450         delete m_dropMenu.data();
451     } else if (mimeData->hasUrls()) {
452         // TODO: collect the mimetypes of available script engines and offer
453         //      to create widgets out of the matching URLs, if any
454         const QList<QUrl> urls = KUrlMimeData::urlsFromMimeData(mimeData);
455         m_dropMenu->setUrls(urls);
456 
457         if (!urls.at(0).isLocalFile()) {
458             QApplication::setOverrideCursor(Qt::WaitCursor);
459         }
460 
461         QMimeDatabase db;
462         QMimeType firstMimetype = db.mimeTypeForUrl(urls.at(0));
463         for (const QUrl &url : urls) {
464             if (firstMimetype != db.mimeTypeForUrl(url)) {
465                 m_dropMenu->setMultipleMimetypes(true);
466                 break;
467             }
468         }
469 
470         // It may be a directory or a file, let's stat
471         KIO::JobFlags flags = KIO::HideProgressInfo;
472         KIO::MimetypeJob *job = KIO::mimetype(m_dropMenu->urls().at(0), flags);
473         job->setParent(m_dropMenu.data());
474 
475         QObject::connect(job, &KJob::result, this, &ContainmentInterface::dropJobResult);
476         QObject::connect(job, &KIO::MimetypeJob::mimeTypeFound, this, &ContainmentInterface::mimeTypeRetrieved);
477 
478     } else {
479         bool deleteDropMenu = true;
480 
481         const QStringList formats = mimeData->formats();
482         QHash<QString, KPluginMetaData> seenPlugins;
483         QHash<QString, QString> pluginFormats;
484 
485         for (const QString &format : formats) {
486             const auto plugins = Plasma::PluginLoader::self()->listAppletMetaDataForMimeType(format);
487 
488             for (const auto &plugin : plugins) {
489                 if (seenPlugins.contains(plugin.pluginId())) {
490                     continue;
491                 }
492 
493                 seenPlugins.insert(plugin.pluginId(), plugin);
494                 pluginFormats.insert(plugin.pluginId(), format);
495             }
496         }
497         // qDebug() << "Mimetype ..." << formats << seenPlugins.keys() << pluginFormats.values();
498 
499         QString selectedPlugin;
500 
501         if (seenPlugins.isEmpty()) {
502             // do nothing
503             // directly create if only one offer only if the containment didn't pass an existing plugin
504         } else if (seenPlugins.count() == 1) {
505             selectedPlugin = seenPlugins.constBegin().key();
506             Plasma::Applet *applet = createApplet(selectedPlugin, QVariantList(), QRect(x, y, -1, -1));
507             setAppletArgs(applet, pluginFormats[selectedPlugin], QString::fromUtf8(mimeData->data(pluginFormats[selectedPlugin])));
508         } else {
509             QHash<QAction *, QString> actionsToPlugins;
510             for (const auto &info : std::as_const(seenPlugins)) {
511                 QAction *action;
512                 if (!info.iconName().isEmpty()) {
513                     action = new QAction(QIcon::fromTheme(info.iconName()), info.name(), m_dropMenu);
514                 } else {
515                     action = new QAction(info.name(), m_dropMenu);
516                 }
517                 m_dropMenu->addAction(action);
518                 action->setData(info.pluginId());
519                 connect(action, &QAction::triggered, this, [this, x, y, mimeData, action]() {
520                     const QString selectedPlugin = action->data().toString();
521                     Plasma::Applet *applet = createApplet(selectedPlugin, QVariantList(), QRect(x, y, -1, -1));
522                     setAppletArgs(applet, selectedPlugin, QString::fromUtf8(mimeData->data(selectedPlugin)));
523                 });
524 
525                 actionsToPlugins.insert(action, info.pluginId());
526             }
527             m_dropMenu->show();
528             deleteDropMenu = false;
529         }
530 
531         if (deleteDropMenu) {
532             // in case m_dropMenu has not been shown
533             delete m_dropMenu.data();
534         }
535     }
536 }
537 
clearDataForMimeJob(KIO::Job * job)538 void ContainmentInterface::clearDataForMimeJob(KIO::Job *job)
539 {
540     QObject::disconnect(job, nullptr, this, nullptr);
541     job->kill();
542 
543     m_dropMenu->show();
544 
545     if (!m_dropMenu->urls().at(0).isLocalFile()) {
546         QApplication::restoreOverrideCursor();
547     }
548 }
549 
dropJobResult(KJob * job)550 void ContainmentInterface::dropJobResult(KJob *job)
551 {
552     if (job->error()) {
553         qDebug() << "ERROR" << job->error() << ' ' << job->errorString();
554         clearDataForMimeJob(dynamic_cast<KIO::Job *>(job));
555     }
556 }
557 
mimeTypeRetrieved(KIO::Job * job,const QString & mimetype)558 void ContainmentInterface::mimeTypeRetrieved(KIO::Job *job, const QString &mimetype)
559 {
560     qDebug() << "Mimetype Job returns." << mimetype;
561 
562     KIO::TransferJob *tjob = dynamic_cast<KIO::TransferJob *>(job);
563     if (!tjob) {
564         qDebug() << "job should be a TransferJob, but isn't";
565         clearDataForMimeJob(job);
566         return;
567     }
568 
569     QList<KPluginMetaData> appletList = Plasma::PluginLoader::self()->listAppletMetaDataForUrl(tjob->url());
570     if (mimetype.isEmpty() && appletList.isEmpty()) {
571         clearDataForMimeJob(job);
572         qDebug() << "No applets found matching the url (" << tjob->url() << ") or the mimetype (" << mimetype << ")";
573         return;
574     } else {
575         qDebug() << "Received a suitable dropEvent at " << m_dropMenu->dropPoint();
576         qDebug() << "Bailing out. Cannot find associated dropEvent related to the TransferJob";
577 
578         qDebug() << "Creating menu for: " << mimetype;
579 
580         appletList << Plasma::PluginLoader::self()->listAppletMetaDataForMimeType(mimetype);
581 
582         QList<KPluginMetaData> wallpaperList;
583 
584         if (m_containment->containmentType() != Plasma::Types::PanelContainment && m_containment->containmentType() != Plasma::Types::CustomPanelContainment) {
585             if (m_wallpaperInterface && m_wallpaperInterface->supportsMimetype(mimetype)) {
586                 wallpaperList << m_wallpaperInterface->kPackage().metadata();
587             } else {
588                 wallpaperList = WallpaperInterface::listWallpaperMetadataForMimetype(mimetype);
589             }
590         }
591 
592         const bool isPlasmaPackage = (mimetype == QLatin1String("application/x-plasma"));
593 
594         if ((!appletList.isEmpty() || !wallpaperList.isEmpty() || isPlasmaPackage) && !m_dropMenu->isMultipleMimetypes()) {
595             QAction *installPlasmaPackageAction = nullptr;
596             if (isPlasmaPackage) {
597                 QAction *action = new QAction(i18n("Plasma Package"), m_dropMenu);
598                 action->setSeparator(true);
599                 m_dropMenu->addAction(action);
600 
601                 installPlasmaPackageAction = new QAction(QIcon::fromTheme(QStringLiteral("application-x-plasma")), i18n("Install"), m_dropMenu);
602                 m_dropMenu->addAction(installPlasmaPackageAction);
603 
604                 const QString &packagePath = tjob->url().toLocalFile();
605                 connect(installPlasmaPackageAction, &QAction::triggered, this, [this, packagePath]() {
606                     using namespace KPackage;
607                     PackageStructure *structure = PackageLoader::self()->loadPackageStructure(QStringLiteral("Plasma/Applet"));
608                     Package package(structure);
609 
610                     KJob *installJob = package.update(packagePath);
611                     connect(installJob, &KJob::result, this, [this, packagePath, structure](KJob *job) {
612                         auto fail = [](const QString &text) {
613                             KNotification::event(QStringLiteral("plasmoidInstallationFailed"),
614                                                  i18n("Package Installation Failed"),
615                                                  text,
616                                                  QStringLiteral("dialog-error"),
617                                                  nullptr,
618                                                  KNotification::CloseOnTimeout,
619                                                  QStringLiteral("plasma_workspace"));
620                         };
621 
622                         // if the applet is already installed, just add it to the containment
623                         if (job->error() != KJob::NoError && job->error() != Package::PackageAlreadyInstalledError
624                             && job->error() != Package::NewerVersionAlreadyInstalledError) {
625                             fail(job->errorText());
626                             return;
627                         }
628 
629                         using namespace KPackage;
630                         Package package(structure);
631                         // TODO how can I get the path of the actual package?
632 
633                         package.setPath(packagePath);
634 
635                         // TODO how can I get the plugin id? Package::metadata() is deprecated
636                         if (!package.isValid() || !package.metadata().isValid()) {
637                             fail(i18n("The package you just dropped is invalid."));
638                             return;
639                         }
640 
641                         createApplet(package.metadata().pluginId(), QVariantList(), QRect(m_dropMenu->dropPoint(), QSize(-1, -1)));
642                     });
643                 });
644             }
645 
646             QAction *action = new QAction(i18n("Widgets"), m_dropMenu);
647             action->setSeparator(true);
648             m_dropMenu->addAction(action);
649 
650             for (const auto &info : std::as_const(appletList)) {
651                 const QString actionText = i18nc("Add widget", "Add %1", info.name());
652                 QAction *action = new QAction(actionText, m_dropMenu);
653                 if (!info.iconName().isEmpty()) {
654                     action->setIcon(QIcon::fromTheme(info.iconName()));
655                 }
656                 m_dropMenu->addAction(action);
657                 action->setData(info.pluginId());
658                 const QUrl url = tjob->url();
659                 connect(action, &QAction::triggered, this, [this, action, mimetype, url]() {
660                     Plasma::Applet *applet = createApplet(action->data().toString(), QVariantList(), QRect(m_dropMenu->dropPoint(), QSize(-1, -1)));
661                     setAppletArgs(applet, mimetype, url.toString());
662                 });
663             }
664             {
665                 QAction *action = new QAction(i18nc("Add icon widget", "Add Icon"), m_dropMenu);
666                 m_dropMenu->addAction(action);
667                 action->setData(QStringLiteral("org.kde.plasma.icon"));
668                 const QUrl url = tjob->url();
669                 connect(action, &QAction::triggered, this, [this, action, mimetype, url]() {
670                     Plasma::Applet *applet = createApplet(action->data().toString(), QVariantList(), QRect(m_dropMenu->dropPoint(), QSize(-1, -1)));
671                     setAppletArgs(applet, mimetype, url.toString());
672                 });
673             }
674 
675             QHash<QAction *, QString> actionsToWallpapers;
676             if (!wallpaperList.isEmpty()) {
677                 QAction *action = new QAction(i18n("Wallpaper"), m_dropMenu);
678                 action->setSeparator(true);
679                 m_dropMenu->addAction(action);
680 
681                 QMap<QString, KPluginMetaData> sorted;
682                 for (const auto &info : std::as_const(appletList)) {
683                     sorted.insert(info.name(), info);
684                 }
685 
686                 for (const KPluginMetaData &info : std::as_const(wallpaperList)) {
687                     const QString actionText = i18nc("Set wallpaper", "Set %1", info.name());
688                     QAction *action = new QAction(actionText, m_dropMenu);
689                     if (!info.iconName().isEmpty()) {
690                         action->setIcon(QIcon::fromTheme(info.iconName()));
691                     }
692                     m_dropMenu->addAction(action);
693                     actionsToWallpapers.insert(action, info.pluginId());
694                     const QUrl url = tjob->url();
695                     connect(action, &QAction::triggered, this, [this, url]() {
696                         // set wallpapery stuff
697                         if (m_wallpaperInterface && url.isValid()) {
698                             m_wallpaperInterface->setUrl(url);
699                         }
700                     });
701                 }
702             }
703         } else {
704             // case in which we created the menu ourselves, just the "fetching type entry, directly create the icon applet
705             if (!m_dropMenu->isDropjobMenu()) {
706                 Plasma::Applet *applet = createApplet(QStringLiteral("org.kde.plasma.icon"), QVariantList(), QRect(m_dropMenu->dropPoint(), QSize(-1, -1)));
707                 setAppletArgs(applet, mimetype, tjob->url().toString());
708             } else {
709                 QAction *action;
710                 QAction *sep = new QAction(i18n("Widgets"), m_dropMenu);
711                 sep->setSeparator(true);
712                 m_dropMenu->addAction(sep);
713                 // we can at least create an icon as a link to the URL
714                 action = new QAction(i18nc("Add icon widget", "Add Icon"), m_dropMenu);
715                 m_dropMenu->addAction(action);
716 
717                 const QUrl url = tjob->url();
718                 connect(action, &QAction::triggered, this, [this, mimetype, url]() {
719                     Plasma::Applet *applet = createApplet(QStringLiteral("org.kde.plasma.icon"), QVariantList(), QRect(m_dropMenu->dropPoint(), QSize(-1, -1)));
720                     setAppletArgs(applet, mimetype, url.toString());
721                 });
722             }
723         }
724         clearDataForMimeJob(tjob);
725     }
726 }
727 
appletAddedForward(Plasma::Applet * applet)728 void ContainmentInterface::appletAddedForward(Plasma::Applet *applet)
729 {
730     if (!applet) {
731         return;
732     }
733 
734     AppletInterface *appletGraphicObject = applet->property("_plasma_graphicObject").value<AppletInterface *>();
735     AppletInterface *contGraphicObject = m_containment->property("_plasma_graphicObject").value<AppletInterface *>();
736 
737     //     qDebug() << "Applet added on containment:" << m_containment->title() << contGraphicObject
738     //              << "Applet: " << applet << applet->title() << appletGraphicObject;
739 
740     // applets can not have a graphic object if they don't have a script engine loaded
741     // this can happen if they were loaded with an invalid metadata
742     if (!appletGraphicObject) {
743         return;
744     }
745 
746     if (contGraphicObject) {
747         appletGraphicObject->setProperty("visible", false);
748         appletGraphicObject->setProperty("parent", QVariant::fromValue(contGraphicObject));
749     }
750 
751     m_appletInterfaces << appletGraphicObject;
752     connect(appletGraphicObject, &QObject::destroyed, this, [this](QObject *obj) {
753         m_appletInterfaces.removeAll(obj);
754     });
755     Q_EMIT appletAdded(appletGraphicObject, appletGraphicObject->m_positionBeforeRemoval.x(), appletGraphicObject->m_positionBeforeRemoval.y());
756     Q_EMIT appletsChanged();
757 }
758 
appletRemovedForward(Plasma::Applet * applet)759 void ContainmentInterface::appletRemovedForward(Plasma::Applet *applet)
760 {
761     AppletInterface *appletGraphicObject = applet->property("_plasma_graphicObject").value<AppletInterface *>();
762     if (appletGraphicObject) {
763         m_appletInterfaces.removeAll(appletGraphicObject);
764         appletGraphicObject->m_positionBeforeRemoval = appletGraphicObject->mapToItem(this, QPointF());
765     }
766     Q_EMIT appletRemoved(appletGraphicObject);
767     Q_EMIT appletsChanged();
768 }
769 
loadWallpaper()770 void ContainmentInterface::loadWallpaper()
771 {
772     if (m_containment->containmentType() != Plasma::Types::DesktopContainment && m_containment->containmentType() != Plasma::Types::CustomContainment) {
773         return;
774     }
775 
776     if (!m_wallpaperInterface && !m_containment->wallpaper().isEmpty()) {
777         m_wallpaperInterface = new WallpaperInterface(this);
778 
779         m_wallpaperInterface->setZ(-1000);
780         // Qml seems happier if the parent gets set in this way
781         m_wallpaperInterface->setProperty("parent", QVariant::fromValue(this));
782 
783         connect(m_wallpaperInterface, &WallpaperInterface::isLoadingChanged, this, &AppletInterface::updateUiReadyConstraint);
784 
785         // set anchors
786         QQmlExpression expr(qmlObject()->engine()->rootContext(), m_wallpaperInterface, QStringLiteral("parent"));
787         QQmlProperty prop(m_wallpaperInterface, QStringLiteral("anchors.fill"));
788         prop.write(expr.evaluate());
789 
790         m_containment->setProperty("wallpaperGraphicsObject", QVariant::fromValue(m_wallpaperInterface));
791     } else if (m_wallpaperInterface && m_containment->wallpaper().isEmpty()) {
792         m_wallpaperInterface->deleteLater();
793         m_wallpaperInterface = nullptr;
794     }
795 
796     Q_EMIT wallpaperInterfaceChanged();
797 }
798 
activity() const799 QString ContainmentInterface::activity() const
800 {
801     return m_containment->activity();
802 }
803 
activityName() const804 QString ContainmentInterface::activityName() const
805 {
806     if (!m_activityInfo) {
807         return QString();
808     }
809     return m_activityInfo->name();
810 }
811 
actions() const812 QList<QObject *> ContainmentInterface::actions() const
813 {
814     // FIXME: giving directly a QList<QAction*> crashes
815 
816     QStringList actionOrder;
817     actionOrder << QStringLiteral("add widgets") << QStringLiteral("manage activities") << QStringLiteral("remove") << QStringLiteral("lock widgets")
818                 << QStringLiteral("run associated application") << QStringLiteral("configure");
819     QHash<QString, QAction *> orderedActions;
820     // use a multimap to sort by action type
821     QMultiMap<int, QObject *> actions;
822     int i = 0;
823     auto listActions = m_containment->actions()->actions();
824     for (QAction *a : std::as_const(listActions)) {
825         if (!actionOrder.contains(a->objectName())) {
826             // FIXME QML visualizations don't support menus for now, *and* there is no way to
827             // distinguish them on QML side
828             if (!a->menu()) {
829                 actions.insert(a->data().toInt() * 100 + i, a);
830                 ++i;
831             }
832         } else {
833             orderedActions[a->objectName()] = a;
834         }
835     }
836 
837     i = 0;
838     listActions = m_containment->corona()->actions()->actions();
839     for (QAction *a : std::as_const(listActions)) {
840         if (a->objectName() == QLatin1String("lock widgets") || a->menu()) {
841             // It is up to the Containment to decide if the user is allowed or not
842             // to lock/unluck the widgets, so corona should not add one when there is none
843             //(user is not allow) and it shouldn't add another one when there is already
844             // one
845             continue;
846         }
847 
848         if (!actionOrder.contains(a->objectName())) {
849             actions.insert(a->data().toInt() * 100 + i, a);
850         } else {
851             orderedActions[a->objectName()] = a;
852         }
853         ++i;
854     }
855     QList<QObject *> actionList = actions.values();
856 
857     for (const QString &name : std::as_const(actionOrder)) {
858         QAction *a = orderedActions.value(name);
859         if (a && !a->menu()) {
860             actionList << a;
861         }
862         ++i;
863     }
864 
865     return actionList;
866 }
867 
setContainmentDisplayHints(Plasma::Types::ContainmentDisplayHints hints)868 void ContainmentInterface::setContainmentDisplayHints(Plasma::Types::ContainmentDisplayHints hints)
869 {
870     m_containment->setContainmentDisplayHints(hints);
871 }
872 
873 // PROTECTED--------------------
874 
mouseReleaseEvent(QMouseEvent * event)875 void ContainmentInterface::mouseReleaseEvent(QMouseEvent *event)
876 {
877     event->setAccepted(m_containment->containmentActions().contains(Plasma::ContainmentActions::eventToString(event)));
878 }
879 
mousePressEvent(QMouseEvent * event)880 void ContainmentInterface::mousePressEvent(QMouseEvent *event)
881 
882 {
883     // even if the menu is executed synchronously, other events may be processed
884     // by the qml incubator when plasma is loading, so we need to guard there
885     if (m_contextMenu) {
886         m_contextMenu.data()->close();
887         return;
888     }
889 
890     const QString trigger = Plasma::ContainmentActions::eventToString(event);
891     Plasma::ContainmentActions *plugin = m_containment->containmentActions().value(trigger);
892 
893     if (!plugin || plugin->contextualActions().isEmpty()) {
894         event->setAccepted(false);
895         return;
896     }
897 
898     // the plugin can be a single action or a context menu
899     // Don't have an action list? execute as single action
900     // and set the event position as action data
901     if (plugin->contextualActions().length() == 1) {
902         QAction *action = plugin->contextualActions().at(0);
903         action->setData(event->pos());
904         action->trigger();
905         event->accept();
906         return;
907     }
908 
909     // FIXME: very inefficient appletAt() implementation
910     Plasma::Applet *applet = nullptr;
911     for (QObject *appletObject : std::as_const(m_appletInterfaces)) {
912         if (AppletInterface *ai = qobject_cast<AppletInterface *>(appletObject)) {
913             if (ai->isVisible() && ai->contains(ai->mapFromItem(this, event->localPos()))) {
914                 applet = ai->applet();
915                 break;
916             } else {
917                 ai = nullptr;
918             }
919         }
920     }
921     // qDebug() << "Invoking menu for applet" << applet;
922 
923     QMenu *desktopMenu = new QMenu;
924 
925     // this is a workaround where Qt now creates the menu widget
926     // in .exec before oxygen can polish it and set the following attribute
927     desktopMenu->setAttribute(Qt::WA_TranslucentBackground);
928     // end workaround
929 
930     if (desktopMenu->winId()) {
931         desktopMenu->windowHandle()->setTransientParent(window());
932     }
933     desktopMenu->setAttribute(Qt::WA_DeleteOnClose);
934 
935     m_contextMenu = desktopMenu;
936 
937     Q_EMIT m_containment->contextualActionsAboutToShow();
938 
939     if (applet) {
940         Q_EMIT applet->contextualActionsAboutToShow();
941         addAppletActions(desktopMenu, applet, event);
942     } else {
943         addContainmentActions(desktopMenu, event);
944     }
945 
946     // this is a workaround where Qt will fail to realize a mouse has been released
947 
948     // this happens if a window which does not accept focus spawns a new window that takes focus and X grab
949     // whilst the mouse is depressed
950     // https://bugreports.qt.io/browse/QTBUG-59044
951     // this causes the next click to go missing
952 
953     // by releasing manually we avoid that situation
954     auto ungrabMouseHack = [this]() {
955         if (window() && window()->mouseGrabberItem()) {
956             window()->mouseGrabberItem()->ungrabMouse();
957         }
958     };
959 
960     // pre 5.8.0 QQuickWindow code is "item->grabMouse(); sendEvent(item, mouseEvent)"
961     // post 5.8.0 QQuickWindow code is sendEvent(item, mouseEvent); item->grabMouse()
962     if (QVersionNumber::fromString(QLatin1String(qVersion())) > QVersionNumber(5, 8, 0)) {
963         QTimer::singleShot(0, this, ungrabMouseHack);
964     } else {
965         ungrabMouseHack();
966     }
967     // end workaround
968 
969     QPoint pos = event->globalPos();
970     if (window() && m_containment->containmentType() == Plasma::Types::PanelContainment) {
971         desktopMenu->adjustSize();
972 
973         if (QScreen *screen = window()->screen()) {
974             const QRect geo = screen->availableGeometry();
975 
976             pos = QPoint(qBound(geo.left(), pos.x(), geo.right() + 1 - desktopMenu->width()),
977                          qBound(geo.top(), pos.y(), geo.bottom() + 1 - desktopMenu->height()));
978         }
979     }
980 
981     if (desktopMenu->isEmpty()) {
982         delete desktopMenu;
983         event->accept();
984         return;
985     }
986 
987     // Bug 344205 keep panel visible while menu is open
988     const auto oldStatus = m_containment->status();
989     m_containment->setStatus(Plasma::Types::RequiresAttentionStatus);
990 
991     connect(desktopMenu, &QMenu::aboutToHide, m_containment, [this, oldStatus] {
992         m_containment->setStatus(oldStatus);
993     });
994 
995     KAcceleratorManager::manage(desktopMenu);
996 
997     for (auto action : desktopMenu->actions()) {
998         if (action->menu()) {
999             connect(action->menu(), &QMenu::aboutToShow, desktopMenu, [action, desktopMenu] {
1000                 if (action->menu()->windowHandle()) {
1001                     // Need to add the transient parent otherwise Qt will create a new toplevel
1002                     action->menu()->windowHandle()->setTransientParent(desktopMenu->windowHandle());
1003                 }
1004             });
1005         }
1006     }
1007 
1008     desktopMenu->popup(pos);
1009     event->setAccepted(true);
1010 }
1011 
wheelEvent(QWheelEvent * event)1012 void ContainmentInterface::wheelEvent(QWheelEvent *event)
1013 {
1014     const QString trigger = Plasma::ContainmentActions::eventToString(event);
1015     Plasma::ContainmentActions *plugin = m_containment->containmentActions().value(trigger);
1016 
1017     if (!plugin) {
1018         event->setAccepted(false);
1019         return;
1020     }
1021 
1022     m_wheelDelta += event->angleDelta().y();
1023 
1024     // Angle delta 120 for common "one click"
1025     // See: https://doc.qt.io/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop
1026     while (m_wheelDelta >= 120) {
1027         m_wheelDelta -= 120;
1028         plugin->performPreviousAction();
1029     }
1030     while (m_wheelDelta <= -120) {
1031         m_wheelDelta += 120;
1032         plugin->performNextAction();
1033     }
1034 }
1035 
keyPressEvent(QKeyEvent * event)1036 void ContainmentInterface::keyPressEvent(QKeyEvent *event)
1037 {
1038     if (event->key() == Qt::Key_Menu) {
1039         QMouseEvent me(QEvent::MouseButtonRelease, QPoint(), Qt::RightButton, Qt::RightButton, event->modifiers());
1040         mousePressEvent(&me);
1041         event->accept();
1042     }
1043 
1044     AppletInterface::keyPressEvent(event);
1045 }
1046 
addAppletActions(QMenu * desktopMenu,Plasma::Applet * applet,QEvent * event)1047 void ContainmentInterface::addAppletActions(QMenu *desktopMenu, Plasma::Applet *applet, QEvent *event)
1048 {
1049     const auto listActions = applet->contextualActions();
1050     for (QAction *action : listActions) {
1051         if (action) {
1052             desktopMenu->addAction(action);
1053         }
1054     }
1055 
1056     if (!applet->failedToLaunch()) {
1057         QAction *runAssociatedApplication = applet->actions()->action(QStringLiteral("run associated application"));
1058         if (runAssociatedApplication && runAssociatedApplication->isEnabled()) {
1059             desktopMenu->addAction(runAssociatedApplication);
1060         }
1061 
1062         QAction *configureApplet = applet->actions()->action(QStringLiteral("configure"));
1063         if (configureApplet && configureApplet->isEnabled()) {
1064             desktopMenu->addAction(configureApplet);
1065         }
1066         QAction *appletAlternatives = applet->actions()->action(QStringLiteral("alternatives"));
1067         if (appletAlternatives && appletAlternatives->isEnabled()) {
1068             desktopMenu->addAction(appletAlternatives);
1069         }
1070     }
1071 
1072     desktopMenu->addSeparator();
1073     if (m_containment->containmentType() == Plasma::Types::DesktopContainment) {
1074         auto action = m_containment->corona()->actions()->action(QStringLiteral("edit mode"));
1075         if (action) {
1076             desktopMenu->addAction(action);
1077         }
1078     } else {
1079         addContainmentActions(desktopMenu, event);
1080     }
1081 
1082     if (m_containment->immutability() == Plasma::Types::Mutable
1083         && (m_containment->containmentType() != Plasma::Types::PanelContainment || m_containment->isUserConfiguring())) {
1084         QAction *closeApplet = applet->actions()->action(QStringLiteral("remove"));
1085         // qDebug() << "checking for removal" << closeApplet;
1086         if (closeApplet) {
1087             if (!desktopMenu->isEmpty()) {
1088                 desktopMenu->addSeparator();
1089             }
1090 
1091             // qDebug() << "adding close action" << closeApplet->isEnabled() << closeApplet->isVisible();
1092             desktopMenu->addAction(closeApplet);
1093         }
1094     }
1095 }
1096 
addContainmentActions(QMenu * desktopMenu,QEvent * event)1097 void ContainmentInterface::addContainmentActions(QMenu *desktopMenu, QEvent *event)
1098 {
1099     if (m_containment->corona()->immutability() != Plasma::Types::Mutable //
1100         && !KAuthorized::authorizeAction(QStringLiteral("plasma/containment_actions"))) {
1101         // qDebug() << "immutability";
1102         return;
1103     }
1104 
1105     // this is what ContainmentPrivate::prepareContainmentActions was
1106     const QString trigger = Plasma::ContainmentActions::eventToString(event);
1107     Plasma::ContainmentActions *plugin = m_containment->containmentActions().value(trigger);
1108 
1109     if (!plugin) {
1110         return;
1111     }
1112 
1113     if (plugin->containment() != m_containment) {
1114         plugin->setContainment(m_containment);
1115 
1116         // now configure it
1117         KConfigGroup cfg(m_containment->corona()->config(), "ActionPlugins");
1118         cfg = KConfigGroup(&cfg, QString::number(m_containment->containmentType()));
1119         KConfigGroup pluginConfig = KConfigGroup(&cfg, trigger);
1120         plugin->restore(pluginConfig);
1121     }
1122 
1123     QList<QAction *> actions = plugin->contextualActions();
1124 
1125     if (actions.isEmpty()) {
1126         // it probably didn't bother implementing the function. give the user a chance to set
1127         // a better plugin.  note that if the user sets no-plugin this won't happen...
1128         /* clang-format off */
1129         if ((m_containment->containmentType() != Plasma::Types::PanelContainment
1130                 && m_containment->containmentType() != Plasma::Types::CustomPanelContainment)
1131             && m_containment->actions()->action(QStringLiteral("configure"))) { /* clang-format on */
1132             desktopMenu->addAction(m_containment->actions()->action(QStringLiteral("configure")));
1133         }
1134     } else {
1135         desktopMenu->addActions(actions);
1136     }
1137 
1138     return;
1139 }
1140 
isLoading() const1141 bool ContainmentInterface::isLoading() const
1142 {
1143     bool loading = AppletInterface::isLoading();
1144     if (m_wallpaperInterface) {
1145         loading |= m_wallpaperInterface->isLoading();
1146     }
1147     return loading;
1148 }
1149 
1150 #include "moc_containmentinterface.cpp"
1151