diff options
Diffstat (limited to 'src/gui/platform/unix/qgenericunixservices.cpp')
-rw-r--r-- | src/gui/platform/unix/qgenericunixservices.cpp | 465 |
1 files changed, 351 insertions, 114 deletions
diff --git a/src/gui/platform/unix/qgenericunixservices.cpp b/src/gui/platform/unix/qgenericunixservices.cpp index e7b9ae3368..bfd2556b1e 100644 --- a/src/gui/platform/unix/qgenericunixservices.cpp +++ b/src/gui/platform/unix/qgenericunixservices.cpp @@ -1,44 +1,13 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the plugins of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qgenericunixservices_p.h" #include <QtGui/private/qtguiglobal_p.h> +#include "qguiapplication.h" +#include "qwindow.h" +#include <QtGui/qpa/qplatformwindow_p.h> +#include <QtGui/qpa/qplatformwindow.h> +#include <QtGui/qpa/qplatformnativeinterface.h> #include <QtCore/QDebug> #include <QtCore/QFile> @@ -73,6 +42,8 @@ QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + #if QT_CONFIG(multiprocess) enum { debug = 0 }; @@ -142,9 +113,11 @@ static inline bool detectWebBrowser(const QByteArray &desktop, } if (desktop == QByteArray("KDE")) { + if (checkExecutable(QStringLiteral("kde-open5"), browser)) + return true; // Konqueror launcher if (checkExecutable(QStringLiteral("kfmclient"), browser)) { - browser->append(QLatin1String(" exec")); + browser->append(" exec"_L1); return true; } } else if (desktop == QByteArray("GNOME")) { @@ -153,18 +126,23 @@ static inline bool detectWebBrowser(const QByteArray &desktop, } for (size_t i = 0; i < sizeof(browsers)/sizeof(char *); ++i) - if (checkExecutable(QLatin1String(browsers[i]), browser)) + if (checkExecutable(QLatin1StringView(browsers[i]), browser)) return true; return false; } -static inline bool launch(const QString &launcher, const QUrl &url) +static inline bool launch(const QString &launcher, const QUrl &url, + const QString &xdgActivationToken) { - const QString command = launcher + QLatin1Char(' ') + QLatin1String(url.toEncoded()); + if (!xdgActivationToken.isEmpty()) { + qputenv("XDG_ACTIVATION_TOKEN", xdgActivationToken.toUtf8()); + } + + const QString command = launcher + u' ' + QLatin1StringView(url.toEncoded()); if (debug) qDebug("Launching %s", qPrintable(command)); #if !QT_CONFIG(process) - const bool ok = ::system(qPrintable(command + QLatin1String(" &"))); + const bool ok = ::system(qPrintable(command + " &"_L1)); #else QStringList args = QProcess::splitCommand(command); bool ok = false; @@ -175,24 +153,20 @@ static inline bool launch(const QString &launcher, const QUrl &url) #endif if (!ok) qWarning("Launch failed (%s)", qPrintable(command)); + + qunsetenv("XDG_ACTIVATION_TOKEN"); + return ok; } #if QT_CONFIG(dbus) static inline bool checkNeedPortalSupport() { - return !QStandardPaths::locate(QStandardPaths::RuntimeLocation, QLatin1String("flatpak-info")).isEmpty() || qEnvironmentVariableIsSet("SNAP"); -} - -static inline bool isPortalReturnPermanent(const QDBusError &error) -{ - // A service unknown error isn't permanent, it just indicates that we - // should fall back to the regular way. This check includes - // QDBusError::NoError. - return error.type() != QDBusError::ServiceUnknown && error.type() != QDBusError::AccessDenied; + return QFileInfo::exists("/.flatpak-info"_L1) || qEnvironmentVariableIsSet("SNAP"); } -static inline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url) +static inline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url, const QString &parentWindow, + const QString &xdgActivationToken) { // DBus signature: // OpenFile (IN s parent_window, @@ -203,32 +177,32 @@ static inline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url) // handle_token (s) - A string that will be used as the last element of the @handle. // writable (b) - Whether to allow the chosen application to write to the file. -#ifdef O_PATH - const int fd = qt_safe_open(QFile::encodeName(url.toLocalFile()), O_PATH); + const int fd = qt_safe_open(QFile::encodeName(url.toLocalFile()), O_RDONLY); if (fd != -1) { - QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop"), - QLatin1String("/org/freedesktop/portal/desktop"), - QLatin1String("org.freedesktop.portal.OpenURI"), - QLatin1String("OpenFile")); + QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.portal.Desktop"_L1, + "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.portal.OpenURI"_L1, + "OpenFile"_L1); QDBusUnixFileDescriptor descriptor; descriptor.giveFileDescriptor(fd); - const QVariantMap options = {{QLatin1String("writable"), true}}; + QVariantMap options = {}; - // FIXME parent_window_id - message << QString() << QVariant::fromValue(descriptor) << options; + if (!xdgActivationToken.isEmpty()) { + options.insert("activation_token"_L1, xdgActivationToken); + } + + message << parentWindow << QVariant::fromValue(descriptor) << options; return QDBusConnection::sessionBus().call(message); } -#else - Q_UNUSED(url); -#endif return QDBusMessage::createError(QDBusError::InternalError, qt_error_string()); } -static inline QDBusMessage xdgDesktopPortalOpenUrl(const QUrl &url) +static inline QDBusMessage xdgDesktopPortalOpenUrl(const QUrl &url, const QString &parentWindow, + const QString &xdgActivationToken) { // DBus signature: // OpenURI (IN s parent_window, @@ -241,17 +215,24 @@ static inline QDBusMessage xdgDesktopPortalOpenUrl(const QUrl &url) // This key only takes effect the uri points to a local file that is exported in the document portal, // and the chosen application is sandboxed itself. - QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop"), - QLatin1String("/org/freedesktop/portal/desktop"), - QLatin1String("org.freedesktop.portal.OpenURI"), - QLatin1String("OpenURI")); + QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.portal.Desktop"_L1, + "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.portal.OpenURI"_L1, + "OpenURI"_L1); // FIXME parent_window_id and handle writable option - message << QString() << url.toString() << QVariantMap(); + QVariantMap options; + + if (!xdgActivationToken.isEmpty()) { + options.insert("activation_token"_L1, xdgActivationToken); + } + + message << parentWindow << url.toString() << options; return QDBusConnection::sessionBus().call(message); } -static inline QDBusMessage xdgDesktopPortalSendEmail(const QUrl &url) +static inline QDBusMessage xdgDesktopPortalSendEmail(const QUrl &url, const QString &parentWindow, + const QString &xdgActivationToken) { // DBus signature: // ComposeEmail (IN s parent_window, @@ -265,14 +246,14 @@ static inline QDBusMessage xdgDesktopPortalSendEmail(const QUrl &url) QUrlQuery urlQuery(url); QVariantMap options; - options.insert(QLatin1String("address"), url.path()); - options.insert(QLatin1String("subject"), urlQuery.queryItemValue(QLatin1String("subject"))); - options.insert(QLatin1String("body"), urlQuery.queryItemValue(QLatin1String("body"))); + options.insert("address"_L1, url.path()); + options.insert("subject"_L1, urlQuery.queryItemValue("subject"_L1)); + options.insert("body"_L1, urlQuery.queryItemValue("body"_L1)); // O_PATH seems to be present since Linux 2.6.39, which is not case of RHEL 6 #ifdef O_PATH QList<QDBusUnixFileDescriptor> attachments; - const QStringList attachmentUris = urlQuery.allQueryItemValues(QLatin1String("attachment")); + const QStringList attachmentUris = urlQuery.allQueryItemValues("attachment"_L1); for (const QString &attachmentUri : attachmentUris) { const int fd = qt_safe_open(QFile::encodeName(attachmentUri), O_PATH); @@ -283,75 +264,277 @@ static inline QDBusMessage xdgDesktopPortalSendEmail(const QUrl &url) } } - options.insert(QLatin1String("attachment_fds"), QVariant::fromValue(attachments)); + options.insert("attachment_fds"_L1, QVariant::fromValue(attachments)); #endif - QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop"), - QLatin1String("/org/freedesktop/portal/desktop"), - QLatin1String("org.freedesktop.portal.Email"), - QLatin1String("ComposeEmail")); + if (!xdgActivationToken.isEmpty()) { + options.insert("activation_token"_L1, xdgActivationToken); + } + + QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.portal.Desktop"_L1, + "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.portal.Email"_L1, + "ComposeEmail"_L1); - // FIXME parent_window_id - message << QString() << options; + message << parentWindow << options; return QDBusConnection::sessionBus().call(message); } + +namespace { +struct XDGDesktopColor +{ + double r = 0; + double g = 0; + double b = 0; + + QColor toQColor() const + { + constexpr auto rgbMax = 255; + return { static_cast<int>(r * rgbMax), static_cast<int>(g * rgbMax), + static_cast<int>(b * rgbMax) }; + } +}; + +const QDBusArgument &operator>>(const QDBusArgument &argument, XDGDesktopColor &myStruct) +{ + argument.beginStructure(); + argument >> myStruct.r >> myStruct.g >> myStruct.b; + argument.endStructure(); + return argument; +} + +class XdgDesktopPortalColorPicker : public QPlatformServiceColorPicker +{ + Q_OBJECT +public: + XdgDesktopPortalColorPicker(const QString &parentWindowId, QWindow *parent) + : QPlatformServiceColorPicker(parent), m_parentWindowId(parentWindowId) + { + } + + void pickColor() override + { + // DBus signature: + // PickColor (IN s parent_window, + // IN a{sv} options + // OUT o handle) + // Options: + // handle_token (s) - A string that will be used as the last element of the @handle. + + QDBusMessage message = QDBusMessage::createMethodCall( + "org.freedesktop.portal.Desktop"_L1, "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.portal.Screenshot"_L1, "PickColor"_L1); + message << m_parentWindowId << QVariantMap(); + + QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); + auto watcher = new QDBusPendingCallWatcher(pendingCall, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [this](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + QDBusPendingReply<QDBusObjectPath> reply = *watcher; + if (reply.isError()) { + qWarning("DBus call to pick color failed: %s", + qPrintable(reply.error().message())); + Q_EMIT colorPicked({}); + } else { + QDBusConnection::sessionBus().connect( + "org.freedesktop.portal.Desktop"_L1, reply.value().path(), + "org.freedesktop.portal.Request"_L1, "Response"_L1, this, + // clang-format off + SLOT(gotColorResponse(uint,QVariantMap)) + // clang-format on + ); + } + }); + } + +private Q_SLOTS: + void gotColorResponse(uint result, const QVariantMap &map) + { + if (result != 0) + return; + if (map.contains(u"color"_s)) { + XDGDesktopColor color{}; + map.value(u"color"_s).value<QDBusArgument>() >> color; + Q_EMIT colorPicked(color.toQColor()); + } else { + Q_EMIT colorPicked({}); + } + deleteLater(); + } + +private: + const QString m_parentWindowId; +}; +} // namespace + #endif // QT_CONFIG(dbus) +QGenericUnixServices::QGenericUnixServices() +{ +#if QT_CONFIG(dbus) + if (qEnvironmentVariableIntValue("QT_NO_XDG_DESKTOP_PORTAL") > 0) { + return; + } + QDBusMessage message = QDBusMessage::createMethodCall( + "org.freedesktop.portal.Desktop"_L1, "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.DBus.Properties"_L1, "Get"_L1); + message << "org.freedesktop.portal.Screenshot"_L1 + << "version"_L1; + + QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); + auto watcher = new QDBusPendingCallWatcher(pendingCall); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, + [this](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + QDBusPendingReply<QVariant> reply = *watcher; + if (!reply.isError() && reply.value().toUInt() >= 2) + m_hasScreenshotPortalWithColorPicking = true; + }); + +#endif +} + +QPlatformServiceColorPicker *QGenericUnixServices::colorPicker(QWindow *parent) +{ +#if QT_CONFIG(dbus) + // Make double sure that we are in a wayland environment. In particular check + // WAYLAND_DISPLAY so also XWayland apps benefit from portal-based color picking. + // Outside wayland we'll rather rely on other means than the XDG desktop portal. + if (!qEnvironmentVariableIsEmpty("WAYLAND_DISPLAY") + || QGuiApplication::platformName().startsWith("wayland"_L1)) { + return new XdgDesktopPortalColorPicker(portalWindowIdentifier(parent), parent); + } + return nullptr; +#else + Q_UNUSED(parent); + return nullptr; +#endif +} + QByteArray QGenericUnixServices::desktopEnvironment() const { static const QByteArray result = detectDesktopEnvironment(); return result; } -bool QGenericUnixServices::openUrl(const QUrl &url) +template<typename F> +void runWithXdgActivationToken(F &&functionToCall) { - if (url.scheme() == QLatin1String("mailto")) { -#if QT_CONFIG(dbus) - if (checkNeedPortalSupport()) { - QDBusError error = xdgDesktopPortalSendEmail(url); - if (isPortalReturnPermanent(error)) - return !error.isValid(); +#if QT_CONFIG(wayland) + QWindow *window = qGuiApp->focusWindow(); - // service not running, fall back - } -#endif - return openDocument(url); + if (!window) { + functionToCall({}); + return; } -#if QT_CONFIG(dbus) - if (checkNeedPortalSupport()) { - QDBusError error = xdgDesktopPortalOpenUrl(url); - if (isPortalReturnPermanent(error)) - return !error.isValid(); + auto waylandApp = dynamic_cast<QNativeInterface::QWaylandApplication *>( + qGuiApp->platformNativeInterface()); + auto waylandWindow = + dynamic_cast<QNativeInterface::Private::QWaylandWindow *>(window->handle()); + + if (!waylandWindow || !waylandApp) { + functionToCall({}); + return; } + + QObject::connect(waylandWindow, + &QNativeInterface::Private::QWaylandWindow::xdgActivationTokenCreated, + waylandWindow, functionToCall, Qt::SingleShotConnection); + waylandWindow->requestXdgActivationToken(waylandApp->lastInputSerial()); +#else + functionToCall({}); #endif +} + +bool QGenericUnixServices::openUrl(const QUrl &url) +{ + auto openUrlInternal = [this](const QUrl &url, const QString &xdgActivationToken) { + if (url.scheme() == "mailto"_L1) { +# if QT_CONFIG(dbus) + if (checkNeedPortalSupport()) { + const QString parentWindow = QGuiApplication::focusWindow() + ? portalWindowIdentifier(QGuiApplication::focusWindow()) + : QString(); + QDBusError error = xdgDesktopPortalSendEmail(url, parentWindow, xdgActivationToken); + if (!error.isValid()) + return true; + + // service not running, fall back + } +# endif + return openDocument(url); + } + +# if QT_CONFIG(dbus) + if (checkNeedPortalSupport()) { + const QString parentWindow = QGuiApplication::focusWindow() + ? portalWindowIdentifier(QGuiApplication::focusWindow()) + : QString(); + QDBusError error = xdgDesktopPortalOpenUrl(url, parentWindow, xdgActivationToken); + if (!error.isValid()) + return true; + } +# endif + + if (m_webBrowser.isEmpty() + && !detectWebBrowser(desktopEnvironment(), true, &m_webBrowser)) { + qWarning("Unable to detect a web browser to launch '%s'", qPrintable(url.toString())); + return false; + } + return launch(m_webBrowser, url, xdgActivationToken); + }; + + if (QGuiApplication::platformName().startsWith("wayland"_L1)) { + runWithXdgActivationToken( + [openUrlInternal, url](const QString &token) { openUrlInternal(url, token); }); + + return true; - if (m_webBrowser.isEmpty() && !detectWebBrowser(desktopEnvironment(), true, &m_webBrowser)) { - qWarning("Unable to detect a web browser to launch '%s'", qPrintable(url.toString())); - return false; + } else { + return openUrlInternal(url, QString()); } - return launch(m_webBrowser, url); } bool QGenericUnixServices::openDocument(const QUrl &url) { -#if QT_CONFIG(dbus) - if (checkNeedPortalSupport()) { - QDBusError error = xdgDesktopPortalOpenFile(url); - if (isPortalReturnPermanent(error)) - return !error.isValid(); - } -#endif + auto openDocumentInternal = [this](const QUrl &url, const QString &xdgActivationToken) { - if (m_documentLauncher.isEmpty() && !detectWebBrowser(desktopEnvironment(), false, &m_documentLauncher)) { - qWarning("Unable to detect a launcher for '%s'", qPrintable(url.toString())); - return false; +# if QT_CONFIG(dbus) + if (checkNeedPortalSupport()) { + const QString parentWindow = QGuiApplication::focusWindow() + ? portalWindowIdentifier(QGuiApplication::focusWindow()) + : QString(); + QDBusError error = xdgDesktopPortalOpenFile(url, parentWindow, xdgActivationToken); + if (!error.isValid()) + return true; + } +# endif + + if (m_documentLauncher.isEmpty() + && !detectWebBrowser(desktopEnvironment(), false, &m_documentLauncher)) { + qWarning("Unable to detect a launcher for '%s'", qPrintable(url.toString())); + return false; + } + return launch(m_documentLauncher, url, xdgActivationToken); + }; + + if (QGuiApplication::platformName().startsWith("wayland"_L1)) { + runWithXdgActivationToken([openDocumentInternal, url](const QString &token) { + openDocumentInternal(url, token); + }); + + return true; + } else { + return openDocumentInternal(url, QString()); } - return launch(m_documentLauncher, url); } #else +QGenericUnixServices::QGenericUnixServices() = default; + QByteArray QGenericUnixServices::desktopEnvironment() const { return QByteArrayLiteral("UNKNOWN"); @@ -371,6 +554,60 @@ bool QGenericUnixServices::openDocument(const QUrl &url) return false; } +QPlatformServiceColorPicker *QGenericUnixServices::colorPicker(QWindow *parent) +{ + Q_UNUSED(parent); + return nullptr; +} + #endif // QT_NO_MULTIPROCESS +QString QGenericUnixServices::portalWindowIdentifier(QWindow *window) +{ + Q_UNUSED(window); + return QString(); +} + +bool QGenericUnixServices::hasCapability(Capability capability) const +{ + switch (capability) { + case Capability::ColorPicking: + return m_hasScreenshotPortalWithColorPicking; + } + return false; +} + +void QGenericUnixServices::setApplicationBadge(qint64 number) +{ +#if QT_CONFIG(dbus) + if (qGuiApp->desktopFileName().isEmpty()) { + qWarning("QGuiApplication::desktopFileName() is empty"); + return; + } + + + const QString launcherUrl = QStringLiteral("application://") + qGuiApp->desktopFileName() + QStringLiteral(".desktop"); + const qint64 count = qBound(0, number, 9999); + QVariantMap dbusUnityProperties; + + if (count > 0) { + dbusUnityProperties[QStringLiteral("count")] = count; + dbusUnityProperties[QStringLiteral("count-visible")] = true; + } else { + dbusUnityProperties[QStringLiteral("count-visible")] = false; + } + + auto signal = QDBusMessage::createSignal(QStringLiteral("/com/canonical/unity/launcherentry/") + + qGuiApp->applicationName(), QStringLiteral("com.canonical.Unity.LauncherEntry"), QStringLiteral("Update")); + + signal.setArguments({launcherUrl, dbusUnityProperties}); + + QDBusConnection::sessionBus().send(signal); +#else + Q_UNUSED(number) +#endif +} + QT_END_NAMESPACE + +#include "qgenericunixservices.moc" |