diff --git a/docs/plugin/API_en.md b/docs/plugin/API_en.md index 90552fcee..9eb6fcb2e 100644 --- a/docs/plugin/API_en.md +++ b/docs/plugin/API_en.md @@ -257,6 +257,19 @@ class DS_SHARE DPluginMetaData : public QObject }; ``` +### Applet Enable Configuration + +`AppletConfigManager` in the `dde-shell` process reads `org.deepin.dde.shell / org.deepin.dde.shell / enable`, uses the applet's `Plugin.Id` as the DConfig subpath, and passes the enabled state to `DAppletLoader`. This configuration applies only to child applets that declare `Plugin.Parent`; top-level panels such as the Dock are always created by the startup flow and are not controlled by DConfig. When the value is `false`, the corresponding child applet instance is not created. Runtime configuration changes unload or reload that child applet immediately. `dde-shell-frame` does not read or store this configuration state. + +The `-d` command-line option is handled by `DPluginLoader`, which filters the corresponding applet, including a top-level panel, while scanning metadata. It has higher priority than DConfig; an applet disabled with `-d` cannot be re-enabled through DConfig during the lifetime of the process. + +For example, to disable the launchpad applet: + +```bash +dde-dconfig set -a org.deepin.dde.shell -r org.deepin.dde.shell \ + -s /org.deepin.ds.dock.launcherapplet -k enable -v false +``` + ### DAppletBridge API Bridge for communicating with applet from C++: diff --git a/docs/plugin/API_zh_CN.md b/docs/plugin/API_zh_CN.md index f4316d2a3..1b2d1a971 100644 --- a/docs/plugin/API_zh_CN.md +++ b/docs/plugin/API_zh_CN.md @@ -257,6 +257,19 @@ class DS_SHARE DPluginMetaData : public QObject }; ``` +### Applet 启用配置 + +`dde-shell` 进程中的 `AppletConfigManager` 读取 `org.deepin.dde.shell / org.deepin.dde.shell / enable`,并使用 Applet 的 `Plugin.Id` 作为 DConfig subpath,再将启用状态交给 `DAppletLoader`。该配置只作用于包含 `Plugin.Parent` 的子 Applet;Dock 等顶层 Panel 始终由启动流程创建,不受 DConfig 控制。值为 `false` 时不会创建对应的子 Applet 实例;运行时修改配置会实时卸载或重新加载对应子 Applet。`dde-shell-frame` 本身不读取或保存此配置状态。 + +命令行参数 `-d` 由 `DPluginLoader` 处理,会在扫描 metadata 时直接过滤对应 Applet,包括顶层 Panel。它的优先级高于 DConfig;在当前进程生命周期内,被 `-d` 禁用的 Applet 无法通过 DConfig 重新启用。 + +例如禁用启动器 Applet: + +```bash +dde-dconfig set -a org.deepin.dde.shell -r org.deepin.dde.shell \ + -s /org.deepin.ds.dock.launcherapplet -k enable -v false +``` + ### DAppletBridge API 从 C++ 与小部件通信的桥接: diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index c201baa5b..8f6adb1c9 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -15,6 +15,8 @@ endif() add_executable(dde-shell main.cpp + appletconfigmanager.h + appletconfigmanager.cpp appletloader.h appletloader.cpp shell.h diff --git a/shell/appletconfigmanager.cpp b/shell/appletconfigmanager.cpp new file mode 100644 index 000000000..3f65f9b45 --- /dev/null +++ b/shell/appletconfigmanager.cpp @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "appletconfigmanager.h" + +#include "pluginloader.h" + +#include +#include +#include + +DS_BEGIN_NAMESPACE + +DCORE_USE_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(dsLoaderLog) + +static constexpr auto AppId{"org.deepin.dde.shell"}; +static constexpr auto ConfigName{"org.deepin.dde.shell"}; +static constexpr auto EnableKey{"enable"}; + +AppletConfigManager::AppletConfigManager(QObject *parent) + : QObject(parent) +{ +} + +QStringList AppletConfigManager::disabledPlugins() const +{ + return m_disabledPlugins; +} + +bool AppletConfigManager::isAppletEnabled(const QString &pluginId) const +{ + return !m_disabledPlugins.contains(pluginId); +} + +void AppletConfigManager::setAppletEnabled(const QString &pluginId, bool enabled) +{ + const bool currentlyEnabled = isAppletEnabled(pluginId); + if (currentlyEnabled == enabled) + return; + + if (enabled) + m_disabledPlugins.removeAll(pluginId); + else + m_disabledPlugins.append(pluginId); + Q_EMIT appletEnabledChanged(pluginId, enabled); +} + +void AppletConfigManager::ensureAppletConfigs() +{ + auto loader = DPluginLoader::instance(); + const auto plugins = loader->plugins(); + QSet pluginIds; + for (const auto &plugin : plugins) { + // Top-level panels/applets are created by AppletManager and are not + // controlled by DConfig. Only child applets can be toggled at runtime. + if (plugin.value(QLatin1String("Parent")).toString().isEmpty()) + continue; + pluginIds.insert(plugin.pluginId()); + } + + for (const auto &pluginId : pluginIds) { + if (m_appletConfigs.contains(pluginId)) + continue; + if (!isAppletEnabled(pluginId)) + continue; + + auto config = DConfig::create(QLatin1String(AppId), + QLatin1String(ConfigName), + QLatin1Char('/') + pluginId, + this); + m_appletConfigs.insert(pluginId, config); + if (!config || !config->isValid()) { + qCWarning(dsLoaderLog) << "Unable to create applet DConfig; applet remains enabled:" << pluginId; + setAppletEnabled(pluginId, true); + continue; + } + + setAppletEnabled(pluginId, config->value(QLatin1String(EnableKey), true).toBool()); + QObject::connect(config, &DConfig::valueChanged, this, [this, pluginId, config](const QString &key) { + if (key == QLatin1String(EnableKey)) + setAppletEnabled(pluginId, config->value(QLatin1String(EnableKey), true).toBool()); + }); + } +} + +DS_END_NAMESPACE diff --git a/shell/appletconfigmanager.h b/shell/appletconfigmanager.h new file mode 100644 index 000000000..4dbb0fd7d --- /dev/null +++ b/shell/appletconfigmanager.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "dsglobal.h" + +#include +#include +#include + +namespace Dtk::Core { +class DConfig; +} + +DS_BEGIN_NAMESPACE + +class AppletConfigManager : public QObject +{ + Q_OBJECT +public: + explicit AppletConfigManager(QObject *parent = nullptr); + + QStringList disabledPlugins() const; + bool isAppletEnabled(const QString &pluginId) const; + void ensureAppletConfigs(); + +Q_SIGNALS: + void appletEnabledChanged(const QString &pluginId, bool enabled); + +private: + void setAppletEnabled(const QString &pluginId, bool enabled); + + QMap m_appletConfigs; + QStringList m_disabledPlugins; +}; + +DS_END_NAMESPACE diff --git a/shell/appletloader.cpp b/shell/appletloader.cpp index 3564f91a0..ef12e2566 100644 --- a/shell/appletloader.cpp +++ b/shell/appletloader.cpp @@ -1,8 +1,9 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 -2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later #include "appletloader.h" +#include "appletconfigmanager.h" #include "pluginloader.h" #include "applet.h" #include "containment.h" @@ -84,22 +85,35 @@ class DAppletLoaderPrivate : public DObjectPrivate bool init(DApplet *applet); void createChildren(DApplet *applet); + void handleAppletEnabledChanged(const QString &pluginId, bool enabled); + void createEnabledApplets(DApplet *applet, const QString &pluginId); + void removeDisabledApplets(DApplet *applet, const QString &pluginId); void loadTranslation(const DPluginMetaData &pluginData); void removeTranslation(const QString &pluginId); + void removeTranslations(const QString &pluginId); QPointer m_applet = nullptr; + QPointer m_configManager = nullptr; QMap m_pluginTranslators; D_DECLARE_PUBLIC(DAppletLoader); }; -DAppletLoader::DAppletLoader(class DApplet *applet, QObject *parent) +DAppletLoader::DAppletLoader(DApplet *applet, AppletConfigManager *configManager, QObject *parent) : QObject(parent) , DObject(*new DAppletLoaderPrivate(this)) { D_D(DAppletLoader); d->m_applet = applet; + d->m_configManager = configManager; + Q_ASSERT(configManager); + QObject::connect(configManager, + &AppletConfigManager::appletEnabledChanged, + this, + [d](const QString &pluginId, bool enabled) { + d->handleAppletEnabledChanged(pluginId, enabled); + }); } DAppletLoader::~DAppletLoader() @@ -115,10 +129,10 @@ void DAppletLoader::exec() if (!d->load(d->m_applet)) return; - d->createRootObject(d->m_applet); - if (!d->init(d->m_applet)) return; + + d->createRootObject(d->m_applet); } DApplet *DAppletLoader::applet() const @@ -146,10 +160,11 @@ void DAppletLoaderPrivate::doCreateRootObject(DApplet *applet) } }); - qCDebug(dsLoaderLog) << "Begin to create rootObject the applet:" << applet->pluginId(); - if (!engine->create()) { - engine->deleteLater(); - } + QMetaObject::invokeMethod(engine, [engine]() { + if (!engine->create()) { + engine->deleteLater(); + } + }, Qt::QueuedConnection); } bool DAppletLoaderPrivate::doLoad(DApplet *applet) @@ -190,6 +205,8 @@ void DAppletLoaderPrivate::createChildren(DApplet *applet) const auto data = applet->appletData(); auto groups = groupList(applet, data); for (const auto &item : std::as_const(groups)) { + if (!m_configManager->isAppletEnabled(item.pluginId())) + continue; auto child = containment->createApplet(item); if (!child) { @@ -199,6 +216,67 @@ void DAppletLoaderPrivate::createChildren(DApplet *applet) } } +void DAppletLoaderPrivate::handleAppletEnabledChanged(const QString &pluginId, bool enabled) +{ + if (!m_applet) + return; + + if (enabled) { + createEnabledApplets(m_applet, pluginId); + return; + } + + removeDisabledApplets(m_applet, pluginId); + removeTranslations(pluginId); +} + +void DAppletLoaderPrivate::createEnabledApplets(DApplet *applet, const QString &pluginId) +{ + auto containment = qobject_cast(applet); + if (!containment) + return; + + const auto groups = groupList(applet, applet->appletData()); + for (const auto &data : groups) { + if (data.pluginId() != pluginId) + continue; + + auto child = containment->createApplet(data); + if (!child) + continue; + + loadTranslation(child->pluginMetaData()); + if (!load(child) || !init(child)) { + removeTranslations(pluginId); + continue; + } + createRootObject(child); + } + + const auto children = containment->applets(); + for (auto child : children) { + if (child->pluginId() == pluginId) + continue; + createEnabledApplets(child, pluginId); + } +} + +void DAppletLoaderPrivate::removeDisabledApplets(DApplet *applet, const QString &pluginId) +{ + auto containment = qobject_cast(applet); + if (!containment) + return; + + const auto children = containment->applets(); + for (auto child : children) { + if (child->pluginId() == pluginId) { + containment->removeApplet(child); + continue; + } + removeDisabledApplets(child, pluginId); + } +} + bool DAppletLoaderPrivate::load(DApplet *applet) { if (!doLoad(applet)) { @@ -248,6 +326,9 @@ void DAppletLoaderPrivate::loadTranslation(const DPluginMetaData &pluginData) const QString baseDir = pluginData.pluginDir(); const QString pluginId = pluginData.pluginId(); + if (!m_configManager->isAppletEnabled(pluginId)) + return; + auto translator = new QTranslator(qApp); const QString pluginTranslationDir(baseDir + "/translations/"); if (translator->load(QLocale::system(), pluginId, QLatin1String("_"), pluginTranslationDir)) { @@ -275,4 +356,12 @@ void DAppletLoaderPrivate::removeTranslation(const QString &pluginId) } } +void DAppletLoaderPrivate::removeTranslations(const QString &pluginId) +{ + const auto children = DPluginLoader::instance()->childrenPlugin(pluginId); + for (const auto &child : children) + removeTranslations(child.pluginId()); + removeTranslation(pluginId); +} + DS_END_NAMESPACE diff --git a/shell/appletloader.h b/shell/appletloader.h index 5f088ded4..01e185b9f 100644 --- a/shell/appletloader.h +++ b/shell/appletloader.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -12,13 +12,14 @@ DS_BEGIN_NAMESPACE class DApplet; +class AppletConfigManager; class DAppletLoaderPrivate; class DAppletLoader : public QObject, public DTK_CORE_NAMESPACE::DObject { Q_OBJECT D_DECLARE_PRIVATE(DAppletLoader) public: - explicit DAppletLoader(DApplet *applet, QObject *parent = nullptr); + explicit DAppletLoader(DApplet *applet, AppletConfigManager *configManager, QObject *parent = nullptr); virtual ~DAppletLoader() override; void exec(); diff --git a/shell/main.cpp b/shell/main.cpp index eebd0069c..9fdbc0a94 100644 --- a/shell/main.cpp +++ b/shell/main.cpp @@ -19,6 +19,7 @@ #include "applet.h" #include "containment.h" #include "pluginloader.h" +#include "appletconfigmanager.h" #include "appletloader.h" #include "qmlengine.h" #include "shell.h" @@ -49,9 +50,11 @@ static void disableLogOutput() class AppletManager { public: - explicit AppletManager(const QStringList &pluginIds) + explicit AppletManager(const QStringList &pluginIds, AppletConfigManager *configManager) { qCDebug(dsLog) << "Preloading plugins:" << pluginIds; + Q_ASSERT(configManager); + auto rootApplet = qobject_cast(DPluginLoader::instance()->rootApplet()); Q_ASSERT(rootApplet); @@ -62,9 +65,8 @@ class AppletManager continue; } - auto loader = new DAppletLoader(applet); + auto loader = new DAppletLoader(applet, configManager); m_loaders << loader; - QObject::connect(loader, &DAppletLoader::failed, qApp, [this, loader, pluginIds](const QString &pluginId) { if (pluginIds.contains(pluginId)) { m_loaders.removeOne(loader); @@ -90,6 +92,7 @@ class AppletManager item->deleteLater(); } } + QList m_loaders; }; @@ -200,11 +203,13 @@ int main(int argc, char *argv[]) } shell.dconfigsMigrate(); + AppletConfigManager appletConfigManager; + appletConfigManager.ensureAppletConfigs(); // TODO disable qml's cache avoid to parsing error for ExecutionEngine. shell.disableQmlCache(); shell.setFlickableWheelDeceleration(6000); - AppletManager manager(pluginIds); + AppletManager manager(pluginIds, &appletConfigManager); if (parser.isSet(sceneviewOption)) manager.enableSceneview(); diff --git a/shell/org.deepin.dde.shell.json b/shell/org.deepin.dde.shell.json index 55af2a67a..9c2b926ae 100644 --- a/shell/org.deepin.dde.shell.json +++ b/shell/org.deepin.dde.shell.json @@ -11,6 +11,17 @@ "description": "DConfig that has completed migration", "permissions": "readwrite", "visibility": "private" + }, + "enable": { + "value": true, + "serial": 0, + "flags": [], + "name": "Enable Applet", + "name[zh_CN]": "启用 Applet", + "description": "Enable or disable the dde-shell applet selected by the DConfig subpath.", + "description[zh_CN]": "启用或禁用由 DConfig subpath 指定的 dde-shell Applet。", + "permissions": "readwrite", + "visibility": "private" } } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 77e6bee23..17d7acad5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,6 @@ -# SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +# SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. # # SPDX-License-Identifier: CC0-1.0 add_subdirectory(panels) +add_subdirectory(frame) diff --git a/tests/frame/CMakeLists.txt b/tests/frame/CMakeLists.txt new file mode 100644 index 000000000..21a5c36c0 --- /dev/null +++ b/tests/frame/CMakeLists.txt @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: CC0-1.0 + +find_package(GTest REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS Core) + +include(GoogleTest) + +add_executable(appletitemmodel_tests + ${CMAKE_SOURCE_DIR}/frame/appletitemmodel.cpp + ${CMAKE_SOURCE_DIR}/frame/appletitemmodel.h + appletitemmodeltests.cpp +) + +target_link_libraries(appletitemmodel_tests + GTest::GTest + GTest::Main + Qt${QT_VERSION_MAJOR}::Core +) + +target_include_directories(appletitemmodel_tests PRIVATE + ${CMAKE_SOURCE_DIR}/frame +) + +gtest_discover_tests(appletitemmodel_tests) + +add_executable(applet_tests + applettests.cpp +) + +target_link_libraries(applet_tests + dde-shell-frame + GTest::GTest + GTest::Main + Qt${QT_VERSION_MAJOR}::Core +) + +add_test( + NAME DApplet.DoesNotExposeEnableProperty + COMMAND ${CMAKE_COMMAND} -E env + LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/frame + $ + --gtest_filter=DApplet.DoesNotExposeEnableProperty +) + +add_executable(pluginloader_tests + pluginloadertests.cpp +) + +target_link_libraries(pluginloader_tests + dde-shell-frame + GTest::GTest + Qt${QT_VERSION_MAJOR}::Core +) + +add_test( + NAME DPluginLoader.DisabledAppletIsExcludedFromMetadata + COMMAND ${CMAKE_COMMAND} -E env + LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/frame + $ + --gtest_filter=DPluginLoader.DisabledAppletIsExcludedFromMetadata +) diff --git a/tests/frame/appletitemmodeltests.cpp b/tests/frame/appletitemmodeltests.cpp new file mode 100644 index 000000000..dce36d16a --- /dev/null +++ b/tests/frame/appletitemmodeltests.cpp @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "appletitemmodel.h" + +#include + +DS_USE_NAMESPACE + +namespace { +QObject *rootObjectAt(const DAppletItemModel &model, int row) +{ + return model.data(model.index(row, 0), DAppletItemModel::Data).value(); +} +} + +TEST(DAppletItemModel, AppendsRootObjects) +{ + DAppletItemModel model; + QObject first; + QObject second; + QObject third; + + model.append(&first); + model.append(&second); + model.append(&third); + + ASSERT_EQ(model.rowCount(QModelIndex()), 3); + EXPECT_EQ(rootObjectAt(model, 0), &first); + EXPECT_EQ(rootObjectAt(model, 1), &second); + EXPECT_EQ(rootObjectAt(model, 2), &third); +} + +TEST(DAppletItemModel, RemovesRootObject) +{ + DAppletItemModel model; + QObject first; + QObject middle; + QObject last; + + model.append(&first); + model.append(&middle); + model.append(&last); + model.remove(&middle); + + ASSERT_EQ(model.rowCount(QModelIndex()), 2); + EXPECT_EQ(model.rootObjects(), QList({&first, &last})); +} diff --git a/tests/frame/applettests.cpp b/tests/frame/applettests.cpp new file mode 100644 index 000000000..01846aa0f --- /dev/null +++ b/tests/frame/applettests.cpp @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "applet.h" + +#include + +DS_USE_NAMESPACE + +TEST(DApplet, DoesNotExposeEnableProperty) +{ + DApplet applet; + + EXPECT_EQ(applet.metaObject()->indexOfProperty("enable"), -1); + EXPECT_EQ(applet.metaObject()->indexOfSignal("enableChanged(bool)"), -1); +} diff --git a/tests/frame/pluginloadertests.cpp b/tests/frame/pluginloadertests.cpp new file mode 100644 index 000000000..e91f0bde7 --- /dev/null +++ b/tests/frame/pluginloadertests.cpp @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "pluginloader.h" + +#include +#include +#include +#include + +#include + +DS_USE_NAMESPACE + +TEST(DPluginLoader, DisabledAppletIsExcludedFromMetadata) +{ + QTemporaryDir packageRoot; + ASSERT_TRUE(packageRoot.isValid()); + + const QString pluginId = QStringLiteral("org.deepin.ds.test.disabled-applet"); + const QString packageDir = packageRoot.filePath(pluginId); + ASSERT_TRUE(QDir().mkpath(packageDir)); + + QFile metadataFile(packageDir + QStringLiteral("/metadata.json")); + ASSERT_TRUE(metadataFile.open(QIODevice::WriteOnly)); + metadataFile.write(R"({ + "Plugin": { + "Version": "1.0", + "Id": "org.deepin.ds.test.disabled-applet", + "Url": "main.qml" + } + })"); + metadataFile.close(); + + auto loader = DPluginLoader::instance(); + loader->addPackageDir(packageRoot.path()); + ASSERT_TRUE(loader->plugin(pluginId).isValid()); + + loader->setDisabledApplets({pluginId}); + + EXPECT_TRUE(loader->disabledApplets().contains(pluginId)); + EXPECT_FALSE(loader->plugin(pluginId).isValid()); +} + +int main(int argc, char **argv) +{ + QCoreApplication application(argc, argv); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}