summaryrefslogtreecommitdiffstats
path: root/src/gui/platform/unix/qdesktopunixservices.cpp
blob: 144a33614c2d6b397e979aa0a2fd36fcbbed6369 (plain)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
// 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"qdesktopunixservices_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>#if QT_CONFIG(process)# include <QtCore/QProcess>#endif#if QT_CONFIG(settings)#include <QtCore/QSettings>#endif#include <QtCore/QStandardPaths>#include <QtCore/QUrl>#if QT_CONFIG(dbus)// These QtCore includes are needed for xdg-desktop-portal support#include <QtCore/private/qcore_unix_p.h>#include <QtCore/QFileInfo>#include <QtCore/QUrlQuery>#include <QtDBus/QDBusConnection>#include <QtDBus/QDBusMessage>#include <QtDBus/QDBusPendingCall>#include <QtDBus/QDBusPendingCallWatcher>#include <QtDBus/QDBusPendingReply>#include <QtDBus/QDBusUnixFileDescriptor>#include <fcntl.h>#endif// QT_CONFIG(dbus)#include <stdlib.h> QT_BEGIN_NAMESPACE using namespaceQt::StringLiterals;#if QT_CONFIG(multiprocess)staticinline QByteArray detectDesktopEnvironment(){const QByteArray xdgCurrentDesktop =qgetenv("XDG_CURRENT_DESKTOP");if(!xdgCurrentDesktop.isEmpty())return xdgCurrentDesktop.toUpper();// KDE, GNOME, UNITY, LXDE, MATE, XFCE...// Classic fallbacksif(!qEnvironmentVariableIsEmpty("KDE_FULL_SESSION"))returnQByteArrayLiteral("KDE");if(!qEnvironmentVariableIsEmpty("GNOME_DESKTOP_SESSION_ID"))returnQByteArrayLiteral("GNOME");// Fallback to checking $DESKTOP_SESSION (unreliable) QByteArray desktopSession =qgetenv("DESKTOP_SESSION");// This can be a path in /usr/share/xsessionsint slash = desktopSession.lastIndexOf('/');if(slash != -1) {#if QT_CONFIG(settings) QSettings desktopFile(QFile::decodeName(desktopSession +".desktop"),QSettings::IniFormat); desktopFile.beginGroup(QStringLiteral("Desktop Entry")); QByteArray desktopName = desktopFile.value(QStringLiteral("DesktopNames")).toByteArray();if(!desktopName.isEmpty())return desktopName;#endif// try decoding just the basename desktopSession = desktopSession.mid(slash +1);}if(desktopSession =="gnome")returnQByteArrayLiteral("GNOME");else if(desktopSession =="xfce")returnQByteArrayLiteral("XFCE");else if(desktopSession =="kde")returnQByteArrayLiteral("KDE");returnQByteArrayLiteral("UNKNOWN");}staticinlineboolcheckExecutable(const QString &candidate, QString *result){*result =QStandardPaths::findExecutable(candidate);return!result->isEmpty();}staticinlinebooldetectWebBrowser(const QByteArray &desktop,bool checkBrowserVariable, QString *browser){const char*browsers[] = {"google-chrome","firefox","mozilla","opera"}; browser->clear();if(checkExecutable(QStringLiteral("xdg-open"), browser))return true;if(checkBrowserVariable) { QString browserVariable =qEnvironmentVariable("DEFAULT_BROWSER");if(browserVariable.isEmpty()) browserVariable =qEnvironmentVariable("BROWSER");if(!browserVariable.isEmpty() &&checkExecutable(browserVariable, browser))return true;}if(desktop ==QByteArray("KDE")) {if(checkExecutable(QStringLiteral("kde-open5"), browser))return true;// Konqueror launcherif(checkExecutable(QStringLiteral("kfmclient"), browser)) { browser->append(" exec"_L1);return true;}}else if(desktop ==QByteArray("GNOME")) {if(checkExecutable(QStringLiteral("gnome-open"), browser))return true;}for(size_t i =0; i <sizeof(browsers)/sizeof(char*); ++i)if(checkExecutable(QLatin1StringView(browsers[i]), browser))return true;return false;}staticinlineboollaunch(const QString &launcher,const QUrl &url,const QString &xdgActivationToken){const QString command = launcher + u' '+QLatin1StringView(url.toEncoded());qCDebug(lcQpaServices,"Launching %s",qPrintable(command));#if !QT_CONFIG(process)if(!xdgActivationToken.isEmpty())qputenv("XDG_ACTIVATION_TOKEN", xdgActivationToken.toUtf8());const bool ok = ::system(qPrintable(command +" &"_L1));if(!xdgActivationToken.isEmpty())qunsetenv("XDG_ACTIVATION_TOKEN");# else QStringList args =QProcess::splitCommand(command);bool ok =false;if(!args.isEmpty()) { QString program = args.takeFirst(); QProcess process; process.setProgram(program); process.setArguments(args);if(!xdgActivationToken.isEmpty()) {auto env =QProcessEnvironment::systemEnvironment(); env.insert(u"XDG_ACTIVATION_TOKEN"_s, xdgActivationToken); process.setEnvironment(env.toStringList());} ok = process.startDetached(nullptr);}# endifif(!ok)qCWarning(lcQpaServices,"Launch failed (%s)",qPrintable(command));return ok;}#if QT_CONFIG(dbus)staticinlineboolcheckNeedPortalSupport(){returnQFileInfo::exists("/.flatpak-info"_L1) ||qEnvironmentVariableIsSet("SNAP");}staticinline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url,const QString &parentWindow,const QString &xdgActivationToken){// DBus signature:// OpenFile (IN s parent_window,// IN h fd,// IN a{sv} options,// OUT o handle)// Options:// 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.const int fd =qt_safe_open(QFile::encodeName(url.toLocalFile()), O_RDONLY);if(fd != -1) { 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); QVariantMap options = {};if(!xdgActivationToken.isEmpty()) { options.insert("activation_token"_L1, xdgActivationToken);} message << parentWindow <<QVariant::fromValue(descriptor) << options;returnQDBusConnection::sessionBus().call(message);}returnQDBusMessage::createError(QDBusError::InternalError,qt_error_string());}staticinline QDBusMessage xdgDesktopPortalOpenUrl(const QUrl &url,const QString &parentWindow,const QString &xdgActivationToken){// DBus signature:// OpenURI (IN s parent_window,// IN s uri,// IN a{sv} options,// OUT o handle)// Options:// 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.// 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("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 QVariantMap options;if(!xdgActivationToken.isEmpty()) { options.insert("activation_token"_L1, xdgActivationToken);} message << parentWindow << url.toString() << options;returnQDBusConnection::sessionBus().call(message);}staticinline QDBusMessage xdgDesktopPortalSendEmail(const QUrl &url,const QString &parentWindow,const QString &xdgActivationToken){// DBus signature:// ComposeEmail (IN s parent_window,// IN a{sv} options,// OUT o handle)// Options:// address (s) - The email address to send to.// subject (s) - The subject for the email.// body (s) - The body for the email.// attachment_fds (ah) - File descriptors for files to attach. QUrlQuery urlQuery(url); QVariantMap options; 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("attachment"_L1);for(const QString &attachmentUri : attachmentUris) {const int fd =qt_safe_open(QFile::encodeName(attachmentUri), O_PATH);if(fd != -1) { QDBusUnixFileDescriptor descriptor(fd); attachments << descriptor;qt_safe_close(fd);}} options.insert("attachment_fds"_L1,QVariant::fromValue(attachments));#endifif(!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); message << parentWindow << options;returnQDBusConnection::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){}voidpickColor() 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 =newQDBusPendingCallWatcher(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 offSLOT(gotColorResponse(uint,QVariantMap))// clang-format on);}});}private Q_SLOTS:voidgotColorResponse(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)QDesktopUnixServices::QDesktopUnixServices(){if(detectDesktopEnvironment() ==QByteArrayLiteral("UNKNOWN"))return;#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 =newQDBusPendingCallWatcher(pendingCall); m_watcher = watcher;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}QDesktopUnixServices::~QDesktopUnixServices(){#if QT_CONFIG(dbus)delete m_watcher;#endif} QPlatformServiceColorPicker *QDesktopUnixServices::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 newXdgDesktopPortalColorPicker(portalWindowIdentifier(parent), parent);}returnnullptr;#elseQ_UNUSED(parent);returnnullptr;#endif} QByteArray QDesktopUnixServices::desktopEnvironment()const{static const QByteArray result =detectDesktopEnvironment();return result;}template<typename F>voidrunWithXdgActivationToken(F &&functionToCall){#if QT_CONFIG(wayland) QWindow *window = qGuiApp->focusWindow();if(!window) {functionToCall({});return;}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());#elsefunctionToCall({});#endif}boolQDesktopUnixServices::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}# endifreturnopenDocument(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;}# endifif(m_webBrowser.isEmpty()&& !detectWebBrowser(desktopEnvironment(),true, &m_webBrowser)) {qCWarning(lcQpaServices,"Unable to detect a web browser to launch '%s'",qPrintable(url.toString()));return false;}returnlaunch(m_webBrowser, url, xdgActivationToken);};if(QGuiApplication::platformName().startsWith("wayland"_L1)) {runWithXdgActivationToken([openUrlInternal, url](const QString &token) {openUrlInternal(url, token); });return true;}else{returnopenUrlInternal(url,QString());}}boolQDesktopUnixServices::openDocument(const QUrl &url){auto openDocumentInternal = [this](const QUrl &url,const QString &xdgActivationToken) {# 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;}# endifif(m_documentLauncher.isEmpty()&& !detectWebBrowser(desktopEnvironment(),false, &m_documentLauncher)) {qCWarning(lcQpaServices,"Unable to detect a launcher for '%s'",qPrintable(url.toString()));return false;}returnlaunch(m_documentLauncher, url, xdgActivationToken);};if(QGuiApplication::platformName().startsWith("wayland"_L1)) {runWithXdgActivationToken([openDocumentInternal, url](const QString &token) {openDocumentInternal(url, token);});return true;}else{returnopenDocumentInternal(url,QString());}}#elseQDesktopUnixServices::QDesktopUnixServices() =default;QDesktopUnixServices::~QDesktopUnixServices() =default; QByteArray QDesktopUnixServices::desktopEnvironment()const{returnQByteArrayLiteral("UNKNOWN");}boolQDesktopUnixServices::openUrl(const QUrl &url){Q_UNUSED(url);qWarning("openUrl() not supported on this platform");return false;}boolQDesktopUnixServices::openDocument(const QUrl &url){Q_UNUSED(url);qWarning("openDocument() not supported on this platform");return false;} QPlatformServiceColorPicker *QDesktopUnixServices::colorPicker(QWindow *parent){Q_UNUSED(parent);returnnullptr;}#endif// QT_CONFIG(multiprocess) QString QDesktopUnixServices::portalWindowIdentifier(QWindow *window){Q_UNUSED(window);returnQString();}voidQDesktopUnixServices::registerDBusMenuForWindow(QWindow *window,const QString &service,const QString &path){Q_UNUSED(window);Q_UNUSED(service);Q_UNUSED(path);}voidQDesktopUnixServices::unregisterDBusMenuForWindow(QWindow *window){Q_UNUSED(window);}boolQDesktopUnixServices::hasCapability(Capability capability)const{switch(capability) {caseCapability::ColorPicking:return m_hasScreenshotPortalWithColorPicking;}return false;}voidQDesktopUnixServices::setApplicationBadge(qint64 number){#if QT_CONFIG(dbus)if(qGuiApp->desktopFileName().isEmpty()) {qCWarning(lcQpaServices,"Cannot set badge number - 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);#elseQ_UNUSED(number)#endif} QT_END_NAMESPACE #include"qdesktopunixservices.moc"
close