1 /*
2     SPDX-FileCopyrightText: 2015 Marco Martin <mart@kde.org>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "systemtray.h"
8 #include "debug.h"
9 
10 #include "plasmoidregistry.h"
11 #include "sortedsystemtraymodel.h"
12 #include "systemtraymodel.h"
13 #include "systemtraysettings.h"
14 
15 #include <QMenu>
16 #include <QQuickItem>
17 #include <QQuickWindow>
18 #include <QScreen>
19 #include <QTimer>
20 
21 #include <Plasma/Applet>
22 #include <Plasma/PluginLoader>
23 #include <Plasma/ServiceJob>
24 
25 #include <KAcceleratorManager>
26 #include <KActionCollection>
27 
SystemTray(QObject * parent,const QVariantList & args)28 SystemTray::SystemTray(QObject *parent, const QVariantList &args)
29     : Plasma::Containment(parent, args)
30     , m_plasmoidModel(nullptr)
31     , m_statusNotifierModel(nullptr)
32     , m_systemTrayModel(nullptr)
33     , m_sortedSystemTrayModel(nullptr)
34     , m_configSystemTrayModel(nullptr)
35 {
36     setHasConfigurationInterface(true);
37     setContainmentType(Plasma::Types::CustomEmbeddedContainment);
38     setContainmentDisplayHints(Plasma::Types::ContainmentDrawsPlasmoidHeading | Plasma::Types::ContainmentForcesSquarePlasmoids);
39 }
40 
~SystemTray()41 SystemTray::~SystemTray()
42 {
43 }
44 
init()45 void SystemTray::init()
46 {
47     Containment::init();
48 
49     m_settings = new SystemTraySettings(configScheme(), this);
50     connect(m_settings, &SystemTraySettings::enabledPluginsChanged, this, &SystemTray::onEnabledAppletsChanged);
51 
52     m_plasmoidRegistry = new PlasmoidRegistry(m_settings, this);
53     connect(m_plasmoidRegistry, &PlasmoidRegistry::plasmoidEnabled, this, &SystemTray::startApplet);
54     connect(m_plasmoidRegistry, &PlasmoidRegistry::plasmoidStopped, this, &SystemTray::stopApplet);
55 
56     // we don't want to automatically propagate the activated signal from the Applet to the Containment
57     // even if SystemTray is of type Containment, it is de facto Applet and should act like one
58     connect(this, &Containment::appletAdded, this, [this](Plasma::Applet *applet) {
59         disconnect(applet, &Applet::activated, this, &Applet::activated);
60     });
61 }
62 
restoreContents(KConfigGroup & group)63 void SystemTray::restoreContents(KConfigGroup &group)
64 {
65     if (!isContainment()) {
66         qCWarning(SYSTEM_TRAY) << "Loaded as an applet, this shouldn't have happened";
67         return;
68     }
69 
70     KConfigGroup shortcutConfig(&group, "Shortcuts");
71     QString shortcutText = shortcutConfig.readEntryUntranslated("global", QString());
72     if (!shortcutText.isEmpty()) {
73         setGlobalShortcut(QKeySequence(shortcutText));
74     }
75 
76     // cache known config group ids for applets
77     KConfigGroup cg = group.group("Applets");
78     for (const QString &group : cg.groupList()) {
79         KConfigGroup appletConfig(&cg, group);
80         QString plugin = appletConfig.readEntry("plugin");
81         if (!plugin.isEmpty()) {
82             m_configGroupIds[plugin] = group.toInt();
83         }
84     }
85 
86     m_plasmoidRegistry->init();
87 }
88 
showPlasmoidMenu(QQuickItem * appletInterface,int x,int y)89 void SystemTray::showPlasmoidMenu(QQuickItem *appletInterface, int x, int y)
90 {
91     if (!appletInterface) {
92         return;
93     }
94 
95     Plasma::Applet *applet = appletInterface->property("_plasma_applet").value<Plasma::Applet *>();
96 
97     QPointF pos = appletInterface->mapToScene(QPointF(x, y));
98 
99     if (appletInterface->window() && appletInterface->window()->screen()) {
100         pos = appletInterface->window()->mapToGlobal(pos.toPoint());
101     } else {
102         pos = QPoint();
103     }
104 
105     QMenu *desktopMenu = new QMenu;
106     connect(this, &QObject::destroyed, desktopMenu, &QMenu::close);
107     desktopMenu->setAttribute(Qt::WA_DeleteOnClose);
108 
109     // this is a workaround where Qt will fail to realize a mouse has been released
110 
111     // this happens if a window which does not accept focus spawns a new window that takes focus and X grab
112     // whilst the mouse is depressed
113     // https://bugreports.qt.io/browse/QTBUG-59044
114     // this causes the next click to go missing
115 
116     // by releasing manually we avoid that situation
117     auto ungrabMouseHack = [appletInterface]() {
118         if (appletInterface->window() && appletInterface->window()->mouseGrabberItem()) {
119             appletInterface->window()->mouseGrabberItem()->ungrabMouse();
120         }
121     };
122 
123     QTimer::singleShot(0, appletInterface, ungrabMouseHack);
124     // end workaround
125 
126     emit applet->contextualActionsAboutToShow();
127     const auto contextActions = applet->contextualActions();
128     for (QAction *action : contextActions) {
129         if (action) {
130             desktopMenu->addAction(action);
131         }
132     }
133 
134     QAction *runAssociatedApplication = applet->actions()->action(QStringLiteral("run associated application"));
135     if (runAssociatedApplication && runAssociatedApplication->isEnabled()) {
136         desktopMenu->addAction(runAssociatedApplication);
137     }
138 
139     if (applet->actions()->action(QStringLiteral("configure"))) {
140         desktopMenu->addAction(applet->actions()->action(QStringLiteral("configure")));
141     }
142 
143     if (desktopMenu->isEmpty()) {
144         delete desktopMenu;
145         return;
146     }
147 
148     desktopMenu->adjustSize();
149 
150     if (QScreen *screen = appletInterface->window()->screen()) {
151         const QRect geo = screen->availableGeometry();
152 
153         pos = QPoint(qBound(geo.left(), (int)pos.x(), geo.right() - desktopMenu->width()), //
154                      qBound(geo.top(), (int)pos.y(), geo.bottom() - desktopMenu->height()));
155     }
156 
157     KAcceleratorManager::manage(desktopMenu);
158     desktopMenu->winId();
159     desktopMenu->windowHandle()->setTransientParent(appletInterface->window());
160     desktopMenu->popup(pos.toPoint());
161 }
162 
showStatusNotifierContextMenu(KJob * job,QQuickItem * statusNotifierIcon)163 void SystemTray::showStatusNotifierContextMenu(KJob *job, QQuickItem *statusNotifierIcon)
164 {
165     if (QCoreApplication::closingDown() || !statusNotifierIcon) {
166         // apparently an edge case can be triggered due to the async nature of all this
167         // see: https://bugs.kde.org/show_bug.cgi?id=251977
168         return;
169     }
170 
171     Plasma::ServiceJob *sjob = qobject_cast<Plasma::ServiceJob *>(job);
172     if (!sjob) {
173         return;
174     }
175 
176     QMenu *menu = qobject_cast<QMenu *>(sjob->result().value<QObject *>());
177 
178     if (menu && !menu->isEmpty()) {
179         menu->adjustSize();
180         const auto parameters = sjob->parameters();
181         int x = parameters[QStringLiteral("x")].toInt();
182         int y = parameters[QStringLiteral("y")].toInt();
183 
184         // try tofind the icon screen coordinates, and adjust the position as a poor
185         // man's popupPosition
186 
187         QRect screenItemRect(statusNotifierIcon->mapToScene(QPointF(0, 0)).toPoint(), QSize(statusNotifierIcon->width(), statusNotifierIcon->height()));
188 
189         if (statusNotifierIcon->window()) {
190             screenItemRect.moveTopLeft(statusNotifierIcon->window()->mapToGlobal(screenItemRect.topLeft()));
191         }
192 
193         switch (location()) {
194         case Plasma::Types::LeftEdge:
195             x = screenItemRect.right();
196             y = screenItemRect.top();
197             break;
198         case Plasma::Types::RightEdge:
199             x = screenItemRect.left() - menu->width();
200             y = screenItemRect.top();
201             break;
202         case Plasma::Types::TopEdge:
203             x = screenItemRect.left();
204             y = screenItemRect.bottom();
205             break;
206         case Plasma::Types::BottomEdge:
207             x = screenItemRect.left();
208             y = screenItemRect.top() - menu->height();
209             break;
210         default:
211             x = screenItemRect.left();
212             if (screenItemRect.top() - menu->height() >= statusNotifierIcon->window()->screen()->geometry().top()) {
213                 y = screenItemRect.top() - menu->height();
214             } else {
215                 y = screenItemRect.bottom();
216             }
217         }
218 
219         KAcceleratorManager::manage(menu);
220         menu->winId();
221         menu->windowHandle()->setTransientParent(statusNotifierIcon->window());
222         menu->popup(QPoint(x, y));
223     }
224 }
225 
popupPosition(QQuickItem * visualParent,int x,int y)226 QPointF SystemTray::popupPosition(QQuickItem *visualParent, int x, int y)
227 {
228     if (!visualParent) {
229         return QPointF(0, 0);
230     }
231 
232     QPointF pos = visualParent->mapToScene(QPointF(x, y));
233 
234     if (visualParent->window() && visualParent->window()->screen()) {
235         pos = visualParent->window()->mapToGlobal(pos.toPoint());
236     } else {
237         return QPoint();
238     }
239     return pos;
240 }
241 
isSystemTrayApplet(const QString & appletId)242 bool SystemTray::isSystemTrayApplet(const QString &appletId)
243 {
244     if (m_plasmoidRegistry) {
245         return m_plasmoidRegistry->isSystemTrayApplet(appletId);
246     }
247     return false;
248 }
249 
systemTrayModel()250 SystemTrayModel *SystemTray::systemTrayModel()
251 {
252     if (!m_systemTrayModel) {
253         m_systemTrayModel = new SystemTrayModel(this);
254 
255         m_plasmoidModel = new PlasmoidModel(m_settings, m_plasmoidRegistry, m_systemTrayModel);
256         connect(this, &SystemTray::appletAdded, m_plasmoidModel, &PlasmoidModel::addApplet);
257         connect(this, &SystemTray::appletRemoved, m_plasmoidModel, &PlasmoidModel::removeApplet);
258         for (auto applet : applets()) {
259             m_plasmoidModel->addApplet(applet);
260         }
261 
262         m_statusNotifierModel = new StatusNotifierModel(m_settings, m_systemTrayModel);
263 
264         m_systemTrayModel->addSourceModel(m_plasmoidModel);
265         m_systemTrayModel->addSourceModel(m_statusNotifierModel);
266     }
267 
268     return m_systemTrayModel;
269 }
270 
sortedSystemTrayModel()271 QAbstractItemModel *SystemTray::sortedSystemTrayModel()
272 {
273     if (!m_sortedSystemTrayModel) {
274         m_sortedSystemTrayModel = new SortedSystemTrayModel(SortedSystemTrayModel::SortingType::SystemTray, this);
275         m_sortedSystemTrayModel->setSourceModel(systemTrayModel());
276     }
277     return m_sortedSystemTrayModel;
278 }
279 
configSystemTrayModel()280 QAbstractItemModel *SystemTray::configSystemTrayModel()
281 {
282     if (!m_configSystemTrayModel) {
283         m_configSystemTrayModel = new SortedSystemTrayModel(SortedSystemTrayModel::SortingType::ConfigurationPage, this);
284         m_configSystemTrayModel->setSourceModel(systemTrayModel());
285     }
286     return m_configSystemTrayModel;
287 }
288 
onEnabledAppletsChanged()289 void SystemTray::onEnabledAppletsChanged()
290 {
291     // remove all that are not allowed anymore
292     const auto appletsList = applets();
293     for (Plasma::Applet *applet : appletsList) {
294         // Here it should always be valid.
295         // for some reason it not always is.
296         if (!applet->pluginMetaData().isValid()) {
297             applet->config().parent().deleteGroup();
298             applet->deleteLater();
299         } else {
300             const QString task = applet->pluginMetaData().pluginId();
301             if (!m_settings->isEnabledPlugin(task)) {
302                 // in those cases we do delete the applet config completely
303                 // as they were explicitly disabled by the user
304                 applet->config().parent().deleteGroup();
305                 applet->deleteLater();
306                 m_configGroupIds.remove(task);
307             }
308         }
309     }
310 }
311 
startApplet(const QString & pluginId)312 void SystemTray::startApplet(const QString &pluginId)
313 {
314     const auto appletsList = applets();
315     for (Plasma::Applet *applet : appletsList) {
316         if (!applet->pluginMetaData().isValid()) {
317             continue;
318         }
319 
320         // only allow one instance per applet
321         if (pluginId == applet->pluginMetaData().pluginId()) {
322             // Applet::destroy doesn't delete the applet from Containment::applets in the same event
323             // potentially a dbus activated service being restarted can be added in this time.
324             if (!applet->destroyed()) {
325                 return;
326             }
327         }
328     }
329 
330     qCDebug(SYSTEM_TRAY) << "Adding applet:" << pluginId;
331 
332     // known one, recycle the id to reuse old config
333     if (m_configGroupIds.contains(pluginId)) {
334         Applet *applet = Plasma::PluginLoader::self()->loadApplet(pluginId, m_configGroupIds.value(pluginId), QVariantList());
335         // this should never happen unless explicitly wrong config is hand-written or
336         //(more likely) a previously added applet is uninstalled
337         if (!applet) {
338             qWarning() << "Unable to find applet" << pluginId;
339             return;
340         }
341         applet->setProperty("org.kde.plasma:force-create", true);
342         addApplet(applet);
343         // create a new one automatic id, new config group
344     } else {
345         Applet *applet = createApplet(pluginId, QVariantList() << "org.kde.plasma:force-create");
346         if (applet) {
347             m_configGroupIds[pluginId] = applet->id();
348         }
349     }
350 }
351 
stopApplet(const QString & pluginId)352 void SystemTray::stopApplet(const QString &pluginId)
353 {
354     const auto appletsList = applets();
355     for (Plasma::Applet *applet : appletsList) {
356         if (applet->pluginMetaData().isValid() && pluginId == applet->pluginMetaData().pluginId()) {
357             // we are *not* cleaning the config here, because since is one
358             // of those automatically loaded/unloaded by dbus, we want to recycle
359             // the config the next time it's loaded, in case the user configured something here
360             applet->deleteLater();
361             // HACK: we need to remove the applet from Containment::applets() as soon as possible
362             // otherwise we may have disappearing applets for restarting dbus services
363             // this may be removed when we depend from a frameworks version in which appletDeleted is emitted as soon as deleteLater() is called
364             emit appletDeleted(applet);
365         }
366     }
367 }
368 
369 K_PLUGIN_CLASS_WITH_JSON(SystemTray, "metadata.json")
370 
371 #include "systemtray.moc"
372