6#include <QtGui/private/qtguiglobal_p.h>
9#include <QtGui/qpa/qplatformwindow_p.h>
10#include <QtGui/qpa/qplatformwindow.h>
11#include <QtGui/qpa/qplatformnativeinterface.h>
13#include <QtCore/QDebug>
14#include <QtCore/QFile>
15#include <QtCore/QFileInfo>
17# include <QtCore/QProcess>
19#if QT_CONFIG(settings)
20#include <QtCore/QSettings>
22#include <QtCore/QStandardPaths>
27#include <QtCore/private/qcore_unix_p.h>
29#include <QtCore/QFileInfo>
30#include <QtCore/QUrlQuery>
32#include <QtDBus/QDBusConnection>
33#include <QtDBus/QDBusServiceWatcher>
34#include <QtDBus/QDBusMessage>
35#include <QtDBus/QDBusPendingCall>
36#include <QtDBus/QDBusPendingCallWatcher>
37#include <QtDBus/QDBusPendingReply>
38#include <QtDBus/QDBusUnixFileDescriptor>
48using namespace Qt::StringLiterals;
50#if QT_CONFIG(multiprocess)
52static inline QByteArray detectDesktopEnvironment()
54 const QByteArray xdgCurrentDesktop = qgetenv(
"XDG_CURRENT_DESKTOP");
55 if (!xdgCurrentDesktop.isEmpty())
56 return xdgCurrentDesktop.toUpper();
59 if (!qEnvironmentVariableIsEmpty(
"KDE_FULL_SESSION"))
60 return QByteArrayLiteral(
"KDE");
61 if (!qEnvironmentVariableIsEmpty(
"GNOME_DESKTOP_SESSION_ID"))
62 return QByteArrayLiteral(
"GNOME");
65 QByteArray desktopSession = qgetenv(
"DESKTOP_SESSION");
68 int slash = desktopSession.lastIndexOf(
'/');
70#if QT_CONFIG(settings)
71 QSettings desktopFile(QFile::decodeName(desktopSession +
".desktop"), QSettings::IniFormat);
72 desktopFile.beginGroup(QStringLiteral(
"Desktop Entry"));
73 QByteArray desktopName = desktopFile.value(QStringLiteral(
"DesktopNames")).toByteArray();
74 if (!desktopName.isEmpty())
79 desktopSession = desktopSession.mid(slash + 1);
82 if (desktopSession ==
"gnome")
83 return QByteArrayLiteral(
"GNOME");
84 else if (desktopSession ==
"xfce")
85 return QByteArrayLiteral(
"XFCE");
86 else if (desktopSession ==
"kde")
87 return QByteArrayLiteral(
"KDE");
89 return QByteArrayLiteral(
"UNKNOWN");
92static inline bool checkExecutable(
const QString &candidate, QString *result)
94 *result = QStandardPaths::findExecutable(candidate);
95 return !result->isEmpty();
99static inline bool detectWebBrowser(QDesktopUnixServices::LaunchType type,
100 const QByteArray &desktop, QString *browser)
102 const char *browsers[] = {
"google-chrome",
"firefox",
"mozilla",
"opera"};
104 Q_PRE(browser->isEmpty());
105 if (checkExecutable(QStringLiteral(
"xdg-open"), browser))
108 if (type == QDesktopUnixServices::LaunchType::Browser) {
109 QString browserVariable = qEnvironmentVariable(
"DEFAULT_BROWSER");
110 if (browserVariable.isEmpty())
111 browserVariable = qEnvironmentVariable(
"BROWSER");
112 if (!browserVariable.isEmpty() && checkExecutable(browserVariable, browser))
116 if (desktop == QByteArrayView(
"KDE")) {
117 if (checkExecutable(QStringLiteral(
"kde-open"), browser))
119 if (checkExecutable(QStringLiteral(
"kde-open5"), browser))
121 }
else if (desktop == QByteArrayView(
"GNOME")) {
122 if (checkExecutable(QStringLiteral(
"gnome-open"), browser))
126 for (size_t i = 0; i <
sizeof(browsers)/
sizeof(
char *); ++i)
127 if (checkExecutable(QLatin1StringView(browsers[i]), browser))
132bool QDesktopUnixServices::launchProcess(LaunchType type,
const QUrl &url,
133 const QString &xdgActivationToken)
135#if QT_CONFIG(process)
136 QString &program = type == LaunchType::Browser ? m_webBrowser : m_documentLauncher;
137 if (program.isEmpty())
138 detectWebBrowser(type, desktopEnvironment(), &program);
141 QString urlString = url.toString(QUrl::FullyEncoded);
142 if (!program.isEmpty()) {
144 Q_ASSERT(QFileInfo(program).isAbsolute());
146 process.setProgram(program);
147 process.setArguments({ urlString });
148 qCDebug(lcQpaServices,
"Launching %ls %ls", qUtf16Printable(program),
149 qUtf16Printable(urlString));
151 if (!xdgActivationToken.isEmpty()) {
152 auto env = QProcessEnvironment::systemEnvironment();
153 env.insert(u"XDG_ACTIVATION_TOKEN"_s, xdgActivationToken);
154 process.setProcessEnvironment(env);
157 if (process.startDetached(
nullptr))
159 errorString = process.errorString();
161 errorString = u"Unable to detect a launcher"_s;
164 qCWarning(lcQpaServices,
"Launch of '%ls %ls' failed: %ls",
165 qUtf16Printable(program), qUtf16Printable(urlString),
166 qUtf16Printable(errorString));
169 Q_UNUSED(xdgActivationToken);
170 qCWarning(lcQpaServices,
"Launch for '%ls' failed: QProcess not available",
171 qUtf16Printable(url.toEncoded()));
177static inline bool checkNeedPortalSupport()
179 return QFileInfo::exists(
"/.flatpak-info"_L1) || qEnvironmentVariableIsSet(
"SNAP");
182static inline QDBusMessage xdgDesktopPortalOpenFile(
const QUrl &url,
const QString &parentWindow,
183 const QString &xdgActivationToken)
194 const int fd = qt_safe_open(QFile::encodeName(url.toLocalFile()), O_RDONLY);
196 QDBusMessage message = QDBusMessage::createMethodCall(
"org.freedesktop.portal.Desktop"_L1,
197 "/org/freedesktop/portal/desktop"_L1,
198 "org.freedesktop.portal.OpenURI"_L1,
201 QDBusUnixFileDescriptor descriptor;
202 descriptor.giveFileDescriptor(fd);
204 QVariantMap options = {};
206 if (!xdgActivationToken.isEmpty()) {
207 options.insert(
"activation_token"_L1, xdgActivationToken);
210 message << parentWindow << QVariant::fromValue(descriptor) << options;
212 return QDBusConnection::sessionBus().call(message);
215 return QDBusMessage::createError(QDBusError::InternalError, qt_error_string());
218static inline QDBusMessage xdgDesktopPortalOpenUrl(
const QUrl &url,
const QString &parentWindow,
219 const QString &xdgActivationToken)
232 QDBusMessage message = QDBusMessage::createMethodCall(
"org.freedesktop.portal.Desktop"_L1,
233 "/org/freedesktop/portal/desktop"_L1,
234 "org.freedesktop.portal.OpenURI"_L1,
239 if (!xdgActivationToken.isEmpty()) {
240 options.insert(
"activation_token"_L1, xdgActivationToken);
243 message << parentWindow << url.toString() << options;
245 return QDBusConnection::sessionBus().call(message);
248static inline QDBusMessage xdgDesktopPortalSendEmail(
const QUrl &url,
const QString &parentWindow,
249 const QString &xdgActivationToken)
261 QUrlQuery urlQuery(url);
263 options.insert(
"address"_L1, url.path());
264 options.insert(
"subject"_L1, urlQuery.queryItemValue(
"subject"_L1));
265 options.insert(
"body"_L1, urlQuery.queryItemValue(
"body"_L1));
269 QList<QDBusUnixFileDescriptor> attachments;
270 const QStringList attachmentUris = urlQuery.allQueryItemValues(
"attachment"_L1);
272 for (
const QString &attachmentUri : attachmentUris) {
273 const int fd = qt_safe_open(QFile::encodeName(attachmentUri), O_PATH);
275 QDBusUnixFileDescriptor descriptor(fd);
276 attachments << descriptor;
281 options.insert(
"attachment_fds"_L1, QVariant::fromValue(attachments));
284 if (!xdgActivationToken.isEmpty()) {
285 options.insert(
"activation_token"_L1, xdgActivationToken);
288 QDBusMessage message = QDBusMessage::createMethodCall(
"org.freedesktop.portal.Desktop"_L1,
289 "/org/freedesktop/portal/desktop"_L1,
290 "org.freedesktop.portal.Email"_L1,
293 message << parentWindow << options;
295 return QDBusConnection::sessionBus().call(message);
299struct XDGDesktopColor
305 QColor toQColor()
const
307 constexpr auto rgbMax = 255;
308 return {
static_cast<
int>(r * rgbMax),
static_cast<
int>(g * rgbMax),
309 static_cast<
int>(b * rgbMax) };
313const QDBusArgument &operator>>(
const QDBusArgument &argument, XDGDesktopColor &myStruct)
315 argument.beginStructure();
316 argument >> myStruct.r >> myStruct.g >> myStruct.b;
317 argument.endStructure();
321class XdgDesktopPortalColorPicker :
public QPlatformServiceColorPicker
325 XdgDesktopPortalColorPicker(
const QString &parentWindowId, QWindow *parent)
326 : QPlatformServiceColorPicker(parent), m_parentWindowId(parentWindowId)
330 void pickColor() override
339 QDBusMessage message = QDBusMessage::createMethodCall(
340 "org.freedesktop.portal.Desktop"_L1,
"/org/freedesktop/portal/desktop"_L1,
341 "org.freedesktop.portal.Screenshot"_L1,
"PickColor"_L1);
342 message << m_parentWindowId << QVariantMap();
344 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
345 auto watcher =
new QDBusPendingCallWatcher(pendingCall,
this);
346 connect(watcher, &QDBusPendingCallWatcher::finished,
this,
347 [
this](QDBusPendingCallWatcher *watcher) {
348 watcher->deleteLater();
349 QDBusPendingReply<QDBusObjectPath> reply = *watcher;
350 if (reply.isError()) {
351 qWarning(
"DBus call to pick color failed: %s",
352 qPrintable(reply.error().message()));
353 Q_EMIT colorPicked({});
355 QDBusConnection::sessionBus().connect(
356 "org.freedesktop.portal.Desktop"_L1, reply.value().path(),
357 "org.freedesktop.portal.Request"_L1,
"Response"_L1,
this,
359 SLOT(gotColorResponse(uint,QVariantMap))
367 void gotColorResponse(uint result,
const QVariantMap &map)
371 if (map.contains(u"color"_s)) {
372 XDGDesktopColor color{};
373 map.value(u"color"_s).value<QDBusArgument>() >> color;
374 Q_EMIT colorPicked(color.toQColor());
376 Q_EMIT colorPicked({});
382 const QString m_parentWindowId;
385void registerWithHostPortal()
387 static bool registered =
false;
392 auto message = QDBusMessage::createMethodCall(
393 "org.freedesktop.portal.Desktop"_L1,
"/org/freedesktop/portal/desktop"_L1,
394 "org.freedesktop.host.portal.Registry"_L1,
"Register"_L1);
395 message.setArguments({ QGuiApplication::desktopFileName(), QVariantMap() });
397 new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), qGuiApp);
398 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, [watcher] {
399 watcher->deleteLater();
400 if (watcher->isError()) {
402 if (watcher->error().type() == QDBusError::UnknownInterface || watcher->error().type() == QDBusError::UnknownMethod)
403 qCInfo(lcQpaServices) <<
"Failed to register with host portal" << watcher->error();
405 qCWarning(lcQpaServices) <<
"Failed to register with host portal" << watcher->error();
407 qCDebug(lcQpaServices) <<
"Successfully registered with host portal as" << QGuiApplication::desktopFileName();
416QDesktopUnixServices::QDesktopUnixServices()
418 if (detectDesktopEnvironment() == QByteArrayLiteral(
"UNKNOWN"))
422 if (qEnvironmentVariableIntValue(
"QT_NO_XDG_DESKTOP_PORTAL") > 0) {
425 QDBusMessage message = QDBusMessage::createMethodCall(
426 "org.freedesktop.portal.Desktop"_L1,
"/org/freedesktop/portal/desktop"_L1,
427 "org.freedesktop.DBus.Properties"_L1,
"Get"_L1);
428 message <<
"org.freedesktop.portal.Screenshot"_L1
431 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
432 auto watcher =
new QDBusPendingCallWatcher(pendingCall);
434 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher,
435 [
this](QDBusPendingCallWatcher *watcher) {
436 watcher->deleteLater();
437 QDBusPendingReply<QVariant> reply = *watcher;
438 if (reply.isError()) {
439 m_hasNoPortal = (reply.error().type() == QDBusError::ServiceUnknown);
441 if (reply.value().toUInt() >= 2)
442 m_hasScreenshotPortalWithColorPicking =
true;
446 if (checkNeedPortalSupport()) {
452 if (!QGuiApplication::desktopFileName().isEmpty()) {
453 registerWithHostPortal();
455 QMetaObject::invokeMethod(
458 if (QGuiApplication::desktopFileName().isEmpty()) {
459 qCInfo(lcQpaServices) <<
"QGuiApplication::desktopFileName not set. Unable to register application with portal registry";
462 registerWithHostPortal();
464 Qt::QueuedConnection);
466 m_portalWatcher = std::make_unique<QDBusServiceWatcher>(
467 "org.freedesktop.portal.Desktop"_L1, QDBusConnection::sessionBus(),
468 QDBusServiceWatcher::WatchForRegistration);
469 QObject::connect(m_portalWatcher.get(), &QDBusServiceWatcher::serviceRegistered,
470 m_portalWatcher.get(), ®isterWithHostPortal);
474QDesktopUnixServices::~QDesktopUnixServices()
481QPlatformServiceColorPicker *QDesktopUnixServices::colorPicker(QWindow *parent)
487 if (!qEnvironmentVariableIsEmpty(
"WAYLAND_DISPLAY")
488 || QGuiApplication::platformName().startsWith(
"wayland"_L1)) {
489 return new XdgDesktopPortalColorPicker(portalWindowIdentifier(parent), parent);
498QByteArray QDesktopUnixServices::desktopEnvironment()
const
500 static const QByteArray result = detectDesktopEnvironment();
504QString QDesktopUnixServices::portalFocusWindowIdentifier()
506 if (QWindow *focusWindow = QGuiApplication::focusWindow())
507 return portalWindowIdentifier(focusWindow);
512bool runWithXdgActivationToken(F &&functionToCall)
514#if QT_CONFIG(wayland)
515 QWindow *window = qGuiApp->focusWindow();
518 return functionToCall({});
520 auto waylandApp =
dynamic_cast<QNativeInterface::QWaylandApplication *>(
521 qGuiApp->platformNativeInterface());
523 dynamic_cast<QNativeInterface::Private::QWaylandWindow *>(window->handle());
525 if (!waylandWindow || !waylandApp)
526 return functionToCall({});
528 QObject::connect(waylandWindow,
529 &QNativeInterface::Private::QWaylandWindow::xdgActivationTokenCreated,
530 waylandWindow, functionToCall, Qt::SingleShotConnection);
531 waylandWindow->requestXdgActivationToken(waylandApp->lastInputSerial());
534 return functionToCall({});
538bool QDesktopUnixServices::openUrl(
const QUrl &url)
540 auto openUrlWithoutPortal = [&](
const QString &xdgActivationToken) {
541 if (url.scheme() ==
"mailto"_L1)
542 return openDocument(url);
544 return launchProcess(LaunchType::Browser, url, xdgActivationToken);
548 if (!m_hasNoPortal) {
549 auto openUrlWithPortal = [&](
const QString &xdgActivationToken) {
550 const QString parentWindow = portalFocusWindowIdentifier();
551 QDBusError error = url.scheme() ==
"mailto"_L1
552 ? xdgDesktopPortalSendEmail(url, parentWindow, xdgActivationToken)
553 : xdgDesktopPortalOpenUrl(url, parentWindow, xdgActivationToken);
554 if (!error.isValid())
556 if (error.type() == QDBusError::ServiceUnknown)
557 m_hasNoPortal =
true;
559 return openUrlWithoutPortal(xdgActivationToken);
561 return runWithXdgActivationToken(openUrlWithPortal);
565 return runWithXdgActivationToken(openUrlWithoutPortal);
568bool QDesktopUnixServices::openDocument(
const QUrl &url)
570 auto openDocumentWithoutPortal = [&](
const QString &xdgActivationToken) {
571 return launchProcess(LaunchType::Document, url, xdgActivationToken);
575 if (!m_hasNoPortal) {
576 auto openDocumentWithPortal = [&](
const QString &xdgActivationToken) {
577 const QString parentWindow = portalFocusWindowIdentifier();
578 QDBusError error = xdgDesktopPortalOpenFile(url, parentWindow, xdgActivationToken);
579 if (!error.isValid())
581 if (error.type() == QDBusError::ServiceUnknown)
582 m_hasNoPortal =
true;
584 return openDocumentWithoutPortal(xdgActivationToken);
586 return runWithXdgActivationToken(openDocumentWithPortal);
590 return runWithXdgActivationToken(openDocumentWithoutPortal);
594QDesktopUnixServices::QDesktopUnixServices() =
default;
595QDesktopUnixServices::~QDesktopUnixServices() =
default;
597QByteArray QDesktopUnixServices::desktopEnvironment()
const
599 return QByteArrayLiteral(
"UNKNOWN");
602bool QDesktopUnixServices::openUrl(
const QUrl &url)
605 qWarning(
"openUrl() not supported on this platform");
609bool QDesktopUnixServices::openDocument(
const QUrl &url)
612 qWarning(
"openDocument() not supported on this platform");
616QPlatformServiceColorPicker *QDesktopUnixServices::colorPicker(QWindow *parent)
624QString QDesktopUnixServices::portalWindowIdentifier(QWindow *window)
631void QDesktopUnixServices::registerDBusMenuForWindow(QWindow *window,
const QString &service,
const QString &path)
638void QDesktopUnixServices::unregisterDBusMenuForWindow(QWindow *window)
644bool QDesktopUnixServices::hasCapability(Capability capability)
const
646 switch (capability) {
647 case Capability::ColorPicking:
648 return m_hasScreenshotPortalWithColorPicking;
653void QDesktopUnixServices::setApplicationBadge(qint64 number)
656 if (qGuiApp->desktopFileName().isEmpty()) {
657 qCWarning(lcQpaServices,
"Cannot set badge number - QGuiApplication::desktopFileName() is empty");
662 const QString launcherUrl = QStringLiteral(
"application://") + qGuiApp->desktopFileName() + QStringLiteral(
".desktop");
663 const qint64 count = qBound(0, number, 9999);
664 QVariantMap dbusUnityProperties;
667 dbusUnityProperties[QStringLiteral(
"count")] = count;
668 dbusUnityProperties[QStringLiteral(
"count-visible")] =
true;
670 dbusUnityProperties[QStringLiteral(
"count-visible")] =
false;
673 auto signal = QDBusMessage::createSignal(QStringLiteral(
"/com/canonical/unity/launcherentry/")
674 + qGuiApp->applicationName(), QStringLiteral(
"com.canonical.Unity.LauncherEntry"), QStringLiteral(
"Update"));
676 signal.setArguments({launcherUrl, dbusUnityProperties});
678 QDBusConnection::sessionBus().send(signal);
686#include "qdesktopunixservices.moc"