From 4eb643a26fee4c009f3d965d03ad71e8332fb790 Mon Sep 17 00:00:00 2001 From: forbes Date: Sun, 1 Mar 2026 09:32:25 -0600 Subject: [PATCH] feat(sdk): add IToolbarProvider interface to kcsdk (#354) --- mods/sdk/kindred_sdk/__init__.py | 28 ++++---- mods/sdk/kindred_sdk/toolbar.py | 37 +++++++++++ src/Gui/SDK/CMakeLists.txt | 1 + src/Gui/SDK/IToolbarProvider.h | 60 +++++++++++++++++ src/Gui/SDK/SDKRegistry.cpp | 55 ++++++++++++++- src/Gui/SDK/SDKRegistry.h | 14 ++++ src/Gui/SDK/bindings/CMakeLists.txt | 2 + src/Gui/SDK/bindings/PyIToolbarProvider.h | 65 ++++++++++++++++++ src/Gui/SDK/bindings/PyToolbarHolder.h | 81 +++++++++++++++++++++++ src/Gui/SDK/bindings/kcsdk_py.cpp | 37 +++++++++++ 10 files changed, 366 insertions(+), 14 deletions(-) create mode 100644 mods/sdk/kindred_sdk/toolbar.py create mode 100644 src/Gui/SDK/IToolbarProvider.h create mode 100644 src/Gui/SDK/bindings/PyIToolbarProvider.h create mode 100644 src/Gui/SDK/bindings/PyToolbarHolder.h diff --git a/mods/sdk/kindred_sdk/__init__.py b/mods/sdk/kindred_sdk/__init__.py index 67f8e400b9..d889224ff5 100644 --- a/mods/sdk/kindred_sdk/__init__.py +++ b/mods/sdk/kindred_sdk/__init__.py @@ -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", diff --git a/mods/sdk/kindred_sdk/toolbar.py b/mods/sdk/kindred_sdk/toolbar.py new file mode 100644 index 0000000000..c14a295626 --- /dev/null +++ b/mods/sdk/kindred_sdk/toolbar.py @@ -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()) diff --git a/src/Gui/SDK/CMakeLists.txt b/src/Gui/SDK/CMakeLists.txt index 981c4ad7a7..0a6a8b53a2 100644 --- a/src/Gui/SDK/CMakeLists.txt +++ b/src/Gui/SDK/CMakeLists.txt @@ -4,6 +4,7 @@ set(KCSDK_SRCS KCSDKGlobal.h Types.h IPanelProvider.h + IToolbarProvider.h WidgetBridge.h WidgetBridge.cpp ThemeEngine.h diff --git a/src/Gui/SDK/IToolbarProvider.h b/src/Gui/SDK/IToolbarProvider.h new file mode 100644 index 0000000000..d4b2ffd13d --- /dev/null +++ b/src/Gui/SDK/IToolbarProvider.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kindred Systems * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#ifndef KCSDK_ITOOLBARPROVIDER_H +#define KCSDK_ITOOLBARPROVIDER_H + +#include +#include + +#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 context_ids() const = 0; + + /// Command names to inject into the toolbar. + virtual std::vector commands() const = 0; +}; + +} // namespace KCSDK + +#endif // KCSDK_ITOOLBARPROVIDER_H diff --git a/src/Gui/SDK/SDKRegistry.cpp b/src/Gui/SDK/SDKRegistry.cpp index 3c051c9d64..6aee06a833 100644 --- a/src/Gui/SDK/SDKRegistry.cpp +++ b/src/Gui/SDK/SDKRegistry.cpp @@ -23,6 +23,7 @@ #include "SDKRegistry.h" #include "IPanelProvider.h" +#include "IToolbarProvider.h" #include #include @@ -91,10 +92,13 @@ std::vector SDKRegistry::available() const { std::lock_guard lock(mutex_); std::vector 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 SDKRegistry::registeredPanels() const return result; } +// -- Toolbar provider API --------------------------------------------------- + +void SDKRegistry::registerToolbar(std::unique_ptr provider) +{ + if (!provider) { + return; + } + std::string id = provider->id(); + std::string toolbarName = provider->toolbar_name(); + std::vector contextIds = provider->context_ids(); + std::vector cmds = provider->commands(); + + { + std::lock_guard 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 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 SDKRegistry::registeredToolbars() const +{ + std::lock_guard lock(mutex_); + std::vector result; + result.reserve(toolbars_.size()); + for (const auto& [id, _] : toolbars_) { + result.push_back(id); + } + return result; +} + } // namespace KCSDK diff --git a/src/Gui/SDK/SDKRegistry.h b/src/Gui/SDK/SDKRegistry.h index c002e3ca8b..eeada713b1 100644 --- a/src/Gui/SDK/SDKRegistry.h +++ b/src/Gui/SDK/SDKRegistry.h @@ -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 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 provider); + + /// Remove a registered toolbar provider. + void unregisterToolbar(const std::string& id); + + /// Return IDs of all registered toolbar providers. + std::vector registeredToolbars() const; + private: SDKRegistry(); @@ -112,6 +125,7 @@ private: mutable std::mutex mutex_; std::unordered_map> panels_; + std::unordered_map> toolbars_; }; } // namespace KCSDK diff --git a/src/Gui/SDK/bindings/CMakeLists.txt b/src/Gui/SDK/bindings/CMakeLists.txt index 29cf90a1af..8ac1af9043 100644 --- a/src/Gui/SDK/bindings/CMakeLists.txt +++ b/src/Gui/SDK/bindings/CMakeLists.txt @@ -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}) diff --git a/src/Gui/SDK/bindings/PyIToolbarProvider.h b/src/Gui/SDK/bindings/PyIToolbarProvider.h new file mode 100644 index 0000000000..7c350d4e51 --- /dev/null +++ b/src/Gui/SDK/bindings/PyIToolbarProvider.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kindred Systems * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#ifndef KCSDK_PYITOOLBARPROVIDER_H +#define KCSDK_PYITOOLBARPROVIDER_H + +#include +#include + +#include + +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 context_ids() const override + { + PYBIND11_OVERRIDE_PURE(std::vector, IToolbarProvider, context_ids); + } + + std::vector commands() const override + { + PYBIND11_OVERRIDE_PURE(std::vector, IToolbarProvider, commands); + } +}; + +} // namespace KCSDK + +#endif // KCSDK_PYITOOLBARPROVIDER_H diff --git a/src/Gui/SDK/bindings/PyToolbarHolder.h b/src/Gui/SDK/bindings/PyToolbarHolder.h new file mode 100644 index 0000000000..c0fd0bbb62 --- /dev/null +++ b/src/Gui/SDK/bindings/PyToolbarHolder.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kindred Systems * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#ifndef KCSDK_PYTOOLBARHOLDER_H +#define KCSDK_PYTOOLBARHOLDER_H + +#include +#include + +#include + +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()) + {} + + 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 context_ids() const override + { + py::gil_scoped_acquire gil; + return provider_->context_ids(); + } + + std::vector 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 diff --git a/src/Gui/SDK/bindings/kcsdk_py.cpp b/src/Gui/SDK/bindings/kcsdk_py.cpp index fa19879e59..160e3ae277 100644 --- a/src/Gui/SDK/bindings/kcsdk_py.cpp +++ b/src/Gui/SDK/bindings/kcsdk_py.cpp @@ -26,12 +26,15 @@ #include #include +#include #include #include #include #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_(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(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",