File indexing completed on 2024-05-12 05:37:10

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 <QGuiApplication>
0019 #include <QMenu>
0020 #include <QMetaMethod>
0021 #include <QMetaObject>
0022 #include <QQuickItem>
0023 #include <QQuickWindow>
0024 #include <QScreen>
0025 #include <QTimer>
0026 #include <qpa/qplatformscreen.h>
0027 
0028 #include <Plasma/Applet>
0029 #include <Plasma/PluginLoader>
0030 #include <Plasma5Support/ServiceJob>
0031 
0032 #include <KAcceleratorManager>
0033 #include <KActionCollection>
0034 #include <KSharedConfig>
0035 #include <KWindowSystem>
0036 
0037 using namespace Qt::StringLiterals;
0038 
0039 SystemTray::SystemTray(QObject *parent, const KPluginMetaData &data, const QVariantList &args)
0040     : Plasma::Containment(parent, data, args)
0041 {
0042     setHasConfigurationInterface(true);
0043     setContainmentDisplayHints(Plasma::Types::ContainmentDrawsPlasmoidHeading | Plasma::Types::ContainmentForcesSquarePlasmoids);
0044 }
0045 
0046 SystemTray::~SystemTray()
0047 {
0048     // When the applet is about to be deleted, delete now to avoid calling loadConfig()
0049     delete m_settings;
0050 }
0051 
0052 void SystemTray::init()
0053 {
0054     Containment::init();
0055 
0056     m_settings = new SystemTraySettings(configScheme(), this);
0057     connect(m_settings, &SystemTraySettings::enabledPluginsChanged, this, &SystemTray::onEnabledAppletsChanged);
0058 
0059     m_plasmoidRegistry = new PlasmoidRegistry(m_settings, this);
0060     connect(m_plasmoidRegistry, &PlasmoidRegistry::plasmoidEnabled, this, &SystemTray::startApplet);
0061     connect(m_plasmoidRegistry, &PlasmoidRegistry::plasmoidStopped, this, &SystemTray::stopApplet);
0062 
0063     // we don't want to automatically propagate the activated signal from the Applet to the Containment
0064     // even if SystemTray is of type Containment, it is de facto Applet and should act like one
0065     connect(this, &Containment::appletAdded, this, [this](Plasma::Applet *applet) {
0066         disconnect(applet, &Applet::activated, this, &Applet::activated);
0067     });
0068 
0069     if (KWindowSystem::isPlatformWayland()) {
0070         auto config = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::NoGlobals);
0071         KConfigGroup kscreenGroup = config->group(QStringLiteral("KScreen"));
0072         m_xwaylandClientsScale = kscreenGroup.readEntry("XwaylandClientsScale", 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 (group.name() == QStringLiteral("KScreen") && names.contains(QByteArrayLiteral("XwaylandClientsScale"))) {
0077                 m_xwaylandClientsScale = group.readEntry("XwaylandClientsScale", true);
0078             }
0079         });
0080     }
0081 }
0082 
0083 void SystemTray::restoreContents(KConfigGroup &group)
0084 {
0085     if (!isContainment()) {
0086         qCWarning(SYSTEM_TRAY) << "Loaded as an applet, this shouldn't have happened";
0087         return;
0088     }
0089 
0090     KConfigGroup shortcutConfig(&group, u"Shortcuts"_s);
0091     QString shortcutText = shortcutConfig.readEntryUntranslated("global", QString());
0092     if (!shortcutText.isEmpty()) {
0093         setGlobalShortcut(QKeySequence(shortcutText));
0094     }
0095 
0096     // cache known config group ids for applets
0097     KConfigGroup cg = group.group(u"Applets"_s);
0098     for (const QString &group : cg.groupList()) {
0099         KConfigGroup appletConfig(&cg, group);
0100         QString plugin = appletConfig.readEntry("plugin");
0101         if (!plugin.isEmpty()) {
0102             m_configGroupIds[plugin] = group.toInt();
0103         }
0104     }
0105 
0106     m_plasmoidRegistry->init();
0107 }
0108 
0109 void SystemTray::showPlasmoidMenu(QQuickItem *appletInterface, int x, int y)
0110 {
0111     if (!appletInterface) {
0112         return;
0113     }
0114 
0115     Plasma::Applet *applet = appletInterface->property("_plasma_applet").value<Plasma::Applet *>();
0116 
0117     QPointF pos = appletInterface->mapToScene(QPointF(x, y));
0118 
0119     if (appletInterface->window() && appletInterface->window()->screen()) {
0120         pos = appletInterface->window()->mapToGlobal(pos.toPoint());
0121     } else {
0122         pos = QPoint();
0123     }
0124 
0125     QMenu *desktopMenu = new QMenu;
0126     connect(this, &QObject::destroyed, desktopMenu, &QMenu::close);
0127     desktopMenu->setAttribute(Qt::WA_DeleteOnClose);
0128 
0129     // this is a workaround where Qt will fail to realize a mouse has been released
0130 
0131     // this happens if a window which does not accept focus spawns a new window that takes focus and X grab
0132     // whilst the mouse is depressed
0133     // https://bugreports.qt.io/browse/QTBUG-59044
0134     // this causes the next click to go missing
0135 
0136     // by releasing manually we avoid that situation
0137     auto ungrabMouseHack = [appletInterface]() {
0138         if (appletInterface->window() && appletInterface->window()->mouseGrabberItem()) {
0139             appletInterface->window()->mouseGrabberItem()->ungrabMouse();
0140         }
0141     };
0142 
0143     QTimer::singleShot(0, appletInterface, ungrabMouseHack);
0144     // end workaround
0145 
0146     Q_EMIT applet->contextualActionsAboutToShow();
0147     const auto contextActions = applet->contextualActions();
0148     for (QAction *action : contextActions) {
0149         if (action) {
0150             desktopMenu->addAction(action);
0151         }
0152     }
0153 
0154     if (applet->internalAction(QStringLiteral("configure"))) {
0155         desktopMenu->addAction(applet->internalAction(QStringLiteral("configure")));
0156     }
0157 
0158     if (desktopMenu->isEmpty()) {
0159         delete desktopMenu;
0160         return;
0161     }
0162 
0163     desktopMenu->adjustSize();
0164 
0165     if (QScreen *screen = appletInterface->window()->screen()) {
0166         const QRect geo = screen->availableGeometry();
0167 
0168         pos = QPoint(qBound(geo.left(), (int)pos.x(), geo.right() - desktopMenu->width()), //
0169                      qBound(geo.top(), (int)pos.y(), geo.bottom() - desktopMenu->height()));
0170     }
0171 
0172     KAcceleratorManager::manage(desktopMenu);
0173     desktopMenu->winId();
0174     desktopMenu->windowHandle()->setTransientParent(appletInterface->window());
0175     desktopMenu->popup(pos.toPoint());
0176 }
0177 
0178 void SystemTray::showStatusNotifierContextMenu(KJob *job, QQuickItem *statusNotifierIcon)
0179 {
0180     if (QCoreApplication::closingDown() || !statusNotifierIcon) {
0181         // apparently an edge case can be triggered due to the async nature of all this
0182         // see: https://bugs.kde.org/show_bug.cgi?id=251977
0183         return;
0184     }
0185 
0186     Plasma5Support::ServiceJob *sjob = qobject_cast<Plasma5Support::ServiceJob *>(job);
0187     if (!sjob) {
0188         return;
0189     }
0190 
0191     QMenu *menu = qobject_cast<QMenu *>(sjob->result().value<QObject *>());
0192 
0193     if (menu && !menu->isEmpty()) {
0194         menu->adjustSize();
0195         const auto parameters = sjob->parameters();
0196         int x = parameters[QStringLiteral("x")].toInt();
0197         int y = parameters[QStringLiteral("y")].toInt();
0198 
0199         // try tofind the icon screen coordinates, and adjust the position as a poor
0200         // man's popupPosition
0201 
0202         QRect screenItemRect(statusNotifierIcon->mapToScene(QPointF(0, 0)).toPoint(), QSize(statusNotifierIcon->width(), statusNotifierIcon->height()));
0203 
0204         if (statusNotifierIcon->window()) {
0205             screenItemRect.moveTopLeft(statusNotifierIcon->window()->mapToGlobal(screenItemRect.topLeft()));
0206         }
0207 
0208         switch (location()) {
0209         case Plasma::Types::LeftEdge:
0210             x = screenItemRect.right();
0211             y = screenItemRect.top();
0212             break;
0213         case Plasma::Types::RightEdge:
0214             x = screenItemRect.left() - menu->width();
0215             y = screenItemRect.top();
0216             break;
0217         case Plasma::Types::TopEdge:
0218             x = screenItemRect.left();
0219             y = screenItemRect.bottom();
0220             break;
0221         case Plasma::Types::BottomEdge:
0222             x = screenItemRect.left();
0223             y = screenItemRect.top() - menu->height();
0224             break;
0225         default:
0226             x = screenItemRect.left();
0227             if (screenItemRect.top() - menu->height() >= statusNotifierIcon->window()->screen()->geometry().top()) {
0228                 y = screenItemRect.top() - menu->height();
0229             } else {
0230                 y = screenItemRect.bottom();
0231             }
0232         }
0233 
0234         KAcceleratorManager::manage(menu);
0235         menu->winId();
0236         menu->windowHandle()->setTransientParent(statusNotifierIcon->window());
0237         menu->popup(QPoint(x, y));
0238         // Workaround for QTBUG-59044
0239         if (auto item = statusNotifierIcon->window()->mouseGrabberItem()) {
0240             item->ungrabMouse();
0241         }
0242     }
0243 }
0244 
0245 QPointF SystemTray::popupPosition(QQuickItem *visualParent, int x, int y)
0246 {
0247     if (!visualParent) {
0248         return QPointF(0, 0);
0249     }
0250 
0251     QPointF pos = visualParent->mapToScene(QPointF(x, y));
0252 
0253     QQuickWindow *const window = visualParent->window();
0254     if (window && window->screen()) {
0255         pos = window->mapToGlobal(pos.toPoint());
0256 #if HAVE_X11
0257         if (KWindowSystem::isPlatformX11()) {
0258             const auto devicePixelRatio = window->screen()->devicePixelRatio();
0259             if (QGuiApplication::screens().size() == 1) {
0260                 return pos * devicePixelRatio;
0261             }
0262 
0263             const QRect geometry = window->screen()->geometry();
0264             const QRect nativeGeometry = window->screen()->handle()->geometry();
0265             const QPointF nativeGlobalPosOnCurrentScreen = (pos - geometry.topLeft()) * devicePixelRatio;
0266 
0267             return nativeGeometry.topLeft() + nativeGlobalPosOnCurrentScreen;
0268         }
0269 #endif
0270 
0271         if (KWindowSystem::isPlatformWayland()) {
0272             if (!m_xwaylandClientsScale) {
0273                 return pos;
0274             }
0275 
0276             const qreal devicePixelRatio = window->devicePixelRatio();
0277 
0278             if (QGuiApplication::screens().size() == 1) {
0279                 return pos * devicePixelRatio;
0280             }
0281 
0282             const QRect geometry = window->screen()->geometry();
0283             const QRect nativeGeometry = window->screen()->handle()->geometry();
0284             const QPointF nativeGlobalPosOnCurrentScreen = (pos - geometry.topLeft()) * devicePixelRatio;
0285 
0286             return nativeGeometry.topLeft() + nativeGlobalPosOnCurrentScreen;
0287         }
0288     }
0289 
0290     return QPoint();
0291 }
0292 
0293 bool SystemTray::isSystemTrayApplet(const QString &appletId)
0294 {
0295     if (m_plasmoidRegistry) {
0296         return m_plasmoidRegistry->isSystemTrayApplet(appletId);
0297     }
0298     return false;
0299 }
0300 
0301 SystemTrayModel *SystemTray::systemTrayModel()
0302 {
0303     if (!m_systemTrayModel) {
0304         m_systemTrayModel = new SystemTrayModel(this);
0305 
0306         m_plasmoidModel = new PlasmoidModel(m_settings, m_plasmoidRegistry, m_systemTrayModel);
0307         connect(this, &SystemTray::appletAdded, m_plasmoidModel, &PlasmoidModel::addApplet);
0308         connect(this, &SystemTray::appletRemoved, m_plasmoidModel, &PlasmoidModel::removeApplet);
0309         for (auto applet : applets()) {
0310             m_plasmoidModel->addApplet(applet);
0311         }
0312 
0313         m_statusNotifierModel = new StatusNotifierModel(m_settings, m_systemTrayModel);
0314 
0315         m_systemTrayModel->addSourceModel(m_plasmoidModel);
0316         m_systemTrayModel->addSourceModel(m_statusNotifierModel);
0317     }
0318 
0319     return m_systemTrayModel;
0320 }
0321 
0322 QAbstractItemModel *SystemTray::sortedSystemTrayModel()
0323 {
0324     if (!m_sortedSystemTrayModel) {
0325         m_sortedSystemTrayModel = new SortedSystemTrayModel(SortedSystemTrayModel::SortingType::SystemTray, this);
0326         m_sortedSystemTrayModel->setSourceModel(systemTrayModel());
0327     }
0328     return m_sortedSystemTrayModel;
0329 }
0330 
0331 QAbstractItemModel *SystemTray::configSystemTrayModel()
0332 {
0333     if (!m_configSystemTrayModel) {
0334         m_configSystemTrayModel = new SortedSystemTrayModel(SortedSystemTrayModel::SortingType::ConfigurationPage, this);
0335         m_configSystemTrayModel->setSourceModel(systemTrayModel());
0336     }
0337     return m_configSystemTrayModel;
0338 }
0339 
0340 void SystemTray::onEnabledAppletsChanged()
0341 {
0342     // remove all that are not allowed anymore
0343     const auto appletsList = applets();
0344     for (Plasma::Applet *applet : appletsList) {
0345         // Here it should always be valid.
0346         // for some reason it not always is.
0347         if (!applet->pluginMetaData().isValid()) {
0348             applet->config().parent().deleteGroup();
0349             applet->deleteLater();
0350         } else {
0351             const QString task = applet->pluginMetaData().pluginId();
0352             if (!m_settings->isEnabledPlugin(task)) {
0353                 // in those cases we do delete the applet config completely
0354                 // as they were explicitly disabled by the user
0355                 applet->config().parent().deleteGroup();
0356                 applet->deleteLater();
0357                 m_configGroupIds.remove(task);
0358             }
0359         }
0360     }
0361 }
0362 
0363 void SystemTray::startApplet(const QString &pluginId)
0364 {
0365     const auto appletsList = applets();
0366     for (Plasma::Applet *applet : appletsList) {
0367         if (!applet->pluginMetaData().isValid()) {
0368             continue;
0369         }
0370 
0371         // only allow one instance per applet
0372         if (pluginId == applet->pluginMetaData().pluginId()) {
0373             // Applet::destroy doesn't delete the applet from Containment::applets in the same event
0374             // potentially a dbus activated service being restarted can be added in this time.
0375             if (!applet->destroyed()) {
0376                 return;
0377             }
0378         }
0379     }
0380 
0381     qCDebug(SYSTEM_TRAY) << "Adding applet:" << pluginId;
0382 
0383     // known one, recycle the id to reuse old config
0384     if (m_configGroupIds.contains(pluginId)) {
0385         Applet *applet = Plasma::PluginLoader::self()->loadApplet(pluginId, m_configGroupIds.value(pluginId), QVariantList());
0386         // this should never happen unless explicitly wrong config is hand-written or
0387         //(more likely) a previously added applet is uninstalled
0388         if (!applet) {
0389             qCWarning(SYSTEM_TRAY) << "Unable to find applet" << pluginId;
0390             return;
0391         }
0392         applet->setProperty("org.kde.plasma:force-create", true);
0393         addApplet(applet);
0394         // create a new one automatic id, new config group
0395     } else {
0396         Applet *applet = createApplet(pluginId, QVariantList() << "org.kde.plasma:force-create");
0397         if (applet) {
0398             m_configGroupIds[pluginId] = applet->id();
0399         }
0400     }
0401 }
0402 
0403 void SystemTray::stopApplet(const QString &pluginId)
0404 {
0405     const auto appletsList = applets();
0406     for (Plasma::Applet *applet : appletsList) {
0407         if (applet->pluginMetaData().isValid() && pluginId == applet->pluginMetaData().pluginId()) {
0408             // we are *not* cleaning the config here, because since is one
0409             // of those automatically loaded/unloaded by dbus, we want to recycle
0410             // the config the next time it's loaded, in case the user configured something here
0411             applet->deleteLater();
0412             // HACK: we need to remove the applet from Containment::applets() as soon as possible
0413             // otherwise we may have disappearing applets for restarting dbus services
0414             // this may be removed when we depend from a frameworks version in which appletDeleted is emitted as soon as deleteLater() is called
0415             Q_EMIT appletDeleted(applet);
0416         }
0417     }
0418 }
0419 
0420 void SystemTray::stackItemBefore(QQuickItem *newItem, QQuickItem *beforeItem)
0421 {
0422     if (!newItem || !beforeItem) {
0423         return;
0424     }
0425     newItem->stackBefore(beforeItem);
0426 }
0427 
0428 void SystemTray::stackItemAfter(QQuickItem *newItem, QQuickItem *afterItem)
0429 {
0430     if (!newItem || !afterItem) {
0431         return;
0432     }
0433     newItem->stackAfter(afterItem);
0434 }
0435 
0436 K_PLUGIN_CLASS(SystemTray)
0437 
0438 #include "systemtray.moc"