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 QStringList addresses = url.path().split(u',') + urlQuery.allQueryItemValues(
"to"_L1, QUrl::FullyDecoded);
264 if (!addresses.isEmpty())
265 options.insert(
"addresses"_L1, addresses);
266 if (
const auto subject = urlQuery.queryItemValue(
"subject"_L1); !subject.isEmpty())
267 options.insert(
"subject"_L1, subject);
268 if (
const auto body = urlQuery.queryItemValue(
"body"_L1); !body.isEmpty())
269 options.insert(
"body"_L1, body);
270 if (
const auto cc = urlQuery.allQueryItemValues(
"cc"_L1, QUrl::FullyDecoded); !cc.isEmpty())
271 options.insert(
"cc"_L1, cc);
272 if (
const auto bcc = urlQuery.allQueryItemValues(
"bcc"_L1, QUrl::FullyDecoded); !bcc.isEmpty())
273 options.insert(
"bcc"_L1, bcc);
277 QList<QDBusUnixFileDescriptor> attachments;
278 const QStringList attachmentUris = urlQuery.allQueryItemValues(
"attachment"_L1);
280 for (
const QString &attachmentUri : attachmentUris) {
281 const int fd = qt_safe_open(QFile::encodeName(attachmentUri), O_PATH);
283 QDBusUnixFileDescriptor descriptor(fd);
284 attachments << descriptor;
289 if (attachments.size() > 0)
290 options.insert(
"attachment_fds"_L1, QVariant::fromValue(attachments));
293 if (!xdgActivationToken.isEmpty()) {
294 options.insert(
"activation_token"_L1, xdgActivationToken);
297 QDBusMessage message = QDBusMessage::createMethodCall(
"org.freedesktop.portal.Desktop"_L1,
298 "/org/freedesktop/portal/desktop"_L1,
299 "org.freedesktop.portal.Email"_L1,
302 message << parentWindow << options;
304 return QDBusConnection::sessionBus().call(message);
308struct XDGDesktopColor
314 QColor toQColor()
const
316 constexpr auto rgbMax = 255;
317 return {
static_cast<
int>(r * rgbMax),
static_cast<
int>(g * rgbMax),
318 static_cast<
int>(b * rgbMax) };
322const QDBusArgument &operator>>(
const QDBusArgument &argument, XDGDesktopColor &myStruct)
324 argument.beginStructure();
325 argument >> myStruct.r >> myStruct.g >> myStruct.b;
326 argument.endStructure();
330class XdgDesktopPortalColorPicker :
public QPlatformServiceColorPicker
334 XdgDesktopPortalColorPicker(
const QString &parentWindowId, QWindow *parent)
335 : QPlatformServiceColorPicker(parent), m_parentWindowId(parentWindowId)
339 void pickColor() override
348 QDBusMessage message = QDBusMessage::createMethodCall(
349 "org.freedesktop.portal.Desktop"_L1,
"/org/freedesktop/portal/desktop"_L1,
350 "org.freedesktop.portal.Screenshot"_L1,
"PickColor"_L1);
351 message << m_parentWindowId << QVariantMap();
353 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
354 auto watcher =
new QDBusPendingCallWatcher(pendingCall,
this);
355 connect(watcher, &QDBusPendingCallWatcher::finished,
this,
356 [
this](QDBusPendingCallWatcher *watcher) {
357 watcher->deleteLater();
358 QDBusPendingReply<QDBusObjectPath> reply = *watcher;
359 if (reply.isError()) {
360 qWarning(
"DBus call to pick color failed: %s",
361 qPrintable(reply.error().message()));
362 Q_EMIT colorPicked({});
364 QDBusConnection::sessionBus().connect(
365 "org.freedesktop.portal.Desktop"_L1, reply.value().path(),
366 "org.freedesktop.portal.Request"_L1,
"Response"_L1,
this,
368 SLOT(gotColorResponse(uint,QVariantMap))
376 void gotColorResponse(uint result,
const QVariantMap &map)
380 if (map.contains(u"color"_s)) {
381 XDGDesktopColor color{};
382 map.value(u"color"_s).value<QDBusArgument>() >> color;
383 Q_EMIT colorPicked(color.toQColor());
385 Q_EMIT colorPicked({});
391 const QString m_parentWindowId;
394void registerWithHostPortal()
396 static bool registered =
false;
401 auto message = QDBusMessage::createMethodCall(
402 "org.freedesktop.portal.Desktop"_L1,
"/org/freedesktop/portal/desktop"_L1,
403 "org.freedesktop.host.portal.Registry"_L1,
"Register"_L1);
404 message.setArguments({ QGuiApplication::desktopFileName(), QVariantMap() });
406 new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), qGuiApp);
407 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, [watcher] {
408 watcher->deleteLater();
409 if (watcher->isError()) {
411 if (watcher->error().type() == QDBusError::UnknownInterface || watcher->error().type() == QDBusError::UnknownMethod)
412 qCInfo(lcQpaServices) <<
"Failed to register with host portal" << watcher->error();
414 qCWarning(lcQpaServices) <<
"Failed to register with host portal" << watcher->error();
416 qCDebug(lcQpaServices) <<
"Successfully registered with host portal as" << QGuiApplication::desktopFileName();
425QDesktopUnixServices::QDesktopUnixServices()
427 if (detectDesktopEnvironment() == QByteArrayLiteral(
"UNKNOWN"))
431 if (qEnvironmentVariableIntValue(
"QT_NO_XDG_DESKTOP_PORTAL") > 0) {
434 QDBusMessage message = QDBusMessage::createMethodCall(
435 "org.freedesktop.portal.Desktop"_L1,
"/org/freedesktop/portal/desktop"_L1,
436 "org.freedesktop.DBus.Properties"_L1,
"Get"_L1);
437 message <<
"org.freedesktop.portal.Screenshot"_L1
440 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
441 auto watcher =
new QDBusPendingCallWatcher(pendingCall);
443 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher,
444 [
this](QDBusPendingCallWatcher *watcher) {
445 watcher->deleteLater();
446 QDBusPendingReply<QVariant> reply = *watcher;
447 if (reply.isError()) {
448 m_hasNoPortal = (reply.error().type() == QDBusError::ServiceUnknown);
450 if (reply.value().toUInt() >= 2)
451 m_hasScreenshotPortalWithColorPicking =
true;
455 if (checkNeedPortalSupport()) {
461 if (!QGuiApplication::desktopFileName().isEmpty()) {
462 registerWithHostPortal();
464 QMetaObject::invokeMethod(
467 if (QGuiApplication::desktopFileName().isEmpty()) {
468 qCInfo(lcQpaServices) <<
"QGuiApplication::desktopFileName not set. Unable to register application with portal registry";
471 registerWithHostPortal();
473 Qt::QueuedConnection);
475 m_portalWatcher = std::make_unique<QDBusServiceWatcher>(
476 "org.freedesktop.portal.Desktop"_L1, QDBusConnection::sessionBus(),
477 QDBusServiceWatcher::WatchForRegistration);
478 QObject::connect(m_portalWatcher.get(), &QDBusServiceWatcher::serviceRegistered,
479 m_portalWatcher.get(), ®isterWithHostPortal);
483QDesktopUnixServices::~QDesktopUnixServices()
490QPlatformServiceColorPicker *QDesktopUnixServices::colorPicker(QWindow *parent)
496 if (!qEnvironmentVariableIsEmpty(
"WAYLAND_DISPLAY")
497 || QGuiApplication::platformName().startsWith(
"wayland"_L1)) {
498 return new XdgDesktopPortalColorPicker(portalWindowIdentifier(parent), parent);
507QByteArray QDesktopUnixServices::desktopEnvironment()
const
509 static const QByteArray result = detectDesktopEnvironment();
513QString QDesktopUnixServices::portalFocusWindowIdentifier()
515 if (QWindow *focusWindow = QGuiApplication::focusWindow())
516 return portalWindowIdentifier(focusWindow);
521bool QDesktopUnixServices::runWithXdgActivationToken(F callback,
const QUrl &url)
523#if QT_CONFIG(wayland)
524 QWindow *window = qGuiApp->focusWindow();
527 return (
this->*callback)(url, {});
529 auto waylandApp =
dynamic_cast<QNativeInterface::QWaylandApplication *>(
530 qGuiApp->platformNativeInterface());
532 dynamic_cast<QNativeInterface::Private::QWaylandWindow *>(window->handle());
534 if (!waylandWindow || !waylandApp)
535 return (
this->*callback)(url, {});
539 auto functionToCall = [
this, callback, url](
const QString &xdgActivationToken) {
540 return (
this->*callback)(url, xdgActivationToken);
542 QObject::connect(waylandWindow,
543 &QNativeInterface::Private::QWaylandWindow::xdgActivationTokenCreated,
544 waylandWindow, functionToCall, Qt::SingleShotConnection);
545 waylandWindow->requestXdgActivationToken(waylandApp->lastInputSerial());
548 return (
this->*callback)(url, {});
552bool QDesktopUnixServices::openUrlWithoutPortal(
const QUrl &url,
const QString &xdgActivationToken)
554 LaunchType launchType = url.scheme() ==
"mailto"_L1 ? LaunchType::Document : LaunchType::Browser;
555 return launchProcess(launchType, url, xdgActivationToken);
558bool QDesktopUnixServices::openUrlWithPortal(
const QUrl &url,
const QString &xdgActivationToken)
561 const QString parentWindow = portalFocusWindowIdentifier();
562 QDBusError error = url.scheme() ==
"mailto"_L1
563 ? xdgDesktopPortalSendEmail(url, parentWindow, xdgActivationToken)
564 : xdgDesktopPortalOpenUrl(url, parentWindow, xdgActivationToken);
565 if (!error.isValid())
567 if (error.type() == QDBusError::ServiceUnknown)
568 m_hasNoPortal =
true;
570 return openUrlWithoutPortal(url, xdgActivationToken);
573 Q_UNUSED(xdgActivationToken);
574 Q_UNREACHABLE_RETURN(
false);
578bool QDesktopUnixServices::openUrl(
const QUrl &url)
581 return runWithXdgActivationToken(&QDesktopUnixServices::openUrlWithPortal, url);
583 return runWithXdgActivationToken(&QDesktopUnixServices::openUrlWithoutPortal, url);
586bool QDesktopUnixServices::openDocumentWithoutPortal(
const QUrl &url,
const QString &xdgActivationToken)
588 return launchProcess(LaunchType::Document, url, xdgActivationToken);
591bool QDesktopUnixServices::openDocumentWithPortal(
const QUrl &url,
const QString &xdgActivationToken)
594 const QString parentWindow = portalFocusWindowIdentifier();
595 QDBusError error = xdgDesktopPortalOpenFile(url, parentWindow, xdgActivationToken);
596 if (!error.isValid())
598 if (error.type() == QDBusError::ServiceUnknown)
599 m_hasNoPortal =
true;
601 return openDocumentWithoutPortal(url, xdgActivationToken);
604 Q_UNUSED(xdgActivationToken);
605 Q_UNREACHABLE_RETURN(
false);
609bool QDesktopUnixServices::openDocument(
const QUrl &url)
612 return runWithXdgActivationToken(&QDesktopUnixServices::openDocumentWithPortal, url);
614 return runWithXdgActivationToken(&QDesktopUnixServices::openDocumentWithoutPortal, url);
618QDesktopUnixServices::QDesktopUnixServices() =
default;
619QDesktopUnixServices::~QDesktopUnixServices() =
default;
621QByteArray QDesktopUnixServices::desktopEnvironment()
const
623 return QByteArrayLiteral(
"UNKNOWN");
626bool QDesktopUnixServices::openUrl(
const QUrl &url)
629 qWarning(
"openUrl() not supported on this platform");
633bool QDesktopUnixServices::openDocument(
const QUrl &url)
636 qWarning(
"openDocument() not supported on this platform");
640QPlatformServiceColorPicker *QDesktopUnixServices::colorPicker(QWindow *parent)
648QString QDesktopUnixServices::portalWindowIdentifier(QWindow *window)
655void QDesktopUnixServices::registerDBusMenuForWindow(QWindow *window,
const QString &service,
const QString &path)
662void QDesktopUnixServices::unregisterDBusMenuForWindow(QWindow *window)
668bool QDesktopUnixServices::hasCapability(Capability capability)
const
670 switch (capability) {
671 case Capability::ColorPicking:
672 return m_hasScreenshotPortalWithColorPicking;
677void QDesktopUnixServices::setApplicationBadge(qint64 number)
680 if (qGuiApp->desktopFileName().isEmpty()) {
681 qCWarning(lcQpaServices,
"Cannot set badge number - QGuiApplication::desktopFileName() is empty");
686 const QString launcherUrl = QStringLiteral(
"application://") + qGuiApp->desktopFileName() + QStringLiteral(
".desktop");
687 const qint64 count = qBound(0, number, 9999);
688 QVariantMap dbusUnityProperties;
691 dbusUnityProperties[QStringLiteral(
"count")] = count;
692 dbusUnityProperties[QStringLiteral(
"count-visible")] =
true;
694 dbusUnityProperties[QStringLiteral(
"count-visible")] =
false;
697 auto signal = QDBusMessage::createSignal(QStringLiteral(
"/com/canonical/unity/launcherentry/")
698 + qGuiApp->applicationName(), QStringLiteral(
"com.canonical.Unity.LauncherEntry"), QStringLiteral(
"Update"));
700 signal.setArguments({launcherUrl, dbusUnityProperties});
702 QDBusConnection::sessionBus().send(signal);
710#include "qdesktopunixservices.moc"