feat(sdk): add IToolbarProvider interface to kcsdk (#354)
All checks were successful
Build and Test / build (pull_request) Successful in 30m28s

This commit is contained in:
forbes
2026-03-01 09:32:25 -06:00
parent 51f7635ceb
commit 76e448d0d7
10 changed files with 366 additions and 14 deletions

View File

@@ -1,19 +1,20 @@
# kindred-addon-sdk — stable API for Kindred Create addon integration
from kindred_sdk.version import SDK_VERSION
from kindred_sdk.context import (
register_context,
unregister_context,
register_overlay,
unregister_overlay,
inject_commands,
current_context,
refresh_context,
)
from kindred_sdk.theme import get_theme_tokens, load_palette
from kindred_sdk.origin import register_origin, unregister_origin
from kindred_sdk.dock import register_dock_panel
from kindred_sdk.compat import create_version, freecad_version
from kindred_sdk.context import (
current_context,
inject_commands,
refresh_context,
register_context,
register_overlay,
unregister_context,
unregister_overlay,
)
from kindred_sdk.dock import register_dock_panel
from kindred_sdk.origin import register_origin, unregister_origin
from kindred_sdk.theme import get_theme_tokens, load_palette
from kindred_sdk.toolbar import register_toolbar
from kindred_sdk.version import SDK_VERSION
__all__ = [
"SDK_VERSION",
@@ -26,6 +27,7 @@ __all__ = [
"refresh_context",
"get_theme_tokens",
"load_palette",
"register_toolbar",
"register_origin",
"unregister_origin",
"register_dock_panel",

View File

@@ -0,0 +1,37 @@
"""Toolbar provider registration.
Wraps the C++ ``kcsdk.register_toolbar()`` API with a Python fallback
that extracts toolbar data and calls ``inject_commands()`` directly.
"""
def _kcsdk_available():
"""Return the kcsdk module if available, else None."""
try:
import kcsdk
return kcsdk
except ImportError:
return None
def register_toolbar(provider):
"""Register a toolbar provider for automatic context injection.
When the C++ ``kcsdk`` module is available, delegates to its
``register_toolbar()`` which stores the provider and auto-injects
commands into the target editing contexts.
Falls back to extracting data from the provider and calling
``inject_commands()`` directly for each target context.
"""
kcsdk = _kcsdk_available()
if kcsdk is not None:
kcsdk.register_toolbar(provider)
return
# Fallback: extract data and call inject_commands directly
from kindred_sdk import inject_commands
for ctx_id in provider.context_ids():
inject_commands(ctx_id, provider.toolbar_name(), provider.commands())

View File

@@ -4,6 +4,7 @@ set(KCSDK_SRCS
KCSDKGlobal.h
Types.h
IPanelProvider.h
IToolbarProvider.h
WidgetBridge.h
WidgetBridge.cpp
ThemeEngine.h

View File

@@ -0,0 +1,60 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
* *
* This file is part of FreeCAD. *
* *
* FreeCAD is free software: you can redistribute it and/or modify it *
* under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation, either version 2.1 of the *
* License, or (at your option) any later version. *
* *
* FreeCAD is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with FreeCAD. If not, see *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef KCSDK_ITOOLBARPROVIDER_H
#define KCSDK_ITOOLBARPROVIDER_H
#include <string>
#include <vector>
#include "KCSDKGlobal.h"
namespace KCSDK
{
/// Abstract interface for addon-provided toolbar declarations.
///
/// Addons implement this interface to declaratively register toolbar
/// configurations. On registration the SDK auto-injects the declared
/// commands into the specified editing contexts.
class KCSDKExport IToolbarProvider
{
public:
virtual ~IToolbarProvider() = default;
/// Unique provider identifier (e.g. "ztools.partdesign", "gears.gear").
virtual std::string id() const = 0;
/// Toolbar name shown in the UI.
virtual std::string toolbar_name() const = 0;
/// Editing context IDs this toolbar applies to.
virtual std::vector<std::string> context_ids() const = 0;
/// Command names to inject into the toolbar.
virtual std::vector<std::string> commands() const = 0;
};
} // namespace KCSDK
#endif // KCSDK_ITOOLBARPROVIDER_H

View File

@@ -23,6 +23,7 @@
#include "SDKRegistry.h"
#include "IPanelProvider.h"
#include "IToolbarProvider.h"
#include <QString>
#include <QStringList>
@@ -91,10 +92,13 @@ std::vector<std::string> SDKRegistry::available() const
{
std::lock_guard<std::mutex> lock(mutex_);
std::vector<std::string> result;
result.reserve(panels_.size());
result.reserve(panels_.size() + toolbars_.size());
for (const auto& [id, _] : panels_) {
result.push_back(id);
}
for (const auto& [id, _] : toolbars_) {
result.push_back(id);
}
return result;
}
@@ -252,4 +256,53 @@ std::vector<std::string> SDKRegistry::registeredPanels() const
return result;
}
// -- Toolbar provider API ---------------------------------------------------
void SDKRegistry::registerToolbar(std::unique_ptr<IToolbarProvider> provider)
{
if (!provider) {
return;
}
std::string id = provider->id();
std::string toolbarName = provider->toolbar_name();
std::vector<std::string> contextIds = provider->context_ids();
std::vector<std::string> cmds = provider->commands();
{
std::lock_guard<std::mutex> lock(mutex_);
toolbars_[id] = std::move(provider);
}
// Auto-inject commands into each target context.
for (const auto& ctxId : contextIds) {
injectCommands(ctxId, toolbarName, cmds);
}
Base::Console().log("KCSDK: registered toolbar provider '%s'\n", id.c_str());
}
void SDKRegistry::unregisterToolbar(const std::string& id)
{
std::lock_guard<std::mutex> lock(mutex_);
auto it = toolbars_.find(id);
if (it == toolbars_.end()) {
return;
}
toolbars_.erase(it);
Base::Console().log("KCSDK: unregistered toolbar provider '%s'\n", id.c_str());
}
std::vector<std::string> SDKRegistry::registeredToolbars() const
{
std::lock_guard<std::mutex> lock(mutex_);
std::vector<std::string> result;
result.reserve(toolbars_.size());
for (const auto& [id, _] : toolbars_) {
result.push_back(id);
}
return result;
}
} // namespace KCSDK

View File

@@ -37,6 +37,7 @@ namespace KCSDK
{
class IPanelProvider;
class IToolbarProvider;
/// Current KCSDK API major version. Addons should check this at load time.
constexpr int API_VERSION_MAJOR = 1;
@@ -102,6 +103,18 @@ public:
/// Return IDs of all registered panel providers.
std::vector<std::string> registeredPanels() const;
// -- Toolbar provider API ----------------------------------------------
/// Register a toolbar provider. Ownership transfers to the registry.
/// Auto-injects the declared commands into the target editing contexts.
void registerToolbar(std::unique_ptr<IToolbarProvider> provider);
/// Remove a registered toolbar provider.
void unregisterToolbar(const std::string& id);
/// Return IDs of all registered toolbar providers.
std::vector<std::string> registeredToolbars() const;
private:
SDKRegistry();
@@ -112,6 +125,7 @@ private:
mutable std::mutex mutex_;
std::unordered_map<std::string, std::unique_ptr<IPanelProvider>> panels_;
std::unordered_map<std::string, std::unique_ptr<IToolbarProvider>> toolbars_;
};
} // namespace KCSDK

View File

@@ -4,6 +4,8 @@ set(KCSDKPy_SRCS
kcsdk_py.cpp
PyIPanelProvider.h
PyProviderHolder.h
PyIToolbarProvider.h
PyToolbarHolder.h
)
add_library(kcsdk_py SHARED ${KCSDKPy_SRCS})

View File

@@ -0,0 +1,65 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
* *
* This file is part of FreeCAD. *
* *
* FreeCAD is free software: you can redistribute it and/or modify it *
* under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation, either version 2.1 of the *
* License, or (at your option) any later version. *
* *
* FreeCAD is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with FreeCAD. If not, see *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef KCSDK_PYITOOLBARPROVIDER_H
#define KCSDK_PYITOOLBARPROVIDER_H
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <Gui/SDK/IToolbarProvider.h>
namespace KCSDK
{
/// pybind11 trampoline class for IToolbarProvider.
/// Enables Python subclasses that override virtual methods.
class PyIToolbarProvider : public IToolbarProvider
{
public:
using IToolbarProvider::IToolbarProvider;
std::string id() const override
{
PYBIND11_OVERRIDE_PURE(std::string, IToolbarProvider, id);
}
std::string toolbar_name() const override
{
PYBIND11_OVERRIDE_PURE(std::string, IToolbarProvider, toolbar_name);
}
std::vector<std::string> context_ids() const override
{
PYBIND11_OVERRIDE_PURE(std::vector<std::string>, IToolbarProvider, context_ids);
}
std::vector<std::string> commands() const override
{
PYBIND11_OVERRIDE_PURE(std::vector<std::string>, IToolbarProvider, commands);
}
};
} // namespace KCSDK
#endif // KCSDK_PYITOOLBARPROVIDER_H

View File

@@ -0,0 +1,81 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
* *
* This file is part of FreeCAD. *
* *
* FreeCAD is free software: you can redistribute it and/or modify it *
* under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation, either version 2.1 of the *
* License, or (at your option) any later version. *
* *
* FreeCAD is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with FreeCAD. If not, see *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef KCSDK_PYTOOLBARHOLDER_H
#define KCSDK_PYTOOLBARHOLDER_H
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <Gui/SDK/IToolbarProvider.h>
namespace py = pybind11;
namespace KCSDK
{
/// GIL-safe forwarding wrapper that holds a Python IToolbarProvider instance.
///
/// Stores the py::object to prevent garbage collection. Acquires the GIL
/// before every call into Python. All methods return std types so no
/// Qt/PySide bridging is needed.
class PyToolbarHolder : public IToolbarProvider
{
public:
explicit PyToolbarHolder(py::object obj)
: obj_(std::move(obj))
, provider_(obj_.cast<IToolbarProvider*>())
{}
std::string id() const override
{
py::gil_scoped_acquire gil;
return provider_->id();
}
std::string toolbar_name() const override
{
py::gil_scoped_acquire gil;
return provider_->toolbar_name();
}
std::vector<std::string> context_ids() const override
{
py::gil_scoped_acquire gil;
return provider_->context_ids();
}
std::vector<std::string> commands() const override
{
py::gil_scoped_acquire gil;
return provider_->commands();
}
private:
py::object obj_; ///< Prevents Python GC — keeps reference alive.
IToolbarProvider* provider_; ///< Raw pointer into trampoline inside obj_.
};
} // namespace KCSDK
#endif // KCSDK_PYTOOLBARHOLDER_H

View File

@@ -26,12 +26,15 @@
#include <pybind11/functional.h>
#include <Gui/SDK/IPanelProvider.h>
#include <Gui/SDK/IToolbarProvider.h>
#include <Gui/SDK/SDKRegistry.h>
#include <Gui/SDK/ThemeEngine.h>
#include <Gui/SDK/Types.h>
#include "PyIPanelProvider.h"
#include "PyIToolbarProvider.h"
#include "PyProviderHolder.h"
#include "PyToolbarHolder.h"
namespace py = pybind11;
using namespace KCSDK;
@@ -247,6 +250,40 @@ PYBIND11_MODULE(kcsdk, m)
},
"Return IDs of all registered panel providers.");
// -- Toolbar provider API -----------------------------------------------
py::class_<IToolbarProvider, PyIToolbarProvider>(m, "IToolbarProvider")
.def(py::init<>())
.def("id", &IToolbarProvider::id)
.def("toolbar_name", &IToolbarProvider::toolbar_name)
.def("context_ids", &IToolbarProvider::context_ids)
.def("commands", &IToolbarProvider::commands);
m.def("register_toolbar",
[](py::object provider) {
auto holder = std::make_unique<PyToolbarHolder>(std::move(provider));
SDKRegistry::instance().registerToolbar(std::move(holder));
},
py::arg("provider"),
"Register a toolbar provider for automatic context injection.\n\n"
"Parameters\n"
"----------\n"
"provider : IToolbarProvider\n"
" Toolbar provider implementing id(), toolbar_name(), context_ids(), commands().");
m.def("unregister_toolbar",
[](const std::string& id) {
SDKRegistry::instance().unregisterToolbar(id);
},
py::arg("id"),
"Remove a registered toolbar provider.");
m.def("registered_toolbars",
[]() {
return SDKRegistry::instance().registeredToolbars();
},
"Return IDs of all registered toolbar providers.");
// -- Theme engine API ---------------------------------------------------
m.def("theme_color",