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"