File indexing completed on 2024-04-28 16:54:57

0001 /*
0002     SPDX-FileCopyrightText: 2013 Marco Martin <mart@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "desktopview.h"
0008 #include "containmentconfigview.h"
0009 #include "krunner_interface.h"
0010 #include "shellcorona.h"
0011 
0012 #include <QDBusConnection>
0013 #include <QDBusMessage>
0014 #include <QQmlContext>
0015 #include <QQmlEngine>
0016 #include <QQuickItem>
0017 #include <QScreen>
0018 #include <qopenglshaderprogram.h>
0019 
0020 #include <KAuthorized>
0021 #include <KStartupInfo>
0022 #include <kactivities/controller.h>
0023 #include <klocalizedstring.h>
0024 #include <kwindowsystem.h>
0025 
0026 #include <KPackage/Package>
0027 
0028 #include <KWayland/Client/plasmashell.h>
0029 #include <KWayland/Client/surface.h>
0030 
0031 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0032 #include <private/qtx11extras_p.h>
0033 #else
0034 #include <QX11Info>
0035 #endif
0036 
0037 DesktopView::DesktopView(Plasma::Corona *corona, QScreen *targetScreen)
0038     : PlasmaQuick::ContainmentView(corona, nullptr)
0039     , m_accentColor(Qt::transparent)
0040     , m_windowType(Desktop)
0041     , m_shellSurface(nullptr)
0042 {
0043     QObject::setParent(corona);
0044 
0045     // Setting clear color to black makes the panel lose alpha channel on X11. This looks like
0046     // a QtXCB bug, so set clear color only on Wayland to let the compositor optimize rendering.
0047     if (KWindowSystem::isPlatformWayland()) {
0048         setColor(Qt::black);
0049     }
0050 
0051     if (targetScreen) {
0052         setScreenToFollow(targetScreen);
0053     } else {
0054         setTitle(corona->kPackage().metadata().name());
0055     }
0056 
0057     setFlags(Qt::Window | Qt::FramelessWindowHint);
0058     rootContext()->setContextProperty(QStringLiteral("desktop"), this);
0059     setSource(corona->kPackage().fileUrl("views", QStringLiteral("Desktop.qml")));
0060     connect(this, &ContainmentView::containmentChanged, this, &DesktopView::slotContainmentChanged);
0061 
0062     QObject::connect(corona, &Plasma::Corona::kPackageChanged, this, &DesktopView::coronaPackageChanged);
0063 
0064     KActivities::Controller *m_activityController = new KActivities::Controller(this);
0065 
0066     QObject::connect(m_activityController, &KActivities::Controller::activityAdded, this, &DesktopView::candidateContainmentsChanged);
0067     QObject::connect(m_activityController, &KActivities::Controller::activityRemoved, this, &DesktopView::candidateContainmentsChanged);
0068 
0069     // KRunner settings
0070     KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("krunnerrc"));
0071     KConfigGroup configGroup(config, "General");
0072     m_activateKRunnerWhenTypingOnDesktop = configGroup.readEntry("ActivateWhenTypingOnDesktop", true);
0073 
0074     m_configWatcher = KConfigWatcher::create(config);
0075     connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) {
0076         if (names.contains(QByteArray("ActivateWhenTypingOnDesktop"))) {
0077             m_activateKRunnerWhenTypingOnDesktop = group.readEntry("ActivateWhenTypingOnDesktop", true);
0078         }
0079     });
0080 
0081     // Accent color setting
0082     connect(static_cast<ShellCorona *>(corona), &ShellCorona::accentColorFromWallpaperEnabledChanged, this, &DesktopView::usedInAccentColorChanged);
0083     connect(this, &DesktopView::usedInAccentColorChanged, this, [this] {
0084         if (!usedInAccentColor()) {
0085             resetAccentColor();
0086         }
0087     });
0088     connect(this, &ContainmentView::containmentChanged, this, &DesktopView::slotContainmentChanged);
0089 }
0090 
0091 DesktopView::~DesktopView()
0092 {
0093 }
0094 
0095 void DesktopView::showEvent(QShowEvent *e)
0096 {
0097     QQuickWindow::showEvent(e);
0098     adaptToScreen();
0099 }
0100 
0101 void DesktopView::setScreenToFollow(QScreen *screen)
0102 {
0103     Q_ASSERT(screen);
0104     if (screen == m_screenToFollow) {
0105         return;
0106     }
0107 
0108     if (m_screenToFollow) {
0109         disconnect(m_screenToFollow.data(), &QScreen::geometryChanged, this, &DesktopView::screenGeometryChanged);
0110     }
0111     m_screenToFollow = screen;
0112     setScreen(screen);
0113     connect(m_screenToFollow.data(), &QScreen::geometryChanged, this, &DesktopView::screenGeometryChanged);
0114 
0115     QString rectString;
0116     QDebug(&rectString) << screen->geometry();
0117     setTitle(QStringLiteral("%1 @ %2").arg(corona()->kPackage().metadata().name()).arg(rectString));
0118     adaptToScreen();
0119 }
0120 
0121 QScreen *DesktopView::screenToFollow() const
0122 {
0123     return m_screenToFollow;
0124 }
0125 
0126 void DesktopView::adaptToScreen()
0127 {
0128     ensureWindowType();
0129 
0130     // This happens sometimes, when shutting down the process
0131     if (!m_screenToFollow) {
0132         return;
0133     }
0134 
0135     if (m_windowType == Desktop || m_windowType == WindowedDesktop) {
0136         screenGeometryChanged();
0137     }
0138 }
0139 
0140 bool DesktopView::usedInAccentColor() const
0141 {
0142     if (!m_containment) {
0143         return false;
0144     }
0145 
0146     const bool notPrimaryDisplay = m_containment->screen() != 0;
0147     if (notPrimaryDisplay) {
0148         return false;
0149     }
0150 
0151     return static_cast<ShellCorona *>(corona())->accentColorFromWallpaperEnabled();
0152 }
0153 
0154 QColor DesktopView::accentColor() const
0155 {
0156     return m_accentColor.value_or(QColor(Qt::transparent));
0157 }
0158 
0159 void DesktopView::setAccentColor(const QColor &accentColor)
0160 {
0161     if (accentColor == m_accentColor) {
0162         return;
0163     }
0164 
0165     m_accentColor = accentColor;
0166     Q_EMIT accentColorChanged(accentColor);
0167     if (usedInAccentColor()) {
0168         Q_EMIT static_cast<ShellCorona *>(corona())->colorChanged(accentColor);
0169     }
0170 
0171     setAccentColorFromWallpaper(accentColor);
0172 }
0173 
0174 void DesktopView::resetAccentColor()
0175 {
0176     if (!m_accentColor.has_value()) {
0177         return;
0178     }
0179 
0180     m_accentColor.reset();
0181     Q_EMIT accentColorChanged(Qt::transparent);
0182 }
0183 
0184 DesktopView::WindowType DesktopView::windowType() const
0185 {
0186     return m_windowType;
0187 }
0188 
0189 void DesktopView::setWindowType(DesktopView::WindowType type)
0190 {
0191     if (m_windowType == type) {
0192         return;
0193     }
0194 
0195     m_windowType = type;
0196 
0197     adaptToScreen();
0198 
0199     Q_EMIT windowTypeChanged();
0200 }
0201 
0202 void DesktopView::ensureWindowType()
0203 {
0204     // This happens sometimes, when shutting down the process
0205     if (!screen()) {
0206         return;
0207     }
0208 
0209     if (m_windowType == Window) {
0210         setFlags(Qt::Window);
0211         KWindowSystem::setType(winId(), NET::Normal);
0212         KWindowSystem::clearState(winId(), NET::FullScreen);
0213         if (m_shellSurface) {
0214             m_shellSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Normal);
0215             m_shellSurface->setSkipTaskbar(false);
0216         }
0217 
0218     } else if (m_windowType == Desktop) {
0219         setFlags(Qt::Window | Qt::FramelessWindowHint);
0220         KWindowSystem::setType(winId(), NET::Desktop);
0221         KWindowSystem::setState(winId(), NET::KeepBelow);
0222         if (m_shellSurface) {
0223             m_shellSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Desktop);
0224             m_shellSurface->setSkipTaskbar(true);
0225         }
0226 
0227     } else if (m_windowType == WindowedDesktop) {
0228         KWindowSystem::setType(winId(), NET::Normal);
0229         KWindowSystem::clearState(winId(), NET::FullScreen);
0230         setFlags(Qt::FramelessWindowHint | flags());
0231         if (m_shellSurface) {
0232             m_shellSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Normal);
0233             m_shellSurface->setSkipTaskbar(false);
0234         }
0235 
0236     } else if (m_windowType == FullScreen) {
0237         setFlags(Qt::Window);
0238         KWindowSystem::setType(winId(), NET::Normal);
0239         KWindowSystem::setState(winId(), NET::FullScreen);
0240         if (m_shellSurface) {
0241             m_shellSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Normal);
0242             m_shellSurface->setSkipTaskbar(false);
0243         }
0244     }
0245 }
0246 
0247 DesktopView::SessionType DesktopView::sessionType() const
0248 {
0249     if (qobject_cast<ShellCorona *>(corona())) {
0250         return ShellSession;
0251     } else {
0252         return ApplicationSession;
0253     }
0254 }
0255 
0256 QVariantMap DesktopView::candidateContainmentsGraphicItems() const
0257 {
0258     QVariantMap map;
0259     if (!containment()) {
0260         return map;
0261     }
0262 
0263     for (auto cont : corona()->containmentsForScreen(containment()->screen())) {
0264         map[cont->activity()] = cont->property("_plasma_graphicObject");
0265     }
0266     return map;
0267 }
0268 
0269 Q_INVOKABLE QString DesktopView::fileFromPackage(const QString &key, const QString &fileName)
0270 {
0271     return corona()->kPackage().filePath(key.toUtf8(), fileName);
0272 }
0273 
0274 bool DesktopView::event(QEvent *e)
0275 {
0276     if (e->type() == QEvent::PlatformSurface) {
0277         switch (static_cast<QPlatformSurfaceEvent *>(e)->surfaceEventType()) {
0278         case QPlatformSurfaceEvent::SurfaceCreated:
0279             setupWaylandIntegration();
0280             ensureWindowType();
0281             break;
0282         case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed:
0283             delete m_shellSurface;
0284             m_shellSurface = nullptr;
0285             break;
0286         }
0287     } else if (e->type() == QEvent::FocusOut) {
0288         m_krunnerText.clear();
0289     }
0290 
0291     return PlasmaQuick::ContainmentView::event(e);
0292 }
0293 
0294 bool DesktopView::handleKRunnerTextInput(QKeyEvent *e)
0295 {
0296     // allow only Shift and GroupSwitch modifiers
0297     if (e->modifiers() & ~Qt::ShiftModifier & ~Qt::GroupSwitchModifier) {
0298         return false;
0299     }
0300     bool krunnerTextChanged = false;
0301     const QString eventText = e->text();
0302     for (const QChar ch : eventText) {
0303         if (!ch.isPrint()) {
0304             continue;
0305         }
0306         if (ch.isSpace() && m_krunnerText.isEmpty()) {
0307             continue;
0308         }
0309         m_krunnerText += ch;
0310         krunnerTextChanged = true;
0311     }
0312     if (krunnerTextChanged) {
0313         const QString interface(QStringLiteral("org.kde.krunner"));
0314         if (!KAuthorized::authorize(QStringLiteral("run_command"))) {
0315             return false;
0316         }
0317         org::kde::krunner::App krunner(interface, QStringLiteral("/App"), QDBusConnection::sessionBus());
0318         krunner.query(m_krunnerText);
0319         return true;
0320     }
0321     return false;
0322 }
0323 
0324 void DesktopView::keyPressEvent(QKeyEvent *e)
0325 {
0326     ContainmentView::keyPressEvent(e);
0327 
0328     if (e->isAccepted()) {
0329         return;
0330     }
0331 
0332     if (e->key() == Qt::Key_Escape && KWindowSystem::showingDesktop()) {
0333         KWindowSystem::setShowingDesktop(false);
0334         e->accept();
0335         return;
0336     }
0337 
0338     if (!m_activateKRunnerWhenTypingOnDesktop) {
0339         return;
0340     }
0341 
0342     // When a key is pressed on desktop when nothing else is active forward the key to krunner
0343     if (handleKRunnerTextInput(e)) {
0344         e->accept();
0345         return;
0346     }
0347 }
0348 
0349 void DesktopView::showConfigurationInterface(Plasma::Applet *applet)
0350 {
0351     if (m_configView) {
0352         if (m_configView->applet() != applet) {
0353             m_configView->hide();
0354             m_configView->deleteLater();
0355         } else {
0356             m_configView->show();
0357             auto window = qobject_cast<QWindow *>(m_configView);
0358             if (window && QX11Info::isPlatformX11()) {
0359                 KStartupInfo::setNewStartupId(window, QX11Info::nextStartupId());
0360             }
0361             m_configView->requestActivate();
0362             return;
0363         }
0364     }
0365 
0366     if (!applet || !applet->containment()) {
0367         return;
0368     }
0369 
0370     Plasma::Containment *cont = qobject_cast<Plasma::Containment *>(applet);
0371 
0372     if (cont && cont->isContainment() && cont->containmentType() == Plasma::Types::DesktopContainment) {
0373         m_configView = new ContainmentConfigView(cont);
0374         // if we changed containment with the config open, relaunch the config dialog but for the new containment
0375         // third arg is used to disconnect when the config closes
0376         connect(this, &ContainmentView::containmentChanged, m_configView.data(), [this]() {
0377             showConfigurationInterface(containment());
0378         });
0379     } else {
0380         m_configView = new PlasmaQuick::ConfigView(applet);
0381     }
0382     m_configView->init();
0383     m_configView->setTransientParent(this);
0384     m_configView->show();
0385     m_configView->requestActivate();
0386 
0387     auto window = qobject_cast<QWindow *>(m_configView);
0388     if (window && QX11Info::isPlatformX11()) {
0389         KStartupInfo::setNewStartupId(window, QX11Info::nextStartupId());
0390     }
0391     m_configView->requestActivate();
0392 }
0393 
0394 void DesktopView::slotContainmentChanged()
0395 {
0396     if (m_containment) {
0397         disconnect(m_containment, &Plasma::Containment::screenChanged, this, &DesktopView::slotScreenChanged);
0398     }
0399 
0400     m_containment = containment();
0401 
0402     if (m_containment) {
0403         connect(m_containment, &Plasma::Containment::screenChanged, this, &DesktopView::slotScreenChanged);
0404         slotScreenChanged(m_containment->screen());
0405     }
0406 }
0407 
0408 void DesktopView::slotScreenChanged(int newId)
0409 {
0410     if (m_containmentScreenId == newId) {
0411         return;
0412     }
0413 
0414     m_containmentScreenId = newId;
0415     Q_EMIT usedInAccentColorChanged();
0416 }
0417 
0418 void DesktopView::screenGeometryChanged()
0419 {
0420     const QRect geo = m_screenToFollow->geometry();
0421     //     qDebug() << "newGeometry" << this << geo << geometry();
0422     setGeometry(geo);
0423     if (m_shellSurface) {
0424         m_shellSurface->setPosition(geo.topLeft());
0425     }
0426     Q_EMIT geometryChanged();
0427 }
0428 
0429 void DesktopView::coronaPackageChanged(const KPackage::Package &package)
0430 {
0431     setContainment(nullptr);
0432     setSource(package.fileUrl("views", QStringLiteral("Desktop.qml")));
0433 }
0434 
0435 void DesktopView::setupWaylandIntegration()
0436 {
0437     if (m_shellSurface) {
0438         // already setup
0439         return;
0440     }
0441     if (ShellCorona *c = qobject_cast<ShellCorona *>(corona())) {
0442         using namespace KWayland::Client;
0443         PlasmaShell *interface = c->waylandPlasmaShellInterface();
0444         if (!interface) {
0445             return;
0446         }
0447         Surface *s = Surface::fromWindow(this);
0448         if (!s) {
0449             return;
0450         }
0451         m_shellSurface = interface->createSurface(s, this);
0452         m_shellSurface->setPosition(m_screenToFollow->geometry().topLeft());
0453     }
0454 }
0455 
0456 void DesktopView::setAccentColorFromWallpaper(const QColor &accentColor)
0457 {
0458     if (!usedInAccentColor()) {
0459         return;
0460     }
0461     QDBusMessage applyAccentColor = QDBusMessage::createMethodCall("org.kde.plasmashell.accentColor", "/AccentColor", "", "setAccentColor");
0462     applyAccentColor << accentColor.rgba();
0463     QDBusConnection::sessionBus().send(applyAccentColor);
0464 }