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