-
Notifications
You must be signed in to change notification settings - Fork 929
Implement XDG Desktop Portal support #9727
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
|
|
||
| #include "utility.h" | ||
| #include "config.h" | ||
| #include "xdg_portal.h" | ||
|
|
||
| #include <QCoreApplication> | ||
| #include <QDir> | ||
|
|
@@ -72,6 +73,10 @@ bool Utility::hasSystemLaunchOnStartup(const QString &appName) | |
|
|
||
| bool Utility::hasLaunchOnStartup(const QString &appName) | ||
| { | ||
| //! if we are using portals this check will not work, that's why its usage in generalsettings.cpp is currently disabled. | ||
| //! as far as i can tell the portal does not let us do this check. how badly do we want it? the only two times its used is | ||
| //! that time in generalsettings.cpp and in tests, for settings as far as i can tell that check isn't really *needed* and | ||
| //! we might want to adapt tests to portals anyway... | ||
| const QString desktopFileLocation = getUserAutostartDir() + appName + QLatin1String(".desktop"); | ||
| return QFile::exists(desktopFileLocation); | ||
| } | ||
|
|
@@ -96,43 +101,58 @@ QString Utility::syncFolderDisplayName(const QString &folder, const QString &dis | |
|
|
||
| void Utility::setLaunchOnStartup(const QString &appName, const QString &guiName, bool enable) | ||
| { | ||
| const auto userAutoStartPath = getUserAutostartDir(); | ||
| const QString desktopFileLocation = userAutoStartPath + appName + QLatin1String(".desktop"); | ||
| if (enable) { | ||
| if (!QDir().exists(userAutoStartPath) && !QDir().mkpath(userAutoStartPath)) { | ||
| qCWarning(lcUtility) << "Could not create autostart folder" << userAutoStartPath; | ||
| return; | ||
| } | ||
| QFile iniFile(desktopFileLocation); | ||
| if (!iniFile.open(QIODevice::WriteOnly)) { | ||
| qCWarning(lcUtility) << "Could not write auto start entry" << desktopFileLocation; | ||
| return; | ||
| } | ||
| // When running inside an AppImage, we need to set the path to the | ||
| // AppImage instead of the path to the executable | ||
| const QString appImagePath = qEnvironmentVariable("APPIMAGE"); | ||
| const bool runningInsideAppImage = !appImagePath.isNull() && QFile::exists(appImagePath); | ||
| const QString executablePath = runningInsideAppImage ? appImagePath : QCoreApplication::applicationFilePath(); | ||
|
|
||
| QTextStream ts(&iniFile); | ||
| ts << QLatin1String("[Desktop Entry]\n") | ||
| << QLatin1String("Name=") << guiName << QLatin1Char('\n') | ||
| << QLatin1String("GenericName=") << QLatin1String("File Synchronizer\n") | ||
| << QLatin1String("Exec=\"") << executablePath << "\" --background\n" | ||
| << QLatin1String("Terminal=") << "false\n" | ||
| << QLatin1String("Icon=") << APPLICATION_ICON_NAME << QLatin1Char('\n') | ||
| << QLatin1String("Categories=") << QLatin1String("Network\n") | ||
| << QLatin1String("Type=") << QLatin1String("Application\n") | ||
| << QLatin1String("StartupNotify=") << "false\n" | ||
| << QLatin1String("X-GNOME-Autostart-enabled=") << "true\n" | ||
| << QLatin1String("X-GNOME-Autostart-Delay=10") << Qt::endl; | ||
| const bool usePortals = true; // TODO: how should this be configured? probably a build flag? or should we just default always use portals and if the call fails *then* fallback? XdgPortal::background does return a bool for if it was successful so that wouldn't be a difficulty. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using XDG portals when available with a fallback to the old way when not seems sensible to me. if possible we should also remove the old non-portal autostart entry while toggling this setting. This is probably not too relevant for Flatpak, but for packages provided by distros this would make a lot of sense |
||
| if (usePortals) { | ||
| XdgPortal portal; | ||
| portal.background(appName, enable); | ||
| } else { | ||
| if (!QFile::remove(desktopFileLocation)) { | ||
| qCWarning(lcUtility) << "Could not remove autostart desktop file"; | ||
| const auto userAutoStartPath = getUserAutostartDir(); | ||
| const QString desktopFileLocation = userAutoStartPath + appName + QLatin1String(".desktop"); | ||
|
Comment on lines
+105
to
+110
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make use of early-returns when possible: if (usePortals) {
XdgPortal portal;
portal.background(appName, enable);
return;
}
const auto userAutoStartPath = getUserAutostartDir();
const QString desktopFileLocation = userAutoStartPath + appName + QLatin1String(".desktop");
// etc, etc |
||
| if (enable) { | ||
| if (!QDir().exists(userAutoStartPath) && !QDir().mkpath(userAutoStartPath)) { | ||
| qCWarning(lcUtility) << "Could not create autostart folder" << userAutoStartPath; | ||
| return; | ||
| } | ||
| QFile iniFile(desktopFileLocation); | ||
| if (!iniFile.open(QIODevice::WriteOnly)) { | ||
| qCWarning(lcUtility) << "Could not write auto start entry" << desktopFileLocation; | ||
| return; | ||
| } | ||
|
|
||
| const auto executablePath = Utility::getAppExecutablePath(); | ||
|
|
||
| QTextStream ts(&iniFile); | ||
| ts << QLatin1String("[Desktop Entry]\n") | ||
| << QLatin1String("Name=") << guiName << QLatin1Char('\n') | ||
| << QLatin1String("GenericName=") << QLatin1String("File Synchronizer\n") | ||
| << QLatin1String("Exec=\"") << executablePath << "\" --background\n" | ||
| << QLatin1String("Terminal=") << "false\n" | ||
| << QLatin1String("Icon=") << APPLICATION_ICON_NAME << QLatin1Char('\n') | ||
| << QLatin1String("Categories=") << QLatin1String("Network\n") | ||
| << QLatin1String("Type=") << QLatin1String("Application\n") | ||
| << QLatin1String("StartupNotify=") << "false\n" | ||
| << QLatin1String("X-GNOME-Autostart-enabled=") << "true\n" | ||
| << QLatin1String("X-GNOME-Autostart-Delay=10") << Qt::endl; | ||
| } else { | ||
| if (!QFile::remove(desktopFileLocation)) { | ||
| qCWarning(lcUtility) << "Could not remove autostart desktop file"; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| QString Utility::getAppExecutablePath() | ||
| { | ||
| // When running inside an AppImage, we need to set the path to the | ||
| // AppImage instead of the path to the executable | ||
| const QString appImagePath = qEnvironmentVariable("APPIMAGE"); | ||
| const QString flatpakId = qEnvironmentVariable("FLATPAK_ID"); | ||
| const bool runningInsideAppImage = !appImagePath.isNull() && QFile::exists(appImagePath); | ||
| const bool runningInsideFlatpak = !flatpakId.isNull(); | ||
| const QString executablePath = runningInsideAppImage ? QLatin1String("\"%1\"").arg(appImagePath) : runningInsideFlatpak ? QLatin1String("flatpak run %1").arg(flatpakId) : QLatin1String("\"%1\"").arg(QCoreApplication::applicationFilePath()); | ||
| return executablePath; | ||
| } | ||
|
|
||
| bool Utility::hasDarkSystray() | ||
| { | ||
| return true; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: GPL-2.0-or-later | ||
| */ | ||
|
|
||
| #include "xdg_portal.h" | ||
| #include "utility.h" | ||
| #include <QtDBus/QDBusConnection> | ||
| #include <QtDBus/QDBusConnectionInterface> | ||
| #include <QtDBus/QDBusInterface> | ||
| #include <QtDBus/QDBusMessage> | ||
| #include <QDebug> | ||
| #include <QFileInfo> | ||
| #include <QCoreApplication> | ||
|
|
||
| namespace OCC { | ||
|
|
||
| constexpr auto portalDesktopService = "org.freedesktop.portal.Desktop"; | ||
| constexpr auto portalDesktopPath = "/org/freedesktop/portal/desktop"; | ||
|
|
||
| XdgPortal::XdgPortal(QObject *parent) | ||
| : QObject(parent) | ||
| , m_available(false) | ||
| { | ||
| initPortalInterface(); | ||
| } | ||
|
|
||
| void XdgPortal::initPortalInterface() | ||
| { | ||
| auto *busInterface = QDBusConnection::sessionBus().interface(); | ||
| if (!busInterface) { | ||
| qWarning() << "XDG Desktop Portal not available"; | ||
| return; | ||
| } | ||
|
|
||
| const auto registered = busInterface->isServiceRegistered(QLatin1String(portalDesktopService)); | ||
| m_available = registered.isValid() && registered.value(); | ||
| if (!m_available) { | ||
| qWarning() << "XDG Desktop Portal not available"; | ||
| } | ||
| } | ||
|
|
||
| bool XdgPortal::background(const QString &handle_token, const bool &autostart) | ||
| { | ||
| if (!m_available) { | ||
| return false; | ||
| } | ||
|
|
||
| QDBusInterface backgroundInterface( | ||
| QLatin1String(portalDesktopService), | ||
| QLatin1String(portalDesktopPath), | ||
| QLatin1String("org.freedesktop.portal.Background"), | ||
| QDBusConnection::sessionBus()); | ||
| if (!backgroundInterface.isValid()) { // just in case | ||
| qWarning() << "org.freedesktop.portal.Background not available"; | ||
| return false; | ||
| } | ||
|
|
||
| QVariantMap options; | ||
| options[QLatin1String("autostart")] = autostart; | ||
| const QString flatpakId = qEnvironmentVariable("FLATPAK_ID"); | ||
| if (flatpakId.isNull()) { | ||
| QStringList list = {Utility::getAppExecutablePath(), QLatin1String("--background")}; | ||
| options[QLatin1String("commandline")] = list; | ||
| } else { // when an app is running as a flatpak its "commandline" parameter is handled differently | ||
| QStringList list = {QFileInfo(QCoreApplication::applicationFilePath()).fileName(), QLatin1String("--background")}; | ||
|
Comment on lines
+65
to
+66
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is it different? does
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Something (either D-Bus, Qt, or Flatpak, I'm not exactly sure what) changes how the When not running in a flatpak then the As for the |
||
| options[QLatin1String("commandline")] = list; | ||
| } | ||
|
|
||
| QDBusMessage reply = backgroundInterface.call( | ||
| QLatin1String("RequestBackground"), | ||
| handle_token, | ||
| options | ||
| ); | ||
|
|
||
| if (reply.type() == QDBusMessage::ErrorMessage) { | ||
| qWarning() << "Background request failed: " << reply.errorMessage(); | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| } // namespace OCC | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: GPL-2.0-or-later | ||
| */ | ||
|
|
||
| #ifndef XDG_PORTAL_H | ||
| #define XDG_PORTAL_H | ||
|
|
||
| #include <QObject> | ||
| #include <QString> | ||
| #include <QVariantMap> | ||
|
|
||
| namespace OCC { | ||
|
|
||
| /** | ||
| * @brief XDG Desktop Portal interface for Qt D-Bus | ||
| * | ||
| * Abstracts D-Bus calls behind simpler methods. | ||
| */ | ||
| class XdgPortal : public QObject | ||
| { | ||
| Q_OBJECT | ||
|
|
||
| public: | ||
| explicit XdgPortal(QObject *parent = nullptr); | ||
|
|
||
| /** | ||
| * @brief Requests that the application is allowed to run in the background; see https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Background.html | ||
| * @param handle_token A string that will be used as the last element of the handle. Must be a valid object path element. See the [Request](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request) documentation for more information about the handle. | ||
| * @param autostart true if the app also wants to be started automatically at login. | ||
| * @return True if successful | ||
| */ | ||
| bool background(const QString &handle_token, const bool &autostart); | ||
|
|
||
| private: | ||
| bool m_available; | ||
|
|
||
| void initPortalInterface(); | ||
| }; | ||
|
|
||
| } // namespace OCC | ||
|
|
||
| #endif // XDG_PORTAL_H |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, I guess when portals are in use we have no other option than relying on the value of
launchOnSystemStartupfrom the config fileperhaps we need to keep track of whether the background portal was used when setting this property