File indexing completed on 2024-05-12 17:08:51

0001 /*
0002     SPDX-FileCopyrightText: 2015 Marco Martin <mart@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include <optional>
0008 
0009 #include "config-X11.h"
0010 #include "debug.h"
0011 #include "systemtray.h"
0012 
0013 #include "plasmoidregistry.h"
0014 #include "sortedsystemtraymodel.h"
0015 #include "systemtraymodel.h"
0016 #include "systemtraysettings.h"
0017 
0018 #include <QMenu>
0019 #include <QMetaMethod>
0020 #include <QMetaObject>
0021 #include <QQuickItem>
0022 #include <QQuickWindow>
0023 #include <QScreen>
0024 #include <QTimer>
0025 #include <qpa/qplatformscreen.h>
0026 
0027 #include <Plasma/Applet>
0028 #include <Plasma/PluginLoader>
0029 #include <Plasma/ServiceJob>
0030 
0031 #include <KAcceleratorManager>
0032 #include <KActionCollection>
0033 #include <KSharedConfig>
0034 #include <KWindowSystem>
0035 
0036 #if HAVE_WaylandProtocols
0037 #include "qwayland-fractional-scale-v1.h"
0038 #include <QtWaylandClient/qwaylandclientextension.h>
0039 #include <qpa/qplatformnativeinterface.h>
0040 #endif
0041 
0042 #if HAVE_WaylandProtocols // TODO Qt6: check window()->devicePixelRatio() is usable
0043 class FractionalScaleManagerV1 : public QWaylandClientExtensionTemplate<FractionalScaleManagerV1>, public QtWayland::wp_fractional_scale_manager_v1
0044 {
0045 public:
0046     FractionalScaleManagerV1()
0047         : QWaylandClientExtensionTemplate<FractionalScaleManagerV1>(1)
0048         , QtWayland::wp_fractional_scale_manager_v1()
0049     {
0050     }
0051 
0052     ~FractionalScaleManagerV1() override
0053     {
0054         QtWayland::wp_fractional_scale_manager_v1::destroy();
0055     }
0056 };
0057 
0058 class FractionalScaleV1 : public QtWayland::wp_fractional_scale_v1
0059 {
0060 public:
0061     FractionalScaleV1(struct ::wp_fractional_scale_v1 *object)
0062         : QtWayland::wp_fractional_scale_v1(object)
0063     {
0064     }
0065 
0066     ~FractionalScaleV1() override
0067     {
0068         QtWayland::wp_fractional_scale_v1::destroy();
0069     }
0070 
0071     double devicePixelRatio()
0072     {
0073         return m_preferredScale.value_or(120) / 120.0;
0074     }
0075 
0076     void ensureReady()
0077     {
0078         if (m_preferredScale.has_value()) {
0079             return;
0080         }
0081 
0082         QPlatformNativeInterface *const native = qGuiApp->platformNativeInterface();
0083         const auto display = static_cast<struct wl_display *>(native->nativeResourceForIntegration("wl_display"));
0084         wl_display_roundtrip(display);
0085     }
0086 
0087 protected:
0088     void wp_fractional_scale_v1_preferred_scale(uint32_t scale) override
0089     {
0090         m_preferredScale = scale;
0091     }
0092 
0093 private:
0094     std::optional<unsigned> m_preferredScale;
0095 };
0096 #endif
0097 
0098 SystemTray::SystemTray(QObject *parent, const KPluginMetaData &data, const QVariantList &args)
0099     : Plasma::Containment(parent, data, args)
0100 {
0101     setHasConfigurationInterface(true);
0102     setContainmentType(Plasma::Types::CustomEmbeddedContainment);
0103     setContainmentDisplayHints(Plasma::Types::ContainmentDrawsPlasmoidHeading | Plasma::Types::ContainmentForcesSquarePlasmoids);
0104 }
0105 
0106 SystemTray::~SystemTray()
0107 {
0108     // When the applet is about to be deleted, delete now to avoid calling loadConfig()
0109     delete m_settings;
0110 }
0111 
0112 void SystemTray::init()
0113 {
0114     Containment::init();
0115 
0116     m_settings = new SystemTraySettings(configScheme(), this);
0117     connect(m_settings, &SystemTraySettings::enabledPluginsChanged, this, &SystemTray::onEnabledAppletsChanged);
0118 
0119     m_plasmoidRegistry = new PlasmoidRegistry(m_settings, this);
0120     connect(m_plasmoidRegistry, &PlasmoidRegistry::plasmoidEnabled, this, &SystemTray::startApplet);
0121     connect(m_plasmoidRegistry, &PlasmoidRegistry::plasmoidStopped, this, &SystemTray::stopApplet);
0122 
0123     // we don't want to automatically propagate the activated signal from the Applet to the Containment
0124     // even if SystemTray is of type Containment, it is de facto Applet and should act like one
0125     connect(this, &Containment::appletAdded, this, [this](Plasma::Applet *applet) {
0126         disconnect(applet, &Applet::activated, this, &Applet::activated);
0127     });
0128 
0129 #if HAVE_WaylandProtocols
0130     if (KWindowSystem::isPlatformWayland()) {
0131         m_fractionalScaleManagerV1.reset(new FractionalScaleManagerV1);
0132 
0133         auto config = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::NoGlobals);
0134         KConfigGroup kscreenGroup = config->group("KScreen");
0135         m_xwaylandClientsScale = kscreenGroup.readEntry("XwaylandClientsScale", true);
0136 
0137         m_configWatcher = KConfigWatcher::create(config);
0138         connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) {
0139             if (group.name() == QStringLiteral("KScreen") && names.contains(QByteArrayLiteral("XwaylandClientsScale"))) {
0140                 m_xwaylandClientsScale = group.readEntry("XwaylandClientsScale", true);
0141             }
0142         });
0143     }
0144 #endif
0145 }
0146 
0147 void SystemTray::restoreContents(KConfigGroup &group)
0148 {
0149     if (!isContainment()) {
0150         qCWarning(SYSTEM_TRAY) << "Loaded as an applet, this shouldn't have happened";
0151         return;
0152     }
0153 
0154     KConfigGroup shortcutConfig(&group, "Shortcuts");
0155     QString shortcutText = shortcutConfig.readEntryUntranslated("global", QString());
0156     if (!shortcutText.isEmpty()) {
0157         setGlobalShortcut(QKeySequence(shortcutText));
0158     }
0159 
0160     // cache known config group ids for applets
0161     KConfigGroup cg = group.group("Applets");
0162     for (const QString &group : cg.groupList()) {
0163         KConfigGroup appletConfig(&cg, group);
0164         QString plugin = appletConfig.readEntry("plugin");
0165         if (!plugin.isEmpty()) {
0166             m_configGroupIds[plugin] = group.toInt();
0167         }
0168     }
0169 
0170     m_plasmoidRegistry->init();
0171 }
0172 
0173 void SystemTray::showPlasmoidMenu(QQuickItem *appletInterface, int x, int y)
0174 {
0175     if (!appletInterface) {
0176         return;
0177     }
0178 
0179     Plasma::Applet *applet = appletInterface->property("_plasma_applet").value<Plasma::Applet *>();
0180 
0181     QPointF pos = appletInterface->mapToScene(QPointF(x, y));
0182 
0183     if (appletInterface->window() && appletInterface->window()->screen()) {
0184         pos = appletInterface->window()->mapToGlobal(pos.toPoint());
0185     } else {
0186         pos = QPoint();
0187     }
0188 
0189     QMenu *desktopMenu = new QMenu;
0190     connect(this, &QObject::destroyed, desktopMenu, &QMenu::close);
0191     desktopMenu->setAttribute(Qt::WA_DeleteOnClose);
0192 
0193     // this is a workaround where Qt will fail to realize a mouse has been released
0194 
0195     // this happens if a window which does not accept focus spawns a new window that takes focus and X grab
0196     // whilst the mouse is depressed
0197     // https://bugreports.qt.io/browse/QTBUG-59044
0198     // this causes the next click to go missing
0199 
0200     // by releasing manually we avoid that situation
0201     auto ungrabMouseHack = [appletInterface]() {
0202         if (appletInterface->window() && appletInterface->window()->mouseGrabberItem()) {
0203             appletInterface->window()->mouseGrabberItem()->ungrabMouse();
0204         }
0205     };
0206 
0207     QTimer::singleShot(0, appletInterface, ungrabMouseHack);
0208     // end workaround
0209 
0210     Q_EMIT applet->contextualActionsAboutToShow();
0211     const auto contextActions = applet->contextualActions();
0212     for (QAction *action : contextActions) {
0213         if (action) {
0214             desktopMenu->addAction(action);
0215         }
0216     }
0217 
0218     QAction *runAssociatedApplication = applet->actions()->action(QStringLiteral("run associated application"));
0219     if (runAssociatedApplication && runAssociatedApplication->isEnabled()) {
0220         desktopMenu->addAction(runAssociatedApplication);
0221     }
0222 
0223     if (applet->actions()->action(QStringLiteral("configure"))) {
0224         desktopMenu->addAction(applet->actions()->action(QStringLiteral("configure")));
0225     }
0226 
0227     if (desktopMenu->isEmpty()) {
0228         delete desktopMenu;
0229         return;
0230     }
0231 
0232     desktopMenu->adjustSize();
0233 
0234     if (QScreen *screen = appletInterface->window()->screen()) {
0235         const QRect geo = screen->availableGeometry();
0236 
0237         pos = QPoint(qBound(geo.left(), (int)pos.x(), geo.right() - desktopMenu->width()), //
0238                      qBound(geo.top(), (int)pos.y(), geo.bottom() - desktopMenu->height()));
0239     }
0240 
0241     KAcceleratorManager::manage(desktopMenu);
0242     desktopMenu->winId();
0243     desktopMenu->windowHandle()->setTransientParent(appletInterface->window());
0244     desktopMenu->popup(pos.toPoint());
0245 }
0246 
0247 void SystemTray::showStatusNotifierContextMenu(KJob *job, QQuickItem *statusNotifierIcon)
0248 {
0249     if (QCoreApplication::closingDown() || !statusNotifierIcon) {
0250         // apparently an edge case can be triggered due to the async nature of all this
0251         // see: https://bugs.kde.org/show_bug.cgi?id=251977
0252         return;
0253     }
0254 
0255     Plasma::ServiceJob *sjob = qobject_cast<Plasma::ServiceJob *>(job);
0256     if (!sjob) {
0257         return;
0258     }
0259 
0260     QMenu *menu = qobject_cast<QMenu *>(sjob->result().value<QObject *>());
0261 
0262     if (menu && !menu->isEmpty()) {
0263         menu->adjustSize();
0264         const auto parameters = sjob->parameters();
0265         int x = parameters[QStringLiteral("x")].toInt();
0266         int y = parameters[QStringLiteral("y")].toInt();
0267 
0268         // try tofind the icon screen coordinates, and adjust the position as a poor
0269         // man's popupPosition
0270 
0271         QRect screenItemRect(statusNotifierIcon->mapToScene(QPointF(0, 0)).toPoint(), QSize(statusNotifierIcon->width(), statusNotifierIcon->height()));
0272 
0273         if (statusNotifierIcon->window()) {
0274             screenItemRect.moveTopLeft(statusNotifierIcon->window()->mapToGlobal(screenItemRect.topLeft()));
0275         }
0276 
0277         switch (location()) {
0278         case Plasma::Types::LeftEdge:
0279             x = screenItemRect.right();
0280             y = screenItemRect.top();
0281             break;
0282         case Plasma::Types::RightEdge:
0283             x = screenItemRect.left() - menu->width();
0284             y = screenItemRect.top();
0285             break;
0286         case Plasma::Types::TopEdge:
0287             x = screenItemRect.left();
0288             y = screenItemRect.bottom();
0289             break;
0290         case Plasma::Types::BottomEdge:
0291             x = screenItemRect.left();
0292             y = screenItemRect.top() - menu->height();
0293             break;
0294         default:
0295             x = screenItemRect.left();
0296             if (screenItemRect.top() - menu->height() >= statusNotifierIcon->window()->screen()->geometry().top()) {
0297                 y = screenItemRect.top() - menu->height();
0298             } else {
0299                 y = screenItemRect.bottom();
0300             }
0301         }
0302 
0303         KAcceleratorManager::manage(menu);
0304         menu->winId();
0305         menu->windowHandle()->setTransientParent(statusNotifierIcon->window());
0306         menu->popup(QPoint(x, y));
0307         // Workaround for QTBUG-59044
0308         if (auto item = statusNotifierIcon->window()->mouseGrabberItem()) {
0309             item->ungrabMouse();
0310         }
0311     }
0312 }
0313 
0314 QPointF SystemTray::popupPosition(QQuickItem *visualParent, int x, int y)
0315 {
0316     if (!visualParent) {
0317         return QPointF(0, 0);
0318     }
0319 
0320     QPointF pos = visualParent->mapToScene(QPointF(x, y));
0321 
0322     QQuickWindow *const window = visualParent->window();
0323     if (window && window->screen()) {
0324         pos = window->mapToGlobal(pos.toPoint());
0325 #if HAVE_X11
0326         if (KWindowSystem::isPlatformX11()) {
0327             const auto devicePixelRatio = window->screen()->devicePixelRatio();
0328             if (QGuiApplication::screens().size() == 1) {
0329                 return pos * devicePixelRatio;
0330             }
0331 
0332             const QRect geometry = window->screen()->geometry();
0333             const QRect nativeGeometry = window->screen()->handle()->geometry();
0334             const QPointF nativeGlobalPosOnCurrentScreen = (pos - geometry.topLeft()) * devicePixelRatio;
0335 
0336             return nativeGeometry.topLeft() + nativeGlobalPosOnCurrentScreen;
0337         }
0338 #endif
0339 
0340         if (KWindowSystem::isPlatformWayland()) {
0341             if (!m_xwaylandClientsScale) {
0342                 return pos;
0343             }
0344 
0345             qreal devicePixelRatio = 1.0;
0346 #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
0347             devicePixelRatio = window->devicePixelRatio();
0348 #elif HAVE_WaylandProtocols
0349             if (m_fractionalScaleManagerV1->isActive()) {
0350                 QPlatformNativeInterface *const native = qGuiApp->platformNativeInterface();
0351                 Q_ASSERT(native);
0352                 const auto surface = reinterpret_cast<struct wl_surface *>(native->nativeResourceForWindow(QByteArrayLiteral("surface"), window));
0353                 if (surface) {
0354                     const auto scale = std::make_unique<FractionalScaleV1>(m_fractionalScaleManagerV1->get_fractional_scale(surface));
0355                     if (scale->isInitialized()) {
0356                         scale->ensureReady();
0357                         devicePixelRatio = scale->devicePixelRatio();
0358                     }
0359                 }
0360             }
0361 #endif
0362 
0363             if (QGuiApplication::screens().size() == 1) {
0364                 return pos * devicePixelRatio;
0365             }
0366 
0367             const QRect geometry = window->screen()->geometry();
0368             const QRect nativeGeometry = window->screen()->handle()->geometry();
0369             const QPointF nativeGlobalPosOnCurrentScreen = (pos - geometry.topLeft()) * devicePixelRatio;
0370 
0371             return nativeGeometry.topLeft() + nativeGlobalPosOnCurrentScreen;
0372         }
0373     }
0374 
0375     return QPoint();
0376 }
0377 
0378 bool SystemTray::isSystemTrayApplet(const QString &appletId)
0379 {
0380     if (m_plasmoidRegistry) {
0381         return m_plasmoidRegistry->isSystemTrayApplet(appletId);
0382     }
0383     return false;
0384 }
0385 
0386 void SystemTray::emitPressed(QQuickItem *mouseArea, QObject *mouseEvent)
0387 {
0388     if (!mouseArea || !mouseEvent) {
0389         return;
0390     }
0391 
0392     // QQuickMouseEvent is also private, so we cannot use QMetaObject::invokeMethod with Q_ARG
0393     const QMetaObject *mo = mouseArea->metaObject();
0394 
0395     const int pressedIdx = mo->indexOfSignal("pressed(QQuickMouseEvent*)");
0396     if (pressedIdx < 0) {
0397         qCWarning(SYSTEM_TRAY) << "Failed to find onPressed signal on" << mouseArea;
0398         return;
0399     }
0400 
0401     QMetaMethod pressedMethod = mo->method(pressedIdx);
0402 
0403     if (!pressedMethod.invoke(mouseArea, Q_ARG(QObject *, mouseEvent))) {
0404         qCWarning(SYSTEM_TRAY) << "Failed to invoke onPressed signal on" << mouseArea << "with" << mouseEvent;
0405         return;
0406     }
0407 }
0408 
0409 SystemTrayModel *SystemTray::systemTrayModel()
0410 {
0411     if (!m_systemTrayModel) {
0412         m_systemTrayModel = new SystemTrayModel(this);
0413 
0414         m_plasmoidModel = new PlasmoidModel(m_settings, m_plasmoidRegistry, m_systemTrayModel);
0415         connect(this, &SystemTray::appletAdded, m_plasmoidModel, &PlasmoidModel::addApplet);
0416         connect(this, &SystemTray::appletRemoved, m_plasmoidModel, &PlasmoidModel::removeApplet);
0417         for (auto applet : applets()) {
0418             m_plasmoidModel->addApplet(applet);
0419         }
0420 
0421         m_statusNotifierModel = new StatusNotifierModel(m_settings, m_systemTrayModel);
0422 
0423         m_systemTrayModel->addSourceModel(m_plasmoidModel);
0424         m_systemTrayModel->addSourceModel(m_statusNotifierModel);
0425     }
0426 
0427     return m_systemTrayModel;
0428 }
0429 
0430 QAbstractItemModel *SystemTray::sortedSystemTrayModel()
0431 {
0432     if (!m_sortedSystemTrayModel) {
0433         m_sortedSystemTrayModel = new SortedSystemTrayModel(SortedSystemTrayModel::SortingType::SystemTray, this);
0434         m_sortedSystemTrayModel->setSourceModel(systemTrayModel());
0435     }
0436     return m_sortedSystemTrayModel;
0437 }
0438 
0439 QAbstractItemModel *SystemTray::configSystemTrayModel()
0440 {
0441     if (!m_configSystemTrayModel) {
0442         m_configSystemTrayModel = new SortedSystemTrayModel(SortedSystemTrayModel::SortingType::ConfigurationPage, this);
0443         m_configSystemTrayModel->setSourceModel(systemTrayModel());
0444     }
0445     return m_configSystemTrayModel;
0446 }
0447 
0448 void SystemTray::onEnabledAppletsChanged()
0449 {
0450     // remove all that are not allowed anymore
0451     const auto appletsList = applets();
0452     for (Plasma::Applet *applet : appletsList) {
0453         // Here it should always be valid.
0454         // for some reason it not always is.
0455         if (!applet->pluginMetaData().isValid()) {
0456             applet->config().parent().deleteGroup();
0457             applet->deleteLater();
0458         } else {
0459             const QString task = applet->pluginMetaData().pluginId();
0460             if (!m_settings->isEnabledPlugin(task)) {
0461                 // in those cases we do delete the applet config completely
0462                 // as they were explicitly disabled by the user
0463                 applet->config().parent().deleteGroup();
0464                 applet->deleteLater();
0465                 m_configGroupIds.remove(task);
0466             }
0467         }
0468     }
0469 }
0470 
0471 void SystemTray::startApplet(const QString &pluginId)
0472 {
0473     const auto appletsList = applets();
0474     for (Plasma::Applet *applet : appletsList) {
0475         if (!applet->pluginMetaData().isValid()) {
0476             continue;
0477         }
0478 
0479         // only allow one instance per applet
0480         if (pluginId == applet->pluginMetaData().pluginId()) {
0481             // Applet::destroy doesn't delete the applet from Containment::applets in the same event
0482             // potentially a dbus activated service being restarted can be added in this time.
0483             if (!applet->destroyed()) {
0484                 return;
0485             }
0486         }
0487     }
0488 
0489     qCDebug(SYSTEM_TRAY) << "Adding applet:" << pluginId;
0490 
0491     // known one, recycle the id to reuse old config
0492     if (m_configGroupIds.contains(pluginId)) {
0493         Applet *applet = Plasma::PluginLoader::self()->loadApplet(pluginId, m_configGroupIds.value(pluginId), QVariantList());
0494         // this should never happen unless explicitly wrong config is hand-written or
0495         //(more likely) a previously added applet is uninstalled
0496         if (!applet) {
0497             qCWarning(SYSTEM_TRAY) << "Unable to find applet" << pluginId;
0498             return;
0499         }
0500         applet->setProperty("org.kde.plasma:force-create", true);
0501         addApplet(applet);
0502         // create a new one automatic id, new config group
0503     } else {
0504         Applet *applet = createApplet(pluginId, QVariantList() << "org.kde.plasma:force-create");
0505         if (applet) {
0506             m_configGroupIds[pluginId] = applet->id();
0507         }
0508     }
0509 }
0510 
0511 void SystemTray::stopApplet(const QString &pluginId)
0512 {
0513     const auto appletsList = applets();
0514     for (Plasma::Applet *applet : appletsList) {
0515         if (applet->pluginMetaData().isValid() && pluginId == applet->pluginMetaData().pluginId()) {
0516             // we are *not* cleaning the config here, because since is one
0517             // of those automatically loaded/unloaded by dbus, we want to recycle
0518             // the config the next time it's loaded, in case the user configured something here
0519             applet->deleteLater();
0520             // HACK: we need to remove the applet from Containment::applets() as soon as possible
0521             // otherwise we may have disappearing applets for restarting dbus services
0522             // this may be removed when we depend from a frameworks version in which appletDeleted is emitted as soon as deleteLater() is called
0523             Q_EMIT appletDeleted(applet);
0524         }
0525     }
0526 }
0527 
0528 void SystemTray::stackItemBefore(QQuickItem *newItem, QQuickItem *beforeItem)
0529 {
0530     if (!newItem || !beforeItem) {
0531         return;
0532     }
0533     newItem->stackBefore(beforeItem);
0534 }
0535 
0536 void SystemTray::stackItemAfter(QQuickItem *newItem, QQuickItem *afterItem)
0537 {
0538     if (!newItem || !afterItem) {
0539         return;
0540     }
0541     newItem->stackAfter(afterItem);
0542 }
0543 
0544 K_PLUGIN_CLASS(SystemTray)
0545 
0546 #include "systemtray.moc"