From 6c2bca83270b1816f080451a4a814fdb91eb5c14 Mon Sep 17 00:00:00 2001 From: Wu JiangYu Date: Tue, 27 Oct 2026 13:11:18 +0800 Subject: [PATCH] feat: add folder content provider registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Introduce FolderContentRegistry singleton for resolving folder content references 2. Add FolderPresentation base class for pinned folder icon metadata (title, preview icons, icon/content URLs) 3. Define FolderContentProvider interface for dynamically loaded plugins 4. Implement reference encoding/decoding using base64url-encoded provider IDs 5. Support provider discovery via plugin directories, DDE_SHELL_PLUGIN_PATH, and library paths 6. Add normalizeReference and referenceWithId helpers for id validation and rebuild 7. Preserve pins with fallback presentation when a provider is unavailable 8. Register new source/header files in CMakeLists.txt Log: Added folder content provider plugin system for custom folder pins on the taskbar/dock Influence: 1. Test reference encoding and decoding with valid and invalid inputs 2. Verify provider loading from all search paths (plugins dir, install dir, DDE_SHELL_PLUGIN_PATH, library paths) 3. Test normalizeReference behavior with available and missing providers 4. Verify fallback FolderPresentation is created when provider is missing 5. Test that pins are retained when provider is temporarily unavailable 6. Validate base64url ID encoding handles special characters and empty IDs correctly 7. Confirm source pattern validation rejects malformed references 8. Test referenceWithId rebuilds references while preserving source feat: 添加文件夹内容提供者注册表 1. 引入 FolderContentRegistry 单例,用于解析文件夹内容引用 2. 添加 FolderPresentation 基类,用于固定文件夹图标的元数据(标题、预览 图标、图标/内容 URL) 3. 定义 FolderContentProvider 接口,用于动态加载插件 4. 实现基于 base64url 编码提供者 ID 的引用编解码 5. 支持通过插件目录、DDE_SHELL_PLUGIN_PATH 和库路径发现提供者 6. 添加 normalizeReference 和 referenceWithId 辅助方法,用于 ID 校验和 重建 7. 当提供者不可用时,通过回退展示保留固定项 8. 在 CMakeLists.txt 中注册新的源文件和头文件 Log: 新增文件夹内容提供者插件系统,支持任务栏/停靠栏自定义文件夹固定项 Influence: 1. 使用有效和无效输入测试引用编解码 2. 验证从所有搜索路径(插件目录、安装目录、DDE_SHELL_PLUGIN_PATH、库路 径)加载提供者 3. 测试在提供者可用和缺失情况下 normalizeReference 的行为 4. 验证提供者缺失时创建回退的 FolderPresentation 5. 测试提供者临时不可用时固定项是否保留 6. 验证 base64url ID 编码正确处理特殊字符和空 ID 7. 确认 source 模式校验拒绝格式错误的引用 8. 测试 referenceWithId 在保留 source 的同时重建引用 PMS: TASK-397253 --- frame/CMakeLists.txt | 2 + frame/foldercontent.cpp | 108 ++++++++++++++++++++++++++++++++++++++++ frame/foldercontent.h | 63 +++++++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 frame/foldercontent.cpp create mode 100644 frame/foldercontent.h diff --git a/frame/CMakeLists.txt b/frame/CMakeLists.txt index b54909545..6325d73b9 100644 --- a/frame/CMakeLists.txt +++ b/frame/CMakeLists.txt @@ -18,6 +18,7 @@ set(PUBLIC_HEADERS wayland/xdgactivation.h models/listtotableproxymodel.h dsutility.h + foldercontent.h ) set(PRIVATE_HEADERS @@ -77,6 +78,7 @@ add_library(dde-shell-frame SHARED popupwindow.cpp ddeshell_qml.qrc dsutility.cpp + foldercontent.cpp ) set_target_properties(dde-shell-frame PROPERTIES diff --git a/frame/foldercontent.cpp b/frame/foldercontent.cpp new file mode 100644 index 000000000..a72ebb956 --- /dev/null +++ b/frame/foldercontent.cpp @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +#include "foldercontent.h" + +#include +#include +#include +#include +#include +#include + +DS_BEGIN_NAMESPACE + +FolderContentRegistry *FolderContentRegistry::instance() +{ + static auto registry = new FolderContentRegistry(qApp); + return registry; +} + +QString FolderContentRegistry::reference(const QString &source, const QString &id) +{ + return QStringLiteral("folder/%1/%2").arg(source, QString::fromLatin1(id.toUtf8().toBase64( + QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))); +} + +bool FolderContentRegistry::decode(const QString &value, QString *source, QString *id) +{ + const auto parts = value.split('/'); + static const QRegularExpression sourcePattern(QStringLiteral("^[a-zA-Z0-9][a-zA-Z0-9_.-]*$")); + if (parts.size() != 3 || parts[0] != "folder" || !sourcePattern.match(parts[1]).hasMatch()) + return false; + const auto bytes = QByteArray::fromBase64(parts[2].toLatin1(), + QByteArray::Base64UrlEncoding | QByteArray::AbortOnBase64DecodingErrors); + *source = parts[1]; + *id = QString::fromUtf8(bytes); + return !id->isEmpty() && reference(*source, *id) == value; +} + +bool FolderContentRegistry::isReference(const QString &value) +{ + QString source, id; + return decode(value, &source, &id); +} + +QString FolderContentRegistry::referenceWithId(const QString &value, const QString &id) const +{ + QString source, current; + if (id.isEmpty() || !decode(value, &source, ¤t)) return {}; + return reference(source, id); +} + +FolderContentProvider *FolderContentRegistry::provider(const QString &source) +{ + if (m_providers.contains(source)) + return m_providers.value(source); + + // Content providers live in the regular shell plugin directory and follow + // the same file naming as every other shell plugin: + // org.deepin.ds.folderprovider..so + const auto fileName = QStringLiteral("org.deepin.ds.folderprovider.%1.so").arg(source); + QStringList dirs { + QDir(QCoreApplication::applicationDirPath()).absoluteFilePath("../plugins"), + QStringLiteral(DDE_SHELL_PLUGIN_INSTALL_DIR) + }; + const auto runtimePaths = qEnvironmentVariable("DDE_SHELL_PLUGIN_PATH"); + if (!runtimePaths.isEmpty()) + dirs.append(runtimePaths.split(QDir::listSeparator(), Qt::SkipEmptyParts)); + dirs.append(QCoreApplication::libraryPaths()); + + for (const auto &dir : dirs) { + const auto path = QDir(dir).filePath(fileName); + if (!QFileInfo::exists(path)) + continue; + auto loader = new QPluginLoader(path, this); + auto object = loader->instance(); + auto contentProvider = qobject_cast(object); + if (contentProvider) { + m_providers.insert(source, contentProvider); + return contentProvider; + } + qWarning() << "Failed to load folder content provider:" << path + << (object ? QStringLiteral("FolderContentProvider interface mismatch") : loader->errorString()); + delete loader; + } + qWarning() << "Folder content provider unavailable:" << fileName << "search paths:" << dirs; + return nullptr; +} + +QString FolderContentRegistry::normalizeReference(const QString &value) +{ + QString source, id; + if (!decode(value, &source, &id)) return {}; + auto factory = provider(source); + const auto normalized = factory ? factory->normalizeId(id) : QString(); + return normalized.isEmpty() ? QString() : reference(source, normalized); +} + +FolderPresentation *FolderContentRegistry::create(const QString &value, QObject *parent) +{ + QString source, id; + if (!decode(value, &source, &id)) return nullptr; + if (auto factory = provider(source)) + return factory->create(id, parent); + // Keep pins when a provider is temporarily unavailable. + return new FolderPresentation(parent); +} + +DS_END_NAMESPACE diff --git a/frame/foldercontent.h b/frame/foldercontent.h new file mode 100644 index 000000000..91f87a9ed --- /dev/null +++ b/frame/foldercontent.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "dsglobal.h" +#include +#include +#include +#include + +DS_BEGIN_NAMESPACE + +// Metadata for the pinned icon only. The provider's QML view owns all content +// and operations; the framework and task manager do not model its entries. +class DS_SHARE FolderPresentation : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString title READ title NOTIFY presentationChanged) + Q_PROPERTY(QVariantList previewIcons READ previewIcons NOTIFY presentationChanged) + Q_PROPERTY(QUrl iconUrl READ iconUrl CONSTANT) + Q_PROPERTY(QUrl contentUrl READ contentUrl CONSTANT) +public: + using QObject::QObject; + virtual QString title() const { return tr("Folder"); } + virtual QVariantList previewIcons() const { return {}; } + virtual QUrl iconUrl() const { return {}; } + virtual QUrl contentUrl() const { return {}; } +signals: + void presentationChanged(); + void identityChanged(const QString &id); + void contentLost(); +}; + +class FolderContentProvider +{ +public: + virtual ~FolderContentProvider() = default; + virtual QString normalizeId(const QString &id) const = 0; + virtual FolderPresentation *create(const QString &id, QObject *parent) = 0; +}; + +class DS_SHARE FolderContentRegistry : public QObject +{ + Q_OBJECT +public: + using QObject::QObject; + static FolderContentRegistry *instance(); + static bool isReference(const QString &reference); + static QString reference(const QString &source, const QString &id); + // Rebuild an existing reference with a new provider id, keeping its source. + QString referenceWithId(const QString &reference, const QString &id) const; + QString normalizeReference(const QString &reference); + FolderPresentation *create(const QString &reference, QObject *parent); +private: + FolderContentProvider *provider(const QString &source); + static bool decode(const QString &reference, QString *source, QString *id); + QHash m_providers; +}; + +DS_END_NAMESPACE + +#define DS_FOLDER_CONTENT_PROVIDER_IID "org.deepin.ds.FolderContentProvider/1.0" +Q_DECLARE_INTERFACE(DS_NAMESPACE::FolderContentProvider, DS_FOLDER_CONTENT_PROVIDER_IID)