Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qdesktopunixservices.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:execute-external-code
4
6#include <QtGui/private/qtguiglobal_p.h>
8#include "qwindow.h"
9#include <QtGui/qpa/qplatformwindow_p.h>
10#include <QtGui/qpa/qplatformwindow.h>
11#include <QtGui/qpa/qplatformnativeinterface.h>
12
13#include <QtCore/QDebug>
14#include <QtCore/QFile>
15#if QT_CONFIG(process)
16# include <QtCore/QProcess>
17#endif
18#if QT_CONFIG(settings)
19#include <QtCore/QSettings>
20#endif
21#include <QtCore/QStandardPaths>
22#include <QtCore/QUrl>
23
24#if QT_CONFIG(dbus)
25// These QtCore includes are needed for xdg-desktop-portal support
26#include <QtCore/private/qcore_unix_p.h>
27
28#include <QtCore/QFileInfo>
29#include <QtCore/QUrlQuery>
30
31#include <QtDBus/QDBusConnection>
32#include <QtDBus/QDBusServiceWatcher>
33#include <QtDBus/QDBusMessage>
34#include <QtDBus/QDBusPendingCall>
35#include <QtDBus/QDBusPendingCallWatcher>
36#include <QtDBus/QDBusPendingReply>
37#include <QtDBus/QDBusUnixFileDescriptor>
38
39#include <fcntl.h>
40
41#endif // QT_CONFIG(dbus)
42
43#include <stdlib.h>
44
45QT_BEGIN_NAMESPACE
46
47using namespace Qt::StringLiterals;
48
49#if QT_CONFIG(multiprocess)
50
51static inline QByteArray detectDesktopEnvironment()
52{
53 const QByteArray xdgCurrentDesktop = qgetenv("XDG_CURRENT_DESKTOP");
54 if (!xdgCurrentDesktop.isEmpty())
55 return xdgCurrentDesktop.toUpper(); // KDE, GNOME, UNITY, LXDE, MATE, XFCE...
56
57 // Classic fallbacks
58 if (!qEnvironmentVariableIsEmpty("KDE_FULL_SESSION"))
59 return QByteArrayLiteral("KDE");
60 if (!qEnvironmentVariableIsEmpty("GNOME_DESKTOP_SESSION_ID"))
61 return QByteArrayLiteral("GNOME");
62
63 // Fallback to checking $DESKTOP_SESSION (unreliable)
64 QByteArray desktopSession = qgetenv("DESKTOP_SESSION");
65
66 // This can be a path in /usr/share/xsessions
67 int slash = desktopSession.lastIndexOf('/');
68 if (slash != -1) {
69#if QT_CONFIG(settings)
70 QSettings desktopFile(QFile::decodeName(desktopSession + ".desktop"), QSettings::IniFormat);
71 desktopFile.beginGroup(QStringLiteral("Desktop Entry"));
72 QByteArray desktopName = desktopFile.value(QStringLiteral("DesktopNames")).toByteArray();
73 if (!desktopName.isEmpty())
74 return desktopName;
75#endif
76
77 // try decoding just the basename
78 desktopSession = desktopSession.mid(slash + 1);
79 }
80
81 if (desktopSession == "gnome")
82 return QByteArrayLiteral("GNOME");
83 else if (desktopSession == "xfce")
84 return QByteArrayLiteral("XFCE");
85 else if (desktopSession == "kde")
86 return QByteArrayLiteral("KDE");
87
88 return QByteArrayLiteral("UNKNOWN");
89}
90
91static inline bool checkExecutable(const QString &candidate, QString *result)
92{
93 *result = QStandardPaths::findExecutable(candidate);
94 return !result->isEmpty();
95}
96
97static inline bool detectWebBrowser(const QByteArray &desktop,
98 bool checkBrowserVariable,
99 QString *browser)
100{
101 const char *browsers[] = {"google-chrome", "firefox", "mozilla", "opera"};
102
103 browser->clear();
104 if (checkExecutable(QStringLiteral("xdg-open"), browser))
105 return true;
106
107 if (checkBrowserVariable) {
108 QString browserVariable = qEnvironmentVariable("DEFAULT_BROWSER");
109 if (browserVariable.isEmpty())
110 browserVariable = qEnvironmentVariable("BROWSER");
111 if (!browserVariable.isEmpty() && checkExecutable(browserVariable, browser))
112 return true;
113 }
114
115 if (desktop == QByteArray("KDE")) {
116 if (checkExecutable(QStringLiteral("kde-open5"), browser))
117 return true;
118 // Konqueror launcher
119 if (checkExecutable(QStringLiteral("kfmclient"), browser)) {
120 browser->append(" exec"_L1);
121 return true;
122 }
123 } else if (desktop == QByteArray("GNOME")) {
124 if (checkExecutable(QStringLiteral("gnome-open"), browser))
125 return true;
126 }
127
128 for (size_t i = 0; i < sizeof(browsers)/sizeof(char *); ++i)
129 if (checkExecutable(QLatin1StringView(browsers[i]), browser))
130 return true;
131 return false;
132}
133
134static inline bool launch(const QString &launcher, const QUrl &url,
135 const QString &xdgActivationToken)
136{
137
138 const QString command = launcher + u' ' + QLatin1StringView(url.toEncoded());
139 qCDebug(lcQpaServices, "Launching %s", qPrintable(command));
140#if !QT_CONFIG(process)
141 if (!xdgActivationToken.isEmpty())
142 qputenv("XDG_ACTIVATION_TOKEN", xdgActivationToken.toUtf8());
143 const bool ok = ::system(qPrintable(command + " &"_L1));
144 if (!xdgActivationToken.isEmpty())
145 qunsetenv("XDG_ACTIVATION_TOKEN");
146# else
147 QStringList args = QProcess::splitCommand(command);
148 bool ok = false;
149 if (!args.isEmpty()) {
150 QString program = args.takeFirst();
151 QProcess process;
152 process.setProgram(program);
153 process.setArguments(args);
154
155 if (!xdgActivationToken.isEmpty()) {
156 auto env = QProcessEnvironment::systemEnvironment();
157 env.insert(u"XDG_ACTIVATION_TOKEN"_s, xdgActivationToken);
158 process.setEnvironment(env.toStringList());
159 }
160 ok = process.startDetached(nullptr);
161 }
162# endif
163 if (!ok)
164 qCWarning(lcQpaServices, "Launch failed (%s)", qPrintable(command));
165
166 return ok;
167}
168
169#if QT_CONFIG(dbus)
170static inline bool checkNeedPortalSupport()
171{
172 return QFileInfo::exists("/.flatpak-info"_L1) || qEnvironmentVariableIsSet("SNAP");
173}
174
175static inline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url, const QString &parentWindow,
176 const QString &xdgActivationToken)
177{
178 // DBus signature:
179 // OpenFile (IN s parent_window,
180 // IN h fd,
181 // IN a{sv} options,
182 // OUT o handle)
183 // Options:
184 // handle_token (s) - A string that will be used as the last element of the @handle.
185 // writable (b) - Whether to allow the chosen application to write to the file.
186
187 const int fd = qt_safe_open(QFile::encodeName(url.toLocalFile()), O_RDONLY);
188 if (fd != -1) {
189 QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.portal.Desktop"_L1,
190 "/org/freedesktop/portal/desktop"_L1,
191 "org.freedesktop.portal.OpenURI"_L1,
192 "OpenFile"_L1);
193
194 QDBusUnixFileDescriptor descriptor;
195 descriptor.giveFileDescriptor(fd);
196
197 QVariantMap options = {};
198
199 if (!xdgActivationToken.isEmpty()) {
200 options.insert("activation_token"_L1, xdgActivationToken);
201 }
202
203 message << parentWindow << QVariant::fromValue(descriptor) << options;
204
205 return QDBusConnection::sessionBus().call(message);
206 }
207
208 return QDBusMessage::createError(QDBusError::InternalError, qt_error_string());
209}
210
211static inline QDBusMessage xdgDesktopPortalOpenUrl(const QUrl &url, const QString &parentWindow,
212 const QString &xdgActivationToken)
213{
214 // DBus signature:
215 // OpenURI (IN s parent_window,
216 // IN s uri,
217 // IN a{sv} options,
218 // OUT o handle)
219 // Options:
220 // handle_token (s) - A string that will be used as the last element of the @handle.
221 // writable (b) - Whether to allow the chosen application to write to the file.
222 // This key only takes effect the uri points to a local file that is exported in the document portal,
223 // and the chosen application is sandboxed itself.
224
225 QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.portal.Desktop"_L1,
226 "/org/freedesktop/portal/desktop"_L1,
227 "org.freedesktop.portal.OpenURI"_L1,
228 "OpenURI"_L1);
229 // FIXME parent_window_id and handle writable option
230 QVariantMap options;
231
232 if (!xdgActivationToken.isEmpty()) {
233 options.insert("activation_token"_L1, xdgActivationToken);
234 }
235
236 message << parentWindow << url.toString() << options;
237
238 return QDBusConnection::sessionBus().call(message);
239}
240
241static inline QDBusMessage xdgDesktopPortalSendEmail(const QUrl &url, const QString &parentWindow,
242 const QString &xdgActivationToken)
243{
244 // DBus signature:
245 // ComposeEmail (IN s parent_window,
246 // IN a{sv} options,
247 // OUT o handle)
248 // Options:
249 // address (s) - The email address to send to.
250 // subject (s) - The subject for the email.
251 // body (s) - The body for the email.
252 // attachment_fds (ah) - File descriptors for files to attach.
253
254 QUrlQuery urlQuery(url);
255 QVariantMap options;
256 options.insert("address"_L1, url.path());
257 options.insert("subject"_L1, urlQuery.queryItemValue("subject"_L1));
258 options.insert("body"_L1, urlQuery.queryItemValue("body"_L1));
259
260 // O_PATH seems to be present since Linux 2.6.39, which is not case of RHEL 6
261#ifdef O_PATH
262 QList<QDBusUnixFileDescriptor> attachments;
263 const QStringList attachmentUris = urlQuery.allQueryItemValues("attachment"_L1);
264
265 for (const QString &attachmentUri : attachmentUris) {
266 const int fd = qt_safe_open(QFile::encodeName(attachmentUri), O_PATH);
267 if (fd != -1) {
268 QDBusUnixFileDescriptor descriptor(fd);
269 attachments << descriptor;
270 qt_safe_close(fd);
271 }
272 }
273
274 options.insert("attachment_fds"_L1, QVariant::fromValue(attachments));
275#endif
276
277 if (!xdgActivationToken.isEmpty()) {
278 options.insert("activation_token"_L1, xdgActivationToken);
279 }
280
281 QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.portal.Desktop"_L1,
282 "/org/freedesktop/portal/desktop"_L1,
283 "org.freedesktop.portal.Email"_L1,
284 "ComposeEmail"_L1);
285
286 message << parentWindow << options;
287
288 return QDBusConnection::sessionBus().call(message);
289}
290
291namespace {
292struct XDGDesktopColor
293{
294 double r = 0;
295 double g = 0;
296 double b = 0;
297
298 QColor toQColor() const
299 {
300 constexpr auto rgbMax = 255;
301 return { static_cast<int>(r * rgbMax), static_cast<int>(g * rgbMax),
302 static_cast<int>(b * rgbMax) };
303 }
304};
305
306const QDBusArgument &operator>>(const QDBusArgument &argument, XDGDesktopColor &myStruct)
307{
308 argument.beginStructure();
309 argument >> myStruct.r >> myStruct.g >> myStruct.b;
310 argument.endStructure();
311 return argument;
312}
313
314class XdgDesktopPortalColorPicker : public QPlatformServiceColorPicker
315{
316 Q_OBJECT
317public:
318 XdgDesktopPortalColorPicker(const QString &parentWindowId, QWindow *parent)
319 : QPlatformServiceColorPicker(parent), m_parentWindowId(parentWindowId)
320 {
321 }
322
323 void pickColor() override
324 {
325 // DBus signature:
326 // PickColor (IN s parent_window,
327 // IN a{sv} options
328 // OUT o handle)
329 // Options:
330 // handle_token (s) - A string that will be used as the last element of the @handle.
331
332 QDBusMessage message = QDBusMessage::createMethodCall(
333 "org.freedesktop.portal.Desktop"_L1, "/org/freedesktop/portal/desktop"_L1,
334 "org.freedesktop.portal.Screenshot"_L1, "PickColor"_L1);
335 message << m_parentWindowId << QVariantMap();
336
337 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
338 auto watcher = new QDBusPendingCallWatcher(pendingCall, this);
339 connect(watcher, &QDBusPendingCallWatcher::finished, this,
340 [this](QDBusPendingCallWatcher *watcher) {
341 watcher->deleteLater();
342 QDBusPendingReply<QDBusObjectPath> reply = *watcher;
343 if (reply.isError()) {
344 qWarning("DBus call to pick color failed: %s",
345 qPrintable(reply.error().message()));
346 Q_EMIT colorPicked({});
347 } else {
348 QDBusConnection::sessionBus().connect(
349 "org.freedesktop.portal.Desktop"_L1, reply.value().path(),
350 "org.freedesktop.portal.Request"_L1, "Response"_L1, this,
351 // clang-format off
352 SLOT(gotColorResponse(uint,QVariantMap))
353 // clang-format on
354 );
355 }
356 });
357 }
358
359private Q_SLOTS:
360 void gotColorResponse(uint result, const QVariantMap &map)
361 {
362 if (result != 0)
363 return;
364 if (map.contains(u"color"_s)) {
365 XDGDesktopColor color{};
366 map.value(u"color"_s).value<QDBusArgument>() >> color;
367 Q_EMIT colorPicked(color.toQColor());
368 } else {
369 Q_EMIT colorPicked({});
370 }
371 deleteLater();
372 }
373
374private:
375 const QString m_parentWindowId;
376};
377
378void registerWithHostPortal()
379{
380 static bool registered = false;
381 if (registered) {
382 return;
383 }
384
385 auto message = QDBusMessage::createMethodCall(
386 "org.freedesktop.portal.Desktop"_L1, "/org/freedesktop/portal/desktop"_L1,
387 "org.freedesktop.host.portal.Registry"_L1, "Register"_L1);
388 message.setArguments({ QGuiApplication::desktopFileName(), QVariantMap() });
389 auto watcher =
390 new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), qGuiApp);
391 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, [watcher] {
392 watcher->deleteLater();
393 if (watcher->isError()) {
394 // Expected error when running against an older portal
395 if (watcher->error().type() == QDBusError::UnknownInterface || watcher->error().type() == QDBusError::UnknownMethod)
396 qCInfo(lcQpaServices) << "Failed to register with host portal" << watcher->error();
397 else
398 qCWarning(lcQpaServices) << "Failed to register with host portal" << watcher->error();
399 } else {
400 qCDebug(lcQpaServices) << "Successfully registered with host portal as" << QGuiApplication::desktopFileName();
401 registered = true;
402 }
403 });
404}
405} // namespace
406
407#endif // QT_CONFIG(dbus)
408
409QDesktopUnixServices::QDesktopUnixServices()
410{
411 if (detectDesktopEnvironment() == QByteArrayLiteral("UNKNOWN"))
412 return;
413
414#if QT_CONFIG(dbus)
415 if (qEnvironmentVariableIntValue("QT_NO_XDG_DESKTOP_PORTAL") > 0) {
416 return;
417 }
418 QDBusMessage message = QDBusMessage::createMethodCall(
419 "org.freedesktop.portal.Desktop"_L1, "/org/freedesktop/portal/desktop"_L1,
420 "org.freedesktop.DBus.Properties"_L1, "Get"_L1);
421 message << "org.freedesktop.portal.Screenshot"_L1
422 << "version"_L1;
423
424 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
425 auto watcher = new QDBusPendingCallWatcher(pendingCall);
426 m_watcher = watcher;
427 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher,
428 [this](QDBusPendingCallWatcher *watcher) {
429 watcher->deleteLater();
430 QDBusPendingReply<QVariant> reply = *watcher;
431 if (!reply.isError() && reply.value().toUInt() >= 2)
432 m_hasScreenshotPortalWithColorPicking = true;
433 });
434
435 if (checkNeedPortalSupport()) {
436 return;
437 }
438
439 // The program might only set the desktopfilename after creating the app
440 // try again when it's running
441 if (!QGuiApplication::desktopFileName().isEmpty()) {
442 registerWithHostPortal();
443 } else {
444 QMetaObject::invokeMethod(
445 qGuiApp,
446 [] {
447 if (QGuiApplication::desktopFileName().isEmpty()) {
448 qCInfo(lcQpaServices) << "QGuiApplication::desktopFileName not set. Unable to register application with portal registry";
449 return;
450 }
451 registerWithHostPortal();
452 },
453 Qt::QueuedConnection);
454 }
455 m_portalWatcher = std::make_unique<QDBusServiceWatcher>(
456 "org.freedesktop.portal.Desktop"_L1, QDBusConnection::sessionBus(),
457 QDBusServiceWatcher::WatchForRegistration);
458 QObject::connect(m_portalWatcher.get(), &QDBusServiceWatcher::serviceRegistered,
459 m_portalWatcher.get(), &registerWithHostPortal);
460#endif
461}
462
463QDesktopUnixServices::~QDesktopUnixServices()
464{
465#if QT_CONFIG(dbus)
466 delete m_watcher;
467#endif
468}
469
470QPlatformServiceColorPicker *QDesktopUnixServices::colorPicker(QWindow *parent)
471{
472#if QT_CONFIG(dbus)
473 // Make double sure that we are in a wayland environment. In particular check
474 // WAYLAND_DISPLAY so also XWayland apps benefit from portal-based color picking.
475 // Outside wayland we'll rather rely on other means than the XDG desktop portal.
476 if (!qEnvironmentVariableIsEmpty("WAYLAND_DISPLAY")
477 || QGuiApplication::platformName().startsWith("wayland"_L1)) {
478 return new XdgDesktopPortalColorPicker(portalWindowIdentifier(parent), parent);
479 }
480 return nullptr;
481#else
482 Q_UNUSED(parent);
483 return nullptr;
484#endif
485}
486
487QByteArray QDesktopUnixServices::desktopEnvironment() const
488{
489 static const QByteArray result = detectDesktopEnvironment();
490 return result;
491}
492
493template<typename F>
494void runWithXdgActivationToken(F &&functionToCall)
495{
496#if QT_CONFIG(wayland)
497 QWindow *window = qGuiApp->focusWindow();
498
499 if (!window) {
500 functionToCall({});
501 return;
502 }
503
504 auto waylandApp = dynamic_cast<QNativeInterface::QWaylandApplication *>(
505 qGuiApp->platformNativeInterface());
506 auto waylandWindow =
507 dynamic_cast<QNativeInterface::Private::QWaylandWindow *>(window->handle());
508
509 if (!waylandWindow || !waylandApp) {
510 functionToCall({});
511 return;
512 }
513
514 QObject::connect(waylandWindow,
515 &QNativeInterface::Private::QWaylandWindow::xdgActivationTokenCreated,
516 waylandWindow, functionToCall, Qt::SingleShotConnection);
517 waylandWindow->requestXdgActivationToken(waylandApp->lastInputSerial());
518#else
519 functionToCall({});
520#endif
521}
522
523bool QDesktopUnixServices::openUrl(const QUrl &url)
524{
525 auto openUrlInternal = [this](const QUrl &url, const QString &xdgActivationToken) {
526 if (url.scheme() == "mailto"_L1) {
527# if QT_CONFIG(dbus)
528 if (checkNeedPortalSupport()) {
529 const QString parentWindow = QGuiApplication::focusWindow()
530 ? portalWindowIdentifier(QGuiApplication::focusWindow())
531 : QString();
532 QDBusError error = xdgDesktopPortalSendEmail(url, parentWindow, xdgActivationToken);
533 if (!error.isValid())
534 return true;
535
536 // service not running, fall back
537 }
538# endif
539 return openDocument(url);
540 }
541
542# if QT_CONFIG(dbus)
543 if (checkNeedPortalSupport()) {
544 const QString parentWindow = QGuiApplication::focusWindow()
545 ? portalWindowIdentifier(QGuiApplication::focusWindow())
546 : QString();
547 QDBusError error = xdgDesktopPortalOpenUrl(url, parentWindow, xdgActivationToken);
548 if (!error.isValid())
549 return true;
550 }
551# endif
552
553 if (m_webBrowser.isEmpty()
554 && !detectWebBrowser(desktopEnvironment(), true, &m_webBrowser)) {
555 qCWarning(lcQpaServices, "Unable to detect a web browser to launch '%s'", qPrintable(url.toString()));
556 return false;
557 }
558 return launch(m_webBrowser, url, xdgActivationToken);
559 };
560
561 if (QGuiApplication::platformName().startsWith("wayland"_L1)) {
562 runWithXdgActivationToken(
563 [openUrlInternal, url](const QString &token) { openUrlInternal(url, token); });
564
565 return true;
566
567 } else {
568 return openUrlInternal(url, QString());
569 }
570}
571
572bool QDesktopUnixServices::openDocument(const QUrl &url)
573{
574 auto openDocumentInternal = [this](const QUrl &url, const QString &xdgActivationToken) {
575
576# if QT_CONFIG(dbus)
577 if (checkNeedPortalSupport()) {
578 const QString parentWindow = QGuiApplication::focusWindow()
579 ? portalWindowIdentifier(QGuiApplication::focusWindow())
580 : QString();
581 QDBusError error = xdgDesktopPortalOpenFile(url, parentWindow, xdgActivationToken);
582 if (!error.isValid())
583 return true;
584 }
585# endif
586
587 if (m_documentLauncher.isEmpty()
588 && !detectWebBrowser(desktopEnvironment(), false, &m_documentLauncher)) {
589 qCWarning(lcQpaServices, "Unable to detect a launcher for '%s'", qPrintable(url.toString()));
590 return false;
591 }
592 return launch(m_documentLauncher, url, xdgActivationToken);
593 };
594
595 if (QGuiApplication::platformName().startsWith("wayland"_L1)) {
596 runWithXdgActivationToken([openDocumentInternal, url](const QString &token) {
597 openDocumentInternal(url, token);
598 });
599
600 return true;
601 } else {
602 return openDocumentInternal(url, QString());
603 }
604}
605
606#else
607QDesktopUnixServices::QDesktopUnixServices() = default;
608QDesktopUnixServices::~QDesktopUnixServices() = default;
609
610QByteArray QDesktopUnixServices::desktopEnvironment() const
611{
612 return QByteArrayLiteral("UNKNOWN");
613}
614
615bool QDesktopUnixServices::openUrl(const QUrl &url)
616{
617 Q_UNUSED(url);
618 qWarning("openUrl() not supported on this platform");
619 return false;
620}
621
622bool QDesktopUnixServices::openDocument(const QUrl &url)
623{
624 Q_UNUSED(url);
625 qWarning("openDocument() not supported on this platform");
626 return false;
627}
628
629QPlatformServiceColorPicker *QDesktopUnixServices::colorPicker(QWindow *parent)
630{
631 Q_UNUSED(parent);
632 return nullptr;
633}
634
635#endif // QT_CONFIG(multiprocess)
636
637QString QDesktopUnixServices::portalWindowIdentifier(QWindow *window)
638{
639 Q_UNUSED(window);
640 return QString();
641}
642
643
644void QDesktopUnixServices::registerDBusMenuForWindow(QWindow *window, const QString &service, const QString &path)
645{
646 Q_UNUSED(window);
647 Q_UNUSED(service);
648 Q_UNUSED(path);
649}
650
651void QDesktopUnixServices::unregisterDBusMenuForWindow(QWindow *window)
652{
653 Q_UNUSED(window);
654}
655
656
657bool QDesktopUnixServices::hasCapability(Capability capability) const
658{
659 switch (capability) {
660 case Capability::ColorPicking:
661 return m_hasScreenshotPortalWithColorPicking;
662 }
663 return false;
664}
665
666void QDesktopUnixServices::setApplicationBadge(qint64 number)
667{
668#if QT_CONFIG(dbus)
669 if (qGuiApp->desktopFileName().isEmpty()) {
670 qCWarning(lcQpaServices, "Cannot set badge number - QGuiApplication::desktopFileName() is empty");
671 return;
672 }
673
674
675 const QString launcherUrl = QStringLiteral("application://") + qGuiApp->desktopFileName() + QStringLiteral(".desktop");
676 const qint64 count = qBound(0, number, 9999);
677 QVariantMap dbusUnityProperties;
678
679 if (count > 0) {
680 dbusUnityProperties[QStringLiteral("count")] = count;
681 dbusUnityProperties[QStringLiteral("count-visible")] = true;
682 } else {
683 dbusUnityProperties[QStringLiteral("count-visible")] = false;
684 }
685
686 auto signal = QDBusMessage::createSignal(QStringLiteral("/com/canonical/unity/launcherentry/")
687 + qGuiApp->applicationName(), QStringLiteral("com.canonical.Unity.LauncherEntry"), QStringLiteral("Update"));
688
689 signal.setArguments({launcherUrl, dbusUnityProperties});
690
691 QDBusConnection::sessionBus().send(signal);
692#else
693 Q_UNUSED(number)
694#endif
695}
696
697QT_END_NAMESPACE
698
699#include "qdesktopunixservices.moc"