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"