Commit d5f958e1 authored by Harald Sitter's avatar Harald Sitter 🏳️‍🌈
Browse files

add DynamicLauncher portal

this portal enables sandboxed applications to create desktop entries on
the host

BUG: 451510
parent 75cb34d2
Pipeline #166814 passed with stage
in 48 seconds
......@@ -49,6 +49,7 @@ find_package(KF5 ${KF5_MIN_VERSION} REQUIRED
Wayland
WidgetsAddons
WindowSystem
IconThemes
)
find_package(Wayland 1.15 REQUIRED COMPONENTS Client)
find_package(PlasmaWaylandProtocols 1.7.0 REQUIRED)
......
[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.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
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;org.freedesktop.impl.portal.DynamicLauncher
UseIn=KDE
......@@ -65,6 +65,8 @@ set(xdg_desktop_portal_kde_SRCS
xdg-desktop-portal-kde.cpp
resources.qrc
portalicon.cpp
dynamiclauncher.cpp
dynamiclauncherdialog.cpp
)
if (QT_MAJOR_VERSION EQUAL "5")
......@@ -104,6 +106,7 @@ target_link_libraries(xdg-desktop-portal-kde
KF5::WaylandClient
KF5::WidgetsAddons
KF5::WindowSystem
KF5::IconThemes
KirigamiFilepicker
Wayland::Client
)
......
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
// SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.14 as Kirigami
import org.kde.plasma.workspace.dialogs 1.0 as PWD
import org.kde.kquickcontrolsaddons 2.0 as KQuickAddons
PWD.SystemDialog
{
id: root
property var dialog
property string appID
property var launcherURL: ""
property bool edit: false
readonly property var displayComponent: Component {
ColumnLayout {
Kirigami.Icon {
id: icon
Layout.alignment: Qt.AlignHCenter
implicitWidth: Kirigami.Units.iconSizes.enormous
implicitHeight: implicitWidth
source: dialog.icon
}
Kirigami.Heading {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
level: 3
wrapMode: Text.Wrap
text: dialog.name
verticalAlignment: Qt.AlignTop
}
Kirigami.LinkButton {
Layout.fillWidth: true
visible: text.length > 0
Layout.alignment: Qt.AlignHCenter
elide: Text.ElideMiddle
text: launcherURL
onClicked: Qt.openUrlExternally(launcherURL)
}
}
}
readonly property var editComponent: Component {
ColumnLayout {
QQC2.Button {
Layout.alignment: Qt.AlignHCenter
contentItem: Kirigami.Icon {
id: icon
implicitHeight: implicitWidth
implicitWidth: Kirigami.Units.iconSizes.enormous
source: dialog.icon
KQuickAddons.IconDialog {
id: iconDialog
onIconNameChanged: dialog.icon = iconName
}
TapHandler {
onTapped: iconDialog.open()
}
}
}
QQC2.Label {
text: i18nc("@label name of a launcher/application", "Name")
}
QQC2.TextField {
verticalAlignment: Qt.AlignTop
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
onTextChanged: dialog.name = text
Component.onCompleted: text = dialog.name
}
}
}
Loader {
id: contentLoader
sourceComponent: root.edit ? editComponent : displayComponent
}
actions: [
Kirigami.Action {
text: i18nc("@action edit launcher name/icon", "Edit Info…")
iconName: "document-edit"
onCheckedChanged: root.edit = checked
checkable: true
},
Kirigami.Action {
text: i18nc("@action accept dialog and create launcher", "Accept")
iconName: "dialog-ok"
onTriggered: accept()
},
Kirigami.Action {
text: i18nc("@action", "Cancel")
iconName: "dialog-cancel"
onTriggered: reject()
}
]
}
......@@ -10,6 +10,8 @@
#include <QLoggingCategory>
#include "dynamiclauncher.h"
Q_LOGGING_CATEGORY(XdgDesktopPortalKdeDesktopPortal, "xdp-kde-desktop-portal")
DesktopPortal::DesktopPortal(QObject *parent)
......@@ -23,6 +25,7 @@ DesktopPortal::DesktopPortal(QObject *parent)
, m_notification(new NotificationPortal(this))
, m_print(new PrintPortal(this))
, m_settings(new SettingsPortal(this))
, m_dynamicLauncher(new DynamicLauncherPortal(this))
{
const QByteArray xdgCurrentDesktop = qgetenv("XDG_CURRENT_DESKTOP");
if (xdgCurrentDesktop.compare("KDE", Qt::CaseInsensitive) == 0) {
......
......@@ -28,6 +28,8 @@
#include "remotedesktop.h"
#include "screencast.h"
class DynamicLauncherPortal;
class DesktopPortal : public QObject, public QDBusContext
{
Q_OBJECT
......@@ -49,6 +51,7 @@ private:
SettingsPortal *const m_settings;
ScreenCastPortal *m_screenCast = nullptr;
RemoteDesktopPortal *m_remoteDesktop = nullptr;
DynamicLauncherPortal *const m_dynamicLauncher;
};
#endif // XDG_DESKTOP_PORTAL_KDE_DESKTOP_PORTAL_H
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
// SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
#include "dynamiclauncher.h"
#include <optional>
#include <QFile>
#include <QGuiApplication>
#include <QLoggingCategory>
#include <QUrl>
#include <QWindow>
#include <KIconLoader>
#include <KLocalizedString>
#include "dynamiclauncherdialog.h"
#include "portalicon.h"
#include "utils.h"
Q_LOGGING_CATEGORY(XdgDesktopPortalKdeDynamicLauncher, "xdp-kde-dynamic-launcher")
DynamicLauncherPortal::DynamicLauncherPortal(QObject *parent)
: QDBusAbstractAdaptor(parent)
{
PortalIcon::registerDBusType();
}
// We want explicit support for types lest we incorrectly demarshal something.
template<typename T>
std::optional<T> readOption(const QString &key, const QVariantMap &options) = delete;
template<>
std::optional<bool> readOption(const QString &key, const QVariantMap &options)
{
if (options.contains(key)) {
return options.value(key).toBool();
}
return std::nullopt;
}
template<>
std::optional<QString> readOption(const QString &key, const QVariantMap &options)
{
if (options.contains(key)) {
return options.value(key).toString();
}
return std::nullopt;
}
template<>
std::optional<uint> readOption(const QString &key, const QVariantMap &options)
{
if (options.contains(key)) {
return options.value(key).toUInt();
}
return std::nullopt;
}
QIcon static extractIcon(const QDBusVariant &iconVariant)
{
auto icon = qdbus_cast<PortalIcon>(iconVariant.variant());
const QVariant iconData = icon.data.variant();
// NB: The DynamicLauncher portal only accept GByteIcons, i.e. the only type we'll ever get are bytes.
if (icon.str == QStringLiteral("bytes") && iconData.type() == QVariant::ByteArray) {
QPixmap pixmap;
pixmap.loadFromData(iconData.toByteArray());
return pixmap;
}
return {};
}
static QString typeToTitle(uint type)
{
switch (static_cast<DynamicLauncherPortal::Type>(type)) {
case DynamicLauncherPortal::Type::Webapp:
return i18nc("@title", "Add Web Application…");
case DynamicLauncherPortal::Type::Application:
break;
}
// Default value is Application; we treat all unmapped, possibly future, types as generic Application.
return i18nc("@title", "Add Application…");
}
static QByteArray iconFromName(const QString &name)
{
static constexpr auto maxSize = 512; // the portal never deals with larger icons; they'd likely be SVGs at that point anyway.
const auto iconPath = KIconLoader::global()->iconPath(name, -maxSize, false);
QFile iconFile(iconPath);
if (!iconFile.open(QFile::ReadOnly)) {
qWarning() << "Failed to read icon" << iconPath;
return {};
}
return iconFile.readAll();
}
uint DynamicLauncherPortal::PrepareInstall(const QDBusObjectPath &handle,
const QString &app_id,
const QString &parent_window,
const QString &name,
const QDBusVariant &iconVariant,
const QVariantMap &options,
QVariantMap &results)
{
qCDebug(XdgDesktopPortalKdeDynamicLauncher) << "PrepareInstall called with parameters:";
qCDebug(XdgDesktopPortalKdeDynamicLauncher) << " handle: " << handle.path();
qCDebug(XdgDesktopPortalKdeDynamicLauncher) << " app_id: " << app_id;
qCDebug(XdgDesktopPortalKdeDynamicLauncher) << " parent_window: " << parent_window;
qCDebug(XdgDesktopPortalKdeDynamicLauncher) << " name: " << name;
qCDebug(XdgDesktopPortalKdeDynamicLauncher) << " iconVariant: " << iconVariant.variant();
qCDebug(XdgDesktopPortalKdeDynamicLauncher) << " options: " << options;
const auto modal = readOption<bool>(QStringLiteral("modal"), options).value_or(true);
const auto launcherType = readOption<uint>(QStringLiteral("launcher_type"), options).value_or(uint(Type::Application));
const auto optionalTarget = readOption<QString>(QStringLiteral("target"), options);
const auto editableName = readOption<bool>(QStringLiteral("editable_name"), options).value_or(true);
const auto editableIcon = readOption<bool>(QStringLiteral("editable_icon"), options).value_or(false);
static const QString nameKey = QStringLiteral("name");
static const QString iconKey = QStringLiteral("icon");
results[nameKey] = name;
results[iconKey] = QVariant::fromValue(iconVariant);
const auto icon = extractIcon(iconVariant);
DynamicLauncherDialog dialog(typeToTitle(launcherType), icon, name, optionalTarget ? QUrl(optionalTarget.value()) : QUrl());
dialog.windowHandle()->setModality(modal ? Qt::WindowModal : Qt::NonModal);
Utils::setParentWindow(dialog.windowHandle(), parent_window);
const bool result = dialog.exec();
if (editableName) {
results[nameKey] = dialog.m_name;
}
if (editableIcon && dialog.m_icon != icon && dialog.m_icon.type() == QVariant::String) {
const auto data = iconFromName(dialog.m_icon.toString());
if (!data.isEmpty()) {
const PortalIcon portalIcon{"bytes", QDBusVariant(data)};
results[iconKey] = QVariant::fromValue(QDBusVariant(QVariant::fromValue(portalIcon)));
}
}
return result ? 0 : 1;
}
uint DynamicLauncherPortal::RequestInstallToken(const QString &app_id, const QVariantMap &options)
{
Q_UNUSED(options);
// Blanket allow certain apps to create app entries. Ported from GTK portal.
// Perhaps this should also include browsers? Unclear at the time of writing.
static QStringList allowedIDs = {
QStringLiteral("org.gnome.Software"),
QStringLiteral("org.gnome.SoftwareDevel"),
QStringLiteral("io.elementary.appcenter"),
QStringLiteral("org.kde.discover"),
};
return allowedIDs.contains(app_id) ? 0 : 2;
}
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
// SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
#pragma once
#include <QDBusAbstractAdaptor>
#include <QDBusObjectPath>
class DynamicLauncherPortal : public QDBusAbstractAdaptor
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.freedesktop.impl.portal.DynamicLauncher")
Q_PROPERTY(uint version MEMBER m_version CONSTANT)
const uint m_version = 1;
Q_PROPERTY(uint SupportedLauncherTypes MEMBER m_supportedTypes CONSTANT)
const uint m_supportedTypes = uint(Type::Application) | uint(Type::Webapp);
public:
enum class Type { Application = 1, Webapp = 2 };
explicit DynamicLauncherPortal(QObject *parent = nullptr);
public Q_SLOTS:
uint PrepareInstall(const QDBusObjectPath &handle,
const QString &app_id,
const QString &parent_window,
const QString &name,
const QDBusVariant &icon_v,
const QVariantMap &options,
QVariantMap &results);
uint RequestInstallToken(const QString &app_id, const QVariantMap &options);
};
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
// SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
#include "dynamiclauncherdialog.h"
#include <QIcon>
#include <QLoggingCategory>
Q_LOGGING_CATEGORY(XdgDesktopPortalKdeDynamicLauncherDialog, "xdp-kde-dynamic-launcher-dialog")
DynamicLauncherDialog::DynamicLauncherDialog(const QString &title, const QIcon &icon, const QString &name, const QUrl &launcherURL, QObject *parent)
: QuickDialog(parent)
, m_name(name)
, m_icon(icon)
{
create(QStringLiteral("qrc:/DynamicLauncherDialog.qml"),
{
{QStringLiteral("title"), title},
{QStringLiteral("launcherName"), name},
{QStringLiteral("launcherIcon"), icon},
{QStringLiteral("launcherURL"), launcherURL},
{QStringLiteral("dialog"), QVariant::fromValue(this)},
});
}
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
// SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
#pragma once
#include <QVariant>
#include "quickdialog.h"
class DynamicLauncherDialog : public QuickDialog
{
Q_OBJECT
public:
explicit DynamicLauncherDialog(const QString &title, const QIcon &icon, const QString &name, const QUrl &launcherURL, QObject *parent = nullptr);
Q_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged)
Q_SIGNAL void nameChanged();
QString m_name;
Q_PROPERTY(QVariant icon MEMBER m_icon NOTIFY iconChanged)
Q_SIGNAL void iconChanged();
QVariant m_icon;
};
......@@ -7,5 +7,6 @@
<file>AppChooserDialog.qml</file>
<file>RemoteDesktopDialog.qml</file>
<file>ScreenshotDialog.qml</file>
<file>DynamicLauncherDialog.qml</file>
</qresource>
</RCC>
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment