File indexing completed on 2024-04-21 05:36:06

0001 /*
0002     SPDX-FileCopyrightText: 2008 Aaron Seigo <aseigo@kde.org>
0003     SPDX-FileCopyrightText: 2013 Sebastian Kügler <sebas@kde.org>
0004     SPDX-FileCopyrightText: 2013 Ivan Cukic <ivan.cukic@kde.org>
0005     SPDX-FileCopyrightText: 2013 Marco Martin <mart@kde.org>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "shellcorona.h"
0011 #include "debug.h"
0012 #include "kconfigloader.h"
0013 #include "kconfigpropertymap.h"
0014 #include "strutmanager.h"
0015 
0016 #include <config-plasma.h>
0017 #include <config-workspace.h>
0018 
0019 #include <QApplication>
0020 #include <QDBusConnection>
0021 #include <QDebug>
0022 #include <QMenu>
0023 #include <QQmlContext>
0024 #include <QQuickItemGrabResult>
0025 #include <QScreen>
0026 #include <QUrl>
0027 
0028 #include <QJsonDocument>
0029 #include <QJsonObject>
0030 
0031 #include <Plasma/PluginLoader>
0032 #include <PlasmaQuick/AppletQuickItem>
0033 #include <kactioncollection.h>
0034 #include <klocalizedstring.h>
0035 
0036 #include <KAuthorized>
0037 #include <KGlobalAccel>
0038 #include <KMessageBox>
0039 #include <KWindowSystem>
0040 #include <KX11Extras>
0041 #include <PlasmaQuick/SharedQmlEngine>
0042 #include <kdirwatch.h>
0043 #include <ksycoca.h>
0044 #include <plasmaactivities/consumer.h>
0045 #include <plasmaactivities/controller.h>
0046 
0047 #include <KPackage/PackageLoader>
0048 
0049 #include <KWayland/Client/connection_thread.h>
0050 #include <KWayland/Client/plasmashell.h>
0051 #include <KWayland/Client/plasmawindowmanagement.h>
0052 #include <KWayland/Client/registry.h>
0053 #include <plasma/plasma.h>
0054 
0055 #include "config-ktexteditor.h" // HAVE_KTEXTEDITOR
0056 
0057 #include "alternativeshelper.h"
0058 #include "desktopview.h"
0059 #include "osd.h"
0060 #include "panelview.h"
0061 #include "screenpool.h"
0062 #if USE_SCRIPTING
0063 #include "scripting/scriptengine.h"
0064 #endif
0065 #include "shellcontainmentconfig.h"
0066 
0067 #include "debug.h"
0068 #include "futureutil.h"
0069 #include "plasmashelladaptor.h"
0070 
0071 #ifndef NDEBUG
0072 #define CHECK_SCREEN_INVARIANTS screenInvariants();
0073 #else
0074 #define CHECK_SCREEN_INVARIANTS
0075 #endif
0076 
0077 #if HAVE_X11
0078 #include <NETWM>
0079 #include <private/qtx11extras_p.h>
0080 #include <xcb/xcb.h>
0081 #endif
0082 #include <chrono>
0083 
0084 using namespace std::chrono_literals;
0085 using namespace Qt::StringLiterals;
0086 
0087 static const int s_configSyncDelay = 10000; // 10 seconds
0088 
0089 Q_DECLARE_METATYPE(QColor)
0090 
0091 QDBusArgument &operator<<(QDBusArgument &argument, const QColor &color)
0092 {
0093     argument.beginStructure();
0094     argument << color.rgba();
0095     argument.endStructure();
0096     return argument;
0097 }
0098 
0099 const QDBusArgument &operator>>(const QDBusArgument &argument, QColor &color)
0100 {
0101     argument.beginStructure();
0102     QRgb rgba;
0103     argument >> rgba;
0104     argument.endStructure();
0105     color = QColor::fromRgba(rgba);
0106     return argument;
0107 }
0108 
0109 ShellCorona::ShellCorona(QObject *parent)
0110     : Plasma::Corona(parent)
0111     , m_config(KSharedConfig::openConfig(QStringLiteral("plasmarc")))
0112     , m_screenPool(new ScreenPool(this))
0113     , m_activityController(new KActivities::Controller(this))
0114     , m_addPanelAction(nullptr)
0115     , m_addPanelsMenu(nullptr)
0116     , m_waylandPlasmaShell(nullptr)
0117     , m_closingDown(false)
0118     , m_strutManager(new StrutManager(this))
0119     , m_shellContainmentConfig(nullptr)
0120 {
0121     setupWaylandIntegration();
0122     qmlRegisterUncreatableType<DesktopView>("org.kde.plasma.shell", 2, 0, "Desktop", QStringLiteral("It is not possible to create objects of type Desktop"));
0123     qmlRegisterUncreatableType<PanelView>("org.kde.plasma.shell", 2, 0, "Panel", QStringLiteral("It is not possible to create objects of type Panel"));
0124 
0125     qDBusRegisterMetaType<QColor>();
0126 
0127     KConfigGroup cg(KSharedConfig::openConfig(u"kdeglobals"_s), u"KDE"_s);
0128     const QString packageName = cg.readEntry("LookAndFeelPackage", QString());
0129     m_lookAndFeelPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"), packageName);
0130 
0131     // Accent color setting
0132     KSharedConfigPtr globalConfig = KSharedConfig::openConfig();
0133     KConfigGroup accentColorConfigGroup(globalConfig, u"General"_s);
0134     m_accentColorFromWallpaperEnabled = accentColorConfigGroup.readEntry("accentColorFromWallpaper", false);
0135 
0136     m_accentColorConfigWatcher = KConfigWatcher::create(globalConfig);
0137     connect(m_accentColorConfigWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) {
0138         if (names.contains(QByteArrayLiteral("accentColorFromWallpaper"))) {
0139             const bool result = group.readEntry("accentColorFromWallpaper", false);
0140             if (m_accentColorFromWallpaperEnabled != result) {
0141                 m_accentColorFromWallpaperEnabled = result;
0142                 Q_EMIT accentColorFromWallpaperEnabledChanged();
0143             }
0144         }
0145     });
0146 }
0147 
0148 void ShellCorona::init()
0149 {
0150 #if USE_SCRIPTING
0151     connect(this, &Plasma::Corona::containmentCreated, this, [this](Plasma::Containment *c) {
0152         executeSetupPlasmoidScript(c, c);
0153     });
0154 #endif
0155 
0156     connect(this, &Plasma::Corona::availableScreenRectChanged, this, &Plasma::Corona::availableScreenRegionChanged);
0157 
0158     m_appConfigSyncTimer.setSingleShot(true);
0159     m_appConfigSyncTimer.setInterval(s_configSyncDelay);
0160     connect(&m_appConfigSyncTimer, &QTimer::timeout, this, &ShellCorona::syncAppConfig);
0161     // we want our application config with screen mapping to always be in sync with the applets one, so a crash at any time will still
0162     // leave containments pointing to the correct screens
0163     connect(this, &Corona::configSynced, this, &ShellCorona::syncAppConfig);
0164 
0165     m_waitingPanelsTimer.setSingleShot(true);
0166     m_waitingPanelsTimer.setInterval(250ms);
0167     connect(&m_waitingPanelsTimer, &QTimer::timeout, this, &ShellCorona::createWaitingPanels);
0168 
0169 #ifndef NDEBUG
0170     m_invariantsTimer.setSingleShot(true);
0171     m_invariantsTimer.setInterval(qEnvironmentVariableIsSet("KDECI_BUILD") > 0 ? 30000ms : 250ms);
0172     connect(&m_invariantsTimer, &QTimer::timeout, this, &ShellCorona::screenInvariants);
0173 #endif
0174 
0175     m_desktopDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(kPackage().filePath("defaults")), u"Desktop"_s);
0176     m_lnfDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(m_lookAndFeelPackage.filePath("defaults")), u"Desktop"_s);
0177     m_lnfDefaultsConfig = KConfigGroup(&m_lnfDefaultsConfig, QStringLiteral("org.kde.plasma.desktop"));
0178 
0179     new PlasmaShellAdaptor(this);
0180 
0181     QDBusConnection dbus = QDBusConnection::sessionBus();
0182     dbus.registerObject(QStringLiteral("/PlasmaShell"), this);
0183 
0184     // Look for theme config in plasmarc, if it isn't configured, take the theme from the
0185     // LookAndFeel package, if either is set, change the default theme
0186 
0187     connect(qApp, &QCoreApplication::aboutToQuit, this, [this]() {
0188         // saveLayout is a slot but arguments not compatible
0189         m_closingDown = true;
0190         saveLayout();
0191     });
0192 
0193     connect(this, &ShellCorona::containmentAdded, this, &ShellCorona::handleContainmentAdded);
0194 
0195     QAction *dashboardAction = new QAction(this);
0196     setAction(QStringLiteral("show dashboard"), dashboardAction);
0197     QObject::connect(dashboardAction, &QAction::triggered, this, &ShellCorona::setDashboardShown);
0198     dashboardAction->setText(i18n("Show Desktop"));
0199     connect(KWindowSystem::self(), &KWindowSystem::showingDesktopChanged, dashboardAction, [dashboardAction](bool showing) {
0200         dashboardAction->setText(showing ? i18n("Hide Desktop") : i18n("Show Desktop"));
0201         dashboardAction->setChecked(showing);
0202     });
0203 
0204     dashboardAction->setAutoRepeat(true);
0205     dashboardAction->setCheckable(true);
0206     dashboardAction->setIcon(QIcon::fromTheme(QStringLiteral("dashboard-show")));
0207     KGlobalAccel::self()->setGlobalShortcut(dashboardAction, Qt::CTRL | Qt::Key_F12);
0208 
0209     checkAddPanelAction();
0210     connect(KSycoca::self(), &KSycoca::databaseChanged, this, &ShellCorona::checkAddPanelAction);
0211 
0212     // Activity stuff
0213     QAction *activityAction = new QAction(this);
0214     setAction(QStringLiteral("manage activities"), activityAction);
0215     connect(activityAction, &QAction::triggered, this, &ShellCorona::toggleActivityManager);
0216     activityAction->setText(i18n("Show Activity Switcher"));
0217     activityAction->setIcon(QIcon::fromTheme(QStringLiteral("activities")));
0218     activityAction->setShortcut(QKeySequence(QStringLiteral("alt+d, alt+a")));
0219     activityAction->setShortcutContext(Qt::ApplicationShortcut);
0220 
0221     KGlobalAccel::self()->setGlobalShortcut(activityAction, Qt::META | Qt::Key_Q);
0222 
0223     QAction *stopActivityAction = new QAction(this);
0224     setAction(QStringLiteral("stop current activity"), stopActivityAction);
0225     QObject::connect(stopActivityAction, &QAction::triggered, this, &ShellCorona::stopCurrentActivity);
0226 
0227     stopActivityAction->setText(i18n("Stop Current Activity"));
0228     stopActivityAction->setVisible(false);
0229 
0230     KGlobalAccel::self()->setGlobalShortcut(stopActivityAction, Qt::META | Qt::Key_S);
0231 
0232     QAction *previousActivityAction = new QAction(this);
0233     setAction(QStringLiteral("switch to previous activity"), previousActivityAction);
0234     connect(previousActivityAction, &QAction::triggered, this, &ShellCorona::previousActivity);
0235     previousActivityAction->setText(i18n("Switch to Previous Activity"));
0236     previousActivityAction->setShortcutContext(Qt::ApplicationShortcut);
0237 
0238     KGlobalAccel::self()->setGlobalShortcut(previousActivityAction, QKeySequence());
0239 
0240     QAction *nextActivityAction = new QAction(this);
0241     setAction(QStringLiteral("switch to next activity"), nextActivityAction);
0242     connect(nextActivityAction, &QAction::triggered, this, &ShellCorona::nextActivity);
0243     nextActivityAction->setText(i18n("Switch to Next Activity"));
0244     nextActivityAction->setShortcutContext(Qt::ApplicationShortcut);
0245 
0246     KGlobalAccel::self()->setGlobalShortcut(nextActivityAction, QKeySequence());
0247 
0248     connect(m_activityController, &KActivities::Controller::currentActivityChanged, this, &ShellCorona::currentActivityChanged);
0249     connect(m_activityController, &KActivities::Controller::activityAdded, this, &ShellCorona::activityAdded);
0250     connect(m_activityController, &KActivities::Controller::activityRemoved, this, &ShellCorona::activityRemoved);
0251 
0252     KActionCollection *taskbarActions = new KActionCollection(this);
0253     for (int i = 0; i < 10; ++i) {
0254         const int entryNumber = i + 1;
0255         const Qt::Key key = static_cast<Qt::Key>(Qt::Key_0 + (entryNumber % 10));
0256 
0257         QAction *action = taskbarActions->addAction(QStringLiteral("activate task manager entry %1").arg(QString::number(entryNumber)));
0258         action->setText(i18n("Activate Task Manager Entry %1", entryNumber));
0259         KGlobalAccel::setGlobalShortcut(action, QKeySequence(Qt::META | key));
0260         connect(action, &QAction::triggered, this, [this, i] {
0261             activateTaskManagerEntry(i);
0262         });
0263     }
0264 
0265     new Osd(m_config, this);
0266 
0267     // catch when plasmarc changes, so we e.g. enable/disable the OSd
0268     m_configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + m_config->name();
0269     KDirWatch::self()->addFile(m_configPath);
0270     connect(KDirWatch::self(), &KDirWatch::dirty, this, &ShellCorona::configurationChanged);
0271     connect(KDirWatch::self(), &KDirWatch::created, this, &ShellCorona::configurationChanged);
0272 
0273     connect(qApp, &QGuiApplication::focusWindowChanged, this, [this](QWindow *focusWindow) {
0274         if (!focusWindow) {
0275             setEditMode(false);
0276         }
0277     });
0278     connect(this, &ShellCorona::editModeChanged, this, [this](bool edit) {
0279         setDashboardShown(edit);
0280     });
0281 
0282     QAction *manageContainmentsAction = new QAction(this);
0283     setAction(QStringLiteral("manage-containments"), manageContainmentsAction);
0284     manageContainmentsAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-windows-effect-fadedesktop")));
0285     manageContainmentsAction->setText(i18n("Manage Desktops And Panels..."));
0286     connect(manageContainmentsAction, &QAction::triggered, this, [this]() {
0287         if (m_shellContainmentConfig == nullptr) {
0288             m_shellContainmentConfig = new ShellContainmentConfig(this);
0289             m_shellContainmentConfig->init();
0290         }
0291         // Swapping desktop views around causes problems with the show desktop effect
0292         setEditMode(false);
0293     });
0294     auto updateManageContainmentsVisiblility = [this, manageContainmentsAction]() {
0295         QSet<int> allScreenIds;
0296         for (auto *cont : containments()) {
0297             allScreenIds.insert(cont->lastScreen());
0298         }
0299         manageContainmentsAction->setVisible(allScreenIds.count() > 1);
0300     };
0301     connect(this, &ShellCorona::containmentAdded, this, updateManageContainmentsVisiblility);
0302     connect(this, &ShellCorona::screenRemoved, this, updateManageContainmentsVisiblility);
0303     updateManageContainmentsVisiblility();
0304 
0305     QAction *cyclePanelFocusAction = new QAction(this);
0306     setAction(QStringLiteral("cycle-panels"), cyclePanelFocusAction);
0307     cyclePanelFocusAction->setText(i18n("Move keyboard focus between panels"));
0308     KGlobalAccel::self()->setGlobalShortcut(cyclePanelFocusAction, Qt::META | Qt::ALT | Qt::Key_P);
0309     connect(cyclePanelFocusAction, &QAction::triggered, this, &ShellCorona::slotCyclePanelFocus);
0310 
0311     unload();
0312     /*
0313      * we want to make an initial load once we have loaded the activities _IF_ KAMD is running
0314      * it is valid for KAMD to not be running.
0315      *
0316      * Potentially 2 async jobs
0317      *
0318      * It might seem that we only need this connection if the activityConsumer is currently in state Unknown, however
0319      * there is an issue where m_activityController will start the kactivitymanagerd, as KAMD is starting the serviceStatus will be "not running"
0320      * Whilst we are loading the kscreen config, the event loop runs and we might find KAMD has started.
0321      * m_activityController will change from "not running" to unknown, and might still be unknown when the kscreen fetching is complete.
0322      *
0323      * if that happens we want to continue monitoring for state changes, and only finally load when it is up.
0324      *
0325      * See https://bugs.kde.org/show_bug.cgi?id=342431 be careful about changing
0326      *
0327      * The unique connection makes sure we don't reload plasma if KAMD ever crashes and reloads, the signal is disconnected in the body of load
0328      */
0329     connect(m_activityController, &KActivities::Controller::serviceStatusChanged, this, &ShellCorona::load, Qt::UniqueConnection);
0330     load();
0331 }
0332 
0333 ShellCorona::~ShellCorona()
0334 {
0335     while (!containments().isEmpty()) {
0336         // Deleting a containment will remove it from the list due to QObject::destroyed connect in Corona
0337         // Deleting a containment in turn also kills any panel views
0338         delete containments().constFirst();
0339     }
0340 }
0341 
0342 KPackage::Package ShellCorona::lookAndFeelPackage()
0343 {
0344     return m_lookAndFeelPackage;
0345 }
0346 
0347 void ShellCorona::setShell(const QString &shell)
0348 {
0349     if (m_shell == shell) {
0350         return;
0351     }
0352 
0353     m_shell = shell;
0354     KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Shell"));
0355     package.setPath(shell);
0356     package.setAllowExternalPaths(true);
0357     setKPackage(package);
0358     m_desktopDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(package.filePath("defaults")), u"Desktop"_s);
0359     m_lnfDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(m_lookAndFeelPackage.filePath("defaults")), u"Desktop"_s);
0360     m_lnfDefaultsConfig = KConfigGroup(&m_lnfDefaultsConfig, shell);
0361 
0362     const QString themeGroupKey = QStringLiteral("Theme");
0363     const QString themeNameKey = QStringLiteral("name");
0364 
0365     QString themeName;
0366 
0367     KConfigGroup plasmarc(m_config, themeGroupKey);
0368     themeName = plasmarc.readEntry(themeNameKey, themeName);
0369 
0370     if (themeName.isEmpty()) {
0371         KConfigGroup shellCfg = KConfigGroup(KSharedConfig::openConfig(package.filePath("defaults")), u"Theme"_s);
0372         themeName = shellCfg.readEntry(themeNameKey, "default");
0373     }
0374 
0375     if (!themeName.isEmpty()) {
0376         Plasma::Theme *t = new Plasma::Theme(this);
0377         t->setThemeName(themeName);
0378     }
0379 }
0380 
0381 QJsonObject dumpconfigGroupJS(const KConfigGroup &rootGroup)
0382 {
0383     QJsonObject result;
0384 
0385     QStringList hierarchy;
0386     QStringList escapedHierarchy;
0387     QList<KConfigGroup> groups{rootGroup};
0388     QSet<QString> visitedNodes;
0389 
0390     const QSet<QString> forbiddenKeys{QStringLiteral("activityId"),
0391                                       QStringLiteral("ItemsGeometries"),
0392                                       QStringLiteral("AppletOrder"),
0393                                       QStringLiteral("SystrayContainmentId"),
0394                                       QStringLiteral("location"),
0395                                       QStringLiteral("plugin")};
0396 
0397     auto groupID = [&escapedHierarchy]() {
0398         return '/' + escapedHierarchy.join('/');
0399     };
0400 
0401     // Perform a depth-first tree traversal for config groups
0402     while (!groups.isEmpty()) {
0403         KConfigGroup cg = groups.last();
0404 
0405         KConfigGroup parentCg = cg;
0406         // FIXME: name is not enough
0407 
0408         hierarchy.clear();
0409         escapedHierarchy.clear();
0410         while (parentCg.isValid() && parentCg.name() != rootGroup.name()) {
0411             const auto name = parentCg.name();
0412             hierarchy.prepend(name);
0413             escapedHierarchy.prepend(QString::fromUtf8(QUrl::toPercentEncoding(name.toUtf8())));
0414             parentCg = parentCg.parent();
0415         }
0416 
0417         visitedNodes.insert(groupID());
0418         groups.pop_back();
0419 
0420         QJsonObject configGroupJson;
0421 
0422         if (!cg.keyList().isEmpty()) {
0423             // TODO: this is conditional if applet or containment
0424 
0425             const auto map = cg.entryMap();
0426             auto i = map.cbegin();
0427             for (; i != map.cend(); ++i) {
0428                 // some blacklisted keys we don't want to save
0429                 if (!forbiddenKeys.contains(i.key())) {
0430                     configGroupJson.insert(i.key(), i.value());
0431                 }
0432             }
0433         }
0434 
0435         const auto groupList = cg.groupList();
0436         for (const QString &groupName : groupList) {
0437             if (groupName == QLatin1String("Applets") || visitedNodes.contains(groupID() + '/' + groupName)) {
0438                 continue;
0439             }
0440             groups << KConfigGroup(&cg, groupName);
0441         }
0442 
0443         if (!configGroupJson.isEmpty()) {
0444             result.insert(groupID(), configGroupJson);
0445         }
0446     }
0447 
0448     return result;
0449 }
0450 
0451 QByteArray ShellCorona::dumpCurrentLayoutJS() const
0452 {
0453     QJsonObject root;
0454     root.insert("serializationFormatVersion", "1");
0455 
0456     // same gridUnit calculation as ScriptEngine
0457     int gridUnit = QFontMetrics(QGuiApplication::font()).boundingRect(QStringLiteral("M")).height();
0458     if (gridUnit % 2 != 0) {
0459         gridUnit++;
0460     }
0461 
0462     auto isPanel = [](Plasma::Containment *cont) {
0463         return (cont->formFactor() == Plasma::Types::Horizontal || cont->formFactor() == Plasma::Types::Vertical)
0464             && (cont->location() == Plasma::Types::TopEdge || cont->location() == Plasma::Types::BottomEdge || cont->location() == Plasma::Types::LeftEdge
0465                 || cont->location() == Plasma::Types::RightEdge)
0466             && cont->pluginMetaData().pluginId() != QLatin1String("org.kde.plasma.private.systemtray");
0467     };
0468 
0469     auto isDesktop = [](Plasma::Containment *cont) {
0470         return !cont->activity().isEmpty();
0471     };
0472 
0473     const auto containments = ShellCorona::containments();
0474 
0475     // Collecting panels
0476 
0477     QJsonArray panelsJsonArray;
0478 
0479     for (Plasma::Containment *cont : containments) {
0480         if (!isPanel(cont)) {
0481             continue;
0482         }
0483 
0484         QJsonObject panelJson;
0485 
0486         const PanelView *view = m_panelViews.value(cont);
0487         const auto location = cont->location();
0488 
0489         panelJson.insert("location",
0490                          location == Plasma::Types::TopEdge         ? "top"
0491                              : location == Plasma::Types::LeftEdge  ? "left"
0492                              : location == Plasma::Types::RightEdge ? "right"
0493                                                                     : /* Plasma::Types::BottomEdge */ "bottom");
0494 
0495         const qreal height =
0496             // If we do not have a panel, fallback to 4 units
0497             !view ? 4 : (qreal)view->totalThickness() / gridUnit;
0498 
0499         panelJson.insert("height", height);
0500         if (view) {
0501             const auto alignment = view->alignment();
0502             panelJson.insert("maximumLength", (qreal)view->maximumLength() / gridUnit);
0503             panelJson.insert("minimumLength", (qreal)view->minimumLength() / gridUnit);
0504             panelJson.insert("offset", (qreal)view->offset() / gridUnit);
0505             panelJson.insert("alignment", alignment == Qt::AlignRight ? "right" : alignment == Qt::AlignCenter ? "center" : "left");
0506             switch (view->visibilityMode()) {
0507             case PanelView::AutoHide:
0508                 panelJson.insert("hiding", "autohide");
0509                 break;
0510             case PanelView::DodgeWindows:
0511                 panelJson.insert("hiding", "dodgewindows");
0512                 break;
0513             case PanelView::WindowsGoBelow:
0514                 panelJson.insert("hiding", "windowsgobelow");
0515                 break;
0516             case PanelView::NormalPanel:
0517             default:
0518                 panelJson.insert("hiding", "normal");
0519                 break;
0520             }
0521         }
0522 
0523         // Saving the config keys
0524         const KConfigGroup contConfig = cont->config();
0525 
0526         panelJson.insert("config", dumpconfigGroupJS(contConfig));
0527 
0528         // Generate the applets array
0529         QJsonArray appletsJsonArray;
0530 
0531         // Try to parse the encoded applets order
0532         const KConfigGroup genericConf(&contConfig, QStringLiteral("General"));
0533         const QStringList appletsOrderStrings = genericConf.readEntry(QStringLiteral("AppletOrder"), QString()).split(QChar(';'));
0534 
0535         // Consider the applet order to be valid only if there are as many entries as applets()
0536         if (appletsOrderStrings.length() == cont->applets().length()) {
0537             for (const QString &appletId : appletsOrderStrings) {
0538                 KConfigGroup appletConfig(&contConfig, QStringLiteral("Applets"));
0539                 appletConfig = KConfigGroup(&appletConfig, appletId);
0540 
0541                 const QString pluginName = appletConfig.readEntry(QStringLiteral("plugin"), QString());
0542 
0543                 if (pluginName.isEmpty()) {
0544                     continue;
0545                 }
0546 
0547                 QJsonObject appletJson;
0548 
0549                 appletConfig = KConfigGroup(&appletConfig, QStringLiteral("Configuration"));
0550 
0551                 appletJson.insert("plugin", pluginName);
0552                 appletJson.insert("config", dumpconfigGroupJS(appletConfig));
0553 
0554                 appletsJsonArray << appletJson;
0555             }
0556 
0557         } else {
0558             const auto applets = cont->applets();
0559             for (Plasma::Applet *applet : applets) {
0560                 QJsonObject appletJson;
0561 
0562                 KConfigGroup appletConfig = applet->config();
0563 
0564                 appletJson.insert("plugin", applet->pluginMetaData().pluginId());
0565                 appletJson.insert("config", dumpconfigGroupJS(appletConfig));
0566 
0567                 appletsJsonArray << appletJson;
0568             }
0569         }
0570 
0571         panelJson.insert("applets", appletsJsonArray);
0572 
0573         panelsJsonArray << panelJson;
0574     }
0575 
0576     root.insert("panels", panelsJsonArray);
0577 
0578     // Now we are collecting desktops
0579 
0580     QJsonArray desktopsJson;
0581 
0582     const auto currentActivity = m_activityController->currentActivity();
0583 
0584     for (Plasma::Containment *cont : containments) {
0585         if (!isDesktop(cont) || cont->activity() != currentActivity) {
0586             continue;
0587         }
0588 
0589         QJsonObject desktopJson;
0590 
0591         desktopJson.insert("wallpaperPlugin", cont->wallpaperPlugin());
0592 
0593         // Get the config for the containment
0594         KConfigGroup contConfig = cont->config();
0595         desktopJson.insert("config", dumpconfigGroupJS(contConfig));
0596 
0597         // Try to parse the item geometries
0598         const KConfigGroup genericConf(&contConfig, QStringLiteral("General"));
0599         const QStringList appletsGeomStrings = genericConf.readEntry(QStringLiteral("ItemsGeometries"), QString()).split(QChar(';'));
0600 
0601         QHash<QString, QRect> appletGeometries;
0602         for (const QString &encoded : appletsGeomStrings) {
0603             const QStringList keyValue = encoded.split(QLatin1Char(':'));
0604             if (keyValue.length() != 2) {
0605                 continue;
0606             }
0607 
0608             const QStringList rectPieces = keyValue.crbegin()->split(QLatin1Char(','));
0609             if (rectPieces.length() != 5) {
0610                 continue;
0611             }
0612 
0613             appletGeometries.emplace(*keyValue.cbegin(), rectPieces[0].toInt(), rectPieces[1].toInt(), rectPieces[2].toInt(), rectPieces[3].toInt());
0614         }
0615 
0616         QJsonArray appletsJsonArray;
0617 
0618         const auto applets = cont->applets();
0619         for (Plasma::Applet *applet : applets) {
0620             const QRect geometry = appletGeometries.value(QStringLiteral("Applet-") % QString::number(applet->id()));
0621 
0622             QJsonObject appletJson;
0623 
0624             appletJson.insert("title", applet->title());
0625             appletJson.insert("plugin", applet->pluginMetaData().pluginId());
0626 
0627             appletJson.insert("geometry.x", geometry.x() / gridUnit);
0628             appletJson.insert("geometry.y", geometry.y() / gridUnit);
0629             appletJson.insert("geometry.width", geometry.width() / gridUnit);
0630             appletJson.insert("geometry.height", geometry.height() / gridUnit);
0631 
0632             KConfigGroup appletConfig = applet->config();
0633             appletJson.insert("config", dumpconfigGroupJS(appletConfig));
0634 
0635             appletsJsonArray << appletJson;
0636         }
0637 
0638         desktopJson.insert("applets", appletsJsonArray);
0639         desktopsJson << desktopJson;
0640     }
0641 
0642     root.insert("desktops", desktopsJson);
0643 
0644     QJsonDocument json;
0645     json.setObject(root);
0646 
0647     return
0648         "var plasma = getApiVersion(1);\n\n"
0649         "var layout = " + json.toJson() + ";\n\n"
0650         "plasma.loadSerializedLayout(layout);\n";
0651 }
0652 
0653 void ShellCorona::loadLookAndFeelDefaultLayout(const QString &packageName)
0654 {
0655     KPackage::Package newPack = m_lookAndFeelPackage;
0656     newPack.setPath(packageName);
0657 
0658     if (!newPack.isValid()) {
0659         return;
0660     }
0661 
0662     KSharedConfig::Ptr conf = KSharedConfig::openConfig(QLatin1String("plasma-") + m_shell + QLatin1String("-appletsrc"), KConfig::SimpleConfig);
0663 
0664     m_lookAndFeelPackage.setPath(packageName);
0665 
0666     // get rid of old config
0667     const QStringList groupList = conf->groupList();
0668     for (const QString &group : groupList) {
0669         conf->deleteGroup(group);
0670     }
0671     conf->sync();
0672     unload();
0673     // Put load in queue of the event loop to wait for the whole set of containments to have been deleteLater(), as some like FolderView operate on singletons
0674     // which can cause inconsistent states
0675     QTimer::singleShot(0, this, &ShellCorona::load);
0676 }
0677 
0678 QString ShellCorona::shell() const
0679 {
0680     return m_shell;
0681 }
0682 
0683 void ShellCorona::sanitizeScreenLayout(const QString &configFileName)
0684 {
0685     KConfigGroup cg(KSharedConfig::openConfig(configFileName), QStringLiteral("Containments"));
0686 
0687     // The containment-> screen mappings we found in the config file
0688     QHash<QString, QMap<int, QString>> savedContainmentScreens;
0689 
0690     // Desktop containments with screen = -1 or duplicated wanting to go on the same screen as somebody else
0691     QStringList orphanContainments;
0692     // Panel containments we found we may want to remap the screen
0693     QStringList panelContainments;
0694     QSet<Plasma::Types::Location> panelLocations({Plasma::Types::TopEdge, Plasma::Types::BottomEdge, Plasma::Types::LeftEdge, Plasma::Types::RightEdge});
0695 
0696     for (const QString &idStr : cg.groupList()) {
0697         if (idStr.toInt() <= 0) {
0698             continue;
0699         }
0700 
0701         KConfigGroup contCg(&cg, idStr);
0702         int lastScreen = contCg.readEntry(QStringLiteral("lastScreen"), -1);
0703 
0704         if (panelLocations.contains(Plasma::Types::Location(contCg.readEntry(QStringLiteral("location"), 0)))) {
0705             if (lastScreen >= 0) {
0706                 panelContainments.append(idStr);
0707             }
0708             continue;
0709         }
0710 
0711         const QString &activity = contCg.readEntry(QStringLiteral("activityId"), QString());
0712 
0713         if (lastScreen >= 0 && !savedContainmentScreens[activity].contains(lastScreen)) {
0714             savedContainmentScreens[activity][lastScreen] = idStr;
0715         } else {
0716             orphanContainments.append(idStr);
0717         }
0718     }
0719 
0720     QHash<int, int> screenMapping;
0721 
0722     // Ensure desktops screens are progressive
0723     for (auto activityIt = savedContainmentScreens.begin(); activityIt != savedContainmentScreens.end(); activityIt++) {
0724         const QString &activity = activityIt.key();
0725         int progressiveScreen = 0;
0726         for (auto originalScreenIt = activityIt.value().begin(); originalScreenIt != activityIt.value().end(); originalScreenIt++) {
0727             KConfigGroup contCg(&cg, originalScreenIt.value());
0728             screenMapping[originalScreenIt.key()] = progressiveScreen;
0729             contCg.writeEntry(QStringLiteral("lastScreen"), progressiveScreen++);
0730         }
0731 
0732         for (auto orphanContainmentsIt = orphanContainments.constBegin(); orphanContainmentsIt != orphanContainments.constEnd(); orphanContainmentsIt++) {
0733             KConfigGroup contCg(&cg, (*orphanContainmentsIt));
0734             const QString &orphanActivity = contCg.readEntry(QStringLiteral("activityId"), QString());
0735             if (orphanActivity == activity) {
0736                 contCg.writeEntry(QStringLiteral("lastScreen"), progressiveScreen++);
0737             }
0738         }
0739     }
0740 
0741     // Remap panels to the screen changes we did for desktops
0742     for (auto panelsIt = panelContainments.begin(); panelsIt != panelContainments.end(); panelsIt++) {
0743         KConfigGroup contCg(&cg, (*panelsIt));
0744         int lastScreen = contCg.readEntry(QStringLiteral("lastScreen"), -1);
0745 
0746         // If we don't know where to put the panel, put it on the first screen
0747         contCg.writeEntry(QStringLiteral("lastScreen"), screenMapping.value(lastScreen, 0));
0748     }
0749 }
0750 
0751 void ShellCorona::load()
0752 {
0753     if (m_shell.isEmpty()) {
0754         return;
0755     }
0756 
0757     auto activityStatus = m_activityController->serviceStatus();
0758     if (activityStatus != KActivities::Controller::Running && !qApp->property("org.kde.KActivities.core.disableAutostart").toBool()) {
0759         if (activityStatus == KActivities::Controller::NotRunning) {
0760             qCWarning(PLASMASHELL) << "Aborting shell load: The activity manager daemon (kactivitymanagerd) is not running.";
0761             qCWarning(PLASMASHELL)
0762                 << "If this Plasma has been installed into a custom prefix, verify that its D-Bus services dir is known to the system for the daemon to be "
0763                    "activatable.";
0764         }
0765         return;
0766     }
0767 
0768     disconnect(m_activityController, &KActivities::Controller::serviceStatusChanged, this, &ShellCorona::load);
0769 
0770     // TODO: a kconf_update script is needed
0771     QString configFileName(QStringLiteral("plasma-") + m_shell + QStringLiteral("-appletsrc"));
0772 
0773     // Make sure all containments have screen numbers starting from 0 and are sequential
0774     sanitizeScreenLayout(configFileName);
0775 
0776     loadLayout(configFileName);
0777 
0778     checkActivities();
0779 
0780     if (containments().isEmpty()) {
0781         // Seems like we never really get to this point since loadLayout already
0782         // (virtually) calls loadDefaultLayout if it does not load anything
0783         // from the config file. Maybe if the config file is not empty,
0784         // but still does not have any containments
0785         loadDefaultLayout();
0786         processUpdateScripts();
0787     } else {
0788         processUpdateScripts();
0789         const auto containments = this->containments();
0790 
0791         // Don't give a view to containments that don't want one (negative lastscreen)
0792         // (this is pretty mucha special case for the systray)
0793         // also, make sure we don't have a view already.
0794         // this will be true for first startup as the view has already been created at the new Panel JS call
0795         std::copy_if(containments.constBegin(), containments.constEnd(), std::back_inserter(m_waitingPanels), [this](Plasma::Containment *containment) {
0796             return ((containment->containmentType() == Plasma::Containment::Panel || containment->containmentType() == Plasma::Containment::CustomPanel)
0797                     && !m_waitingPanels.contains(containment) && containment->lastScreen() >= 0 && !m_panelViews.contains(containment));
0798         });
0799     }
0800 
0801     // NOTE: this is needed in case loadLayout() did *not* call loadDefaultLayout()
0802     // it needs to be after of loadLayout() as it would always create new
0803     // containments on each startup otherwise
0804     const auto screens = m_screenPool->screenOrder();
0805     for (QScreen *screen : screens) {
0806         QSet<QScreen *> managedScreens;
0807         for (auto *desk : m_desktopViewForScreen) {
0808             managedScreens.insert(desk->screenToFollow());
0809         }
0810 
0811         // the containments may have been created already by the startup script
0812         // check their existence in order to not have duplicated desktopviews
0813         if (!managedScreens.contains(screen)) {
0814             addOutput(screen);
0815         }
0816     }
0817     connect(m_screenPool, &ScreenPool::screenOrderChanged, this, &ShellCorona::handleScreenOrderChanged, Qt::UniqueConnection);
0818     connect(m_screenPool, &ScreenPool::screenRemoved, this, &ShellCorona::handleScreenRemoved, Qt::UniqueConnection);
0819 
0820     if (!m_waitingPanels.isEmpty()) {
0821         m_waitingPanelsTimer.start();
0822     }
0823 
0824     if (config()->isImmutable() || !KAuthorized::authorize(QStringLiteral("plasma/plasmashell/unlockedDesktop"))) {
0825         setImmutability(Plasma::Types::SystemImmutable);
0826     } else {
0827         KConfigGroup coronaConfig(config(), u"General"_s);
0828         setImmutability((Plasma::Types::ImmutabilityType)coronaConfig.readEntry("immutability", static_cast<int>(Plasma::Types::Mutable)));
0829     }
0830 }
0831 
0832 #ifndef NDEBUG
0833 void ShellCorona::screenInvariants() const
0834 {
0835     if (m_screenPool->noRealOutputsConnected()) {
0836         Q_ASSERT(m_desktopViewForScreen.isEmpty());
0837         Q_ASSERT(m_panelViews.isEmpty());
0838         return;
0839     }
0840 
0841     QSet<QScreen *> managedScreens;
0842     for (auto *desk : m_desktopViewForScreen) {
0843         managedScreens.insert(desk->screenToFollow());
0844     }
0845 
0846     Q_ASSERT(managedScreens.count() <= m_screenPool->screenOrder().count());
0847 
0848     QSet<QScreen *> screens;
0849     for (QScreen *knownScreen : managedScreens) {
0850         const int id = m_screenPool->idForScreen(knownScreen);
0851         const DesktopView *view = desktopForScreen(knownScreen);
0852         Q_ASSERT(view->isVisible());
0853         QScreen *screen = view->screenToFollow();
0854         Q_ASSERT(knownScreen == screen);
0855         Q_ASSERT(!screens.contains(screen));
0856         //         commented out because a different part of the code-base is responsible for this
0857         //         and sometimes is not yet called here.
0858         //         Q_ASSERT(!view->fillScreen() || view->geometry() == screen->geometry());
0859 
0860         Q_ASSERT(view->containment()->screen() == id || view->containment()->screen() == -1);
0861         Q_ASSERT(view->containment()->lastScreen() == id || view->containment()->lastScreen() == -1);
0862         Q_ASSERT(view->isVisible());
0863 
0864         for (const PanelView *panel : m_panelViews) {
0865             if (panel->screenToFollow() == screen) {
0866                 Q_ASSERT(panel->containment());
0867                 Q_ASSERT(panel->containment()->screen() == id || panel->containment()->screen() == -1);
0868                 // If any kscreen related activities occurred
0869                 // during startup, the panel wouldn't be visible yet, and this would assert
0870                 if (panel->containment()->isUiReady()) {
0871                     Q_ASSERT(panel->isVisible());
0872                 }
0873             }
0874         }
0875 
0876         screens.insert(screen);
0877     }
0878 
0879     if (m_desktopViewForScreen.isEmpty()) {
0880         qCWarning(PLASMASHELL) << "no screens!!";
0881     }
0882 }
0883 #endif
0884 
0885 void ShellCorona::showAlternativesForApplet(Plasma::Applet *applet)
0886 {
0887     if (m_showingAlternatives != nullptr && m_showingAlternatives == applet)
0888         return;
0889     const QUrl alternativesQML = kPackage().fileUrl("appletalternativesui");
0890     if (alternativesQML.isEmpty()) {
0891         return;
0892     }
0893 
0894     auto *qmlObj = new PlasmaQuick::SharedQmlEngine(this);
0895     qmlObj->setInitializationDelayed(true);
0896     qmlObj->setSource(alternativesQML);
0897 
0898     AlternativesHelper *helper = new AlternativesHelper(applet, qmlObj);
0899     qmlObj->rootContext()->setContextProperty(QStringLiteral("alternativesHelper"), helper);
0900 
0901     qmlObj->completeInitialization();
0902 
0903     auto dialog = qobject_cast<QQuickWindow *>(qmlObj->rootObject());
0904     if (!dialog) {
0905         qCWarning(PLASMASHELL) << "Alternatives UI does not inherit from Dialog";
0906         delete qmlObj;
0907         return;
0908     }
0909     m_showingAlternatives = applet;
0910     connect(applet, &Plasma::Applet::destroyedChanged, qmlObj, [qmlObj, this](bool destroyed) {
0911         if (!destroyed) {
0912             return;
0913         }
0914         m_showingAlternatives = nullptr;
0915         qmlObj->deleteLater();
0916     });
0917     connect(dialog, &QQuickWindow::visibleChanged, qmlObj, [qmlObj, this](bool visible) {
0918         if (visible) {
0919             return;
0920         }
0921         m_showingAlternatives = nullptr;
0922         qmlObj->deleteLater();
0923     });
0924 }
0925 
0926 void ShellCorona::unload()
0927 {
0928     if (m_shell.isEmpty()) {
0929         return;
0930     }
0931 
0932     // Make double sure that we do not access the members while we are in the process of deleting them.
0933     // Most notably destroying PanelViews may issue signals that in turn cause iteration upon the m_panelViews.
0934     // First clear the members, then delete them.
0935     auto desktopViewForScreen = std::move(m_desktopViewForScreen);
0936     qDeleteAll(desktopViewForScreen);
0937     for (const auto panelViews = std::move(m_panelViews); auto panelView : panelViews) {
0938         panelView->disconnect(this);
0939         delete panelView;
0940     }
0941 
0942     m_waitingPanels.clear();
0943     m_activityContainmentPlugins.clear();
0944 
0945     while (!containments().isEmpty()) {
0946         // Some applets react to destroyedChanged rather just destroyed,
0947         // give them  the possibility to react
0948         // deleting a containment will remove it from the list due to QObject::destroyed connect in Corona
0949         // this form doesn't crash, while qDeleteAll(containments()) does
0950         // And is more correct anyways to use destroy()
0951         containments().constFirst()->destroy();
0952     }
0953 }
0954 
0955 KSharedConfig::Ptr ShellCorona::applicationConfig()
0956 {
0957     return KSharedConfig::openConfig();
0958 }
0959 
0960 void ShellCorona::requestApplicationConfigSync()
0961 {
0962     m_appConfigSyncTimer.start();
0963 }
0964 
0965 void ShellCorona::slotCyclePanelFocus()
0966 {
0967     if (m_panelViews.empty()) {
0968         return;
0969     }
0970 
0971     PanelView *activePanel = qobject_cast<PanelView *>(qGuiApp->focusWindow());
0972     if (!activePanel) {
0973         // Activate the first panel and save the previous window
0974         activePanel = m_panelViews.begin().value();
0975     }
0976 
0977     if (activePanel->containment()->status() != Plasma::Types::AcceptingInputStatus) {
0978         activePanel->containment()->setStatus(Plasma::Types::AcceptingInputStatus);
0979     } else {
0980         // Cancel focus on the current panel
0981         // Block focus on the panel if it's not the last panel
0982         if (activePanel != m_panelViews.last()) {
0983             m_blockRestorePreviousWindow = true;
0984         }
0985         activePanel->containment()->setStatus(Plasma::Types::PassiveStatus);
0986         m_blockRestorePreviousWindow = false;
0987 
0988         // More than one panel and the current panel is not the last panel,
0989         // move focus to next panel.
0990         if (activePanel != m_panelViews.last()) {
0991             auto viewIt = std::find_if(m_panelViews.cbegin(), m_panelViews.cend(), [activePanel](const PanelView *panel) {
0992                 return activePanel == panel;
0993             });
0994 
0995             if (viewIt == m_panelViews.cend()) {
0996                 return;
0997             }
0998 
0999             // Skip destroyed panels
1000             viewIt = std::next(viewIt);
1001             while (viewIt != m_panelViews.cend()) {
1002                 if (!viewIt.value()->containment()->destroyed()) {
1003                     break;
1004                 }
1005 
1006                 viewIt = std::next(viewIt);
1007             }
1008 
1009             if (viewIt != m_panelViews.cend()) {
1010                 viewIt.value()->containment()->setStatus(Plasma::Types::AcceptingInputStatus);
1011             } else {
1012                 restorePreviousWindow();
1013             }
1014         }
1015     }
1016 }
1017 
1018 void ShellCorona::loadDefaultLayout()
1019 {
1020 #if USE_SCRIPTING
1021     // pre-startup scripts
1022     QString script = m_lookAndFeelPackage.filePath("layouts", shell() + "-prelayout.js");
1023     if (!script.isEmpty()) {
1024         QFile file(script);
1025         if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
1026             QString code = file.readAll();
1027             qCDebug(PLASMASHELL) << "evaluating pre-startup script:" << script;
1028 
1029             WorkspaceScripting::ScriptEngine scriptEngine(this);
1030 
1031             connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) {
1032                 qCWarning(PLASMASHELL) << msg;
1033             });
1034             connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) {
1035                 qCDebug(PLASMASHELL) << msg;
1036             });
1037             if (!scriptEngine.evaluateScript(code, script)) {
1038                 qCWarning(PLASMASHELL) << "failed to initialize layout properly:" << script;
1039             }
1040         }
1041     }
1042 
1043     // NOTE: Is important the containments already exist for each screen
1044     // at the moment of the script execution,the same loop in :load()
1045     // is executed too late
1046     const auto screens = m_screenPool->screenOrder();
1047     for (QScreen *screen : screens) {
1048         addOutput(screen);
1049     }
1050 
1051     script = m_testModeLayout;
1052 
1053     if (script.isEmpty()) {
1054         script = m_lookAndFeelPackage.filePath("layouts", shell() + "-layout.js");
1055     }
1056     if (script.isEmpty()) {
1057         script = kPackage().filePath("defaultlayout");
1058     }
1059 
1060     QFile file(script);
1061     if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
1062         QString code = file.readAll();
1063         qCDebug(PLASMASHELL) << "evaluating startup script:" << script;
1064 
1065         // We need to know which activities are here in order for
1066         // the scripting engine to work. activityAdded does not mind
1067         // if we pass it the same activity multiple times
1068         const QStringList existingActivities = m_activityController->activities();
1069         for (const QString &id : existingActivities) {
1070             activityAdded(id);
1071         }
1072 
1073         WorkspaceScripting::ScriptEngine scriptEngine(this);
1074 
1075         connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) {
1076             qCWarning(PLASMASHELL) << msg;
1077         });
1078         connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) {
1079             qCDebug(PLASMASHELL) << msg;
1080         });
1081         if (!scriptEngine.evaluateScript(code, script)) {
1082             qCWarning(PLASMASHELL) << "failed to initialize layout properly:" << script;
1083         }
1084     }
1085 #endif
1086     Q_EMIT startupCompleted();
1087 }
1088 
1089 void ShellCorona::processUpdateScripts()
1090 {
1091 #if USE_SCRIPTING
1092     const QStringList scripts = WorkspaceScripting::ScriptEngine::pendingUpdateScripts(this);
1093     if (scripts.isEmpty()) {
1094         return;
1095     }
1096 
1097     WorkspaceScripting::ScriptEngine scriptEngine(this);
1098 
1099     connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) {
1100         qCWarning(PLASMASHELL) << msg;
1101     });
1102     connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) {
1103         qCDebug(PLASMASHELL) << msg;
1104     });
1105 
1106     for (const QString &script : scripts) {
1107         QFile file(script);
1108         if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
1109             QString code = file.readAll();
1110             scriptEngine.evaluateScript(code);
1111         } else {
1112             qCWarning(PLASMASHELL) << "Unable to open the script file" << script << "for reading";
1113         }
1114     }
1115 #endif
1116 }
1117 
1118 int ShellCorona::numScreens() const
1119 {
1120     return m_screenPool->screenOrder().count();
1121 }
1122 
1123 QRect ShellCorona::screenGeometry(int id) const
1124 {
1125     QScreen *screen = m_screenPool->screenForId(id);
1126     if (!screen) {
1127         qCWarning(PLASMASHELL) << "requesting unexisting screen geometry" << id;
1128         screen = m_screenPool->primaryScreen();
1129         return screen ? screen->geometry() : QRect();
1130     }
1131     return screen->geometry();
1132 }
1133 
1134 QRegion ShellCorona::availableScreenRegion(int id) const
1135 {
1136     return m_strutManager->availableScreenRegion(id);
1137 }
1138 
1139 QRegion ShellCorona::_availableScreenRegion(int id) const
1140 {
1141     QScreen *screen = m_screenPool->screenForId(id);
1142     if (!screen) {
1143         // each screen should have a view
1144         qCWarning(PLASMASHELL) << "requesting unexisting screen region" << id;
1145         screen = m_screenPool->primaryScreen();
1146         return screen ? screen->availableGeometry() : QRegion();
1147     }
1148 
1149     return std::accumulate(m_panelViews.cbegin(), m_panelViews.cend(), QRegion(screen->geometry()), [screen](const QRegion &a, const PanelView *v) {
1150         if (v->isVisible() && screen == v->screen() && v->visibilityMode() != PanelView::AutoHide) {
1151             // if the panel is being moved around, we still want to calculate it from the edge
1152             return a - v->geometryByDistance(0);
1153         }
1154         return a;
1155     });
1156 }
1157 
1158 QRect ShellCorona::availableScreenRect(int id) const
1159 {
1160     return m_strutManager->availableScreenRect(id);
1161 }
1162 
1163 QRect ShellCorona::_availableScreenRect(int id) const
1164 {
1165     QScreen *screen = m_screenPool->screenForId(id);
1166     if (!screen) {
1167         // each screen should have a view
1168         qCWarning(PLASMASHELL) << "requesting unexisting screen available rect" << id;
1169         screen = m_screenPool->primaryScreen();
1170         return screen ? screen->availableGeometry() : QRect();
1171     }
1172 
1173     QRect r = screen->geometry();
1174     int topThickness, leftThickness, rightThickness, bottomThickness;
1175     topThickness = leftThickness = rightThickness = bottomThickness = 0;
1176     for (PanelView *v : m_panelViews) {
1177         if (v->isVisible() && v->screen() == screen && v->visibilityMode() != PanelView::AutoHide) {
1178             switch (v->location()) {
1179             case Plasma::Types::LeftEdge:
1180                 leftThickness = qMax(leftThickness, v->totalThickness());
1181                 break;
1182             case Plasma::Types::RightEdge:
1183                 rightThickness = qMax(rightThickness, v->totalThickness());
1184                 break;
1185             case Plasma::Types::TopEdge:
1186                 topThickness = qMax(topThickness, v->totalThickness());
1187                 break;
1188             case Plasma::Types::BottomEdge:
1189                 bottomThickness = qMax(bottomThickness, v->totalThickness());
1190             default:
1191                 break;
1192             }
1193         }
1194     }
1195     r.setLeft(r.left() + leftThickness);
1196     r.setRight(r.right() - rightThickness);
1197     r.setTop(r.top() + topThickness);
1198     r.setBottom(r.bottom() - bottomThickness);
1199     return r;
1200 }
1201 
1202 QStringList ShellCorona::availableActivities() const
1203 {
1204     return m_activityContainmentPlugins.keys();
1205 }
1206 
1207 void ShellCorona::removeDesktop(DesktopView *desktopView)
1208 {
1209     const int screenId = desktopView->containment()->lastScreen();
1210 
1211     auto result = std::find_if(m_desktopViewForScreen.begin(), m_desktopViewForScreen.end(), [desktopView](DesktopView *v) {
1212         return v == desktopView;
1213     });
1214 
1215     if (result != m_desktopViewForScreen.end()) {
1216         m_desktopViewForScreen.erase(result);
1217     }
1218 
1219     QMutableMapIterator<const Plasma::Containment *, PanelView *> it(m_panelViews);
1220 
1221     while (it.hasNext()) {
1222         it.next();
1223         PanelView *panelView = it.value();
1224 
1225         if (panelView->containment()->lastScreen() == screenId) {
1226             m_waitingPanels << panelView->containment();
1227             it.remove();
1228             panelView->destroy();
1229             panelView->containment()->reactToScreenChange();
1230         }
1231     }
1232 
1233     desktopView->destroy();
1234     desktopView->containment()->reactToScreenChange();
1235 }
1236 
1237 PanelView *ShellCorona::panelView(Plasma::Containment *containment) const
1238 {
1239     return m_panelViews.value(containment);
1240 }
1241 
1242 void ShellCorona::savePreviousWindow()
1243 {
1244 #if HAVE_X11
1245     if (KWindowSystem::isPlatformX11() && m_previousWId == 0) {
1246         m_previousWId = KX11Extras::activeWindow();
1247     }
1248 #endif
1249     if (m_waylandWindowManagement && !m_previousPlasmaWindow) {
1250         m_previousPlasmaWindow = m_waylandWindowManagement->activeWindow();
1251     }
1252 }
1253 
1254 void ShellCorona::restorePreviousWindow()
1255 {
1256     if (m_blockRestorePreviousWindow) {
1257         return;
1258     }
1259 
1260 #if HAVE_X11
1261     if (KWindowSystem::isPlatformX11() && m_previousWId) {
1262         KX11Extras::forceActiveWindow(m_previousWId);
1263     }
1264 #endif
1265     if (m_previousPlasmaWindow) {
1266         m_previousPlasmaWindow->requestActivate();
1267     }
1268 
1269     clearPreviousWindow();
1270 }
1271 
1272 void ShellCorona::clearPreviousWindow()
1273 {
1274     m_previousWId = 0;
1275     m_previousPlasmaWindow = nullptr;
1276 }
1277 
1278 ///// SLOTS
1279 
1280 DesktopView *ShellCorona::desktopForScreen(QScreen *screen) const
1281 {
1282     if (!screen) {
1283         return nullptr;
1284     }
1285     if (auto *v = m_desktopViewForScreen.value(m_screenPool->idForScreen(screen))) {
1286         return v;
1287     }
1288 
1289     return nullptr;
1290 }
1291 
1292 void ShellCorona::handleScreenRemoved(QScreen *screen)
1293 {
1294     if (DesktopView *v = desktopForScreen(screen)) {
1295         // Checking with m_screenPool->screenOrder().count() - 1 because when ScreenPool emits screenRemoved, the screen has not yet been removed from
1296         // ScreenPool::screenORder()
1297         if (v->containment()->lastScreen() < 0 || v->containment()->lastScreen() >= m_screenPool->screenOrder().count() - 1) {
1298             removeDesktop(v);
1299         }
1300         // Else do nothing, the view will be recycled
1301     }
1302 
1303     // The real screen index that has been removed is *always* the highest one, because we enforce order.
1304     // There can't be a containment that has for instance screen 0 and another 2 but nothing on 1
1305     // It's size() - 1 because at this point screenpool didn't remove it from screenOrder() yet
1306     Q_EMIT screenRemoved(m_screenPool->screenOrder().size() - 1);
1307 #ifndef NDEBUG
1308     m_invariantsTimer.start();
1309 #endif
1310 }
1311 
1312 void ShellCorona::handleScreenOrderChanged(QList<QScreen *> screens)
1313 {
1314     m_screenReorderInProgress = true;
1315     // First: reassign existing views if applicable, otherwise remove them
1316     auto allDesktops = m_desktopViewForScreen.values();
1317     m_desktopViewForScreen.clear();
1318     for (auto *v : allDesktops) {
1319         const int screenNumber = v->containment()->lastScreen();
1320         if (screenNumber >= 0 && screenNumber < screens.count()) {
1321             v->setScreenToFollow(screens[screenNumber]);
1322             v->setVisible(true);
1323             m_desktopViewForScreen[screenNumber] = v;
1324         } else {
1325             removeDesktop(v);
1326         }
1327     }
1328 
1329     // Doing it here as m_panelViews might already have been modified by the step before
1330     auto allPanels = m_panelViews.values();
1331     m_panelViews.clear();
1332     for (auto *v : allPanels) {
1333         const int screenNumber = v->containment()->lastScreen();
1334         if (screenNumber >= 0 && screenNumber < screens.count()) {
1335             v->setScreenToFollow(screens[screenNumber]);
1336             v->setVisible(true);
1337             m_panelViews[v->containment()] = v;
1338         } else {
1339             v->destroy();
1340             v->containment()->reactToScreenChange();
1341         }
1342     }
1343 
1344     // Second: add everything that wasn't there yet
1345     for (auto *s : screens) {
1346         if (!desktopForScreen(s)) {
1347             addOutput(s);
1348         }
1349     }
1350 
1351     m_screenReorderInProgress = false;
1352     Q_EMIT screenOrderChanged(screens);
1353 
1354     Q_ASSERT(m_desktopViewForScreen.count() == screens.count());
1355     for (int i = 0; i < screens.count(); ++i) {
1356         Q_EMIT screenGeometryChanged(i);
1357         Q_EMIT availableScreenRectChanged(i);
1358     }
1359 
1360     CHECK_SCREEN_INVARIANTS
1361 }
1362 
1363 void ShellCorona::addOutput(QScreen *screen)
1364 {
1365     Q_ASSERT(screen);
1366     if (desktopForScreen(screen)) {
1367         Q_EMIT screenAdded(m_screenPool->idForScreen(screen));
1368         return;
1369     }
1370     Q_ASSERT(!screen->geometry().isNull());
1371 #ifndef NDEBUG
1372     connect(screen, &QScreen::geometryChanged, &m_invariantsTimer, qOverload<>(&QTimer::start), Qt::UniqueConnection);
1373 #endif
1374     int insertPosition = m_screenPool->idForScreen(screen);
1375     Q_ASSERT(insertPosition >= 0);
1376 
1377     DesktopView *view = new DesktopView(this, screen);
1378 
1379     if (view->rendererInterface()->graphicsApi() != QSGRendererInterface::Software) {
1380         connect(view, &QQuickWindow::sceneGraphError, this, &ShellCorona::glInitializationFailed);
1381     }
1382     connect(view, &DesktopView::geometryChanged, this, [this, view]() {
1383         const int id = m_screenPool->idForScreen(view->screen());
1384         if (id >= 0 && !m_screenReorderInProgress) {
1385             Q_EMIT screenGeometryChanged(id);
1386             Q_EMIT availableScreenRectChanged(id);
1387         }
1388     });
1389 
1390     Plasma::Containment *containment = createContainmentForActivity(m_activityController->currentActivity(), insertPosition);
1391     Q_ASSERT(containment);
1392 
1393     QAction *removeAction = containment->internalAction(QStringLiteral("remove"));
1394     if (removeAction) {
1395         removeAction->deleteLater();
1396     }
1397 
1398     connect(containment, &Plasma::Containment::uiReadyChanged, this, &ShellCorona::checkAllDesktopsUiReady);
1399 
1400     m_desktopViewForScreen[insertPosition] = view;
1401     view->setContainment(containment);
1402     view->show();
1403 
1404     Q_ASSERT(screen == view->screen());
1405 
1406     // need to specifically call the reactToScreenChange, since when the screen is shown it's not yet
1407     // in the list. We still don't want to have an invisible view added.
1408     containment->reactToScreenChange();
1409 
1410     // were there any panels for this screen before it popped up?
1411     if (!m_waitingPanels.isEmpty()) {
1412         m_waitingPanelsTimer.start();
1413     }
1414 
1415     if (!m_screenReorderInProgress) {
1416         Q_EMIT availableScreenRectChanged(m_screenPool->idForScreen(screen));
1417     }
1418     Q_EMIT screenAdded(m_screenPool->idForScreen(screen));
1419 #ifndef NDEBUG
1420     m_invariantsTimer.start();
1421 #endif
1422 }
1423 
1424 void ShellCorona::checkAllDesktopsUiReady(bool ready)
1425 {
1426     if (!ready)
1427         return;
1428     for (auto v : std::as_const(m_desktopViewForScreen)) {
1429         if (!v->containment()->isUiReady())
1430             return;
1431 
1432         qCDebug(PLASMASHELL) << "Plasma Shell startup completed";
1433         QDBusMessage ksplashProgressMessage = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KSplash"),
1434                                                                              QStringLiteral("/KSplash"),
1435                                                                              QStringLiteral("org.kde.KSplash"),
1436                                                                              QStringLiteral("setStage"));
1437         ksplashProgressMessage.setArguments(QList<QVariant>() << QStringLiteral("desktop"));
1438         QDBusConnection::sessionBus().asyncCall(ksplashProgressMessage);
1439     }
1440 }
1441 
1442 Plasma::Containment *ShellCorona::createContainmentForActivity(const QString &activity, int screenNum)
1443 {
1444     Plasma::Containment *lastScreenCont = nullptr;
1445     Plasma::Containment *orphanCont = nullptr;
1446     const auto containments = containmentsForActivity(activity);
1447     for (Plasma::Containment *cont : containments) {
1448         // in the case of a corrupt config file
1449         // with multiple containments with same lastScreen
1450         // Always prefer a containment that already has a screen, if any
1451         // This piece of code always fails at startup, used only later in plasma runtime
1452         if (cont->destroyed()) {
1453             continue;
1454         }
1455         if (cont->screen() == screenNum) {
1456             // Always prefer a containment that already has a view
1457             return cont;
1458         } else if (cont->lastScreen() == screenNum) {
1459             // Otherwise base off lastScreen
1460             lastScreenCont = cont;
1461         } else if (cont->lastScreen() < 0) {
1462             // Last resort, if we found a desktop for the activity that for whatever reason had screen -1 (very unlikely) recycle it
1463             orphanCont = cont;
1464         }
1465     }
1466     if (lastScreenCont) {
1467         return lastScreenCont;
1468     } else if (orphanCont) {
1469         return orphanCont;
1470     }
1471 
1472     QString plugin = m_activityContainmentPlugins.value(activity);
1473 
1474     if (plugin.isEmpty()) {
1475         plugin = defaultContainmentPlugin();
1476     }
1477 
1478     Plasma::Containment *containment = containmentForScreen(screenNum, activity, plugin, QVariantList());
1479     Q_ASSERT(containment);
1480 
1481     return containment;
1482 }
1483 
1484 void ShellCorona::createWaitingPanels()
1485 {
1486     QList<Plasma::Containment *> stillWaitingPanels;
1487 
1488     for (Plasma::Containment *cont : std::as_const(m_waitingPanels)) {
1489         // ignore non existing (yet?) screens
1490         int requestedScreen = cont->lastScreen();
1491         if (requestedScreen < 0) {
1492             requestedScreen = 0;
1493         }
1494 
1495         QScreen *screen = m_screenPool->screenForId(requestedScreen);
1496         DesktopView *desktopView = desktopForScreen(screen);
1497         if (!screen || !desktopView) {
1498             stillWaitingPanels << cont;
1499             continue;
1500         }
1501 
1502         // TODO: does a similar check make sense?
1503         // Q_ASSERT(qBound(0, requestedScreen, m_screenPool->count() - 1) == requestedScreen);
1504         PanelView *panel = new PanelView(this, screen);
1505         if (panel->rendererInterface()->graphicsApi() != QSGRendererInterface::Software) {
1506             connect(panel, &QQuickWindow::sceneGraphError, this, &ShellCorona::glInitializationFailed);
1507         }
1508         auto rectNotify = [this, panel]() {
1509             Q_ASSERT(qobject_cast<PanelView *>(panel)); // https://bugreports.qt.io/browse/QTBUG-118841
1510             if (!m_screenReorderInProgress && panel->containment()) {
1511                 Q_EMIT availableScreenRectChanged(panel->containment()->screen());
1512             }
1513         };
1514 
1515         m_panelViews[cont] = panel;
1516         panel->setContainment(cont);
1517         cont->reactToScreenChange();
1518 
1519         rectNotify();
1520 
1521         connect(cont, &QObject::destroyed, this, &ShellCorona::panelContainmentDestroyed);
1522 
1523         connect(panel, &QWindow::visibleChanged, this, rectNotify);
1524         connect(panel, &QWindow::screenChanged, this, rectNotify);
1525         connect(panel, &PanelView::locationChanged, this, rectNotify);
1526         connect(panel, &PanelView::visibilityModeChanged, this, rectNotify);
1527         connect(panel, &PanelView::thicknessChanged, this, rectNotify);
1528     }
1529     m_waitingPanels = stillWaitingPanels;
1530 }
1531 
1532 void ShellCorona::panelContainmentDestroyed(QObject *obj)
1533 {
1534     auto *cont = static_cast<Plasma::Containment *>(obj);
1535     int screen = cont->screen();
1536     auto view = m_panelViews.take(cont);
1537     // ~PanelView -> visibleChanged -> rectNotify -> "AddressSanitizer: heap-use-after-free"
1538     // https://bugreports.qt.io/browse/QTBUG-118841
1539     view->disconnect(this);
1540     delete view;
1541     // don't make things relayout when the application is quitting
1542     // NOTE: qApp->closingDown() is still false here
1543     if (!m_closingDown && !m_screenReorderInProgress) {
1544         Q_EMIT availableScreenRectChanged(screen);
1545     }
1546 }
1547 
1548 void ShellCorona::handleContainmentAdded(Plasma::Containment *c)
1549 {
1550     connect(c, &Plasma::Containment::showAddWidgetsInterface, this, &ShellCorona::toggleWidgetExplorer);
1551     connect(c, &Plasma::Containment::appletAlternativesRequested, this, &ShellCorona::showAlternativesForApplet);
1552     connect(c, &Plasma::Containment::appletCreated, this, [this, c](Plasma::Applet *applet) {
1553         executeSetupPlasmoidScript(c, applet);
1554     });
1555 
1556     // When a containment is removed, remove the thumbnail as well
1557     connect(c, &Plasma::Containment::destroyedChanged, this, [this, c](bool destroyed) {
1558         if (!destroyed) {
1559             return;
1560         }
1561         const QString snapshotPath = containmentPreviewPath(c);
1562         if (!snapshotPath.isEmpty()) {
1563             QFile f(snapshotPath);
1564             f.remove();
1565         }
1566     });
1567 }
1568 
1569 void ShellCorona::executeSetupPlasmoidScript(Plasma::Containment *containment, Plasma::Applet *applet)
1570 {
1571 #if USE_SCRIPTING
1572     if (!applet->pluginMetaData().isValid() || !containment->pluginMetaData().isValid()) {
1573         return;
1574     }
1575 
1576     const QString scriptFile = m_lookAndFeelPackage.filePath("plasmoidsetupscripts", applet->pluginMetaData().pluginId() + ".js");
1577 
1578     if (scriptFile.isEmpty()) {
1579         return;
1580     }
1581 
1582     WorkspaceScripting::ScriptEngine scriptEngine(this);
1583 
1584     connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) {
1585         qCWarning(PLASMASHELL) << msg;
1586     });
1587     connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) {
1588         qCDebug(PLASMASHELL) << msg;
1589     });
1590 
1591     QFile file(scriptFile);
1592     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
1593         qCWarning(PLASMASHELL) << "Unable to load script file:" << scriptFile;
1594         return;
1595     }
1596 
1597     QString script = file.readAll();
1598     if (script.isEmpty()) {
1599         // qCDebug(PLASMASHELL) << "script is empty";
1600         return;
1601     }
1602 
1603     scriptEngine.globalObject().setProperty(QStringLiteral("applet"), scriptEngine.wrap(applet));
1604     scriptEngine.globalObject().setProperty(QStringLiteral("containment"), scriptEngine.wrap(containment));
1605     scriptEngine.evaluateScript(script, scriptFile);
1606 #else
1607     Q_UNUSED(containment)
1608     Q_UNUSED(applet)
1609 #endif
1610 }
1611 
1612 void ShellCorona::toggleWidgetExplorer()
1613 {
1614     // FIXME: This does not work on wayland
1615     const QPoint cursorPos = QCursor::pos();
1616     for (DesktopView *view : std::as_const(m_desktopViewForScreen)) {
1617         if (view->screen()->geometry().contains(cursorPos)) {
1618             // The view QML has to provide something to display the widget explorer
1619             view->rootObject()->metaObject()->invokeMethod(view->rootObject(), "toggleWidgetExplorer", Q_ARG(QVariant, QVariant::fromValue(sender())));
1620             return;
1621         }
1622     }
1623 }
1624 
1625 void ShellCorona::toggleActivityManager()
1626 {
1627     const QPoint cursorPos = QCursor::pos();
1628     for (DesktopView *view : std::as_const(m_desktopViewForScreen)) {
1629         if (view->screen()->geometry().contains(cursorPos)) {
1630             // The view QML has to provide something to display the activity explorer
1631             view->rootObject()->metaObject()->invokeMethod(view->rootObject(), "toggleActivityManager", Qt::QueuedConnection);
1632             return;
1633         }
1634     }
1635 }
1636 
1637 void ShellCorona::syncAppConfig()
1638 {
1639     applicationConfig()->sync();
1640 }
1641 
1642 void ShellCorona::setDashboardShown(bool show)
1643 {
1644     KWindowSystem::setShowingDesktop(show);
1645 }
1646 
1647 void ShellCorona::toggleDashboard()
1648 {
1649     setDashboardShown(!KWindowSystem::showingDesktop());
1650 }
1651 
1652 void ShellCorona::handleColorRequestedFromDBus(const QDBusMessage &msg)
1653 {
1654     Q_ASSERT(!m_accentColorFromWallpaperEnabled);
1655     Q_ASSERT(!m_fakeColorRequestConn);
1656     msg.setDelayedReply(true);
1657 
1658     m_fakeColorRequestConn = connect(this, &ShellCorona::colorChanged, this, [this, msg] {
1659         disconnect(m_fakeColorRequestConn);
1660         const QRgb color = desktopForScreen(m_screenPool->primaryScreen())->accentColor().rgba();
1661 
1662         m_accentColorFromWallpaperEnabled = false;
1663         Q_EMIT accentColorFromWallpaperEnabledChanged();
1664 
1665         const QDBusMessage reply = msg.createReply(color);
1666         QDBusConnection::sessionBus().send(reply);
1667     });
1668 
1669     m_accentColorFromWallpaperEnabled = true;
1670     Q_EMIT accentColorFromWallpaperEnabledChanged();
1671 }
1672 
1673 QRgb ShellCorona::color() const
1674 {
1675     // Colors from wallpaper are not generated when they are turned off in the settings.
1676     // To return a color we need to fake that the setting is on, and then take the color,
1677     // turn off the setting again(or the color engine will keep runnig) and return the color.
1678 
1679     // Note that whenever a color is generated, it is also set as accent color. When we fake the
1680     // setting, we should not apply the generated color. The color applying kded module take care
1681     // of that by checking for the original state of the setting, so it is important that the check
1682     // may not be removed accidentally.
1683 
1684     static QRgb defaultColor = QColor(Qt::transparent).rgba();
1685     auto const primaryDesktopViewExists = desktopForScreen(m_screenPool->primaryScreen());
1686     if (!primaryDesktopViewExists) {
1687         return defaultColor;
1688     }
1689 
1690     if (m_accentColorFromWallpaperEnabled) {
1691         return desktopForScreen(m_screenPool->primaryScreen())->accentColor().rgba();
1692     } else if (calledFromDBus()) {
1693         if (m_fakeColorRequestConn) {
1694             disconnect(m_fakeColorRequestConn);
1695         }
1696         const_cast<ShellCorona *>(this)->handleColorRequestedFromDBus(message());
1697     }
1698 
1699     return defaultColor;
1700 }
1701 
1702 QVariantMap ShellCorona::wallpaper(uint screenNum)
1703 {
1704     if (!m_desktopViewForScreen.contains(screenNum)) {
1705         qCWarning(PLASMASHELL) << "wallpaper: unknown screen" << screenNum;
1706         return QVariantMap();
1707     }
1708 
1709     auto currentActivity = m_activityController->currentActivity();
1710     currentActivity = QUuid::fromString(currentActivity).isNull() ? QString() : currentActivity;
1711 
1712     Plasma::Containment *containment = containmentForScreen(screenNum, currentActivity, QString());
1713     if (!containment) {
1714         qCWarning(PLASMASHELL) << "wallpaper: containment not found for screen" << screenNum << currentActivity;
1715         return QVariantMap();
1716     }
1717 
1718     const auto wallpaperPlugin = containment->wallpaperPlugin();
1719 
1720     // we have to construct an independent ConfigPropertyMap when we want to configure wallpapers that are not the current one
1721     KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Generic"));
1722     pkg.setDefaultPackageRoot(QStringLiteral(PLASMA_RELATIVE_DATA_INSTALL_DIR "/wallpapers"));
1723     pkg.setPath(wallpaperPlugin);
1724     QFile file(pkg.filePath("config", QStringLiteral("main.xml")));
1725     KConfigGroup cfg = containment->config().group(u"Wallpaper"_s).group(wallpaperPlugin).group(u"General"_s);
1726 
1727     const auto items = cfg.keyList();
1728     QVariantMap parameters;
1729     for (const auto &itemName : items) {
1730         parameters.insert(itemName, cfg.readEntry(itemName));
1731     }
1732 
1733     // add wallpaperPlugin
1734     parameters.insert(QStringLiteral("wallpaperPlugin"), wallpaperPlugin);
1735 
1736     return parameters;
1737 }
1738 
1739 void ShellCorona::setWallpaper(const QString &wallpaperPlugin, const QVariantMap &parameters, uint screenNum)
1740 {
1741     qDBusRegisterMetaType<QColor>();
1742     if (wallpaperPlugin.isEmpty()) {
1743         qCWarning(PLASMASHELL) << "setWallpaper: Missing wallpaperPlugin parameter";
1744         return;
1745     }
1746 
1747     auto currentActivity = m_activityController->currentActivity();
1748     currentActivity = QUuid::fromString(currentActivity).isNull() ? QString() : currentActivity;
1749 
1750     Plasma::Containment *containment = containmentForScreen(screenNum, currentActivity, QString());
1751     if (!containment) {
1752         qCWarning(PLASMASHELL) << "setWallpaper: containment not found for screen" << screenNum << currentActivity;
1753         return;
1754     }
1755 
1756     KConfigPropertyMap *config;
1757     if (wallpaperPlugin != containment->wallpaperPlugin()) {
1758         // we have to construct an independent ConfigPropertyMap when we want to configure wallpapers that are not the current one
1759         KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Generic"));
1760         pkg.setDefaultPackageRoot(QStringLiteral(PLASMA_RELATIVE_DATA_INSTALL_DIR "/wallpapers"));
1761         pkg.setPath(wallpaperPlugin);
1762         QFile file(pkg.filePath("config", QStringLiteral("main.xml")));
1763         KConfigGroup cfg = containment->config().group(u"Wallpaper"_s).group(wallpaperPlugin);
1764         config = new KConfigPropertyMap(new KConfigLoader(cfg, &file, this), this);
1765     } else {
1766         // update current wallpaper to allow animations
1767         QObject *wallpaperGraphicsObject = containment->property("wallpaperGraphicsObject").value<QObject *>();
1768         config = wallpaperGraphicsObject->property("configuration").value<KConfigPropertyMap *>();
1769     }
1770 
1771     const auto items = config->keys();
1772     for (const auto &itemName : items) {
1773         auto it = parameters.find(itemName);
1774         if (it != parameters.end()) {
1775             auto value = it.value();
1776             // for some reason QColor is not properly unmarshalled despite my efforts
1777             if (itemName == QStringLiteral("Color") && value.metaType() == QMetaType::fromType<QDBusArgument>()) {
1778                 QColor v;
1779                 value.value<QDBusArgument>() >> v;
1780                 value = v;
1781             }
1782 
1783             qCDebug(PLASMASHELL) << "setWallpaper: setting" << itemName << value << screenNum;
1784             config->insert(itemName, value);
1785         }
1786     }
1787     config->writeConfig();
1788     containment->setWallpaperPlugin(wallpaperPlugin);
1789     containment->config().sync();
1790 
1791     Q_EMIT wallpaperChanged(screenNum);
1792 }
1793 
1794 QString ShellCorona::evaluateScript(const QString &script)
1795 {
1796 #if USE_SCRIPTING
1797     if (calledFromDBus()) {
1798         if (immutability() == Plasma::Types::SystemImmutable) {
1799             sendErrorReply(QDBusError::Failed, QStringLiteral("Widgets are locked"));
1800             return QString();
1801         } else if (!KAuthorized::authorize(QStringLiteral("plasma-desktop/scripting_console"))) {
1802             sendErrorReply(QDBusError::Failed, QStringLiteral("Administrative policies prevent script execution"));
1803             return QString();
1804         }
1805     }
1806 
1807     WorkspaceScripting::ScriptEngine scriptEngine(this);
1808     QString buffer;
1809     QTextStream bufferStream(&buffer, QIODevice::WriteOnly | QIODevice::Text);
1810 
1811     connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [&bufferStream](const QString &msg) {
1812         qCWarning(PLASMASHELL) << msg;
1813         bufferStream << msg;
1814     });
1815     connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [&bufferStream](const QString &msg) {
1816         qCDebug(PLASMASHELL) << msg;
1817         bufferStream << msg;
1818     });
1819 
1820     scriptEngine.evaluateScript(script);
1821 
1822     bufferStream.flush();
1823 
1824     if (calledFromDBus() && !scriptEngine.errorString().isEmpty()) {
1825         sendErrorReply(QDBusError::Failed, scriptEngine.errorString());
1826         return QString();
1827     }
1828 
1829     return buffer;
1830 #else
1831     Q_UNUSED(script)
1832     return QString();
1833 #endif
1834 }
1835 
1836 void ShellCorona::checkActivities()
1837 {
1838     KActivities::Controller::ServiceStatus status = m_activityController->serviceStatus();
1839     // qCDebug(PLASMASHELL) << "$%$%$#%$%$%Status:" << status;
1840     if (status != KActivities::Controller::Running) {
1841         // panic and give up - better than causing a mess
1842         qCDebug(PLASMASHELL) << "ShellCorona::checkActivities is called whilst activity daemon is still connecting";
1843         return;
1844     }
1845 
1846     const QStringList existingActivities = m_activityController->activities();
1847     for (const QString &id : existingActivities) {
1848         activityAdded(id);
1849     }
1850 
1851     // Checking whether the result we got is valid. Just in case.
1852     Q_ASSERT_X(!existingActivities.isEmpty(), "isEmpty", "There are no activities, and the service is running");
1853     Q_ASSERT_X(existingActivities[0] != QLatin1String("00000000-0000-0000-0000-000000000000"), "null uuid", "There is a nulluuid activity present");
1854 
1855     // Killing the unassigned containments
1856     const auto conts = containments();
1857     for (Plasma::Containment *cont : conts) {
1858         if ((cont->containmentType() == Plasma::Containment::Desktop || cont->containmentType() == Plasma::Containment::Custom)
1859             && !existingActivities.contains(cont->activity()) && m_activityController->currentActivity() != cont->activity()) {
1860             cont->destroy();
1861         }
1862     }
1863 }
1864 
1865 void ShellCorona::currentActivityChanged(const QString &newActivity)
1866 {
1867     //     qCDebug(PLASMASHELL) << "Activity changed:" << newActivity;
1868 
1869     for (auto it = m_desktopViewForScreen.constBegin(); it != m_desktopViewForScreen.constEnd(); ++it) {
1870         Plasma::Containment *c = createContainmentForActivity(newActivity, it.key());
1871 
1872         QAction *removeAction = c->internalAction(QStringLiteral("remove"));
1873         if (removeAction) {
1874             removeAction->deleteLater();
1875         }
1876         (*it)->setContainment(c);
1877     }
1878 }
1879 
1880 void ShellCorona::activityAdded(const QString &id)
1881 {
1882     // TODO more sanity checks
1883     if (m_activityContainmentPlugins.contains(id)) {
1884         qCWarning(PLASMASHELL) << "Activity added twice" << id;
1885         return;
1886     }
1887 
1888     m_activityContainmentPlugins.insert(id, defaultContainmentPlugin());
1889 }
1890 
1891 void ShellCorona::activityRemoved(const QString &id)
1892 {
1893     m_activityContainmentPlugins.remove(id);
1894     const QList<Plasma::Containment *> containments = containmentsForActivity(id);
1895     for (auto cont : containments) {
1896         cont->destroy();
1897     }
1898 }
1899 
1900 void ShellCorona::insertActivity(const QString &id, const QString &plugin)
1901 {
1902     activityAdded(id);
1903 
1904     // TODO: This needs to go away!
1905     // The containment creation API does not know when we have a
1906     // new activity to create a containment for, we need to pretend
1907     // that the current activity has been changed
1908     QFuture<bool> currentActivity = m_activityController->setCurrentActivity(id);
1909     awaitFuture(currentActivity);
1910 
1911     if (!currentActivity.result()) {
1912         qCDebug(PLASMASHELL) << "Failed to create and switch to the activity";
1913         return;
1914     }
1915 
1916     while (m_activityController->currentActivity() != id) {
1917         QCoreApplication::processEvents();
1918     }
1919 
1920     m_activityContainmentPlugins.insert(id, plugin);
1921     for (auto it = m_desktopViewForScreen.constBegin(); it != m_desktopViewForScreen.constEnd(); ++it) {
1922         Plasma::Containment *c = createContainmentForActivity(id, (*it)->containment()->screen());
1923         if (c) {
1924             c->config().writeEntry("lastScreen", (*it)->containment()->screen());
1925         }
1926     }
1927 }
1928 
1929 Plasma::Containment *ShellCorona::setContainmentTypeForScreen(int screen, const QString &plugin)
1930 {
1931     // search but not create
1932     Plasma::Containment *oldContainment = containmentForScreen(screen, m_activityController->currentActivity(), QString());
1933 
1934     // no valid containment in given screen, giving up
1935     if (!oldContainment) {
1936         return nullptr;
1937     }
1938 
1939     if (plugin.isEmpty()) {
1940         return oldContainment;
1941     }
1942 
1943     auto viewIt = std::find_if(m_desktopViewForScreen.cbegin(), m_desktopViewForScreen.cend(), [oldContainment](const DesktopView *v) {
1944         return v->containment() == oldContainment;
1945     });
1946 
1947     // no view? give up
1948     if (viewIt == m_desktopViewForScreen.cend()) {
1949         return oldContainment;
1950     }
1951 
1952     // create a new containment
1953     Plasma::Containment *newContainment = createContainmentDelayed(plugin);
1954 
1955     // if creation failed or invalid plugin, give up
1956     if (!newContainment) {
1957         return oldContainment;
1958     } else if (!newContainment->pluginMetaData().isValid()) {
1959         newContainment->deleteLater();
1960         return oldContainment;
1961     }
1962 
1963     // At this point we have a valid new containment from plugin and a view
1964     // copy all configuration groups (excluded applets)
1965     KConfigGroup oldCg = oldContainment->config();
1966 
1967     // newCg *HAS* to be from a KSharedConfig, because some KConfigSkeleton will need to be synced
1968     // this makes the configscheme work
1969     KConfigGroup newCg(KSharedConfig::openConfig(oldCg.config()->name()), u"Containments"_s);
1970     newCg = KConfigGroup(&newCg, QString::number(newContainment->id()));
1971 
1972     // this makes containment->config() work, is a separate thing from its configscheme
1973     KConfigGroup newCg2 = newContainment->config();
1974 
1975     const auto groups = oldCg.groupList();
1976     for (const QString &group : groups) {
1977         if (group != QLatin1String("Applets")) {
1978             KConfigGroup subGroup(&oldCg, group);
1979             KConfigGroup newSubGroup(&newCg, group);
1980             subGroup.copyTo(&newSubGroup);
1981 
1982             KConfigGroup newSubGroup2(&newCg2, group);
1983             subGroup.copyTo(&newSubGroup2);
1984         }
1985     }
1986 
1987     newContainment->init();
1988     newCg.writeEntry("activityId", oldContainment->activity());
1989     newCg.writeEntry("wallpaperplugin", oldContainment->wallpaperPlugin());
1990     newContainment->restore(newCg);
1991     newContainment->updateConstraints(Plasma::Applet::StartupCompletedConstraint);
1992     newContainment->flushPendingConstraintsEvents();
1993     Q_EMIT containmentAdded(newContainment);
1994 
1995     // Move the applets
1996     const auto applets = oldContainment->applets();
1997     for (Plasma::Applet *applet : applets) {
1998         newContainment->addApplet(applet);
1999     }
2000 
2001     // remove the "remove" action
2002     QAction *removeAction = newContainment->internalAction(QStringLiteral("remove"));
2003     if (removeAction) {
2004         removeAction->deleteLater();
2005     }
2006     (*viewIt)->setContainment(newContainment);
2007     newContainment->setActivity(oldContainment->activity());
2008 
2009     oldContainment->destroy();
2010 
2011     // removing the focus from the item that is going to be destroyed
2012     // fixes a crash
2013     // delayout the destruction of the old containment fixes another crash
2014     (*viewIt)->rootObject()->setFocus(true, Qt::MouseFocusReason);
2015     QTimer::singleShot(2500, oldContainment, &Plasma::Applet::destroy);
2016 
2017     // Save now as we now have a screen, so lastScreen will not be -1
2018     newContainment->save(newCg);
2019     requestConfigSync();
2020     Q_EMIT availableScreenRectChanged(screen);
2021 
2022     return newContainment;
2023 }
2024 
2025 void ShellCorona::checkAddPanelAction()
2026 {
2027     delete m_addPanelAction;
2028     m_addPanelAction = nullptr;
2029 
2030     m_addPanelsMenu.reset(nullptr);
2031 
2032     const QList<KPluginMetaData> panelContainmentPlugins = Plasma::PluginLoader::listContainmentsMetaDataOfType(QStringLiteral("Panel"));
2033 
2034     auto filter = [](const KPluginMetaData &md) -> bool {
2035         return !md.rawData().value(QStringLiteral("NoDisplay")).toBool()
2036             && md.value(QStringLiteral("X-Plasma-ContainmentCategories"), QStringList()).contains(QLatin1String("panel"));
2037     };
2038     QList<KPluginMetaData> templates = KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/LayoutTemplate"), QString(), filter);
2039 
2040     if (panelContainmentPlugins.count() + templates.count() == 1) {
2041         m_addPanelAction = new QAction(this);
2042         connect(m_addPanelAction, &QAction::triggered, this, qOverload<>(&ShellCorona::addPanel));
2043     } else if (!panelContainmentPlugins.isEmpty()) {
2044         m_addPanelAction = new QAction(this);
2045         m_addPanelsMenu.reset(new QMenu);
2046         m_addPanelAction->setMenu(m_addPanelsMenu.get());
2047         connect(m_addPanelsMenu.get(), &QMenu::aboutToShow, this, &ShellCorona::populateAddPanelsMenu);
2048         connect(m_addPanelsMenu.get(), &QMenu::triggered, this, qOverload<QAction *>(&ShellCorona::addPanel));
2049     }
2050 
2051     if (m_addPanelAction) {
2052         m_addPanelAction->setText(i18n("Add Panel"));
2053         m_addPanelAction->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
2054         setAction(QStringLiteral("add panel"), m_addPanelAction);
2055     }
2056 }
2057 
2058 // Used by the plasma-desktop edit ToolBox
2059 void ShellCorona::showAddPanelContextMenu(const QPoint pos)
2060 {
2061     m_addPanelsMenu.get()->exec(pos);
2062 }
2063 
2064 void ShellCorona::populateAddPanelsMenu()
2065 {
2066     m_addPanelsMenu->clear();
2067     const KPluginMetaData emptyInfo;
2068 
2069     const QList<KPluginMetaData> panelContainmentPlugins = Plasma::PluginLoader::listContainmentsMetaDataOfType(QStringLiteral("Panel"));
2070     QMap<QString, QPair<KPluginMetaData, KPluginMetaData>> sorted;
2071     for (const KPluginMetaData &plugin : panelContainmentPlugins) {
2072         if (plugin.rawData().value(QStringLiteral("NoDisplay")).toBool()) {
2073             continue;
2074         }
2075         sorted.insert(plugin.name(), qMakePair(plugin, KPluginMetaData()));
2076     }
2077 
2078     auto filter = [](const KPluginMetaData &md) -> bool {
2079         return !md.rawData().value(QStringLiteral("NoDisplay")).toBool()
2080             && md.value(QStringLiteral("X-Plasma-ContainmentCategories"), QStringList()).contains(QLatin1String("panel"));
2081     };
2082     const QList<KPluginMetaData> templates = KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/LayoutTemplate"), QString(), filter);
2083     for (const auto &tpl : templates) {
2084         sorted.insert(tpl.name(), qMakePair(emptyInfo, tpl));
2085     }
2086 
2087     QMapIterator<QString, QPair<KPluginMetaData, KPluginMetaData>> it(sorted);
2088     KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LayoutTemplate"));
2089     while (it.hasNext()) {
2090         it.next();
2091         QPair<KPluginMetaData, KPluginMetaData> pair = it.value();
2092         if (pair.first.isValid()) {
2093             KPluginMetaData plugin = pair.first;
2094             QAction *action = m_addPanelsMenu->addAction(i18nc("Creates an empty containment (%1 is the containment name)", "Empty %1", plugin.name()));
2095             if (!plugin.iconName().isEmpty()) {
2096                 action->setIcon(QIcon::fromTheme(plugin.iconName()));
2097             }
2098 
2099             action->setData(plugin.pluginId());
2100         } else {
2101             KPluginMetaData plugin(pair.second);
2102             package.setPath(plugin.pluginId());
2103             const QString scriptFile = package.filePath("mainscript");
2104             if (!scriptFile.isEmpty()) {
2105                 QAction *action = m_addPanelsMenu->addAction(plugin.name());
2106                 action->setData(QStringLiteral("plasma-desktop-template:%1").arg(plugin.pluginId()));
2107             }
2108         }
2109     }
2110 }
2111 
2112 void ShellCorona::addPanel()
2113 {
2114     const QList<KPluginMetaData> panelPlugins = Plasma::PluginLoader::listContainmentsMetaDataOfType(QStringLiteral("Panel"));
2115 
2116     if (!panelPlugins.isEmpty()) {
2117         addPanel(panelPlugins.first().pluginId());
2118     }
2119 }
2120 
2121 void ShellCorona::addPanel(QAction *action)
2122 {
2123     const QString plugin = action->data().toString();
2124 #if USE_SCRIPTING
2125     if (plugin.startsWith(QLatin1String("plasma-desktop-template:"))) {
2126         WorkspaceScripting::ScriptEngine scriptEngine(this);
2127 
2128         connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) {
2129             qCWarning(PLASMASHELL) << msg;
2130         });
2131         connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) {
2132             qCDebug(PLASMASHELL) << msg;
2133         });
2134         const QString templateName = plugin.right(plugin.length() - qstrlen("plasma-desktop-template:"));
2135 
2136         scriptEngine.evaluateScript(QStringLiteral("loadTemplate(\"%1\")").arg(templateName));
2137     } else
2138 #endif
2139         if (!plugin.isEmpty()) {
2140         addPanel(plugin);
2141     }
2142 }
2143 
2144 Plasma::Containment *ShellCorona::addPanel(const QString &plugin)
2145 {
2146     Plasma::Containment *panel = createContainment(plugin);
2147     if (!panel) {
2148         return nullptr;
2149     }
2150 
2151     // find out what screen this panel should go on
2152     QScreen *wantedScreen = nullptr;
2153     auto message = QDBusMessage::createMethodCall("org.kde.KWin", "/KWin", "org.kde.KWin", "activeOutputName");
2154     QDBusReply<QString> reply = QDBusConnection::sessionBus().call(message);
2155     if (reply.isValid()) {
2156         const auto screens = QGuiApplication::screens();
2157         auto screenIt = screens.cend();
2158         const QString activeOutputName = reply.value();
2159         screenIt = std::find_if(screens.cbegin(), screens.cend(), [&activeOutputName](QScreen *screen) {
2160             return screen->name() == activeOutputName;
2161         });
2162         if (screenIt != screens.cend()) {
2163             wantedScreen = *screenIt;
2164         }
2165     }
2166     if (!wantedScreen) {
2167         qCWarning(PLASMASHELL) << "Did not find a valid screen to place a new panel." << reply.error();
2168         wantedScreen = qGuiApp->focusWindow() ? qGuiApp->focusWindow()->screen() : m_screenPool->primaryScreen();
2169     }
2170 
2171     QList<Plasma::Types::Location> availableLocations;
2172     availableLocations << Plasma::Types::BottomEdge << Plasma::Types::TopEdge << Plasma::Types::LeftEdge << Plasma::Types::RightEdge;
2173 
2174     for (auto it = m_panelViews.constBegin(); it != m_panelViews.constEnd(); ++it) {
2175         if ((*it)->screenToFollow() == wantedScreen) {
2176             availableLocations.removeAll((*it)->location());
2177         }
2178     }
2179 
2180     Plasma::Types::Location loc;
2181     if (availableLocations.isEmpty()) {
2182         loc = Plasma::Types::TopEdge;
2183     } else {
2184         loc = availableLocations.first();
2185     }
2186 
2187     panel->setLocation(loc);
2188     switch (loc) {
2189     case Plasma::Types::LeftEdge:
2190     case Plasma::Types::RightEdge:
2191         panel->setFormFactor(Plasma::Types::Vertical);
2192         break;
2193     default:
2194         panel->setFormFactor(Plasma::Types::Horizontal);
2195         break;
2196     }
2197 
2198     Q_ASSERT(panel);
2199     m_waitingPanels << panel;
2200     // immediately create the panel here so that we have access to the panel view
2201     createWaitingPanels();
2202 
2203     if (m_panelViews.contains(panel)) {
2204         m_panelViews.value(panel)->setScreenToFollow(wantedScreen);
2205     }
2206 
2207     return panel;
2208 }
2209 
2210 void ShellCorona::swapDesktopScreens(int oldScreen, int newScreen)
2211 {
2212     for (auto *containment : containmentsForScreen(oldScreen)) {
2213         if (containment->containmentType() != Plasma::Containment::Panel && containment->containmentType() != Plasma::Containment::CustomPanel) {
2214             setScreenForContainment(containment, newScreen);
2215         }
2216     }
2217 }
2218 
2219 void ShellCorona::setScreenForContainment(Plasma::Containment *containment, int newScreenId)
2220 {
2221     const int oldScreenId = containment->screen() >= 0 ? containment->screen() : containment->lastScreen();
2222 
2223     if (oldScreenId == newScreenId) {
2224         return;
2225     }
2226 
2227     m_pendingScreenChanges[containment] = newScreenId;
2228 
2229     if (containment->containmentType() == Plasma::Containment::Panel || containment->containmentType() == Plasma::Containment::CustomPanel) {
2230         // Panel Case
2231         containment->reactToScreenChange();
2232         auto *panelView = m_panelViews.value(containment);
2233         // If newScreen != nullptr we are also assured it won't be redundant
2234         QScreen *newScreen = m_screenPool->screenForId(newScreenId);
2235 
2236         if (panelView) {
2237             // There was an existing panel view
2238             if (newScreen) {
2239                 panelView->setScreenToFollow(newScreen);
2240             } else {
2241                 // Not on a connected screen: destroy the panel
2242                 if (!m_waitingPanels.contains(containment)) {
2243                     m_waitingPanels << containment;
2244                 }
2245                 m_panelViews.remove(containment);
2246                 panelView->destroy();
2247             }
2248         } else {
2249             // Didn't have a view, createWaitingPanels() will create it if needed
2250             createWaitingPanels();
2251         }
2252 
2253     } else {
2254         // Desktop case: a bit more complicate because we may have to swap
2255         Plasma::Containment *contSwap = containmentForScreen(newScreenId, containment->activity(), "org.kde.plasma.folder");
2256         Q_ASSERT(contSwap);
2257 
2258         // Perform the lastScreen changes
2259         containment->reactToScreenChange();
2260         if (contSwap) {
2261             m_pendingScreenChanges[contSwap] = oldScreenId;
2262             contSwap->reactToScreenChange();
2263         }
2264 
2265         // If those will be != nullptr we are also assured they won't be redundant
2266         QScreen *oldScreen = m_screenPool->screenForId(oldScreenId);
2267         QScreen *newScreen = m_screenPool->screenForId(newScreenId);
2268 
2269         // Actually perform a view swap when we are dealing with containments of current activity
2270         if (contSwap && containment->activity() == m_activityController->currentActivity()) {
2271             auto *containmentView = m_desktopViewForScreen.value(oldScreenId);
2272             auto *contSwapView = m_desktopViewForScreen.value(newScreenId);
2273 
2274             if (containmentView) {
2275                 Q_ASSERT(containment == containmentView->containment());
2276                 m_desktopViewForScreen.remove(oldScreenId);
2277             }
2278             if (contSwapView) {
2279                 Q_ASSERT(contSwap == contSwapView->containment());
2280                 m_desktopViewForScreen.remove(newScreenId);
2281             }
2282 
2283             if (containmentView) {
2284                 if (newScreen) {
2285                     containmentView->setScreenToFollow(newScreen);
2286                     m_desktopViewForScreen[newScreenId] = containmentView;
2287                 } else {
2288                     containmentView->destroy();
2289                 }
2290             } else if (newScreen) {
2291                 addOutput(newScreen);
2292             }
2293 
2294             if (contSwapView) {
2295                 if (oldScreen) {
2296                     contSwapView->setScreenToFollow(oldScreen);
2297                     m_desktopViewForScreen[oldScreenId] = contSwapView;
2298                 } else {
2299                     contSwapView->destroy();
2300                 }
2301             } else if (oldScreen) {
2302                 addOutput(oldScreen);
2303             }
2304         }
2305     }
2306 
2307     m_pendingScreenChanges.clear();
2308 }
2309 
2310 int ShellCorona::screenForContainment(const Plasma::Containment *containment) const
2311 {
2312     // TODO: when we can depend on a new framework, use a p-f method to actuall set lastScreen instead of this?
2313     // m_pendingScreenChanges controls an explicit user-determined screen change
2314     if (!m_pendingScreenChanges.isEmpty() && m_pendingScreenChanges.contains(containment)) {
2315         return m_pendingScreenChanges.value(containment);
2316     }
2317 
2318     // case in which this containment is child of an applet, hello systray :)
2319     if (Plasma::Applet *parentApplet = qobject_cast<Plasma::Applet *>(containment->parent())) {
2320         if (Plasma::Containment *cont = parentApplet->containment()) {
2321             return screenForContainment(cont);
2322         } else {
2323             return -1;
2324         }
2325     }
2326 
2327     // This part is necessary as is the thing which promotes a containment from screen -1 to
2328     // its final screen which will be saved as lastScreen)
2329     // if the desktop views already exist, base the decision upon them
2330     for (auto it = m_desktopViewForScreen.constBegin(), end = m_desktopViewForScreen.constEnd(); it != end; ++it) {
2331         if (it.value()->containment() == containment && containment->activity() == m_activityController->currentActivity()) {
2332             return m_screenPool->idForScreen(it.value()->screenToFollow());
2333         }
2334     }
2335 
2336     // if the panel views already exist, base upon them
2337     PanelView *view = m_panelViews.value(containment);
2338     if (view && view->screenToFollow()) {
2339         return m_screenPool->idForScreen(view->screenToFollow());
2340     }
2341 
2342     return -1;
2343 }
2344 
2345 void ShellCorona::grabContainmentPreview(Plasma::Containment *containment)
2346 {
2347     QQuickWindow *viewToGrab = nullptr;
2348 
2349     if (containment->containmentType() == Plasma::Containment::Panel || containment->containmentType() == Plasma::Containment::CustomPanel) {
2350         // Panel Containment
2351         auto it = m_panelViews.constBegin();
2352         while (it != m_panelViews.constEnd()) {
2353             if (it.key() == containment) {
2354                 viewToGrab = it.value();
2355                 break;
2356             }
2357             it++;
2358         }
2359     } else if (m_desktopViewForScreen.contains(containment->screen())) {
2360         // Desktop Containment
2361         viewToGrab = m_desktopViewForScreen[containment->screen()];
2362     }
2363 
2364     if (viewToGrab) {
2365         QSize size(512, 512);
2366         size = viewToGrab->size().scaled(size, Qt::KeepAspectRatio);
2367         auto result = viewToGrab->contentItem()->grabToImage(size);
2368 
2369         if (result) {
2370             connect(result.data(), &QQuickItemGrabResult::ready, this, [this, result, containment]() {
2371                 // DataLocation is plasmashell, we need just "plasma"
2372                 QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
2373                 QDir dir(path);
2374                 path += QStringLiteral("/plasma/containmentpreviews/");
2375                 dir.mkpath(path);
2376                 path += QString::number(containment->id()) + QChar('-') + containment->activity() + QStringLiteral(".png");
2377                 result->saveToFile(path);
2378                 emit containmentPreviewReady(containment, path);
2379             });
2380         }
2381     }
2382 }
2383 
2384 QString ShellCorona::containmentPreviewPath(Plasma::Containment *containment) const
2385 {
2386     const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/plasma/containmentpreviews/")
2387         + QString::number(containment->id()) + QChar('-') + containment->activity() + QStringLiteral(".png");
2388     if (QFile::exists(path)) {
2389         return path;
2390     } else {
2391         return QString();
2392     }
2393 }
2394 
2395 bool ShellCorona::accentColorFromWallpaperEnabled() const
2396 {
2397     return m_accentColorFromWallpaperEnabled;
2398 }
2399 
2400 void ShellCorona::nextActivity()
2401 {
2402     m_activityController->nextActivity();
2403 }
2404 
2405 void ShellCorona::previousActivity()
2406 {
2407     m_activityController->previousActivity();
2408 }
2409 
2410 void ShellCorona::stopCurrentActivity()
2411 {
2412     const QStringList list = m_activityController->activities(KActivities::Info::Running);
2413     if (list.isEmpty()) {
2414         return;
2415     }
2416 
2417     m_activityController->stopActivity(m_activityController->currentActivity());
2418 }
2419 
2420 /**
2421  * @internal
2422  *
2423  * The DismissPopupEventFilter class monitors mouse button press events and
2424  * when needed dismisses the active popup widget.
2425  *
2426  * plasmashell uses both QtQuick and QtWidgets under a single roof, QtQuick is
2427  * used for most of things, while QtWidgets is used for things such as context
2428  * menus, etc.
2429  *
2430  * If user clicks outside a popup window, it's expected that the popup window
2431  * will be closed.  On X11, it's achieved by establishing both a keyboard grab
2432  * and a pointer grab. But on Wayland, you can't grab keyboard or pointer. If
2433  * user clicks a surface of another app, the compositor will dismiss the popup
2434  * surface.  However, if user clicks some surface of the same application, the
2435  * popup surface won't be dismissed, it's up to the application to decide
2436  * whether the popup must be closed. In 99% cases, it must.
2437  *
2438  * Qt has some code that dismisses the active popup widget if another window
2439  * of the same app has been clicked. But, that code works only if the
2440  * application uses solely Qt widgets. See QTBUG-83972. For plasma it doesn't
2441  * work, because as we said previously, it uses both Qt Quick and Qt Widgets.
2442  *
2443  * Ideally, this bug needs to be fixed upstream, but given that it'll involve
2444  * major changes in Qt, the chances of it being fixed any time soon are slim.
2445  *
2446  * In order to work around the popup dismissal bug, we install an event filter
2447  * that monitors Qt::MouseButtonPress events. If it happens that user has
2448  * clicked outside an active popup widget, that popup will be closed. This
2449  * event filter is not needed on X11!
2450  */
2451 class DismissPopupEventFilter : public QObject
2452 {
2453     Q_OBJECT
2454 
2455 public:
2456     explicit DismissPopupEventFilter(QObject *parent = nullptr);
2457 
2458 protected:
2459     bool eventFilter(QObject *watched, QEvent *event) override;
2460 
2461 private:
2462     bool m_filterMouseEvents = false;
2463 };
2464 
2465 DismissPopupEventFilter::DismissPopupEventFilter(QObject *parent)
2466     : QObject(parent)
2467 {
2468 }
2469 
2470 bool DismissPopupEventFilter::eventFilter(QObject *watched, QEvent *event)
2471 {
2472     if (event->type() == QEvent::MouseButtonPress) {
2473         if (m_filterMouseEvents) {
2474             // Eat events until all mouse buttons are released.
2475             return true;
2476         }
2477 
2478         QWidget *popup = QApplication::activePopupWidget();
2479         if (!popup) {
2480             return false;
2481         }
2482 
2483         QWindow *window = qobject_cast<QWindow *>(watched);
2484         if (popup->windowHandle() == window) {
2485             // The popup window handles mouse events before the widget.
2486             return false;
2487         }
2488 
2489         QWidget *widget = qobject_cast<QWidget *>(watched);
2490         if (widget) {
2491             // Let the popup widget handle the mouse press event.
2492             return false;
2493         }
2494 
2495         popup->close();
2496         m_filterMouseEvents = true;
2497         return true;
2498 
2499     } else if (event->type() == QEvent::MouseButtonRelease) {
2500         if (m_filterMouseEvents) {
2501             // Eat events until all mouse buttons are released.
2502             QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
2503             if (mouseEvent->buttons() == Qt::NoButton) {
2504                 m_filterMouseEvents = false;
2505             }
2506             return true;
2507         }
2508     }
2509 
2510     return false;
2511 }
2512 
2513 void ShellCorona::setupWaylandIntegration()
2514 {
2515     if (!KWindowSystem::isPlatformWayland()) {
2516         return;
2517     }
2518     using namespace KWayland::Client;
2519     ConnectionThread *connection = ConnectionThread::fromApplication(this);
2520     if (!connection) {
2521         return;
2522     }
2523     Registry *registry = new Registry(this);
2524     registry->create(connection);
2525     connect(registry, &Registry::plasmaShellAnnounced, this, [this, registry](quint32 name, quint32 version) {
2526         m_waylandPlasmaShell = registry->createPlasmaShell(name, version, this);
2527     });
2528     connect(registry, &KWayland::Client::Registry::plasmaWindowManagementAnnounced, this, [this, registry](quint32 name, quint32 version) {
2529         m_waylandWindowManagement = registry->createPlasmaWindowManagement(name, version, this);
2530     });
2531     registry->setup();
2532     connection->roundtrip();
2533     qApp->installEventFilter(new DismissPopupEventFilter(this));
2534 }
2535 
2536 KWayland::Client::PlasmaShell *ShellCorona::waylandPlasmaShellInterface() const
2537 {
2538     return m_waylandPlasmaShell;
2539 }
2540 
2541 ScreenPool *ShellCorona::screenPool() const
2542 {
2543     return m_screenPool;
2544 }
2545 
2546 QList<int> ShellCorona::screenIds() const
2547 {
2548     QList<int> ids;
2549     for (int i = 0; i < m_screenPool->screenOrder().size(); ++i) {
2550         ids.append(i);
2551     }
2552     return ids;
2553 }
2554 
2555 QString ShellCorona::defaultContainmentPlugin() const
2556 {
2557     QString plugin = m_lnfDefaultsConfig.readEntry("Containment", QString());
2558     if (plugin.isEmpty()) {
2559         plugin = m_desktopDefaultsConfig.readEntry("Containment", "org.kde.desktopcontainment");
2560     }
2561     return plugin;
2562 }
2563 
2564 void ShellCorona::updateStruts()
2565 {
2566     for (PanelView *view : std::as_const(m_panelViews)) {
2567         view->updateExclusiveZone();
2568     }
2569 }
2570 
2571 void ShellCorona::configurationChanged(const QString &path)
2572 {
2573     if (path == m_configPath) {
2574         m_config->reparseConfiguration();
2575     }
2576 }
2577 
2578 void ShellCorona::activateLauncherMenu()
2579 {
2580     auto message = QDBusMessage::createMethodCall("org.kde.KWin", "/KWin", "org.kde.KWin", "activeOutputName");
2581     auto watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message));
2582     connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher] {
2583         watcher->deleteLater();
2584         QDBusReply<QString> reply = *watcher;
2585         if (reply.isValid()) {
2586             activateLauncherMenu(reply.value());
2587         }
2588     });
2589 }
2590 
2591 void ShellCorona::activateLauncherMenu(const QString &screenName)
2592 {
2593     auto activateLauncher = [](Plasma::Applet *applet) -> bool {
2594         const auto provides = applet->pluginMetaData().value(QStringLiteral("X-Plasma-Provides"), QStringList());
2595         if (provides.contains(QLatin1String("org.kde.plasma.launchermenu"))) {
2596             Q_EMIT applet->activated();
2597             return true;
2598         }
2599         return false;
2600     };
2601 
2602     int screenId = m_screenPool->idForName(screenName);
2603 
2604     for (auto *cont : containments()) {
2605         if ((screenId < 0 || cont->screen() == screenId)
2606             && (cont->containmentType() == Plasma::Containment::Panel || cont->containmentType() == Plasma::Containment::CustomPanel)) {
2607             const auto applets = cont->applets();
2608             for (auto applet : applets) {
2609                 if (activateLauncher(applet)) {
2610                     return;
2611                 }
2612             }
2613             if (activateLauncher(cont)) {
2614                 return;
2615             }
2616         }
2617     }
2618 
2619     for (auto *cont : containments()) {
2620         if ((screenId < 0 || cont->screen() == screenId) && cont->containmentType() == Plasma::Containment::Desktop) {
2621             const auto applets = cont->applets();
2622             for (auto applet : applets) {
2623                 if (activateLauncher(applet)) {
2624                     return;
2625                 }
2626             }
2627             if (activateLauncher(cont)) {
2628                 return;
2629             }
2630         }
2631     }
2632 
2633     if (screenId >= 0) {
2634         activateLauncherMenu(QString());
2635     }
2636 }
2637 
2638 void ShellCorona::activateTaskManagerEntry(int index)
2639 {
2640     auto activateTaskManagerEntryOnContainment = [](const Plasma::Containment *c, int index) {
2641         const auto &applets = c->applets();
2642         for (auto *applet : applets) {
2643             const auto &provides = applet->pluginMetaData().value(QStringLiteral("X-Plasma-Provides"), QStringList());
2644             if (provides.contains(QLatin1String("org.kde.plasma.multitasking"))) {
2645                 if (QQuickItem *appletInterface = PlasmaQuick::AppletQuickItem::itemForApplet(applet)) {
2646                     if (auto *metaObject = appletInterface->metaObject()) {
2647                         // not using QMetaObject::invokeMethod to avoid warnings when calling
2648                         // this on applets that don't have it or other child items since this
2649                         // is pretty much trial and error.
2650 
2651                         // Also, "var" arguments are treated as QVariant in QMetaObject
2652                         int methodIndex = metaObject->indexOfMethod("activateTaskAtIndex(QVariant)");
2653                         if (methodIndex == -1) {
2654                             continue;
2655                         }
2656 
2657                         QMetaMethod method = metaObject->method(methodIndex);
2658                         if (method.invoke(appletInterface, Q_ARG(QVariant, index))) {
2659                             return true;
2660                         }
2661                     }
2662                 }
2663             }
2664         }
2665         return false;
2666     };
2667 
2668     // To avoid overly complex configuration, we'll try to get the 90% usecase to work
2669     // which is activating a task on the task manager on a panel on the primary screen.
2670 
2671     for (auto it = m_panelViews.constBegin(), end = m_panelViews.constEnd(); it != end; ++it) {
2672         if (it.value()->screen() != m_screenPool->primaryScreen()) {
2673             continue;
2674         }
2675         if (activateTaskManagerEntryOnContainment(it.key(), index)) {
2676             return;
2677         }
2678     }
2679 
2680     // we didn't find anything on primary, try all the panels
2681     for (auto it = m_panelViews.constBegin(), end = m_panelViews.constEnd(); it != end; ++it) {
2682         if (activateTaskManagerEntryOnContainment(it.key(), index)) {
2683             return;
2684         }
2685     }
2686 }
2687 
2688 QString ShellCorona::defaultShell()
2689 {
2690     KSharedConfig::Ptr startupConf = KSharedConfig::openConfig(QStringLiteral("plasmashellrc"));
2691     KConfigGroup startupConfGroup(startupConf, u"Shell"_s);
2692     const QString defaultValue = qEnvironmentVariable("PLASMA_DEFAULT_SHELL", "org.kde.plasma.desktop");
2693     QString value = startupConfGroup.readEntry("ShellPackage", defaultValue);
2694 
2695     // In the global theme an empty value was written, make sure we still return a shell package
2696     return value.isEmpty() ? defaultValue : value;
2697 }
2698 
2699 void ShellCorona::refreshCurrentShell()
2700 {
2701     KSharedConfig::openConfig(QStringLiteral("plasmashellrc"))->reparseConfiguration();
2702     //  FIXME:   setShell(defaultShell());
2703     QProcess::startDetached("plasmashell", {"--replace"});
2704 }
2705 
2706 // Desktop corona handler
2707 
2708 #include "moc_shellcorona.cpp"
2709 #include "shellcorona.moc"