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>
16# include <QtCore/QProcess>
18#if QT_CONFIG(settings)
19#include <QtCore/QSettings>
21#include <QtCore/QStandardPaths>
26#include <QtCore/private/qcore_unix_p.h>
28#include <QtCore/QFileInfo>
29#include <QtCore/QUrlQuery>
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>
47using namespace Qt::StringLiterals;
49#if QT_CONFIG(multiprocess)
51static inline QByteArray detectDesktopEnvironment()
53 const QByteArray xdgCurrentDesktop = qgetenv(
"XDG_CURRENT_DESKTOP");
54 if (!xdgCurrentDesktop.isEmpty())
55 return xdgCurrentDesktop.toUpper();
58 if (!qEnvironmentVariableIsEmpty(
"KDE_FULL_SESSION"))
59 return QByteArrayLiteral(
"KDE");
60 if (!qEnvironmentVariableIsEmpty(
"GNOME_DESKTOP_SESSION_ID"))
61 return QByteArrayLiteral(
"GNOME");
64 QByteArray desktopSession = qgetenv(
"DESKTOP_SESSION");
67 int slash = desktopSession.lastIndexOf(
'/');
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())
78 desktopSession = desktopSession.mid(slash + 1);
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");
88 return QByteArrayLiteral(
"UNKNOWN");
91static inline bool checkExecutable(
const QString &candidate, QString *result)
93 *result = QStandardPaths::findExecutable(candidate);
94 return !result->isEmpty();
97static inline bool detectWebBrowser(
const QByteArray &desktop,
98 bool checkBrowserVariable,
101 const char *browsers[] = {
"google-chrome",
"firefox",
"mozilla",
"opera"};
104 if (checkExecutable(QStringLiteral(
"xdg-open"), browser))
107 if (checkBrowserVariable) {
108 QString browserVariable = qEnvironmentVariable(
"DEFAULT_BROWSER");
109 if (browserVariable.isEmpty())
110 browserVariable = qEnvironmentVariable(
"BROWSER");
111 if (!browserVariable.isEmpty() && checkExecutable(browserVariable, browser))
115 if (desktop == QByteArray(
"KDE")) {
116 if (checkExecutable(QStringLiteral(
"kde-open5"), browser))
119 if (checkExecutable(QStringLiteral(
"kfmclient"), browser)) {
120 browser->append(
" exec"_L1);
123 }
else if (desktop == QByteArray(
"GNOME")) {
124 if (checkExecutable(QStringLiteral(
"gnome-open"), browser))
128 for (size_t i = 0; i <
sizeof(browsers)/
sizeof(
char *); ++i)
129 if (checkExecutable(QLatin1StringView(browsers[i]), browser))
134static inline bool launch(
const QString &launcher,
const QUrl &url,
135 const QString &xdgActivationToken)
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");
147 QStringList args = QProcess::splitCommand(command);
149 if (!args.isEmpty()) {
150 QString program = args.takeFirst();
152 process.setProgram(program);
153 process.setArguments(args);
155 if (!xdgActivationToken.isEmpty()) {
156 auto env = QProcessEnvironment::systemEnvironment();
157 env.insert(u"XDG_ACTIVATION_TOKEN"_s, xdgActivationToken);
158 process.setEnvironment(env.toStringList());
160 ok = process.startDetached(
nullptr);
164 qCWarning(lcQpaServices,
"Launch failed (%s)", qPrintable(command));
170static inline bool checkNeedPortalSupport()
172 return QFileInfo::exists(
"/.flatpak-info"_L1) || qEnvironmentVariableIsSet(
"SNAP");
175static inline QDBusMessage xdgDesktopPortalOpenFile(
const QUrl &url,
const QString &parentWindow,
176 const QString &xdgActivationToken)
187 const int fd = qt_safe_open(QFile::encodeName(url.toLocalFile()), O_RDONLY);
189 QDBusMessage message = QDBusMessage::createMethodCall(
"org.freedesktop.portal.Desktop"_L1,
190 "/org/freedesktop/portal/desktop"_L1,
191 "org.freedesktop.portal.OpenURI"_L1,
194 QDBusUnixFileDescriptor descriptor;
195 descriptor.giveFileDescriptor(fd);
197 QVariantMap options = {};
199 if (!xdgActivationToken.isEmpty()) {
200 options.insert(
"activation_token"_L1, xdgActivationToken);
203 message << parentWindow << QVariant::fromValue(descriptor) << options;
205 return QDBusConnection::sessionBus().call(message);
208 return QDBusMessage::createError(QDBusError::InternalError, qt_error_string());
211static inline QDBusMessage xdgDesktopPortalOpenUrl(
const QUrl &url,
const QString &parentWindow,
212 const QString &xdgActivationToken)
225 QDBusMessage message = QDBusMessage::createMethodCall(
"org.freedesktop.portal.Desktop"_L1,
226 "/org/freedesktop/portal/desktop"_L1,
227 "org.freedesktop.portal.OpenURI"_L1,
232 if (!xdgActivationToken.isEmpty()) {
233 options.insert(
"activation_token"_L1, xdgActivationToken);
236 message << parentWindow << url.toString() << options;
238 return QDBusConnection::sessionBus().call(message);
241static inline QDBusMessage xdgDesktopPortalSendEmail(
const QUrl &url,
const QString &parentWindow,
242 const QString &xdgActivationToken)
254 QUrlQuery urlQuery(url);
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));
262 QList<QDBusUnixFileDescriptor> attachments;
263 const QStringList attachmentUris = urlQuery.allQueryItemValues(
"attachment"_L1);
265 for (
const QString &attachmentUri : attachmentUris) {
266 const int fd = qt_safe_open(QFile::encodeName(attachmentUri), O_PATH);
268 QDBusUnixFileDescriptor descriptor(fd);
269 attachments << descriptor;
274 options.insert(
"attachment_fds"_L1, QVariant::fromValue(attachments));
277 if (!xdgActivationToken.isEmpty()) {
278 options.insert(
"activation_token"_L1, xdgActivationToken);
281 QDBusMessage message = QDBusMessage::createMethodCall(
"org.freedesktop.portal.Desktop"_L1,
282 "/org/freedesktop/portal/desktop"_L1,
283 "org.freedesktop.portal.Email"_L1,
286 message << parentWindow << options;
288 return QDBusConnection::sessionBus().call(message);
292struct XDGDesktopColor
298 QColor toQColor()
const
300 constexpr auto rgbMax = 255;
301 return {
static_cast<
int>(r * rgbMax),
static_cast<
int>(g * rgbMax),
302 static_cast<
int>(b * rgbMax) };
306const QDBusArgument &operator>>(
const QDBusArgument &argument, XDGDesktopColor &myStruct)
308 argument.beginStructure();
309 argument >> myStruct.r >> myStruct.g >> myStruct.b;
310 argument.endStructure();
314class XdgDesktopPortalColorPicker :
public QPlatformServiceColorPicker
318 XdgDesktopPortalColorPicker(
const QString &parentWindowId, QWindow *parent)
319 : QPlatformServiceColorPicker(parent), m_parentWindowId(parentWindowId)
323 void pickColor() override
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();
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({});
348 QDBusConnection::sessionBus().connect(
349 "org.freedesktop.portal.Desktop"_L1, reply.value().path(),
350 "org.freedesktop.portal.Request"_L1,
"Response"_L1,
this,
352 SLOT(gotColorResponse(uint,QVariantMap))
360 void gotColorResponse(uint result,
const QVariantMap &map)
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());
369 Q_EMIT colorPicked({});
375 const QString m_parentWindowId;
378void registerWithHostPortal()
380 static bool registered =
false;
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() });
390 new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), qGuiApp);
391 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, [watcher] {
392 watcher->deleteLater();
393 if (watcher->isError()) {
395 if (watcher->error().type() == QDBusError::UnknownInterface || watcher->error().type() == QDBusError::UnknownMethod)
396 qCInfo(lcQpaServices) <<
"Failed to register with host portal" << watcher->error();
398 qCWarning(lcQpaServices) <<
"Failed to register with host portal" << watcher->error();
400 qCDebug(lcQpaServices) <<
"Successfully registered with host portal as" << QGuiApplication::desktopFileName();
409QDesktopUnixServices::QDesktopUnixServices()
411 if (detectDesktopEnvironment() == QByteArrayLiteral(
"UNKNOWN"))
415 if (qEnvironmentVariableIntValue(
"QT_NO_XDG_DESKTOP_PORTAL") > 0) {
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
424 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
425 auto watcher =
new QDBusPendingCallWatcher(pendingCall);
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;
435 if (checkNeedPortalSupport()) {
441 if (!QGuiApplication::desktopFileName().isEmpty()) {
442 registerWithHostPortal();
444 QMetaObject::invokeMethod(
447 if (QGuiApplication::desktopFileName().isEmpty()) {
448 qCInfo(lcQpaServices) <<
"QGuiApplication::desktopFileName not set. Unable to register application with portal registry";
451 registerWithHostPortal();
453 Qt::QueuedConnection);
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(), ®isterWithHostPortal);
463QDesktopUnixServices::~QDesktopUnixServices()
470QPlatformServiceColorPicker *QDesktopUnixServices::colorPicker(QWindow *parent)
476 if (!qEnvironmentVariableIsEmpty(
"WAYLAND_DISPLAY")
477 || QGuiApplication::platformName().startsWith(
"wayland"_L1)) {
478 return new XdgDesktopPortalColorPicker(portalWindowIdentifier(parent), parent);
487QByteArray QDesktopUnixServices::desktopEnvironment()
const
489 static const QByteArray result = detectDesktopEnvironment();
494void runWithXdgActivationToken(F &&functionToCall)
496#if QT_CONFIG(wayland)
497 QWindow *window = qGuiApp->focusWindow();
504 auto waylandApp =
dynamic_cast<QNativeInterface::QWaylandApplication *>(
505 qGuiApp->platformNativeInterface());
507 dynamic_cast<QNativeInterface::Private::QWaylandWindow *>(window->handle());
509 if (!waylandWindow || !waylandApp) {
514 QObject::connect(waylandWindow,
515 &QNativeInterface::Private::QWaylandWindow::xdgActivationTokenCreated,
516 waylandWindow, functionToCall, Qt::SingleShotConnection);
517 waylandWindow->requestXdgActivationToken(waylandApp->lastInputSerial());
523bool QDesktopUnixServices::openUrl(
const QUrl &url)
525 auto openUrlInternal = [
this](
const QUrl &url,
const QString &xdgActivationToken) {
526 if (url.scheme() ==
"mailto"_L1) {
528 if (checkNeedPortalSupport()) {
529 const QString parentWindow = QGuiApplication::focusWindow()
530 ? portalWindowIdentifier(QGuiApplication::focusWindow())
532 QDBusError error = xdgDesktopPortalSendEmail(url, parentWindow, xdgActivationToken);
533 if (!error.isValid())
539 return openDocument(url);
543 if (checkNeedPortalSupport()) {
544 const QString parentWindow = QGuiApplication::focusWindow()
545 ? portalWindowIdentifier(QGuiApplication::focusWindow())
547 QDBusError error = xdgDesktopPortalOpenUrl(url, parentWindow, xdgActivationToken);
548 if (!error.isValid())
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()));
558 return launch(m_webBrowser, url, xdgActivationToken);
561 if (QGuiApplication::platformName().startsWith(
"wayland"_L1)) {
562 runWithXdgActivationToken(
563 [openUrlInternal, url](
const QString &token) { openUrlInternal(url, token); });
568 return openUrlInternal(url, QString());
572bool QDesktopUnixServices::openDocument(
const QUrl &url)
574 auto openDocumentInternal = [
this](
const QUrl &url,
const QString &xdgActivationToken) {
577 if (checkNeedPortalSupport()) {
578 const QString parentWindow = QGuiApplication::focusWindow()
579 ? portalWindowIdentifier(QGuiApplication::focusWindow())
581 QDBusError error = xdgDesktopPortalOpenFile(url, parentWindow, xdgActivationToken);
582 if (!error.isValid())
587 if (m_documentLauncher.isEmpty()
588 && !detectWebBrowser(desktopEnvironment(),
false, &m_documentLauncher)) {
589 qCWarning(lcQpaServices,
"Unable to detect a launcher for '%s'", qPrintable(url.toString()));
592 return launch(m_documentLauncher, url, xdgActivationToken);
595 if (QGuiApplication::platformName().startsWith(
"wayland"_L1)) {
596 runWithXdgActivationToken([openDocumentInternal, url](
const QString &token) {
597 openDocumentInternal(url, token);
602 return openDocumentInternal(url, QString());
607QDesktopUnixServices::QDesktopUnixServices() =
default;
608QDesktopUnixServices::~QDesktopUnixServices() =
default;
610QByteArray QDesktopUnixServices::desktopEnvironment()
const
612 return QByteArrayLiteral(
"UNKNOWN");
615bool QDesktopUnixServices::openUrl(
const QUrl &url)
618 qWarning(
"openUrl() not supported on this platform");
622bool QDesktopUnixServices::openDocument(
const QUrl &url)
625 qWarning(
"openDocument() not supported on this platform");
629QPlatformServiceColorPicker *QDesktopUnixServices::colorPicker(QWindow *parent)
637QString QDesktopUnixServices::portalWindowIdentifier(QWindow *window)
644void QDesktopUnixServices::registerDBusMenuForWindow(QWindow *window,
const QString &service,
const QString &path)
651void QDesktopUnixServices::unregisterDBusMenuForWindow(QWindow *window)
657bool QDesktopUnixServices::hasCapability(Capability capability)
const
659 switch (capability) {
660 case Capability::ColorPicking:
661 return m_hasScreenshotPortalWithColorPicking;
666void QDesktopUnixServices::setApplicationBadge(qint64 number)
669 if (qGuiApp->desktopFileName().isEmpty()) {
670 qCWarning(lcQpaServices,
"Cannot set badge number - QGuiApplication::desktopFileName() is empty");
675 const QString launcherUrl = QStringLiteral(
"application://") + qGuiApp->desktopFileName() + QStringLiteral(
".desktop");
676 const qint64 count = qBound(0, number, 9999);
677 QVariantMap dbusUnityProperties;
680 dbusUnityProperties[QStringLiteral(
"count")] = count;
681 dbusUnityProperties[QStringLiteral(
"count-visible")] =
true;
683 dbusUnityProperties[QStringLiteral(
"count-visible")] =
false;
686 auto signal = QDBusMessage::createSignal(QStringLiteral(
"/com/canonical/unity/launcherentry/")
687 + qGuiApp->applicationName(), QStringLiteral(
"com.canonical.Unity.LauncherEntry"), QStringLiteral(
"Update"));
689 signal.setArguments({launcherUrl, dbusUnityProperties});
691 QDBusConnection::sessionBus().send(signal);
699#include "qdesktopunixservices.moc"