File indexing completed on 2024-04-28 16:55:02

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