Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions panels/dock/taskmanager/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ add_library(dock-taskmanager SHARED ${DBUS_INTERFACES}
hoverpreviewproxymodel.h
taskmanager.cpp
taskmanager.h
foldermenuregistry.cpp
foldermenuregistry.h
treelandwindow.cpp
treelandwindow.h
treelandwindowmonitor.cpp
Expand Down
90 changes: 90 additions & 0 deletions panels/dock/taskmanager/foldermenuregistry.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later
#include "foldermenuregistry.h"

#include <QCoreApplication>

Check warning on line 5 in panels/dock/taskmanager/foldermenuregistry.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QCoreApplication> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QMetaMethod>

Check warning on line 6 in panels/dock/taskmanager/foldermenuregistry.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QMetaMethod> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QSet>

Check warning on line 7 in panels/dock/taskmanager/foldermenuregistry.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QSet> not found. Please note: Cppcheck does not need standard library headers to get proper results.

namespace dock {
namespace {
const QString OpenFolder = QStringLiteral("open-folder");
const QString UndockFolder = QStringLiteral("undock-folder");
}

FolderMenuRegistry::FolderMenuRegistry(QObject *parent) : QObject(parent) {}

bool FolderMenuRegistry::registerProvider(const QString &folderType, QObject *provider)

Check warning on line 17 in panels/dock/taskmanager/foldermenuregistry.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

The function 'registerProvider' is never used.
{
if (folderType.trimmed().isEmpty() || !provider || provider->thread() != thread()) return false;
const auto meta = provider->metaObject();
const int actions = meta->indexOfMethod("folderMenuActions(QString)");
const int trigger = meta->indexOfMethod("triggerFolderMenuAction(QString,QString)");
if (actions < 0 || trigger < 0 || meta->method(actions).returnType() != QMetaType::QVariantList
|| meta->method(trigger).returnType() != QMetaType::Void) return false;
// A different plugin may not silently replace a registered source.
const auto existing = m_providers.value(folderType);
if (existing && existing != provider) return false;
m_providers.insert(folderType, provider);
return true;
}

void FolderMenuRegistry::unregisterProvider(const QString &folderType, QObject *provider)

Check warning on line 32 in panels/dock/taskmanager/foldermenuregistry.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

The function 'unregisterProvider' is never used.
{
if (provider && m_providers.value(folderType) == provider) m_providers.remove(folderType);
}

QVariantList FolderMenuRegistry::standardItems(const QString &folderName)
{
return {QVariantMap{{"id", OpenFolder}, {"name", folderName}},
QVariantMap{{"id", UndockFolder}, {"name", QCoreApplication::translate("FolderContextMenu", "Undock")}}};
}

QVariantList FolderMenuRegistry::customItems(QObject *provider, const QString &folderId) const
{
QVariantList supplied;
if (!provider || folderId.isEmpty()
|| !QMetaObject::invokeMethod(provider, "folderMenuActions", Qt::DirectConnection,
Q_RETURN_ARG(QVariantList, supplied), Q_ARG(QString, folderId))) return {};
QVariantList items;
QSet<QString> used{OpenFolder, UndockFolder};
for (const auto &value : supplied) {
const auto item = value.toMap();
const QString id = item.value("id").toString();
const QString name = item.value("name").toString();
if (id.trimmed().isEmpty() || name.trimmed().isEmpty() || used.contains(id)
|| !item.value("visible", true).toBool()) continue;
used.insert(id);
const bool checkable = item.value("checkable", false).toBool();
items.append(QVariantMap{{"id", id}, {"name", name},
{"enabled", item.value("enabled", true).toBool()},
{"checkable", checkable},
{"checked", checkable && item.value("checked", false).toBool()}});
}
return items;
}

QVariantList FolderMenuRegistry::menuItems(const QString &folderType, const QString &folderId, const QString &folderName) const

Check warning on line 67 in panels/dock/taskmanager/foldermenuregistry.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

The function 'menuItems' is never used.
{
auto items = standardItems(folderName);
items.append(customItems(m_providers.value(folderType), folderId));
return items;
}

bool FolderMenuRegistry::trigger(const QString &folderType, const QString &folderId, const QString &actionId) const
{
const QPointer<QObject> provider = m_providers.value(folderType);

Check warning on line 76 in panels/dock/taskmanager/foldermenuregistry.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Condition 'm_providers.value(folderType)!=provider' is always false
if (!provider || actionId == OpenFolder || actionId == UndockFolder) return false;
// Revalidate at click time: a plugin may unload or disable an action while
// its native menu is open. Never dispatch arbitrary or stale action IDs.
const auto items = customItems(provider, folderId);
if (!provider || m_providers.value(folderType) != provider) return false;

Check warning on line 81 in panels/dock/taskmanager/foldermenuregistry.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Condition '!provider' is always false
for (const auto &value : items) {
const auto item = value.toMap();
if (item.value("id").toString() != actionId || !item.value("enabled").toBool()) continue;
return QMetaObject::invokeMethod(provider, "triggerFolderMenuAction", Qt::DirectConnection,
Q_ARG(QString, folderId), Q_ARG(QString, actionId));
}
return false;
}
}
31 changes: 31 additions & 0 deletions panels/dock/taskmanager/foldermenuregistry.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include <QHash>

Check warning on line 5 in panels/dock/taskmanager/foldermenuregistry.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QHash> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QObject>

Check warning on line 6 in panels/dock/taskmanager/foldermenuregistry.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QObject> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QPointer>
#include <QVariant>

namespace dock {

// Optional menu extensions owned by folder-source plugins. Providers remain
// owned by their plugin and expose the two invokables documented in docs/.
class FolderMenuRegistry : public QObject
{
Q_OBJECT
public:
explicit FolderMenuRegistry(QObject *parent = nullptr);

bool registerProvider(const QString &folderType, QObject *provider);
void unregisterProvider(const QString &folderType, QObject *provider);
static QVariantList standardItems(const QString &folderName);
QVariantList menuItems(const QString &folderType, const QString &folderId, const QString &folderName) const;
bool trigger(const QString &folderType, const QString &folderId, const QString &actionId) const;

private:
QVariantList customItems(QObject *provider, const QString &folderId) const;
QHash<QString, QPointer<QObject>> m_providers;
};

}
10 changes: 10 additions & 0 deletions tests/panels/dock/taskmanager/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,13 @@ target_include_directories(rolegroupmodel_tests PRIVATE
)

gtest_discover_tests(rolegroupmodel_tests)

# Folder-source menu extensions are tested without a desktop or applet service.
add_executable(foldermenuregistry_tests
foldermenuregistrytests.cpp
${CMAKE_SOURCE_DIR}/panels/dock/taskmanager/foldermenuregistry.cpp
${CMAKE_SOURCE_DIR}/panels/dock/taskmanager/foldermenuregistry.h
)
target_include_directories(foldermenuregistry_tests PRIVATE ${CMAKE_SOURCE_DIR}/panels/dock/taskmanager)
target_link_libraries(foldermenuregistry_tests PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Test)
add_test(NAME folder_menu_registry COMMAND foldermenuregistry_tests)
133 changes: 133 additions & 0 deletions tests/panels/dock/taskmanager/foldermenuregistrytests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later
#include "foldermenuregistry.h"

#include <QTest>
#include <memory>

class TestFolderMenuProvider : public QObject
{
Q_OBJECT
public:
QVariantList actions{QVariantMap{{"id", "details"}, {"name", "Details"}}};
QString requestedFolder;
QString triggeredFolder;
QString triggeredAction;
int triggerCount = 0;
Q_INVOKABLE QVariantList folderMenuActions(const QString &folderId)
{
requestedFolder = folderId;
return actions;
}
Q_INVOKABLE void triggerFolderMenuAction(const QString &folderId, const QString &actionId)
{
triggeredFolder = folderId;
triggeredAction = actionId;
++triggerCount;
}
};

class FolderMenuRegistryTest : public QObject
{
Q_OBJECT
private slots:
void defaultMenuOnlyContainsNameAndUndock()
{
dock::FolderMenuRegistry registry;
const auto items = registry.menuItems("launcher", "internal/folders/12", "My folder");
QCOMPARE(items.size(), 2);
QCOMPARE(items[0].toMap().value("id").toString(), "open-folder");
QCOMPARE(items[0].toMap().value("name").toString(), "My folder");
QCOMPARE(items[1].toMap().value("id").toString(), "undock-folder");
QVERIFY(!registry.trigger("launcher", "internal/folders/12", "details"));
}
void customActionsAreAppendedAndDispatchedToTheirSource()
{
dock::FolderMenuRegistry registry;
TestFolderMenuProvider launcher, files;
QVERIFY(registry.registerProvider("launcher", &launcher));
QVERIFY(registry.registerProvider("files", &files));
const auto items = registry.menuItems("launcher", "internal/folders/12", "My folder");
QCOMPARE(items.size(), 3);
QCOMPARE(items[2].toMap().value("id").toString(), "details");
QCOMPARE(launcher.requestedFolder, "internal/folders/12");
QVERIFY(registry.trigger("launcher", "internal/folders/12", "details"));
QCOMPARE(launcher.triggeredFolder, "internal/folders/12");
QCOMPARE(launcher.triggeredAction, "details");
QCOMPARE(files.triggerCount, 0);
QCOMPARE(registry.menuItems("unregistered", "id", "Name").size(), 2);
}
void reservedDuplicateAndMalformedItemsCannotOverrideTheFramework()
{
dock::FolderMenuRegistry registry;
TestFolderMenuProvider provider;
provider.actions = {
QVariantMap{{"id", "open-folder"}, {"name", "Replace open"}},
QVariantMap{{"id", "undock-folder"}, {"name", "Replace undock"}},
QVariantMap{{"id", "details"}, {"name", "Details"}},
QVariantMap{{"id", "details"}, {"name", "Duplicate"}},
QVariantMap{{"id", ""}, {"name", "No identity"}},
QVariantMap{{"id", "unnamed"}},
QVariantMap{{"id", "hidden"}, {"name", "Hidden"}, {"visible", false}}
};
QVERIFY(registry.registerProvider("launcher", &provider));
QCOMPARE(registry.menuItems("launcher", "id", "Folder").size(), 3);
QVERIFY(!registry.trigger("launcher", "id", "open-folder"));
QVERIFY(!registry.trigger("launcher", "id", "undock-folder"));
QVERIFY(!registry.trigger("launcher", "id", "hidden"));
QVERIFY(!registry.trigger("launcher", "id", "unknown"));
QCOMPARE(provider.triggerCount, 0);
}
void enabledAndCheckedStateIsProvidedByPlugin()
{
dock::FolderMenuRegistry registry;
TestFolderMenuProvider provider;
provider.actions = {QVariantMap{{"id", "details"}, {"name", "Details"},
{"enabled", false}, {"checkable", true}, {"checked", true}}};
QVERIFY(registry.registerProvider("launcher", &provider));
const auto custom = registry.menuItems("launcher", "id", "Folder")[2].toMap();
QCOMPARE(custom.value("enabled").toBool(), false);
QCOMPARE(custom.value("checkable").toBool(), true);
QCOMPARE(custom.value("checked").toBool(), true);
QVERIFY(!registry.trigger("launcher", "id", "details"));
provider.actions = {QVariantMap{{"id", "details"}, {"name", "Details"}}};
QVERIFY(registry.trigger("launcher", "id", "details"));
// Actions can change while a native menu is open.
provider.actions.clear();
QVERIFY(!registry.trigger("launcher", "id", "details"));
QCOMPARE(provider.triggerCount, 1);
}
void registrationIsOwnedByItsPlugin()
{
dock::FolderMenuRegistry registry;
TestFolderMenuProvider first, other;
QObject invalid;
QVERIFY(!registry.registerProvider("", &first));
QVERIFY(!registry.registerProvider("launcher", nullptr));
QVERIFY(!registry.registerProvider("launcher", &invalid));
QVERIFY(registry.registerProvider("launcher", &first));
QVERIFY(registry.registerProvider("launcher", &first));
QVERIFY(!registry.registerProvider("launcher", &other));
registry.unregisterProvider("launcher", &other);
QCOMPARE(registry.menuItems("launcher", "id", "Folder").size(), 3);
registry.unregisterProvider("launcher", &first);
QCOMPARE(registry.menuItems("launcher", "id", "Folder").size(), 2);
QVERIFY(!registry.trigger("launcher", "id", "details"));
QVERIFY(registry.registerProvider("launcher", &other));
}
void destroyingProviderRestoresTheDefaultMenu()
{
dock::FolderMenuRegistry registry;
auto provider = std::make_unique<TestFolderMenuProvider>();
QVERIFY(registry.registerProvider("launcher", provider.get()));
provider.reset();
QCOMPARE(registry.menuItems("launcher", "id", "Folder").size(), 2);
QVERIFY(!registry.trigger("launcher", "id", "details"));
TestFolderMenuProvider replacement;
QVERIFY(registry.registerProvider("launcher", &replacement));
QCOMPARE(registry.menuItems("launcher", "id", "Folder").size(), 3);
}
};

QTEST_GUILESS_MAIN(FolderMenuRegistryTest)
#include "foldermenuregistrytests.moc"
Loading