diff --git a/shell/shellcorona.cpp b/shell/shellcorona.cpp index 3d109ca37..b1fca4f93 100644 --- a/shell/shellcorona.cpp +++ b/shell/shellcorona.cpp @@ -1,2119 +1,2122 @@ /* * Copyright 2008 Aaron Seigo * Copyright 2013 Sebastian Kügler * Copyright 2013 Ivan Cukic * Copyright 2013 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "shellcorona.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config-ktexteditor.h" // HAVE_KTEXTEDITOR #include "alternativeshelper.h" #include "desktopview.h" #include "panelview.h" #include "scripting/scriptengine.h" #include "shellmanager.h" #include "osd.h" #include "screenpool.h" #include "plasmashelladaptor.h" #include "debug.h" #include "futureutil.h" #ifndef NDEBUG #define CHECK_SCREEN_INVARIANTS screenInvariants(); #else #define CHECK_SCREEN_INVARIANTS #endif #if HAVE_X11 #include #include #include #endif static const int s_configSyncDelay = 10000; // 10 seconds ShellCorona::ShellCorona(QObject *parent) : Plasma::Corona(parent), m_config(KSharedConfig::openConfig(QStringLiteral("plasmarc"))), m_screenPool(new ScreenPool(KSharedConfig::openConfig(), this)), m_activityController(new KActivities::Controller(this)), m_addPanelAction(nullptr), m_addPanelsMenu(nullptr), m_interactiveConsole(nullptr), m_waylandPlasmaShell(nullptr), m_closingDown(false) { setupWaylandIntegration(); qmlRegisterUncreatableType("org.kde.plasma.shell", 2, 0, "Desktop", QStringLiteral("It is not possible to create objects of type Desktop")); qmlRegisterUncreatableType("org.kde.plasma.shell", 2, 0, "Panel", QStringLiteral("It is not possible to create objects of type Panel")); m_lookAndFeelPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel")); KConfigGroup cg(KSharedConfig::openConfig(QStringLiteral("kdeglobals")), "KDE"); const QString packageName = cg.readEntry("LookAndFeelPackage", QString()); if (!packageName.isEmpty()) { m_lookAndFeelPackage.setPath(packageName); } connect(this, &Plasma::Corona::containmentCreated, this, [this] (Plasma::Containment *c) { executeSetupPlasmoidScript(c, c); }); connect(this, &Plasma::Corona::availableScreenRectChanged, this, &Plasma::Corona::availableScreenRegionChanged); m_appConfigSyncTimer.setSingleShot(true); m_appConfigSyncTimer.setInterval(s_configSyncDelay); connect(&m_appConfigSyncTimer, &QTimer::timeout, this, &ShellCorona::syncAppConfig); //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 //leave containments pointing to the correct screens connect(this, &Corona::configSynced, this, &ShellCorona::syncAppConfig); m_waitingPanelsTimer.setSingleShot(true); m_waitingPanelsTimer.setInterval(250); connect(&m_waitingPanelsTimer, &QTimer::timeout, this, &ShellCorona::createWaitingPanels); m_reconsiderOutputsTimer.setSingleShot(true); m_reconsiderOutputsTimer.setInterval(1000); connect(&m_reconsiderOutputsTimer, &QTimer::timeout, this, &ShellCorona::reconsiderOutputs); m_desktopDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(package().filePath("defaults")), "Desktop"); m_lnfDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(m_lookAndFeelPackage.filePath("defaults")), "Desktop"); m_lnfDefaultsConfig = KConfigGroup(&m_lnfDefaultsConfig, QStringLiteral("org.kde.plasma.desktop")); new PlasmaShellAdaptor(this); QDBusConnection dbus = QDBusConnection::sessionBus(); dbus.registerObject(QStringLiteral("/PlasmaShell"), this); connect(this, &Plasma::Corona::startupCompleted, this, []() { qDebug() << "Plasma Shell startup completed"; QDBusMessage ksplashProgressMessage = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KSplash"), QStringLiteral("/KSplash"), QStringLiteral("org.kde.KSplash"), QStringLiteral("setStage")); ksplashProgressMessage.setArguments(QList() << QStringLiteral("desktop")); QDBusConnection::sessionBus().asyncCall(ksplashProgressMessage); //TODO: remove }); // Look for theme config in plasmarc, if it isn't configured, take the theme from the // LookAndFeel package, if either is set, change the default theme connect(qApp, &QCoreApplication::aboutToQuit, this, [this]() { //saveLayout is a slot but arguments not compatible m_closingDown = true; saveLayout(); }); connect(this, &ShellCorona::containmentAdded, this, &ShellCorona::handleContainmentAdded); QAction *dashboardAction = actions()->addAction(QStringLiteral("show dashboard")); QObject::connect(dashboardAction, &QAction::triggered, this, &ShellCorona::setDashboardShown); dashboardAction->setText(i18n("Show Desktop")); connect(KWindowSystem::self(), &KWindowSystem::showingDesktopChanged, [dashboardAction](bool showing) { dashboardAction->setText(showing ? i18n("Hide Desktop") : i18n("Show Desktop")); dashboardAction->setChecked(showing); }); dashboardAction->setAutoRepeat(true); dashboardAction->setCheckable(true); dashboardAction->setIcon(QIcon::fromTheme(QStringLiteral("dashboard-show"))); dashboardAction->setData(Plasma::Types::ControlAction); KGlobalAccel::self()->setGlobalShortcut(dashboardAction, Qt::CTRL + Qt::Key_F12); checkAddPanelAction(); connect(KSycoca::self(), SIGNAL(databaseChanged(QStringList)), this, SLOT(checkAddPanelAction(QStringList))); //Activity stuff QAction *activityAction = actions()->addAction(QStringLiteral("manage activities")); connect(activityAction, &QAction::triggered, this, &ShellCorona::toggleActivityManager); activityAction->setText(i18n("Activities...")); activityAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-activities"))); activityAction->setData(Plasma::Types::ConfigureAction); activityAction->setShortcut(QKeySequence(QStringLiteral("alt+d, alt+a"))); activityAction->setShortcutContext(Qt::ApplicationShortcut); KGlobalAccel::self()->setGlobalShortcut(activityAction, Qt::META + Qt::Key_Q); QAction *stopActivityAction = actions()->addAction(QStringLiteral("stop current activity")); QObject::connect(stopActivityAction, &QAction::triggered, this, &ShellCorona::stopCurrentActivity); stopActivityAction->setText(i18n("Stop Current Activity")); stopActivityAction->setData(Plasma::Types::ControlAction); stopActivityAction->setVisible(false); KGlobalAccel::self()->setGlobalShortcut(stopActivityAction, Qt::META + Qt::Key_S); connect(m_activityController, &KActivities::Controller::currentActivityChanged, this, &ShellCorona::currentActivityChanged); connect(m_activityController, &KActivities::Controller::activityAdded, this, &ShellCorona::activityAdded); connect(m_activityController, &KActivities::Controller::activityRemoved, this, &ShellCorona::activityRemoved); KActionCollection *taskbarActions = new KActionCollection(this); for (int i = 0; i < 10; ++i) { const int entryNumber = i + 1; const Qt::Key key = static_cast(Qt::Key_0 + (entryNumber % 10)); QAction *action = taskbarActions->addAction(QStringLiteral("activate task manager entry %1").arg(QString::number(entryNumber))); action->setText(i18n("Activate Task Manager Entry %1", entryNumber)); KGlobalAccel::setGlobalShortcut(action, QKeySequence(Qt::META + key)); connect(action, &QAction::triggered, this, [this, i] { activateTaskManagerEntry(i); }); } new Osd(m_config, this); // catch when plasmarc changes, so we e.g. enable/disable the OSd m_configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + m_config->name(); KDirWatch::self()->addFile(m_configPath); connect(KDirWatch::self(), &KDirWatch::dirty, this, &ShellCorona::configurationChanged); connect(KDirWatch::self(), &KDirWatch::created, this, &ShellCorona::configurationChanged); } ShellCorona::~ShellCorona() { while (!containments().isEmpty()) { //deleting a containment will remove it from the list due to QObject::destroyed connect in Corona delete containments().first(); } qDeleteAll(m_panelViews); m_panelViews.clear(); } KPackage::Package ShellCorona::lookAndFeelPackage() { return m_lookAndFeelPackage; } void ShellCorona::setShell(const QString &shell) { if (m_shell == shell) { return; } m_shell = shell; KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Shell")); package.setPath(shell); package.setAllowExternalPaths(true); setKPackage(package); m_desktopDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(package.filePath("defaults")), "Desktop"); m_lnfDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(m_lookAndFeelPackage.filePath("defaults")), "Desktop"); m_lnfDefaultsConfig = KConfigGroup(&m_lnfDefaultsConfig, shell); const QString themeGroupKey = QStringLiteral("Theme"); const QString themeNameKey = QStringLiteral("name"); QString themeName; KConfigGroup plasmarc(m_config, themeGroupKey); themeName = plasmarc.readEntry(themeNameKey, themeName); if (themeName.isEmpty()) { KConfigGroup shellCfg = KConfigGroup(KSharedConfig::openConfig(package.filePath("defaults")), "Theme"); themeName = shellCfg.readEntry("name", "default"); KConfigGroup lnfCfg = KConfigGroup(KSharedConfig::openConfig( m_lookAndFeelPackage.filePath("defaults")), "plasmarc" ); lnfCfg = KConfigGroup(&lnfCfg, themeGroupKey); themeName = lnfCfg.readEntry(themeNameKey, themeName); } if (!themeName.isEmpty()) { Plasma::Theme *t = new Plasma::Theme(this); t->setThemeName(themeName); } //FIXME: this would change the runtime platform to a fixed one if available // but a different way to load platform specific components is needed beforehand // because if we import and use two different components plugin, the second time // the import is called it will fail /* KConfigGroup cg(KSharedConfig::openConfig(package.filePath("defaults")), "General"); KDeclarative::KDeclarative::setRuntimePlatform(cg.readEntry("DefaultRuntimePlatform", QStringList()));*/ unload(); /* * we want to make an initial load once we have the initial screen config and we have loaded the activities _IF_ KAMD is running * it is valid for KAMD to not be running. * * Potentially 2 async jobs * * here we connect for status changes from KAMD, and fetch the first config from kscreen. * load() will check that we have a kscreen config, and m_activityController->serviceStatus() is not loading (i.e not unknown) * * It might seem that we only need this connection if the activityConsumer is currently in state Unknown, however * there is an issue where m_activityController will start the kactivitymanagerd, as KAMD is starting the serviceStatus will be "not running" * Whilst we are loading the kscreen config, the event loop runs and we might find KAMD has started. * m_activityController will change from "not running" to unknown, and might still be unknown when the kscreen fetching is complete. * * if that happens we want to continue monitoring for state changes, and only finally load when it is up. * * See https://bugs.kde.org/show_bug.cgi?id=342431 be careful about changing * * 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 */ connect(m_activityController, &KActivities::Controller::serviceStatusChanged, this, &ShellCorona::load, Qt::UniqueConnection); load(); } QJsonObject dumpconfigGroupJS(const KConfigGroup &rootGroup) { QJsonObject result; QStringList hierarchy; QStringList escapedHierarchy; QList groups{rootGroup}; QSet visitedNodes; const QSet forbiddenKeys { QStringLiteral("activityId"), QStringLiteral("ItemsGeometries"), QStringLiteral("AppletOrder"), QStringLiteral("SystrayContainmentId"), QStringLiteral("location"), QStringLiteral("plugin") }; auto groupID = [&escapedHierarchy]() { return '/' + escapedHierarchy.join('/'); }; // Perform a depth-first tree traversal for config groups while (!groups.isEmpty()) { KConfigGroup cg = groups.last(); KConfigGroup parentCg = cg; //FIXME: name is not enough hierarchy.clear(); escapedHierarchy.clear(); while (parentCg.isValid() && parentCg.name() != rootGroup.name()) { const auto name = parentCg.name(); hierarchy.prepend(name); escapedHierarchy.prepend(QString::fromUtf8(QUrl::toPercentEncoding(name.toUtf8()))); parentCg = parentCg.parent(); } visitedNodes.insert(groupID()); groups.pop_back(); QJsonObject configGroupJson; if (!cg.keyList().isEmpty()) { //TODO: this is conditional if applet or containment const auto map = cg.entryMap(); auto i = map.cbegin(); for (; i != map.cend(); ++i) { //some blacklisted keys we don't want to save if (!forbiddenKeys.contains(i.key())) { configGroupJson.insert(i.key(), i.value()); } } } foreach (const QString &groupName, cg.groupList()) { if (groupName == QStringLiteral("Applets") || visitedNodes.contains(groupID() + '/' + groupName)) { continue; } groups << KConfigGroup(&cg, groupName); } if (!configGroupJson.isEmpty()) { result.insert(groupID(), configGroupJson); } } return result; } QByteArray ShellCorona::dumpCurrentLayoutJS() const { QJsonObject root; root.insert("serializationFormatVersion", "1"); //same gridUnit calculation as ScriptEngine int gridUnit = QFontMetrics(QGuiApplication::font()).boundingRect(QStringLiteral("M")).height(); if (gridUnit % 2 != 0) { gridUnit++; } auto isPanel = [] (Plasma::Containment *cont) { return (cont->formFactor() == Plasma::Types::Horizontal || cont->formFactor() == Plasma::Types::Vertical) && (cont->location() == Plasma::Types::TopEdge || cont->location() == Plasma::Types::BottomEdge || cont->location() == Plasma::Types::LeftEdge || cont->location() == Plasma::Types::RightEdge) && cont->pluginMetaData().pluginId() != QStringLiteral("org.kde.plasma.private.systemtray"); }; auto isDesktop = [] (Plasma::Containment *cont) { return !cont->activity().isEmpty(); }; const auto containments = ShellCorona::containments(); // Collecting panels QJsonArray panelsJsonArray; foreach (Plasma::Containment *cont, containments) { if (!isPanel(cont)) { continue; } QJsonObject panelJson; const PanelView *view = m_panelViews.value(cont); const auto location = cont->location(); panelJson.insert("location", location == Plasma::Types::TopEdge ? "top" : location == Plasma::Types::LeftEdge ? "left" : location == Plasma::Types::RightEdge ? "right" : /* Plasma::Types::BottomEdge */ "bottom"); const qreal height = // If we do not have a panel, fallback to 4 units !view ? 4 : (qreal)view->thickness() / gridUnit; panelJson.insert("height", height); if (view) { const auto alignment = view->alignment(); panelJson.insert("maximumLength", (qreal)view->maximumLength() / gridUnit); panelJson.insert("minimumLength", (qreal)view->minimumLength() / gridUnit); panelJson.insert("offset", (qreal)view->offset() / gridUnit); panelJson.insert("alignment", alignment == Qt::AlignRight ? "right" : alignment == Qt::AlignCenter ? "center" : "left"); switch (view->visibilityMode()) { case PanelView::AutoHide: panelJson.insert("hiding", "autohide"); break; case PanelView::LetWindowsCover: panelJson.insert("hiding", "windowscover"); break; case PanelView::WindowsGoBelow: panelJson.insert("hiding", "windowsbelow"); break; case PanelView::NormalPanel: default: panelJson.insert("hiding", "normal"); break; } } // Saving the config keys const KConfigGroup contConfig = cont->config(); panelJson.insert("config", dumpconfigGroupJS(contConfig)); // Generate the applets array QJsonArray appletsJsonArray; // Try to parse the encoded applets order const KConfigGroup genericConf(&contConfig, QStringLiteral("General")); const QStringList appletsOrderStrings = genericConf.readEntry(QStringLiteral("AppletOrder"), QString()) .split(QChar(';')); // Consider the applet order to be valid only if there are as many entries as applets() if (appletsOrderStrings.length() == cont->applets().length()) { foreach (const QString &appletId, appletsOrderStrings) { KConfigGroup appletConfig(&contConfig, QStringLiteral("Applets")); appletConfig = KConfigGroup(&appletConfig, appletId); const QString pluginName = appletConfig.readEntry(QStringLiteral("plugin"), QString()); if (pluginName.isEmpty()) { continue; } QJsonObject appletJson; appletJson.insert("plugin", pluginName); appletJson.insert("config", dumpconfigGroupJS(appletConfig)); appletsJsonArray << appletJson; } } else { foreach (Plasma::Applet *applet, cont->applets()) { QJsonObject appletJson; KConfigGroup appletConfig = applet->config(); appletJson.insert("plugin", applet->pluginMetaData().pluginId()); appletJson.insert("config", dumpconfigGroupJS(appletConfig)); appletsJsonArray << appletJson; } } panelJson.insert("applets", appletsJsonArray); panelsJsonArray << panelJson; } root.insert("panels", panelsJsonArray); // Now we are collecting desktops QJsonArray desktopsJson; const auto currentActivity = m_activityController->currentActivity(); foreach (Plasma::Containment *cont, containments) { if (!isDesktop(cont) || cont->activity() != currentActivity) { continue; } QJsonObject desktopJson; desktopJson.insert("wallpaperPlugin", cont->wallpaper()); // Get the config for the containment KConfigGroup contConfig = cont->config(); desktopJson.insert("config", dumpconfigGroupJS(contConfig)); // Try to parse the item geometries const KConfigGroup genericConf(&contConfig, QStringLiteral("General")); const QStringList appletsGeomStrings = genericConf.readEntry(QStringLiteral("ItemsGeometries"), QString()) .split(QChar(';')); QHash appletGeometries; foreach (const QString &encoded, appletsGeomStrings) { const QStringList keyValue = encoded.split(QChar(':')); if (keyValue.length() != 2) { continue; } const QStringList rectPieces = keyValue.last().split(QChar(',')); if (rectPieces.length() != 5) { continue; } QRect rect(rectPieces[0].toInt(), rectPieces[1].toInt(), rectPieces[2].toInt(), rectPieces[3].toInt()); appletGeometries[keyValue.first()] = rect; } QJsonArray appletsJsonArray; foreach (Plasma::Applet *applet, cont->applets()) { const QRect geometry = appletGeometries.value( QStringLiteral("Applet-") % QString::number(applet->id())); QJsonObject appletJson; appletJson.insert("title", applet->title()); appletJson.insert("plugin", applet->pluginMetaData().pluginId()); appletJson.insert("geometry.x", geometry.x() / gridUnit); appletJson.insert("geometry.y", geometry.y() / gridUnit); appletJson.insert("geometry.width", geometry.width() / gridUnit); appletJson.insert("geometry.height", geometry.height() / gridUnit); KConfigGroup appletConfig = applet->config(); appletJson.insert("config", dumpconfigGroupJS(appletConfig)); appletsJsonArray << appletJson; } desktopJson.insert("applets", appletsJsonArray); desktopsJson << desktopJson; } root.insert("desktops", desktopsJson); QJsonDocument json; json.setObject(root); return "var plasma = getApiVersion(1);\n\n" "var layout = " + json.toJson() + ";\n\n" "plasma.loadSerializedLayout(layout);\n"; } void ShellCorona::loadLookAndFeelDefaultLayout(const QString &packageName) { KPackage::Package newPack = m_lookAndFeelPackage; newPack.setPath(packageName); if (!newPack.isValid()) { return; } KSharedConfig::Ptr conf = KSharedConfig::openConfig(QStringLiteral("plasma-") + m_shell + QStringLiteral("-appletsrc"), KConfig::SimpleConfig); m_lookAndFeelPackage.setPath(packageName); //get rid of old config for (const QString &group : conf->groupList()) { conf->deleteGroup(group); } conf->sync(); unload(); load(); } QString ShellCorona::shell() const { return m_shell; } void ShellCorona::load() { if (m_shell.isEmpty()) { return; } - if (m_activityController->serviceStatus() != KActivities::Controller::Running && + auto activityStatus = m_activityController->serviceStatus(); + if (activityStatus != KActivities::Controller::Running && !qApp->property("org.kde.KActivities.core.disableAutostart").toBool()) { - qWarning("Aborting shell load: The activity manager daemon (kactivitymanagerd) is not running."); - qWarning("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 activatable."); + if (activityStatus == KActivities::Controller::NotRunning) { + qWarning("Aborting shell load: The activity manager daemon (kactivitymanagerd) is not running."); + qWarning("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 activatable."); + } return; } disconnect(m_activityController, &KActivities::Controller::serviceStatusChanged, this, &ShellCorona::load); m_screenPool->load(); //TODO: a kconf_update script is needed QString configFileName(QStringLiteral("plasma-") + m_shell + QStringLiteral("-appletsrc")); loadLayout(configFileName); checkActivities(); if (containments().isEmpty()) { // Seems like we never really get to this point since loadLayout already // (virtually) calls loadDefaultLayout if it does not load anything // from the config file. Maybe if the config file is not empty, // but still does not have any containments loadDefaultLayout(); processUpdateScripts(); } else { processUpdateScripts(); foreach(Plasma::Containment *containment, containments()) { if (containment->containmentType() == Plasma::Types::PanelContainment || containment->containmentType() == Plasma::Types::CustomPanelContainment) { //Don't give a view to containments that don't want one (negative lastscreen) //(this is pretty mucha special case for the systray) //also, make sure we don't have a view already. //this will be true for first startup as the view has already been created at the new Panel JS call if (!m_waitingPanels.contains(containment) && containment->lastScreen() >= 0 && !m_panelViews.contains(containment)) { m_waitingPanels << containment; } //historically CustomContainments are treated as desktops } else if (containment->containmentType() == Plasma::Types::DesktopContainment || containment->containmentType() == Plasma::Types::CustomContainment) { //FIXME ideally fix this, or at least document the crap out of it int screen = containment->lastScreen(); if (screen < 0) { screen = 0; qWarning() << "last screen is < 0 so putting containment on screen " << screen; } insertContainment(containment->activity(), screen, containment); } } } //NOTE: this is needed in case loadLayout() did *not* call loadDefaultLayout() //it needs to be after of loadLayout() as it would always create new //containments on each startup otherwise for (QScreen* screen : qGuiApp->screens()) { //the containments may have been created already by the startup script //check their existence in oder to not have duplicated desktopviews if (!m_desktopViewforId.contains(m_screenPool->id(screen->name()))) { addOutput(screen); } } connect(qGuiApp, &QGuiApplication::screenAdded, this, &ShellCorona::addOutput, Qt::UniqueConnection); connect(qGuiApp, &QGuiApplication::primaryScreenChanged, this, &ShellCorona::primaryOutputChanged, Qt::UniqueConnection); connect(qGuiApp, &QGuiApplication::screenRemoved, this, &ShellCorona::handleScreenRemoved, Qt::UniqueConnection); if (!m_waitingPanels.isEmpty()) { m_waitingPanelsTimer.start(); } if (config()->isImmutable() || !KAuthorized::authorize(QStringLiteral("plasma/plasmashell/unlockedDesktop"))) { setImmutability(Plasma::Types::SystemImmutable); } else { KConfigGroup coronaConfig(config(), "General"); setImmutability((Plasma::Types::ImmutabilityType)coronaConfig.readEntry("immutability", static_cast(Plasma::Types::Mutable))); } } void ShellCorona::primaryOutputChanged() { if (!m_desktopViewforId.contains(0)) { return; } //when the appearance of a new primary screen *moves* //the position of the now secondary, the two screens will appear overlapped for an instant, and a spurious output redundant would happen here if checked immediately m_reconsiderOutputsTimer.start(); QScreen *oldPrimary = m_desktopViewforId.value(0)->screen(); QScreen *newPrimary = qGuiApp->primaryScreen(); if (!newPrimary || newPrimary == oldPrimary) { return; } qWarning()<<"Old primary output:"<id(newPrimary->name()); m_screenPool->setPrimaryConnector(newPrimary->name()); //swap order in m_desktopViewforId if (m_desktopViewforId.contains(0) && m_desktopViewforId.contains(oldIdOfPrimary)) { DesktopView *primaryDesktop = m_desktopViewforId.value(0); DesktopView *oldDesktopOfPrimary = m_desktopViewforId.value(oldIdOfPrimary); primaryDesktop->setScreenToFollow(newPrimary); oldDesktopOfPrimary->setScreenToFollow(oldPrimary); primaryDesktop->show(); oldDesktopOfPrimary->show(); //corner case: the new primary screen was added into redundant outputs when appeared, *and* !m_desktopViewforId.contains(oldIdOfPrimary) //meaning that we had only one screen, connected a new oone that //a) is now primary and //b) is at 0,0 position, moving the current screen out of the way // and this will always happen in two events } else if (m_desktopViewforId.contains(0) && m_redundantOutputs.contains(newPrimary)) { m_desktopViewforId[0]->setScreenToFollow(newPrimary); m_redundantOutputs.remove(newPrimary); m_redundantOutputs.insert(oldPrimary); } foreach (PanelView *panel, m_panelViews) { if (panel->screen() == oldPrimary) { panel->setScreenToFollow(newPrimary); } else if (panel->screen() == newPrimary) { panel->setScreenToFollow(oldPrimary); } } //can't do the screen invariant here as reconsideroutputs wasn't executed yet //CHECK_SCREEN_INVARIANTS } #ifndef NDEBUG void ShellCorona::screenInvariants() const { Q_ASSERT(m_desktopViewforId.keys().count() <= QGuiApplication::screens().count()); QSet screens; foreach (const int id, m_desktopViewforId.keys()) { const DesktopView *view = m_desktopViewforId.value(id); QScreen *screen = view->screenToFollow(); Q_ASSERT(!screens.contains(screen)); Q_ASSERT(!m_redundantOutputs.contains(screen)); // commented out because a different part of the code-base is responsible for this // and sometimes is not yet called here. // Q_ASSERT(!view->fillScreen() || view->geometry() == screen->geometry()); Q_ASSERT(view->containment()); Q_ASSERT(view->containment()->screen() == id || view->containment()->screen() == -1); Q_ASSERT(view->containment()->lastScreen() == id || view->containment()->lastScreen() == -1); Q_ASSERT(view->isVisible()); foreach (const PanelView *panel, panelsForScreen(screen)) { Q_ASSERT(panel->containment()); Q_ASSERT(panel->containment()->screen() == id || panel->containment()->screen() == -1); //If any kscreen related activities occurred //during startup, the panel wouldn't be visible yet, and this would assert if (panel->containment()->isUiReady()) { Q_ASSERT(panel->isVisible()); } } screens.insert(screen); } foreach (QScreen* out, m_redundantOutputs) { Q_ASSERT(isOutputRedundant(out)); } if (m_desktopViewforId.isEmpty()) { qWarning() << "no screens!!"; } } #endif void ShellCorona::showAlternativesForApplet(Plasma::Applet *applet) { const QUrl alternativesQML = kPackage().fileUrl("appletalternativesui"); if (alternativesQML.isEmpty()) { return; } auto *qmlObj = new KDeclarative::QmlObjectSharedEngine(this); qmlObj->setInitializationDelayed(true); qmlObj->setSource(alternativesQML); AlternativesHelper *helper = new AlternativesHelper(applet, qmlObj); qmlObj->rootContext()->setContextProperty(QStringLiteral("alternativesHelper"), helper); qmlObj->completeInitialization(); auto dialog = qobject_cast(qmlObj->rootObject()); if (!dialog) { qWarning() << "Alternatives UI does not inherit from Dialog"; delete qmlObj; return; } connect(applet, &Plasma::Applet::destroyedChanged, qmlObj, [qmlObj] (bool destroyed) { if (!destroyed) { return; } qmlObj->deleteLater(); }); connect(dialog, &PlasmaQuick::Dialog::visibleChanged, qmlObj, [qmlObj](bool visible) { if (visible) { return; } qmlObj->deleteLater(); }); } void ShellCorona::unload() { if (m_shell.isEmpty()) { return; } qDeleteAll(m_desktopViewforId); m_desktopViewforId.clear(); qDeleteAll(m_panelViews); m_panelViews.clear(); m_waitingPanels.clear(); m_activityContainmentPlugins.clear(); while (!containments().isEmpty()) { //deleting a containment will remove it from the list due to QObject::destroyed connect in Corona //this form doesn't crash, while qDeleteAll(containments()) does delete containments().first(); } } KSharedConfig::Ptr ShellCorona::applicationConfig() { return KSharedConfig::openConfig(); } void ShellCorona::requestApplicationConfigSync() { m_appConfigSyncTimer.start(); } void ShellCorona::loadDefaultLayout() { //pre-startup scripts QString script = m_lookAndFeelPackage.filePath("layouts", QString(shell() + "-prelayout.js").toLatin1()); if (!script.isEmpty()) { QFile file(script); if (file.open(QIODevice::ReadOnly | QIODevice::Text) ) { QString code = file.readAll(); qDebug() << "evaluating pre-startup script:" << script; WorkspaceScripting::ScriptEngine scriptEngine(this); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { qWarning() << msg; }); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { qDebug() << msg; }); if (!scriptEngine.evaluateScript(code, script)) { qWarning() << "failed to initialize layout properly:" << script; } } } //NOTE: Is important the containments already exist for each screen // at the moment of the script execution,the same loop in :load() // is executed too late for (QScreen* screen : qGuiApp->screens()) { addOutput(screen); } script = ShellManager::s_testModeLayout; if (script.isEmpty()) { script = m_lookAndFeelPackage.filePath("layouts", QString(shell() + "-layout.js").toLatin1()); } if (script.isEmpty()) { script = package().filePath("defaultlayout"); } QFile file(script); if (file.open(QIODevice::ReadOnly | QIODevice::Text) ) { QString code = file.readAll(); qDebug() << "evaluating startup script:" << script; // We need to know which activities are here in order for // the scripting engine to work. activityAdded does not mind // if we pass it the same activity multiple times QStringList existingActivities = m_activityController->activities(); foreach (const QString &id, existingActivities) { activityAdded(id); } WorkspaceScripting::ScriptEngine scriptEngine(this); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { qWarning() << msg; }); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { qDebug() << msg; }); if (!scriptEngine.evaluateScript(code, script)) { qWarning() << "failed to initialize layout properly:" << script; } } Q_EMIT startupCompleted(); } void ShellCorona::processUpdateScripts() { const QStringList scripts = WorkspaceScripting::ScriptEngine::pendingUpdateScripts(this); if (scripts.isEmpty()) { return; } WorkspaceScripting::ScriptEngine scriptEngine(this); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { qWarning() << msg; }); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { qDebug() << msg; }); for (const QString &script : scripts) { QFile file(script); if (file.open(QIODevice::ReadOnly | QIODevice::Text) ) { QString code = file.readAll(); scriptEngine.evaluateScript(code); } else { qWarning() << "Unable to open the script file" << script << "for reading"; } } } int ShellCorona::numScreens() const { return qGuiApp->screens().count(); } QRect ShellCorona::screenGeometry(int id) const { if (!m_desktopViewforId.contains(id)) { qWarning() << "requesting unexisting screen" << id; QScreen *s = qGuiApp->primaryScreen(); return s ? s->geometry() : QRect(); } return m_desktopViewforId.value(id)->geometry(); } QRegion ShellCorona::availableScreenRegion(int id) const { if (!m_desktopViewforId.contains(id)) { //each screen should have a view qWarning() << "requesting unexisting screen" << id; QScreen *s = qGuiApp->primaryScreen(); return s ? s->availableGeometry() : QRegion(); } DesktopView *view = m_desktopViewforId.value(id); QRegion r = view->geometry(); foreach (const PanelView *v, m_panelViews) { if (v->isVisible() && view->screen() == v->screen() && v->visibilityMode() != PanelView::AutoHide) { //if the panel is being moved around, we still want to calculate it from the edge r -= v->geometryByDistance(0); } } return r; } QRect ShellCorona::availableScreenRect(int id) const { if (!m_desktopViewforId.contains(id)) { //each screen should have a view qWarning() << "requesting unexisting screen" << id; QScreen *s = qGuiApp->primaryScreen(); return s ? s->availableGeometry() : QRect(); } DesktopView *view = m_desktopViewforId.value(id); QRect r = view->geometry(); foreach (PanelView *v, m_panelViews) { if (v->isVisible() && v->screen() == view->screen() && v->visibilityMode() != PanelView::AutoHide) { switch (v->location()) { case Plasma::Types::LeftEdge: r.setLeft(r.left() + v->thickness()); break; case Plasma::Types::RightEdge: r.setRight(r.right() - v->thickness()); break; case Plasma::Types::TopEdge: r.setTop(r.top() + v->thickness()); break; case Plasma::Types::BottomEdge: r.setBottom(r.bottom() - v->thickness()); default: break; } } } return r; } QStringList ShellCorona::availableActivities() const { return m_activityContainmentPlugins.keys(); } void ShellCorona::removeDesktop(DesktopView *desktopView) { const int idx = m_screenPool->id(desktopView->screenToFollow()->name()); if (!m_desktopViewforId.contains(idx)) { return; } QMutableHashIterator it(m_panelViews); while (it.hasNext()) { it.next(); PanelView *panelView = it.value(); if (panelView->containment()->screen() == idx) { m_waitingPanels << panelView->containment(); it.remove(); delete panelView; } } Q_ASSERT(m_desktopViewforId.value(idx) == desktopView); m_desktopViewforId.remove(idx); delete desktopView; emit screenRemoved(idx); } PanelView *ShellCorona::panelView(Plasma::Containment *containment) const { return m_panelViews.value(containment); } ///// SLOTS QList ShellCorona::panelsForScreen(QScreen *screen) const { QList ret; foreach (PanelView *v, m_panelViews) { if (v->screenToFollow() == screen) { ret += v; } } return ret; } DesktopView* ShellCorona::desktopForScreen(QScreen* screen) const { return m_desktopViewforId.value(m_screenPool->id(screen->name())); } void ShellCorona::handleScreenRemoved(QScreen* screen) { if (DesktopView* v = desktopForScreen(screen)) { removeDesktop(v); } m_reconsiderOutputsTimer.start(); m_redundantOutputs.remove(screen); } bool ShellCorona::isOutputRedundant(QScreen* screen) const { Q_ASSERT(screen); const QRect thisGeometry = screen->geometry(); const int thisId = m_screenPool->id(screen->name()); //FIXME: QScreen doesn't have any idea of "this qscreen is clone of this other one //so this ultra inefficient heuristic has to stay until we have a slightly better api //logic is: //a screen is redundant if: //* its geometry is contained in another one //* if their resolutions are different, the "biggest" one wins //* if they have the same geometry, the one with the lowest id wins (arbitrary, but gives reproducible behavior and makes the primary screen win) foreach (QScreen* s, qGuiApp->screens()) { //don't compare with itself if (screen == s) { continue; } const QRect otherGeometry = s->geometry(); const int otherId = m_screenPool->id(s->name()); if (otherGeometry.contains(thisGeometry, false) && (//since at this point contains is true, if either //measure of othergeometry is bigger, has a bigger area otherGeometry.width() > thisGeometry.width() || otherGeometry.height() > thisGeometry.height() || //ids not -1 are considered in descending order of importance //-1 means that is a screen not known yet, just arrived and //not yet in screenpool: this happens for screens that //are hotplugged and weren't known. it does NOT happen //at first startup, as screenpool populates on load with all screens connected at the moment before the rest of the shell starts up (thisId == -1 && otherId != -1) || (thisId > otherId && otherId != -1))) { return true; } } return false; } void ShellCorona::reconsiderOutputs() { foreach (QScreen* screen, qGuiApp->screens()) { if (m_redundantOutputs.contains(screen)) { if (!isOutputRedundant(screen)) { //qDebug() << "not redundant anymore" << screen; addOutput(screen); } } else if (isOutputRedundant(screen)) { qDebug() << "new redundant screen" << screen << "with primary screen" << qGuiApp->primaryScreen(); if (DesktopView* v = desktopForScreen(screen)) removeDesktop(v); m_redundantOutputs.insert(screen); } // else // qDebug() << "fine screen" << out; } updateStruts(); CHECK_SCREEN_INVARIANTS } void ShellCorona::addOutput(QScreen* screen) { Q_ASSERT(screen); connect(screen, &QScreen::geometryChanged, &m_reconsiderOutputsTimer, static_cast(&QTimer::start), Qt::UniqueConnection); if (isOutputRedundant(screen)) { m_redundantOutputs.insert(screen); return; } else { m_redundantOutputs.remove(screen); } int insertPosition = m_screenPool->id(screen->name()); if (insertPosition < 0) { insertPosition = m_screenPool->firstAvailableId(); } DesktopView *view = new DesktopView(this, screen); if (view->rendererInterface()->graphicsApi() != QSGRendererInterface::Software) { connect(view, &QQuickWindow::sceneGraphError, this, &ShellCorona::glInitializationFailed); } connect(screen, &QScreen::geometryChanged, this, [=]() { const int id = m_screenPool->id(screen->name()); if (id >= 0) { emit screenGeometryChanged(id); emit availableScreenRegionChanged(); emit availableScreenRectChanged(); } }); Plasma::Containment *containment = createContainmentForActivity(m_activityController->currentActivity(), insertPosition); Q_ASSERT(containment); QAction *removeAction = containment->actions()->action(QStringLiteral("remove")); if (removeAction) { removeAction->deleteLater(); } m_screenPool->insertScreenMapping(insertPosition, screen->name()); m_desktopViewforId[insertPosition] = view; view->setContainment(containment); view->show(); Q_ASSERT(screen == view->screen()); //need to specifically call the reactToScreenChange, since when the screen is shown it's not yet //in the list. We still don't want to have an invisible view added. containment->reactToScreenChange(); //were there any panels for this screen before it popped up? if (!m_waitingPanels.isEmpty()) { m_waitingPanelsTimer.start(); } emit availableScreenRectChanged(); emit screenAdded(m_screenPool->id(screen->name())); CHECK_SCREEN_INVARIANTS } Plasma::Containment *ShellCorona::createContainmentForActivity(const QString& activity, int screenNum) { for (Plasma::Containment *cont : containmentsForActivity(activity)) { //in the case of a corrupt config file //with multiple containments with same lastScreen //it can happen two insertContainment happen for //the same screen, leading to the old containment //to be destroyed if (!cont->destroyed() && cont->screen() == screenNum) { return cont; } } QString plugin = m_activityContainmentPlugins.value(activity); if (plugin.isEmpty()) { plugin = defaultContainmentPlugin(); } Plasma::Containment *containment = containmentForScreen(screenNum, activity, plugin, QVariantList()); Q_ASSERT(containment); if (containment) { insertContainment(activity, screenNum, containment); } return containment; } void ShellCorona::createWaitingPanels() { QList stillWaitingPanels; foreach (Plasma::Containment *cont, m_waitingPanels) { //ignore non existing (yet?) screens int requestedScreen = cont->lastScreen(); if (requestedScreen < 0) { requestedScreen = 0; } if (!m_desktopViewforId.contains(requestedScreen)) { stillWaitingPanels << cont; continue; } //TODO: does a similar check make sense? //Q_ASSERT(qBound(0, requestedScreen, m_screenPool->count() - 1) == requestedScreen); QScreen *screen = m_desktopViewforId.value(requestedScreen)->screenToFollow(); PanelView* panel = new PanelView(this, screen); if (panel->rendererInterface()->graphicsApi() != QSGRendererInterface::Software) { connect(panel, &QQuickWindow::sceneGraphError, this, &ShellCorona::glInitializationFailed); } connect(panel, &QWindow::visibleChanged, this, &Plasma::Corona::availableScreenRectChanged); connect(panel, &PanelView::locationChanged, this, &Plasma::Corona::availableScreenRectChanged); connect(panel, &PanelView::visibilityModeChanged, this, &Plasma::Corona::availableScreenRectChanged); connect(panel, &PanelView::thicknessChanged, this, &Plasma::Corona::availableScreenRectChanged); m_panelViews[cont] = panel; panel->setContainment(cont); cont->reactToScreenChange(); connect(cont, &QObject::destroyed, this, &ShellCorona::panelContainmentDestroyed); } m_waitingPanels = stillWaitingPanels; emit availableScreenRectChanged(); } void ShellCorona::panelContainmentDestroyed(QObject *cont) { auto view = m_panelViews.take(static_cast(cont)); view->deleteLater(); //don't make things relayout when the application is quitting //NOTE: qApp->closingDown() is still false here if (!m_closingDown) { emit availableScreenRectChanged(); } } void ShellCorona::handleContainmentAdded(Plasma::Containment *c) { connect(c, &Plasma::Containment::showAddWidgetsInterface, this, &ShellCorona::toggleWidgetExplorer); connect(c, &Plasma::Containment::appletAlternativesRequested, this, &ShellCorona::showAlternativesForApplet); connect(c, &Plasma::Containment::appletCreated, this, [this, c] (Plasma::Applet *applet) { executeSetupPlasmoidScript(c, applet); }); } void ShellCorona::executeSetupPlasmoidScript(Plasma::Containment *containment, Plasma::Applet *applet) { if (!applet->pluginMetaData().isValid() || !containment->pluginMetaData().isValid()) { return; } const QString scriptFile = m_lookAndFeelPackage.filePath("plasmoidsetupscripts", applet->pluginMetaData().pluginId() + ".js"); if (scriptFile.isEmpty()) { return; } WorkspaceScripting::ScriptEngine scriptEngine(this); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { qWarning() << msg; }); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { qDebug() << msg; }); QFile file(scriptFile); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qWarning() << i18n("Unable to load script file: %1", scriptFile); return; } QString script = file.readAll(); if (script.isEmpty()) { // qDebug() << "script is empty"; return; } scriptEngine.globalObject().setProperty(QStringLiteral("applet"), scriptEngine.wrap(applet)); scriptEngine.globalObject().setProperty(QStringLiteral("containment"), scriptEngine.wrap(containment)); scriptEngine.evaluateScript(script, scriptFile); } void ShellCorona::toggleWidgetExplorer() { const QPoint cursorPos = QCursor::pos(); foreach (DesktopView *view, m_desktopViewforId) { if (view->screen()->geometry().contains(cursorPos)) { //The view QML has to provide something to display the widget explorer view->rootObject()->metaObject()->invokeMethod(view->rootObject(), "toggleWidgetExplorer", Q_ARG(QVariant, QVariant::fromValue(sender()))); return; } } } void ShellCorona::toggleActivityManager() { const QPoint cursorPos = QCursor::pos(); foreach (DesktopView *view, m_desktopViewforId) { if (view->screen()->geometry().contains(cursorPos)) { //The view QML has to provide something to display the activity explorer view->rootObject()->metaObject()->invokeMethod(view->rootObject(), "toggleActivityManager", Qt::QueuedConnection); return; } } } void ShellCorona::syncAppConfig() { applicationConfig()->sync(); } void ShellCorona::setDashboardShown(bool show) { KWindowSystem::setShowingDesktop(show); } void ShellCorona::toggleDashboard() { setDashboardShown(!KWindowSystem::showingDesktop()); } void ShellCorona::loadInteractiveConsole() { if (KSharedConfig::openConfig()->isImmutable() || !KAuthorized::authorize(QStringLiteral("plasma-desktop/scripting_console"))) { delete m_interactiveConsole; m_interactiveConsole = nullptr; return; } if (!m_interactiveConsole) { const QUrl consoleQML = kPackage().fileUrl("interactiveconsole"); if (consoleQML.isEmpty()) { return; } m_interactiveConsole = new KDeclarative::QmlObjectSharedEngine(this); m_interactiveConsole->setInitializationDelayed(true); m_interactiveConsole->setSource(consoleQML); QObject *engine = new WorkspaceScripting::ScriptEngine(this, m_interactiveConsole); m_interactiveConsole->rootContext()->setContextProperty(QStringLiteral("scriptEngine"), engine); m_interactiveConsole->completeInitialization(); if (m_interactiveConsole->rootObject()) { connect(m_interactiveConsole->rootObject(), SIGNAL(visibleChanged(bool)), this, SLOT(interactiveConsoleVisibilityChanged(bool))); } } } void ShellCorona::showInteractiveConsole() { loadInteractiveConsole(); if (m_interactiveConsole && m_interactiveConsole->rootObject()) { m_interactiveConsole->rootObject()->setProperty("mode", "desktop"); m_interactiveConsole->rootObject()->setProperty("visible", true); } } void ShellCorona::loadScriptInInteractiveConsole(const QString &script) { showInteractiveConsole(); if (m_interactiveConsole) { m_interactiveConsole->rootObject()->setProperty("script", script); } } void ShellCorona::showInteractiveKWinConsole() { loadInteractiveConsole(); if (m_interactiveConsole && m_interactiveConsole->rootObject()) { m_interactiveConsole->rootObject()->setProperty("mode", "windowmanager"); m_interactiveConsole->rootObject()->setProperty("visible", true); } } void ShellCorona::loadKWinScriptInInteractiveConsole(const QString &script) { showInteractiveKWinConsole(); if (m_interactiveConsole) { m_interactiveConsole->rootObject()->setProperty("script", script); } } void ShellCorona::evaluateScript(const QString &script) { if (calledFromDBus()) { if (immutability() == Plasma::Types::SystemImmutable) { sendErrorReply(QDBusError::Failed, QStringLiteral("Widgets are locked")); return; } else if (!KAuthorized::authorize(QStringLiteral("plasma-desktop/scripting_console"))) { sendErrorReply(QDBusError::Failed, QStringLiteral("Administrative policies prevent script execution")); return; } } WorkspaceScripting::ScriptEngine scriptEngine(this); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { qWarning() << msg; }); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { qDebug() << msg; }); scriptEngine.evaluateScript(script); if (!scriptEngine.errorString().isEmpty() && calledFromDBus()) { sendErrorReply(QDBusError::Failed, scriptEngine.errorString()); } } void ShellCorona::interactiveConsoleVisibilityChanged(bool visible) { if (!visible) { m_interactiveConsole->deleteLater(); m_interactiveConsole = nullptr; } } void ShellCorona::checkActivities() { KActivities::Controller::ServiceStatus status = m_activityController->serviceStatus(); //qDebug() << "$%$%$#%$%$%Status:" << status; if (status != KActivities::Controller::Running) { //panic and give up - better than causing a mess qDebug() << "ShellCorona::checkActivities is called whilst activity daemon is still connecting"; return; } QStringList existingActivities = m_activityController->activities(); foreach (const QString &id, existingActivities) { activityAdded(id); } // Checking whether the result we got is valid. Just in case. Q_ASSERT_X(!existingActivities.isEmpty(), "isEmpty", "There are no activities, and the service is running"); Q_ASSERT_X(existingActivities[0] != QStringLiteral("00000000-0000-0000-0000-000000000000"), "null uuid", "There is a nulluuid activity present"); // Killing the unassigned containments foreach (Plasma::Containment *cont, containments()) { if ((cont->containmentType() == Plasma::Types::DesktopContainment || cont->containmentType() == Plasma::Types::CustomContainment) && !existingActivities.contains(cont->activity())) { cont->destroy(); } } } void ShellCorona::currentActivityChanged(const QString &newActivity) { // qDebug() << "Activity changed:" << newActivity; foreach (int id, m_desktopViewforId.keys()) { Plasma::Containment *c = createContainmentForActivity(newActivity, id); QAction *removeAction = c->actions()->action(QStringLiteral("remove")); if (removeAction) { removeAction->deleteLater(); } m_desktopViewforId.value(id)->setContainment(c); } } void ShellCorona::activityAdded(const QString &id) { //TODO more sanity checks if (m_activityContainmentPlugins.contains(id)) { qWarning() << "Activity added twice" << id; return; } m_activityContainmentPlugins.insert(id, defaultContainmentPlugin()); } void ShellCorona::activityRemoved(const QString &id) { m_activityContainmentPlugins.remove(id); for (auto cont : containmentsForActivity(id)) { cont->destroy(); } } void ShellCorona::insertActivity(const QString &id, const QString &plugin) { activityAdded(id); const QString currentActivityReally = m_activityController->currentActivity(); // TODO: This needs to go away! // The containment creation API does not know when we have a // new activity to create a containment for, we need to pretend // that the current activity has been changed QFuture currentActivity = m_activityController->setCurrentActivity(id); awaitFuture(currentActivity); if (!currentActivity.result()) { qDebug() << "Failed to create and switch to the activity"; return; } while (m_activityController->currentActivity() != id) { QCoreApplication::processEvents(); } m_activityContainmentPlugins.insert(id, plugin); foreach (int screenId, m_desktopViewforId.keys()) { Plasma::Containment *c = createContainmentForActivity(id, screenId); if (c) { c->config().writeEntry("lastScreen", screenId); } } } Plasma::Containment *ShellCorona::setContainmentTypeForScreen(int screen, const QString &plugin) { //search but not create Plasma::Containment *oldContainment = containmentForScreen(screen, m_activityController->currentActivity(), QString()); //no valid containment in given screen, giving up if (!oldContainment) { return nullptr; } if (plugin.isEmpty()) { return oldContainment; } DesktopView *view = nullptr; foreach (DesktopView *v, m_desktopViewforId) { if (v->containment() == oldContainment) { view = v; break; } } //no view? give up if (!view) { return oldContainment; } //create a new containment Plasma::Containment *newContainment = createContainmentDelayed(plugin); //if creation failed or invalid plugin, give up if (!newContainment) { return oldContainment; } else if (!newContainment->pluginMetaData().isValid()) { newContainment->deleteLater(); return oldContainment; } newContainment->setWallpaper(oldContainment->wallpaper()); //At this point we have a valid new containment from plugin and a view //copy all configuration groups (excluded applets) KConfigGroup oldCg = oldContainment->config(); //newCg *HAS* to be from a KSharedConfig, because some KConfigSkeleton will need to be synced //this makes the configscheme work KConfigGroup newCg(KSharedConfig::openConfig(oldCg.config()->name()), "Containments"); newCg = KConfigGroup(&newCg, QString::number(newContainment->id())); //this makes containment->config() work, is a separate thing from its configscheme KConfigGroup newCg2 = newContainment->config(); foreach (const QString &group, oldCg.groupList()) { if (group != QLatin1String("Applets")) { KConfigGroup subGroup(&oldCg, group); KConfigGroup newSubGroup(&newCg, group); subGroup.copyTo(&newSubGroup); KConfigGroup newSubGroup2(&newCg2, group); subGroup.copyTo(&newSubGroup2); } } newContainment->init(); newCg.writeEntry("activityId", oldContainment->activity()); newContainment->restore(newCg); newContainment->updateConstraints(Plasma::Types::StartupCompletedConstraint); newContainment->flushPendingConstraintsEvents(); emit containmentAdded(newContainment); //Move the applets foreach (Plasma::Applet *applet, oldContainment->applets()) { newContainment->addApplet(applet); } //remove the "remove" action QAction *removeAction = newContainment->actions()->action(QStringLiteral("remove")); if (removeAction) { removeAction->deleteLater(); } view->setContainment(newContainment); newContainment->setActivity(oldContainment->activity()); insertContainment(oldContainment->activity(), screen, newContainment); //removing the focus from the item that is going to be destroyed //fixes a crash //delayout the destruction of the old containment fixes another crash view->rootObject()->setFocus(true, Qt::MouseFocusReason); QTimer::singleShot(2500, oldContainment, &Plasma::Applet::destroy); //Save now as we now have a screen, so lastScreen will not be -1 newContainment->save(newCg); requestConfigSync(); emit availableScreenRectChanged(); return newContainment; } void ShellCorona::checkAddPanelAction(const QStringList &sycocaChanges) { if (!sycocaChanges.isEmpty() && !sycocaChanges.contains(QStringLiteral("services"))) { return; } delete m_addPanelAction; m_addPanelAction = nullptr; m_addPanelsMenu.reset(nullptr); KPluginInfo::List panelContainmentPlugins = Plasma::PluginLoader::listContainmentsOfType(QStringLiteral("Panel")); auto filter = [](const KPluginMetaData &md) -> bool { return md.value(QStringLiteral("NoDisplay")) != QStringLiteral("true") && KPluginMetaData::readStringList(md.rawData(), QStringLiteral("X-Plasma-ContainmentCategories")).contains(QStringLiteral("panel")); }; QList templates = KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/LayoutTemplate"), QString(), filter); if (panelContainmentPlugins.count() + templates.count() == 1) { m_addPanelAction = new QAction(i18n("Add Panel"), this); m_addPanelAction->setData(Plasma::Types::AddAction); connect(m_addPanelAction, SIGNAL(triggered(bool)), this, SLOT(addPanel())); } else if (!panelContainmentPlugins.isEmpty()) { m_addPanelsMenu.reset(new QMenu); m_addPanelAction = m_addPanelsMenu->menuAction(); m_addPanelAction->setText(i18n("Add Panel")); m_addPanelAction->setData(Plasma::Types::AddAction); connect(m_addPanelsMenu.data(), &QMenu::aboutToShow, this, &ShellCorona::populateAddPanelsMenu); connect(m_addPanelsMenu.data(), SIGNAL(triggered(QAction*)), this, SLOT(addPanel(QAction*))); } if (m_addPanelAction) { m_addPanelAction->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); actions()->addAction(QStringLiteral("add panel"), m_addPanelAction); } } void ShellCorona::populateAddPanelsMenu() { m_addPanelsMenu->clear(); const KPluginInfo emptyInfo; KPluginInfo::List panelContainmentPlugins = Plasma::PluginLoader::listContainmentsOfType(QStringLiteral("Panel")); QMap > sorted; foreach (const KPluginInfo &plugin, panelContainmentPlugins) { if (plugin.property(QStringLiteral("NoDisplay")).toString() == QStringLiteral("true")) { continue; } sorted.insert(plugin.name(), qMakePair(plugin, KPluginMetaData())); } auto filter = [](const KPluginMetaData &md) -> bool { return md.value(QStringLiteral("NoDisplay")) != QStringLiteral("true") && KPluginMetaData::readStringList(md.rawData(), QStringLiteral("X-Plasma-ContainmentCategories")).contains(QStringLiteral("panel")); }; const QList templates = KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/LayoutTemplate"), QString(), filter); for (auto tpl : templates) { sorted.insert(tpl.name(), qMakePair(emptyInfo, tpl)); } QMapIterator > it(sorted); KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LayoutTemplate")); while (it.hasNext()) { it.next(); QPair pair = it.value(); if (pair.first.isValid()) { KPluginInfo plugin = pair.first; QAction *action = m_addPanelsMenu->addAction(i18n("Empty %1", plugin.name())); if (!plugin.icon().isEmpty()) { action->setIcon(QIcon::fromTheme(plugin.icon())); } action->setData(plugin.pluginName()); } else { KPluginInfo info(pair.second); package.setPath(info.pluginName()); const QString scriptFile = package.filePath("mainscript"); if (!scriptFile.isEmpty()) { QAction *action = m_addPanelsMenu->addAction(info.name()); action->setData(QStringLiteral("plasma-desktop-template:%1").arg(info.pluginName())); } } } } void ShellCorona::addPanel() { KPluginInfo::List panelPlugins = Plasma::PluginLoader::listContainmentsOfType(QStringLiteral("Panel")); if (!panelPlugins.isEmpty()) { addPanel(panelPlugins.first().pluginName()); } } void ShellCorona::addPanel(QAction *action) { const QString plugin = action->data().toString(); if (plugin.startsWith(QLatin1String("plasma-desktop-template:"))) { WorkspaceScripting::ScriptEngine scriptEngine(this); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { qWarning() << msg; }); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { qDebug() << msg; }); const QString templateName = plugin.right(plugin.length() - qstrlen("plasma-desktop-template:")); scriptEngine.evaluateScript(QStringLiteral("loadTemplate(\"%1\")").arg(templateName)); } else if (!plugin.isEmpty()) { addPanel(plugin); } } Plasma::Containment *ShellCorona::addPanel(const QString &plugin) { Plasma::Containment *panel = createContainment(plugin); if (!panel) { return nullptr; } QList availableLocations; availableLocations << Plasma::Types::LeftEdge << Plasma::Types::TopEdge << Plasma::Types::RightEdge << Plasma::Types::BottomEdge; foreach (const Plasma::Containment *cont, m_panelViews.keys()) { availableLocations.removeAll(cont->location()); } Plasma::Types::Location loc; if (availableLocations.isEmpty()) { loc = Plasma::Types::TopEdge; } else { loc = availableLocations.first(); } panel->setLocation(loc); switch (loc) { case Plasma::Types::LeftEdge: case Plasma::Types::RightEdge: panel->setFormFactor(Plasma::Types::Vertical); break; default: panel->setFormFactor(Plasma::Types::Horizontal); break; } Q_ASSERT(panel); m_waitingPanels << panel; //not creating the panel view yet in order to have the same code path //between the first and subsequent plasma starts. we want to have the panel appearing only when its layout is completed, to not have //many visible relayouts. otherwise we had even panel resizes at startup that //made al lthe full representations be loaded. m_waitingPanelsTimer.start(); const QPoint cursorPos(QCursor::pos()); foreach (QScreen *screen, QGuiApplication::screens()) { //m_panelViews.contains(panel) == false iff addPanel is executed in a startup script if (screen->geometry().contains(cursorPos) && m_panelViews.contains(panel)) { m_panelViews[panel]->setScreenToFollow(screen); break; } } return panel; } int ShellCorona::screenForContainment(const Plasma::Containment *containment) const { //case in which this containment is child of an applet, hello systray :) if (Plasma::Applet *parentApplet = qobject_cast(containment->parent())) { if (Plasma::Containment* cont = parentApplet->containment()) { return screenForContainment(cont); } else { return -1; } } //if the desktop views already exist, base the decision upon them for (auto it = m_desktopViewforId.constBegin(), end = m_desktopViewforId.constEnd(); it != end; ++it) { if (it.value()->containment() == containment && containment->activity() == m_activityController->currentActivity()) { return it.key(); } } //if the panel views already exist, base upon them PanelView *view = m_panelViews.value(containment); if (view && view->screenToFollow()) { return m_screenPool->id(view->screenToFollow()->name()); } //Failed? fallback on lastScreen() //lastScreen() is the correct screen for panels //It is also correct for desktops *that have the correct activity()* //a containment with lastScreen() == 0 but another activity, //won't be associated to a screen // qDebug() << "ShellCorona screenForContainment: " << containment << " Last screen is " << containment->lastScreen(); for (auto screen : qGuiApp->screens()) { // containment->lastScreen() == m_screenPool->id(screen->name()) to check if the lastScreen refers to a screen that exists/it's known if (containment->lastScreen() == m_screenPool->id(screen->name()) && (containment->activity() == m_activityController->currentActivity() || containment->containmentType() == Plasma::Types::PanelContainment || containment->containmentType() == Plasma::Types::CustomPanelContainment)) { return containment->lastScreen(); } } return -1; } void ShellCorona::nextActivity() { const QStringList list = m_activityController->activities(KActivities::Info::Running); if (list.isEmpty()) { return; } const int start = list.indexOf(m_activityController->currentActivity()); const int i = (start + 1) % list.size(); m_activityController->setCurrentActivity(list.at(i)); } void ShellCorona::previousActivity() { const QStringList list = m_activityController->activities(KActivities::Info::Running); if (list.isEmpty()) { return; } const int start = list.indexOf(m_activityController->currentActivity()); int i = start - 1; if(i < 0) { i = list.size() - 1; } m_activityController->setCurrentActivity(list.at(i)); } void ShellCorona::stopCurrentActivity() { const QStringList list = m_activityController->activities(KActivities::Info::Running); if (list.isEmpty()) { return; } m_activityController->stopActivity(m_activityController->currentActivity()); } void ShellCorona::insertContainment(const QString &activity, int screenNum, Plasma::Containment *containment) { Plasma::Containment *cont = nullptr; auto candidates = containmentsForActivity(activity); for (Plasma::Containment *c : candidates) { //using lastScreen() instead of screen() catches also containments of activities that aren't the current one, so not assigned to a screen right now if (c->lastScreen() == screenNum) { cont = c; if (containment == cont) { return; } break; } } Q_ASSERT(containment != cont); //if there was a duplicate containment destroy the old one //the new one replaces it //FIXME: this whole function is probably redundant now if (cont) { cont->destroy(); } } void ShellCorona::setupWaylandIntegration() { if (!KWindowSystem::isPlatformWayland()) { return; } using namespace KWayland::Client; ConnectionThread *connection = ConnectionThread::fromApplication(this); if (!connection) { return; } Registry *registry = new Registry(this); registry->create(connection); connect(registry, &Registry::plasmaShellAnnounced, this, [this, registry] (quint32 name, quint32 version) { m_waylandPlasmaShell = registry->createPlasmaShell(name, version, this); } ); registry->setup(); connection->roundtrip(); } KWayland::Client::PlasmaShell *ShellCorona::waylandPlasmaShellInterface() const { return m_waylandPlasmaShell; } ScreenPool *ShellCorona::screenPool() const { return m_screenPool; } QList ShellCorona::screenIds() const { return m_desktopViewforId.keys(); } QString ShellCorona::defaultContainmentPlugin() const { QString plugin = m_lnfDefaultsConfig.readEntry("Containment", QString()); if (plugin.isEmpty()) { plugin = m_desktopDefaultsConfig.readEntry("Containment", "org.kde.desktopcontainment"); } return plugin; } void ShellCorona::updateStruts() { foreach(PanelView* view, m_panelViews) { view->updateStruts(); } } void ShellCorona::configurationChanged(const QString &path) { if (path == m_configPath) { m_config->reparseConfiguration(); } } void ShellCorona::activateLauncherMenu() { for (auto it = m_panelViews.constBegin(), end = m_panelViews.constEnd(); it != end; ++it) { const auto applets = it.key()->applets(); for (auto applet : applets) { const auto provides = KPluginMetaData::readStringList(applet->pluginMetaData().rawData(), QStringLiteral("X-Plasma-Provides")); if (provides.contains(QLatin1String("org.kde.plasma.launchermenu"))) { if (!applet->globalShortcut().isEmpty()) { emit applet->activated(); return; } } } } } void ShellCorona::activateTaskManagerEntry(int index) { auto activateTaskManagerEntryOnContainment = [](const Plasma::Containment *c, int index) { const auto &applets = c->applets(); for (auto *applet : applets) { const auto &provides = KPluginMetaData::readStringList(applet->pluginMetaData().rawData(), QStringLiteral("X-Plasma-Provides")); if (provides.contains(QLatin1String("org.kde.plasma.multitasking"))) { if (QQuickItem *appletInterface = applet->property("_plasma_graphicObject").value()) { const auto &childItems = appletInterface->childItems(); if (childItems.isEmpty()) { continue; } for (QQuickItem *item : childItems) { if (auto *metaObject = item->metaObject()) { // not using QMetaObject::invokeMethod to avoid warnings when calling // this on applets that don't have it or other child items since this // is pretty much trial and error. // Also, "var" arguments are treated as QVariant in QMetaObject int methodIndex = metaObject->indexOfMethod("activateTaskAtIndex(QVariant)"); if (methodIndex == -1) { continue; } QMetaMethod method = metaObject->method(methodIndex); if (method.invoke(item, Q_ARG(QVariant, index))) { return true; } } } } } } return false; }; // To avoid overly complex configuration, we'll try to get the 90% usecase to work // which is activating a task on the task manager on a panel on the primary screen. for (auto it = m_panelViews.constBegin(), end = m_panelViews.constEnd(); it != end; ++it) { if (it.value()->screen() != qGuiApp->primaryScreen()) { continue; } if (activateTaskManagerEntryOnContainment(it.key(), index)) { return; } } // we didn't find anything on primary, try all the panels for (auto it = m_panelViews.constBegin(), end = m_panelViews.constEnd(); it != end; ++it) { if (activateTaskManagerEntryOnContainment(it.key(), index)) { return; } } } // Desktop corona handler #include "moc_shellcorona.cpp"