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