File indexing completed on 2025-02-16 14:25:41
0001 // SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0002 // SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org> 0003 0004 #include "dynamiclauncher.h" 0005 0006 #include <optional> 0007 0008 #include <QFile> 0009 #include <QGuiApplication> 0010 #include <QUrl> 0011 #include <QWindow> 0012 0013 #include <KIconLoader> 0014 #include <KLocalizedString> 0015 0016 #include "dynamiclauncher_debug.h" 0017 #include "dynamiclauncherdialog.h" 0018 #include "portalicon.h" 0019 #include "request.h" 0020 #include "utils.h" 0021 0022 DynamicLauncherPortal::DynamicLauncherPortal(QObject *parent) 0023 : QDBusAbstractAdaptor(parent) 0024 { 0025 PortalIcon::registerDBusType(); 0026 } 0027 0028 // We want explicit support for types lest we incorrectly demarshal something. 0029 template<typename T> 0030 std::optional<T> readOption(const QString &key, const QVariantMap &options) = delete; 0031 0032 template<> 0033 std::optional<bool> readOption(const QString &key, const QVariantMap &options) 0034 { 0035 if (options.contains(key)) { 0036 return options.value(key).toBool(); 0037 } 0038 return std::nullopt; 0039 } 0040 0041 template<> 0042 std::optional<QString> readOption(const QString &key, const QVariantMap &options) 0043 { 0044 if (options.contains(key)) { 0045 return options.value(key).toString(); 0046 } 0047 return std::nullopt; 0048 } 0049 0050 template<> 0051 std::optional<uint> readOption(const QString &key, const QVariantMap &options) 0052 { 0053 if (options.contains(key)) { 0054 return options.value(key).toUInt(); 0055 } 0056 return std::nullopt; 0057 } 0058 0059 QIcon static extractIcon(const QDBusVariant &iconVariant) 0060 { 0061 auto icon = qdbus_cast<PortalIcon>(iconVariant.variant()); 0062 const QVariant iconData = icon.data.variant(); 0063 // NB: The DynamicLauncher portal only accept GByteIcons, i.e. the only type we'll ever get are bytes. 0064 if (icon.str == QStringLiteral("bytes") && iconData.type() == QVariant::ByteArray) { 0065 QPixmap pixmap; 0066 pixmap.loadFromData(iconData.toByteArray()); 0067 return pixmap; 0068 } 0069 return {}; 0070 } 0071 0072 static QString typeToTitle(uint type) 0073 { 0074 switch (static_cast<DynamicLauncherPortal::Type>(type)) { 0075 case DynamicLauncherPortal::Type::Webapp: 0076 return i18nc("@title", "Add Web Application…"); 0077 case DynamicLauncherPortal::Type::Application: 0078 break; 0079 } 0080 // Default value is Application; we treat all unmapped, possibly future, types as generic Application. 0081 return i18nc("@title", "Add Application…"); 0082 } 0083 0084 static QByteArray iconFromName(const QString &name) 0085 { 0086 static constexpr auto maxSize = 512; // the portal never deals with larger icons; they'd likely be SVGs at that point anyway. 0087 const auto iconPath = KIconLoader::global()->iconPath(name, -maxSize, false); 0088 QFile iconFile(iconPath); 0089 if (!iconFile.open(QFile::ReadOnly)) { 0090 qWarning() << "Failed to read icon" << iconPath; 0091 return {}; 0092 } 0093 return iconFile.readAll(); 0094 } 0095 0096 uint DynamicLauncherPortal::PrepareInstall(const QDBusObjectPath &handle, 0097 const QString &app_id, 0098 const QString &parent_window, 0099 const QString &name, 0100 const QDBusVariant &iconVariant, 0101 const QVariantMap &options, 0102 QVariantMap &results) 0103 { 0104 qCDebug(XdgDesktopPortalKdeDynamicLauncher) << "PrepareInstall called with parameters:"; 0105 qCDebug(XdgDesktopPortalKdeDynamicLauncher) << " handle: " << handle.path(); 0106 qCDebug(XdgDesktopPortalKdeDynamicLauncher) << " app_id: " << app_id; 0107 qCDebug(XdgDesktopPortalKdeDynamicLauncher) << " parent_window: " << parent_window; 0108 qCDebug(XdgDesktopPortalKdeDynamicLauncher) << " name: " << name; 0109 qCDebug(XdgDesktopPortalKdeDynamicLauncher) << " iconVariant: " << iconVariant.variant(); 0110 qCDebug(XdgDesktopPortalKdeDynamicLauncher) << " options: " << options; 0111 0112 const auto modal = readOption<bool>(QStringLiteral("modal"), options).value_or(true); 0113 const auto launcherType = readOption<uint>(QStringLiteral("launcher_type"), options).value_or(uint(Type::Application)); 0114 const auto optionalTarget = readOption<QString>(QStringLiteral("target"), options); 0115 const auto editableName = readOption<bool>(QStringLiteral("editable_name"), options).value_or(true); 0116 const auto editableIcon = readOption<bool>(QStringLiteral("editable_icon"), options).value_or(false); 0117 0118 static const QString nameKey = QStringLiteral("name"); 0119 static const QString iconKey = QStringLiteral("icon"); 0120 results[nameKey] = name; 0121 results[iconKey] = QVariant::fromValue(iconVariant); 0122 0123 const auto icon = extractIcon(iconVariant); 0124 0125 DynamicLauncherDialog dialog(typeToTitle(launcherType), icon, name, optionalTarget ? QUrl(optionalTarget.value()) : QUrl()); 0126 dialog.windowHandle()->setModality(modal ? Qt::WindowModal : Qt::NonModal); 0127 Utils::setParentWindow(dialog.windowHandle(), parent_window); 0128 Request::makeClosableDialogRequest(handle, &dialog); 0129 0130 const bool result = dialog.exec(); 0131 0132 if (editableName) { 0133 results[nameKey] = dialog.m_name; 0134 } 0135 0136 if (editableIcon && dialog.m_icon != icon && dialog.m_icon.type() == QVariant::String) { 0137 const auto data = iconFromName(dialog.m_icon.toString()); 0138 if (!data.isEmpty()) { 0139 const PortalIcon portalIcon{"bytes", QDBusVariant(data)}; 0140 results[iconKey] = QVariant::fromValue(QDBusVariant(QVariant::fromValue(portalIcon))); 0141 } 0142 } 0143 0144 return result ? 0 : 1; 0145 } 0146 0147 uint DynamicLauncherPortal::RequestInstallToken(const QString &app_id, const QVariantMap &options) 0148 { 0149 Q_UNUSED(options); 0150 0151 // Blanket allow certain apps to create app entries. Ported from GTK portal. 0152 // Perhaps this should also include browsers? Unclear at the time of writing. 0153 0154 static QStringList allowedIDs = { 0155 QStringLiteral("org.gnome.Software"), 0156 QStringLiteral("org.gnome.SoftwareDevel"), 0157 QStringLiteral("io.elementary.appcenter"), 0158 QStringLiteral("org.kde.discover"), 0159 }; 0160 0161 return allowedIDs.contains(app_id) ? 0 : 2; 0162 }