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