diff --git a/data/kde.portal b/data/kde.portal --- a/data/kde.portal +++ b/data/kde.portal @@ -1,4 +1,4 @@ [portal] DBusName=org.freedesktop.impl.portal.desktop.kde -Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.Account;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Email;org.freedesktop.impl.portal.FileChooser;org.freedesktop.impl.portal.Inhibit;org.freedesktop.impl.portal.Notification;org.freedesktop.impl.portal.Print;org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.RemoteDesktop;org.freedesktop.impl.portal.Settings +Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.Account;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Email;org.freedesktop.impl.portal.FileChooser;org.freedesktop.impl.portal.Inhibit;org.freedesktop.impl.portal.Notification;org.freedesktop.impl.portal.Print;org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.RemoteDesktop;org.freedesktop.impl.portal.Settings UseIn=KDE diff --git a/data/org.freedesktop.impl.portal.desktop.kde.desktop.in b/data/org.freedesktop.impl.portal.desktop.kde.desktop.in --- a/data/org.freedesktop.impl.portal.desktop.kde.desktop.in +++ b/data/org.freedesktop.impl.portal.desktop.kde.desktop.in @@ -1,6 +1,6 @@ [Desktop Entry] Type=Application Exec=@CMAKE_INSTALL_FULL_LIBEXECDIR@/xdg-desktop-portal-kde -X-KDE-Wayland-Interfaces=org_kde_kwin_fake_input,org_kde_kwin_remote_access_manager +X-KDE-Wayland-Interfaces=org_kde_kwin_fake_input,org_kde_kwin_remote_access_manager,org_kde_plasma_window_management NoDisplay=true Icon=kde diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,6 +9,7 @@ account.cpp appchooser.cpp appchooserdialog.cpp + background.cpp desktopportal.cpp email.cpp filechooser.cpp diff --git a/src/background.h b/src/background.h new file mode 100644 --- /dev/null +++ b/src/background.h @@ -0,0 +1,86 @@ +/* + * Copyright © 2020 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + */ + +#ifndef XDG_DESKTOP_PORTAL_KDE_BACKGROUND_H +#define XDG_DESKTOP_PORTAL_KDE_BACKGROUND_H + +#include +#include + +namespace KWayland { + namespace Client { + class PlasmaWindow; + } +} + +class BackgroundPortal : public QDBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.impl.portal.Background") +public: + explicit BackgroundPortal(QObject *parent); + ~BackgroundPortal(); + + enum ApplicationState { + Background = 0, + Running = 1, + Active = 2 + }; + + enum AutostartFlag { + None = 0x0, + Activatable = 0x1 + }; + Q_DECLARE_FLAGS(AutostartFlags, AutostartFlag) + + enum NotifyResult { + Forbid = 0, + Allow = 1, + Ignore = 2 + }; + +public Q_SLOTS: + QVariantMap GetAppState(); + + uint NotifyBackground(const QDBusObjectPath &handle, + const QString &app_id, + const QString &name, + QVariantMap &results); + + bool EnableAutostart(const QString &app_id, + bool enable, + const QStringList &commandline, + uint flags); +Q_SIGNALS: + void RunningApplicationsChanged(); + +private: + void addWindow(KWayland::Client::PlasmaWindow *window); + void setActiveWindow(const QString &appId, bool active); + + uint m_notificationCounter = 0; + QList m_windows; + QVariantMap m_appStates; +}; +Q_DECLARE_OPERATORS_FOR_FLAGS(BackgroundPortal::AutostartFlags) + +#endif // XDG_DESKTOP_PORTAL_KDE_BACKGROUND_H + + diff --git a/src/background.cpp b/src/background.cpp new file mode 100644 --- /dev/null +++ b/src/background.cpp @@ -0,0 +1,232 @@ +/* + * Copyright © 2020 Red Hat, Inc + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + */ + +#include "background.h" +#include "utils.h" +#include "waylandintegration.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +Q_LOGGING_CATEGORY(XdgDesktopPortalKdeBackground, "xdp-kde-background") + +BackgroundPortal::BackgroundPortal(QObject *parent) + : QDBusAbstractAdaptor(parent) +{ + connect(WaylandIntegration::waylandIntegration(), &WaylandIntegration::WaylandIntegration::plasmaWindowManagementInitialized, this, [=] () { + connect(WaylandIntegration::plasmaWindowManagement(), &KWayland::Client::PlasmaWindowManagement::windowCreated, this, [this] (KWayland::Client::PlasmaWindow *window) { + addWindow(window); + }); + + m_windows = WaylandIntegration::plasmaWindowManagement()->windows(); + for (KWayland::Client::PlasmaWindow *window : m_windows) { + addWindow(window); + } + }); +} + +BackgroundPortal::~BackgroundPortal() +{ +} + +QVariantMap BackgroundPortal::GetAppState() +{ + qCDebug(XdgDesktopPortalKdeBackground) << "GetAppState called: no parameters"; + return m_appStates; +} + +uint BackgroundPortal::NotifyBackground(const QDBusObjectPath &handle, + const QString &app_id, + const QString &name, + QVariantMap &results) +{ + Q_UNUSED(results); + + qCDebug(XdgDesktopPortalKdeBackground) << "NotifyBackground called with parameters:"; + qCDebug(XdgDesktopPortalKdeBackground) << " handle: " << handle.path(); + qCDebug(XdgDesktopPortalKdeBackground) << " app_id: " << app_id; + qCDebug(XdgDesktopPortalKdeBackground) << " name: " << name; + + KNotification *notify = new KNotification(QStringLiteral("notification"), KNotification::Persistent | KNotification::DefaultEvent, this); + notify->setTitle(i18n("Background activity")); + notify->setText(i18n("%1 is running in the background.", app_id)); + notify->setActions({i18n("Find out more")}); + notify->setProperty("activated", false); + + QObject *obj = QObject::parent(); + + if (!obj) { + qCWarning(XdgDesktopPortalKdeBackground) << "Failed to get dbus context"; + return 2; + } + + void *ptr = obj->qt_metacast("QDBusContext"); + QDBusContext *q_ptr = reinterpret_cast(ptr); + + if (!q_ptr) { + qCWarning(XdgDesktopPortalKdeBackground) << "Failed to get dbus context"; + return 2; + } + + QDBusMessage reply; + QDBusMessage message = q_ptr->message(); + + message.setDelayedReply(true); + reply = message.createReply(); + QDBusConnection::sessionBus().send(reply); + + connect(notify, QOverload::of(&KNotification::activated), this, [=] (uint action) { + if (action != 1) { + return; + } + notify->setProperty("activated", true); + + const QString title = i18n("%1 is running in the background", app_id); + const QString text = i18n("This might be for a legitimate reason, but the application has not provided one." + "\n\nNote that forcing an application to quit might cause data loss."); + QMessageBox messageBox(QMessageBox::Question, title, text); + QPushButton *quitButton = messageBox.addButton(i18n("Force quit"), QMessageBox::RejectRole); + QPushButton *allowButton = messageBox.addButton(i18n("Allow"), QMessageBox::AcceptRole); + messageBox.exec(); + + BackgroundPortal::NotifyResult result = BackgroundPortal::Ignore; + if (messageBox.clickedButton() == quitButton) { + result = BackgroundPortal::Forbid; + } else if (messageBox.clickedButton() == allowButton) { + result = BackgroundPortal::Allow; + } + + const QVariantMap map = { {QStringLiteral("result"), static_cast(result)} }; + QDBusMessage reply = message.createReply({0, map}); + if (!QDBusConnection::sessionBus().send(reply)) { + qCWarning(XdgDesktopPortalKdeBackground) << "Failed to send response"; + } + }); + connect(notify, &KNotification::closed, this, [=] () { + if (notify->property("activated").toBool()) { + return; + } + + QVariantMap map; + map.insert(QStringLiteral("result"), static_cast(BackgroundPortal::Ignore)); + QDBusMessage reply = message.createReply({0, map}); + if (!QDBusConnection::sessionBus().send(reply)) { + qCWarning(XdgDesktopPortalKdeBackground) << "Failed to send response"; + } + }); + + notify->sendEvent(); + + return 0; +} + +bool BackgroundPortal::EnableAutostart(const QString &app_id, + bool enable, + const QStringList &commandline, + uint flags) +{ + qCDebug(XdgDesktopPortalKdeBackground) << "EnableAutostart called with parameters:"; + qCDebug(XdgDesktopPortalKdeBackground) << " app_id: " << app_id; + qCDebug(XdgDesktopPortalKdeBackground) << " enable: " << enable; + qCDebug(XdgDesktopPortalKdeBackground) << " commandline: " << commandline; + qCDebug(XdgDesktopPortalKdeBackground) << " flags: " << flags; + + const QString fileName = app_id + QStringLiteral(".desktop"); + const QString directory = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/autostart/"); + const QString fullPath = directory + fileName; + const AutostartFlags autostartFlags = static_cast(flags); + + if (!enable) { + QFile file(fullPath); + if (!file.remove()) { + qCDebug(XdgDesktopPortalKdeBackground) << "Failed to remove " << fileName << " to disable autostart."; + } + return false; + } + + QDir dir(directory); + if (!dir.mkpath(dir.absolutePath())) { + qCDebug(XdgDesktopPortalKdeBackground) << "Failed to create autostart directory."; + return false; + } + + KDesktopFile desktopFile(fullPath); + KConfigGroup desktopEntryConfigGroup = desktopFile.desktopGroup(); + desktopEntryConfigGroup.writeEntry(QStringLiteral("Type"), QStringLiteral("Application")); + desktopEntryConfigGroup.writeEntry(QStringLiteral("Name"), app_id); + desktopEntryConfigGroup.writeEntry(QStringLiteral("Exec"), KShell::joinArgs(commandline)); + if (autostartFlags.testFlag(AutostartFlag::Activatable)) { + desktopEntryConfigGroup.writeEntry(QStringLiteral("DBusActivatable"), true); + } + desktopEntryConfigGroup.writeEntry(QStringLiteral("X-Flatpak"), app_id); + + return true; +} + +void BackgroundPortal::addWindow(KWayland::Client::PlasmaWindow *window) +{ + const QString appId = window->appId(); + const bool isActive = window->isActive(); + m_appStates[appId] = QVariant::fromValue(isActive ? Active : Running); + + connect(window, &KWayland::Client::PlasmaWindow::activeChanged, this, [this, window] () { + setActiveWindow(window->appId(), window->isActive()); + }); + connect(window, &KWayland::Client::PlasmaWindow::unmapped, this, [this, window] () { + uint windows = 0; + const QString appId = window->appId(); + for (KWayland::Client::PlasmaWindow *otherWindow : WaylandIntegration::plasmaWindowManagement()->windows()) { + if (otherWindow->appId() == appId && otherWindow->internalId() != window->internalId()) { + windows++; + } + } + + if (!windows) { + m_appStates.remove(appId); + Q_EMIT RunningApplicationsChanged(); + } + }); + + Q_EMIT RunningApplicationsChanged(); +} + +void BackgroundPortal::setActiveWindow(const QString &appId, bool active) +{ + m_appStates[appId] = QVariant::fromValue(active ? Active : Running); + + Q_EMIT RunningApplicationsChanged(); +} diff --git a/src/desktopportal.h b/src/desktopportal.h --- a/src/desktopportal.h +++ b/src/desktopportal.h @@ -27,18 +27,19 @@ #include "access.h" #include "account.h" #include "appchooser.h" +#include "background.h" #include "email.h" #include "filechooser.h" #include "inhibit.h" #include "notification.h" #include "print.h" #if SCREENCAST_ENABLED #include "screencast.h" #include "remotedesktop.h" -#include "waylandintegration.h" #endif #include "screenshot.h" #include "settings.h" +#include "waylandintegration.h" class DesktopPortal : public QObject, public QDBusContext { @@ -51,6 +52,7 @@ AccessPortal *m_access; AccountPortal *m_account; AppChooserPortal *m_appChooser; + BackgroundPortal *m_background; EmailPortal *m_email; FileChooserPortal *m_fileChooser; InhibitPortal *m_inhibit; diff --git a/src/desktopportal.cpp b/src/desktopportal.cpp --- a/src/desktopportal.cpp +++ b/src/desktopportal.cpp @@ -38,12 +38,13 @@ { const QByteArray xdgCurrentDesktop = qgetenv("XDG_CURRENT_DESKTOP").toUpper(); if (xdgCurrentDesktop == "KDE") { + m_background = new BackgroundPortal(this); m_screenshot = new ScreenshotPortal(this); #if SCREENCAST_ENABLED m_screenCast = new ScreenCastPortal(this); m_remoteDesktop = new RemoteDesktopPortal(this); - WaylandIntegration::init(); #endif + WaylandIntegration::init(); } } diff --git a/src/waylandintegration.h b/src/waylandintegration.h --- a/src/waylandintegration.h +++ b/src/waylandintegration.h @@ -31,6 +31,12 @@ #include #include +namespace KWayland { + namespace Client { + class PlasmaWindowManagement; + } +} + namespace WaylandIntegration { @@ -86,6 +92,7 @@ Q_OBJECT Q_SIGNALS: void newBuffer(uint8_t *screenData); + void plasmaWindowManagementInitialized(); }; const char * formatGLError(GLenum err); @@ -109,6 +116,7 @@ EGLStruct egl(); + KWayland::Client::PlasmaWindowManagement *plasmaWindowManagement(); QMap screens(); QVariant streams(); diff --git a/src/waylandintegration.cpp b/src/waylandintegration.cpp --- a/src/waylandintegration.cpp +++ b/src/waylandintegration.cpp @@ -41,6 +41,7 @@ #include #include #include +#include // system #include @@ -57,7 +58,9 @@ void WaylandIntegration::init() { +#if SCREENCAST_ENABLED globalWaylandIntegration->initDrm(); +#endif globalWaylandIntegration->initWayland(); } @@ -121,6 +124,11 @@ return globalWaylandIntegration->egl(); } +KWayland::Client::PlasmaWindowManagement * WaylandIntegration::plasmaWindowManagement() +{ + return globalWaylandIntegration->plasmaWindowManagement(); +} + QMap WaylandIntegration::screens() { return globalWaylandIntegration->screens(); @@ -406,6 +414,11 @@ return m_outputMap; } +KWayland::Client::PlasmaWindowManagement * WaylandIntegration::WaylandIntegrationPrivate::plasmaWindowManagement() +{ + return m_windowManagement; +} + QVariant WaylandIntegration::WaylandIntegrationPrivate::streams() { Stream stream; @@ -616,11 +629,17 @@ m_registry = new KWayland::Client::Registry(this); +#if SCREENCAST_ENABLED connect(m_registry, &KWayland::Client::Registry::fakeInputAnnounced, this, [this] (quint32 name, quint32 version) { m_fakeInput = m_registry->createFakeInput(name, version, this); }); connect(m_registry, &KWayland::Client::Registry::outputAnnounced, this, &WaylandIntegrationPrivate::addOutput); connect(m_registry, &KWayland::Client::Registry::outputRemoved, this, &WaylandIntegrationPrivate::removeOutput); +#endif + connect(m_registry, &KWayland::Client::Registry::plasmaWindowManagementAnnounced, this, [this] (quint32 name, quint32 version) { + m_windowManagement = m_registry->createPlasmaWindowManagement(name, version, this); + Q_EMIT waylandIntegration()->plasmaWindowManagementInitialized(); + }); connect(m_registry, &KWayland::Client::Registry::interfacesAnnounced, this, [this] { m_registryInitialized = true; diff --git a/src/waylandintegration_p.h b/src/waylandintegration_p.h --- a/src/waylandintegration_p.h +++ b/src/waylandintegration_p.h @@ -37,6 +37,8 @@ class RemoteAccessManager; class RemoteBuffer; class Output; + class PlasmaWindow; + class PlasmaWindowManagement; } } @@ -77,6 +79,7 @@ void requestKeyboardKeycode(int keycode, bool state); EGLStruct egl(); + KWayland::Client::PlasmaWindowManagement *plasmaWindowManagement(); QMap screens(); QVariant streams(); @@ -87,28 +90,29 @@ void setupRegistry(); private: - bool m_eglInitialized; - bool m_streamingEnabled; + bool m_eglInitialized = false; + bool m_streamingEnabled = false; bool m_streamInput = false; - bool m_registryInitialized; - bool m_waylandAuthenticationRequested; + bool m_registryInitialized = false; + bool m_waylandAuthenticationRequested = false; quint32 m_output; QDateTime m_lastFrameTime; - ScreenCastStream *m_stream; + ScreenCastStream *m_stream = nullptr; - QThread *m_thread; + QThread *m_thread = nullptr; QPoint m_streamedScreenPosition; QMap m_outputMap; QList m_bindOutputs; - KWayland::Client::ConnectionThread *m_connection; - KWayland::Client::EventQueue *m_queue; - KWayland::Client::FakeInput *m_fakeInput; - KWayland::Client::Registry *m_registry; - KWayland::Client::RemoteAccessManager *m_remoteAccessManager; + KWayland::Client::ConnectionThread *m_connection = nullptr; + KWayland::Client::EventQueue *m_queue = nullptr; + KWayland::Client::FakeInput *m_fakeInput = nullptr; + KWayland::Client::Registry *m_registry = nullptr; + KWayland::Client::RemoteAccessManager *m_remoteAccessManager = nullptr; + KWayland::Client::PlasmaWindowManagement *m_windowManagement = nullptr; qint32 m_drmFd = 0; // for GBM buffer mmap gbm_device *m_gbmDevice = nullptr; // for passed GBM buffer retrieval