feat(sdk): add IMenuProvider interface and register_command wrapper (#355) #363
@@ -1,5 +1,6 @@
|
|||||||
# kindred-addon-sdk — stable API for Kindred Create addon integration
|
# kindred-addon-sdk — stable API for Kindred Create addon integration
|
||||||
|
|
||||||
|
from kindred_sdk.command import register_command
|
||||||
from kindred_sdk.compat import create_version, freecad_version
|
from kindred_sdk.compat import create_version, freecad_version
|
||||||
from kindred_sdk.context import (
|
from kindred_sdk.context import (
|
||||||
current_context,
|
current_context,
|
||||||
@@ -11,6 +12,7 @@ from kindred_sdk.context import (
|
|||||||
unregister_overlay,
|
unregister_overlay,
|
||||||
)
|
)
|
||||||
from kindred_sdk.dock import register_dock_panel
|
from kindred_sdk.dock import register_dock_panel
|
||||||
|
from kindred_sdk.menu import register_menu
|
||||||
from kindred_sdk.origin import register_origin, unregister_origin
|
from kindred_sdk.origin import register_origin, unregister_origin
|
||||||
from kindred_sdk.theme import get_theme_tokens, load_palette
|
from kindred_sdk.theme import get_theme_tokens, load_palette
|
||||||
from kindred_sdk.toolbar import register_toolbar
|
from kindred_sdk.toolbar import register_toolbar
|
||||||
@@ -18,6 +20,7 @@ from kindred_sdk.version import SDK_VERSION
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"SDK_VERSION",
|
"SDK_VERSION",
|
||||||
|
"register_command",
|
||||||
"register_context",
|
"register_context",
|
||||||
"unregister_context",
|
"unregister_context",
|
||||||
"register_overlay",
|
"register_overlay",
|
||||||
@@ -27,6 +30,7 @@ __all__ = [
|
|||||||
"refresh_context",
|
"refresh_context",
|
||||||
"get_theme_tokens",
|
"get_theme_tokens",
|
||||||
"load_palette",
|
"load_palette",
|
||||||
|
"register_menu",
|
||||||
"register_toolbar",
|
"register_toolbar",
|
||||||
"register_origin",
|
"register_origin",
|
||||||
"unregister_origin",
|
"unregister_origin",
|
||||||
|
|||||||
58
mods/sdk/kindred_sdk/command.py
Normal file
58
mods/sdk/kindred_sdk/command.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"""Command registration wrapper.
|
||||||
|
|
||||||
|
Provides a standardized SDK entry point for registering FreeCAD commands.
|
||||||
|
This is a thin wrapper around ``FreeCADGui.addCommand()`` — no C++ interface
|
||||||
|
is needed since FreeCAD's command system is already stable and well-known.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import FreeCAD
|
||||||
|
|
||||||
|
|
||||||
|
def register_command(name, activated, resources, is_active=None):
|
||||||
|
"""Register a FreeCAD command through the SDK.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name : str
|
||||||
|
Command name (e.g. ``"MyAddon_DoThing"``).
|
||||||
|
activated : callable
|
||||||
|
Called when command is triggered. Receives an optional ``int``
|
||||||
|
index argument (for group commands).
|
||||||
|
resources : dict
|
||||||
|
Command resources passed to ``GetResources()``. Common keys:
|
||||||
|
``MenuText``, ``ToolTip``, ``Pixmap``, ``Accel``.
|
||||||
|
is_active : callable, optional
|
||||||
|
Zero-arg callable returning ``True`` when the command should be
|
||||||
|
enabled. Default: always active.
|
||||||
|
"""
|
||||||
|
if not callable(activated):
|
||||||
|
raise TypeError("activated must be callable")
|
||||||
|
if not isinstance(resources, dict):
|
||||||
|
raise TypeError("resources must be a dict")
|
||||||
|
if is_active is not None and not callable(is_active):
|
||||||
|
raise TypeError("is_active must be callable or None")
|
||||||
|
|
||||||
|
_resources = dict(resources)
|
||||||
|
_activated = activated
|
||||||
|
_is_active = is_active
|
||||||
|
|
||||||
|
class _SDKCommand:
|
||||||
|
def GetResources(self):
|
||||||
|
return _resources
|
||||||
|
|
||||||
|
def Activated(self, index=0):
|
||||||
|
_activated()
|
||||||
|
|
||||||
|
def IsActive(self):
|
||||||
|
if _is_active is not None:
|
||||||
|
return bool(_is_active())
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
import FreeCADGui
|
||||||
|
|
||||||
|
FreeCADGui.addCommand(name, _SDKCommand())
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintWarning(
|
||||||
|
f"kindred_sdk: Failed to register command '{name}': {e}\n"
|
||||||
|
)
|
||||||
53
mods/sdk/kindred_sdk/menu.py
Normal file
53
mods/sdk/kindred_sdk/menu.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"""Menu provider registration.
|
||||||
|
|
||||||
|
Wraps the C++ ``kcsdk.register_menu()`` API with a Python fallback
|
||||||
|
that installs a WorkbenchManipulator to inject menu items.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import FreeCAD
|
||||||
|
|
||||||
|
|
||||||
|
def _kcsdk_available():
|
||||||
|
"""Return the kcsdk module if available, else None."""
|
||||||
|
try:
|
||||||
|
import kcsdk
|
||||||
|
|
||||||
|
return kcsdk
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def register_menu(provider):
|
||||||
|
"""Register a menu provider for declarative menu placement.
|
||||||
|
|
||||||
|
When the C++ ``kcsdk`` module is available, delegates to its
|
||||||
|
``register_menu()`` which installs a shared WorkbenchManipulator
|
||||||
|
that injects items at the specified menu path.
|
||||||
|
|
||||||
|
Falls back to installing a Python WorkbenchManipulator directly.
|
||||||
|
"""
|
||||||
|
kcsdk = _kcsdk_available()
|
||||||
|
if kcsdk is not None:
|
||||||
|
kcsdk.register_menu(provider)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fallback: extract data and install a Python manipulator.
|
||||||
|
menu_path = provider.menu_path()
|
||||||
|
items = provider.items()
|
||||||
|
|
||||||
|
try:
|
||||||
|
import FreeCADGui
|
||||||
|
|
||||||
|
class _SDKMenuManipulator:
|
||||||
|
def modifyMenuBar(self, menuBar):
|
||||||
|
menu_items = [
|
||||||
|
"Separator" if item == "separator" else item for item in items
|
||||||
|
]
|
||||||
|
FreeCADGui.addCommand # Just to verify we're in GUI mode
|
||||||
|
menuBar.appendMenu(menu_path, menu_items)
|
||||||
|
|
||||||
|
FreeCADGui.addWorkbenchManipulator(_SDKMenuManipulator())
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintWarning(
|
||||||
|
f"kindred_sdk: Failed to register menu '{provider.id()}': {e}\n"
|
||||||
|
)
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
set(KCSDK_SRCS
|
set(KCSDK_SRCS
|
||||||
KCSDKGlobal.h
|
KCSDKGlobal.h
|
||||||
Types.h
|
Types.h
|
||||||
|
IMenuProvider.h
|
||||||
IPanelProvider.h
|
IPanelProvider.h
|
||||||
IToolbarProvider.h
|
IToolbarProvider.h
|
||||||
WidgetBridge.h
|
WidgetBridge.h
|
||||||
|
|||||||
67
src/Gui/SDK/IMenuProvider.h
Normal file
67
src/Gui/SDK/IMenuProvider.h
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// 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_IMENUPROVIDER_H
|
||||||
|
#define KCSDK_IMENUPROVIDER_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "KCSDKGlobal.h"
|
||||||
|
|
||||||
|
namespace KCSDK
|
||||||
|
{
|
||||||
|
|
||||||
|
/// Abstract interface for addon-provided menu declarations.
|
||||||
|
///
|
||||||
|
/// Addons implement this interface to declaratively register menu items.
|
||||||
|
/// On workbench switch the SDK's shared WorkbenchManipulator injects the
|
||||||
|
/// declared items at the specified menu path. If context_ids() is
|
||||||
|
/// non-empty, items are only injected when the current editing context
|
||||||
|
/// matches.
|
||||||
|
class KCSDKExport IMenuProvider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IMenuProvider() = default;
|
||||||
|
|
||||||
|
/// Unique provider identifier (e.g. "silo.menu", "gears.tools").
|
||||||
|
virtual std::string id() const = 0;
|
||||||
|
|
||||||
|
/// Menu path where items are inserted.
|
||||||
|
/// Use "/" to separate levels (e.g. "&Tools" or "&Edit/Custom").
|
||||||
|
virtual std::string menu_path() const = 0;
|
||||||
|
|
||||||
|
/// Command names to add as menu items. Use "separator" for dividers.
|
||||||
|
virtual std::vector<std::string> items() const = 0;
|
||||||
|
|
||||||
|
/// Editing context IDs this menu applies to.
|
||||||
|
/// If empty (default), items are shown in all contexts.
|
||||||
|
virtual std::vector<std::string> context_ids() const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace KCSDK
|
||||||
|
|
||||||
|
#endif // KCSDK_IMENUPROVIDER_H
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
|
|
||||||
#include "SDKRegistry.h"
|
#include "SDKRegistry.h"
|
||||||
|
#include "IMenuProvider.h"
|
||||||
#include "IPanelProvider.h"
|
#include "IPanelProvider.h"
|
||||||
#include "IToolbarProvider.h"
|
#include "IToolbarProvider.h"
|
||||||
|
|
||||||
@@ -31,6 +32,8 @@
|
|||||||
#include <Base/Console.h>
|
#include <Base/Console.h>
|
||||||
#include <Gui/DockWindowManager.h>
|
#include <Gui/DockWindowManager.h>
|
||||||
#include <Gui/EditingContext.h>
|
#include <Gui/EditingContext.h>
|
||||||
|
#include <Gui/MenuManager.h>
|
||||||
|
#include <Gui/WorkbenchManipulator.h>
|
||||||
|
|
||||||
namespace KCSDK
|
namespace KCSDK
|
||||||
{
|
{
|
||||||
@@ -92,13 +95,16 @@ std::vector<std::string> SDKRegistry::available() const
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mutex_);
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
std::vector<std::string> result;
|
std::vector<std::string> result;
|
||||||
result.reserve(panels_.size() + toolbars_.size());
|
result.reserve(panels_.size() + toolbars_.size() + menus_.size());
|
||||||
for (const auto& [id, _] : panels_) {
|
for (const auto& [id, _] : panels_) {
|
||||||
result.push_back(id);
|
result.push_back(id);
|
||||||
}
|
}
|
||||||
for (const auto& [id, _] : toolbars_) {
|
for (const auto& [id, _] : toolbars_) {
|
||||||
result.push_back(id);
|
result.push_back(id);
|
||||||
}
|
}
|
||||||
|
for (const auto& [id, _] : menus_) {
|
||||||
|
result.push_back(id);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,4 +311,158 @@ std::vector<std::string> SDKRegistry::registeredToolbars() const
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- Menu provider API ------------------------------------------------------
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
/// Resolve a menu path like "&Tools" or "&Edit/Custom" into a MenuItem,
|
||||||
|
/// creating intermediate nodes as needed.
|
||||||
|
Gui::MenuItem* findOrCreateMenuPath(Gui::MenuItem* root, const std::string& path)
|
||||||
|
{
|
||||||
|
Gui::MenuItem* current = root;
|
||||||
|
|
||||||
|
// Split on '/'
|
||||||
|
std::string remaining = path;
|
||||||
|
while (!remaining.empty()) {
|
||||||
|
std::string segment;
|
||||||
|
auto pos = remaining.find('/');
|
||||||
|
if (pos != std::string::npos) {
|
||||||
|
segment = remaining.substr(0, pos);
|
||||||
|
remaining = remaining.substr(pos + 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
segment = remaining;
|
||||||
|
remaining.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Gui::MenuItem* child = current->findItem(segment);
|
||||||
|
if (!child) {
|
||||||
|
child = new Gui::MenuItem(current);
|
||||||
|
child->setCommand(segment);
|
||||||
|
}
|
||||||
|
current = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
/// Shared WorkbenchManipulator that injects menu items from all
|
||||||
|
/// registered IMenuProvider instances.
|
||||||
|
class SDKMenuManipulator : public Gui::WorkbenchManipulator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit SDKMenuManipulator(SDKRegistry& registry)
|
||||||
|
: registry_(registry)
|
||||||
|
{}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void modifyMenuBar(Gui::MenuItem* menuBar) override
|
||||||
|
{
|
||||||
|
// Snapshot providers under lock.
|
||||||
|
std::vector<IMenuProvider*> providers;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(registry_.mutex_);
|
||||||
|
providers.reserve(registry_.menus_.size());
|
||||||
|
for (const auto& [_, p] : registry_.menus_) {
|
||||||
|
providers.push_back(p.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current editing context for filtering.
|
||||||
|
std::string currentCtxId;
|
||||||
|
auto* resolver = Gui::EditingContextResolver::instance();
|
||||||
|
if (resolver) {
|
||||||
|
currentCtxId = fromQString(resolver->currentContext().id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto* provider : providers) {
|
||||||
|
// Check context filter.
|
||||||
|
auto ctxIds = provider->context_ids();
|
||||||
|
if (!ctxIds.empty()) {
|
||||||
|
bool matches = false;
|
||||||
|
for (const auto& cid : ctxIds) {
|
||||||
|
if (cid == currentCtxId) {
|
||||||
|
matches = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matches) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve menu path and append items.
|
||||||
|
std::string path = provider->menu_path();
|
||||||
|
Gui::MenuItem* target = findOrCreateMenuPath(menuBar, path);
|
||||||
|
if (!target) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& item : provider->items()) {
|
||||||
|
auto* mi = new Gui::MenuItem(target);
|
||||||
|
mi->setCommand(item == "separator" ? "Separator" : item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
SDKRegistry& registry_;
|
||||||
|
};
|
||||||
|
|
||||||
|
void SDKRegistry::ensureMenuManipulator()
|
||||||
|
{
|
||||||
|
if (!menuManipulator_) {
|
||||||
|
menuManipulator_ = std::make_shared<SDKMenuManipulator>(*this);
|
||||||
|
Gui::WorkbenchManipulator::installManipulator(menuManipulator_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDKRegistry::registerMenu(std::unique_ptr<IMenuProvider> provider)
|
||||||
|
{
|
||||||
|
if (!provider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::string id = provider->id();
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
menus_[id] = std::move(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureMenuManipulator();
|
||||||
|
|
||||||
|
Base::Console().log("KCSDK: registered menu provider '%s'\n", id.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDKRegistry::unregisterMenu(const std::string& id)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
auto it = menus_.find(id);
|
||||||
|
if (it == menus_.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
menus_.erase(it);
|
||||||
|
Base::Console().log("KCSDK: unregistered menu provider '%s'\n", id.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> SDKRegistry::registeredMenus() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
std::vector<std::string> result;
|
||||||
|
result.reserve(menus_.size());
|
||||||
|
for (const auto& [id, _] : menus_) {
|
||||||
|
result.push_back(id);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace KCSDK
|
} // namespace KCSDK
|
||||||
|
|||||||
@@ -33,11 +33,17 @@
|
|||||||
#include "KCSDKGlobal.h"
|
#include "KCSDKGlobal.h"
|
||||||
#include "Types.h"
|
#include "Types.h"
|
||||||
|
|
||||||
|
namespace Gui
|
||||||
|
{
|
||||||
|
class WorkbenchManipulator;
|
||||||
|
}
|
||||||
|
|
||||||
namespace KCSDK
|
namespace KCSDK
|
||||||
{
|
{
|
||||||
|
|
||||||
class IPanelProvider;
|
class IPanelProvider;
|
||||||
class IToolbarProvider;
|
class IToolbarProvider;
|
||||||
|
class IMenuProvider;
|
||||||
|
|
||||||
/// Current KCSDK API major version. Addons should check this at load time.
|
/// Current KCSDK API major version. Addons should check this at load time.
|
||||||
constexpr int API_VERSION_MAJOR = 1;
|
constexpr int API_VERSION_MAJOR = 1;
|
||||||
@@ -115,6 +121,18 @@ public:
|
|||||||
/// Return IDs of all registered toolbar providers.
|
/// Return IDs of all registered toolbar providers.
|
||||||
std::vector<std::string> registeredToolbars() const;
|
std::vector<std::string> registeredToolbars() const;
|
||||||
|
|
||||||
|
// -- Menu provider API -------------------------------------------------
|
||||||
|
|
||||||
|
/// Register a menu provider. Ownership transfers to the registry.
|
||||||
|
/// Items are injected into the menu bar on each workbench switch.
|
||||||
|
void registerMenu(std::unique_ptr<IMenuProvider> provider);
|
||||||
|
|
||||||
|
/// Remove a registered menu provider.
|
||||||
|
void unregisterMenu(const std::string& id);
|
||||||
|
|
||||||
|
/// Return IDs of all registered menu providers.
|
||||||
|
std::vector<std::string> registeredMenus() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SDKRegistry();
|
SDKRegistry();
|
||||||
|
|
||||||
@@ -123,9 +141,15 @@ private:
|
|||||||
SDKRegistry(SDKRegistry&&) = delete;
|
SDKRegistry(SDKRegistry&&) = delete;
|
||||||
SDKRegistry& operator=(SDKRegistry&&) = delete;
|
SDKRegistry& operator=(SDKRegistry&&) = delete;
|
||||||
|
|
||||||
|
friend class SDKMenuManipulator;
|
||||||
|
|
||||||
|
void ensureMenuManipulator();
|
||||||
|
|
||||||
mutable std::mutex mutex_;
|
mutable std::mutex mutex_;
|
||||||
std::unordered_map<std::string, std::unique_ptr<IPanelProvider>> panels_;
|
std::unordered_map<std::string, std::unique_ptr<IPanelProvider>> panels_;
|
||||||
std::unordered_map<std::string, std::unique_ptr<IToolbarProvider>> toolbars_;
|
std::unordered_map<std::string, std::unique_ptr<IToolbarProvider>> toolbars_;
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<IMenuProvider>> menus_;
|
||||||
|
std::shared_ptr<Gui::WorkbenchManipulator> menuManipulator_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace KCSDK
|
} // namespace KCSDK
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ set(KCSDKPy_SRCS
|
|||||||
kcsdk_py.cpp
|
kcsdk_py.cpp
|
||||||
PyIPanelProvider.h
|
PyIPanelProvider.h
|
||||||
PyProviderHolder.h
|
PyProviderHolder.h
|
||||||
|
PyIMenuProvider.h
|
||||||
|
PyMenuHolder.h
|
||||||
PyIToolbarProvider.h
|
PyIToolbarProvider.h
|
||||||
PyToolbarHolder.h
|
PyToolbarHolder.h
|
||||||
)
|
)
|
||||||
|
|||||||
69
src/Gui/SDK/bindings/PyIMenuProvider.h
Normal file
69
src/Gui/SDK/bindings/PyIMenuProvider.h
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
// 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_PYIMENUPROVIDER_H
|
||||||
|
#define KCSDK_PYIMENUPROVIDER_H
|
||||||
|
|
||||||
|
#include <pybind11/pybind11.h>
|
||||||
|
#include <pybind11/stl.h>
|
||||||
|
|
||||||
|
#include <Gui/SDK/IMenuProvider.h>
|
||||||
|
|
||||||
|
namespace KCSDK
|
||||||
|
{
|
||||||
|
|
||||||
|
/// pybind11 trampoline class for IMenuProvider.
|
||||||
|
/// Enables Python subclasses that override virtual methods.
|
||||||
|
class PyIMenuProvider : public IMenuProvider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using IMenuProvider::IMenuProvider;
|
||||||
|
|
||||||
|
// ── Pure virtuals ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
std::string id() const override
|
||||||
|
{
|
||||||
|
PYBIND11_OVERRIDE_PURE(std::string, IMenuProvider, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string menu_path() const override
|
||||||
|
{
|
||||||
|
PYBIND11_OVERRIDE_PURE(std::string, IMenuProvider, menu_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> items() const override
|
||||||
|
{
|
||||||
|
PYBIND11_OVERRIDE_PURE(std::vector<std::string>, IMenuProvider, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Virtuals with defaults ─────────────────────────────────────
|
||||||
|
|
||||||
|
std::vector<std::string> context_ids() const override
|
||||||
|
{
|
||||||
|
PYBIND11_OVERRIDE(std::vector<std::string>, IMenuProvider, context_ids);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace KCSDK
|
||||||
|
|
||||||
|
#endif // KCSDK_PYIMENUPROVIDER_H
|
||||||
81
src/Gui/SDK/bindings/PyMenuHolder.h
Normal file
81
src/Gui/SDK/bindings/PyMenuHolder.h
Normal 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_PYMENUHOLDER_H
|
||||||
|
#define KCSDK_PYMENUHOLDER_H
|
||||||
|
|
||||||
|
#include <pybind11/pybind11.h>
|
||||||
|
#include <pybind11/stl.h>
|
||||||
|
|
||||||
|
#include <Gui/SDK/IMenuProvider.h>
|
||||||
|
|
||||||
|
namespace py = pybind11;
|
||||||
|
|
||||||
|
namespace KCSDK
|
||||||
|
{
|
||||||
|
|
||||||
|
/// GIL-safe forwarding wrapper that holds a Python IMenuProvider 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 PyMenuHolder : public IMenuProvider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit PyMenuHolder(py::object obj)
|
||||||
|
: obj_(std::move(obj))
|
||||||
|
, provider_(obj_.cast<IMenuProvider*>())
|
||||||
|
{}
|
||||||
|
|
||||||
|
std::string id() const override
|
||||||
|
{
|
||||||
|
py::gil_scoped_acquire gil;
|
||||||
|
return provider_->id();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string menu_path() const override
|
||||||
|
{
|
||||||
|
py::gil_scoped_acquire gil;
|
||||||
|
return provider_->menu_path();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> items() const override
|
||||||
|
{
|
||||||
|
py::gil_scoped_acquire gil;
|
||||||
|
return provider_->items();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> context_ids() const override
|
||||||
|
{
|
||||||
|
py::gil_scoped_acquire gil;
|
||||||
|
return provider_->context_ids();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
py::object obj_; ///< Prevents Python GC — keeps reference alive.
|
||||||
|
IMenuProvider* provider_; ///< Raw pointer into trampoline inside obj_.
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace KCSDK
|
||||||
|
|
||||||
|
#endif // KCSDK_PYMENUHOLDER_H
|
||||||
@@ -25,14 +25,17 @@
|
|||||||
#include <pybind11/stl.h>
|
#include <pybind11/stl.h>
|
||||||
#include <pybind11/functional.h>
|
#include <pybind11/functional.h>
|
||||||
|
|
||||||
|
#include <Gui/SDK/IMenuProvider.h>
|
||||||
#include <Gui/SDK/IPanelProvider.h>
|
#include <Gui/SDK/IPanelProvider.h>
|
||||||
#include <Gui/SDK/IToolbarProvider.h>
|
#include <Gui/SDK/IToolbarProvider.h>
|
||||||
#include <Gui/SDK/SDKRegistry.h>
|
#include <Gui/SDK/SDKRegistry.h>
|
||||||
#include <Gui/SDK/ThemeEngine.h>
|
#include <Gui/SDK/ThemeEngine.h>
|
||||||
#include <Gui/SDK/Types.h>
|
#include <Gui/SDK/Types.h>
|
||||||
|
|
||||||
|
#include "PyIMenuProvider.h"
|
||||||
#include "PyIPanelProvider.h"
|
#include "PyIPanelProvider.h"
|
||||||
#include "PyIToolbarProvider.h"
|
#include "PyIToolbarProvider.h"
|
||||||
|
#include "PyMenuHolder.h"
|
||||||
#include "PyProviderHolder.h"
|
#include "PyProviderHolder.h"
|
||||||
#include "PyToolbarHolder.h"
|
#include "PyToolbarHolder.h"
|
||||||
|
|
||||||
@@ -284,6 +287,41 @@ PYBIND11_MODULE(kcsdk, m)
|
|||||||
},
|
},
|
||||||
"Return IDs of all registered toolbar providers.");
|
"Return IDs of all registered toolbar providers.");
|
||||||
|
|
||||||
|
// -- Menu provider API --------------------------------------------------
|
||||||
|
|
||||||
|
py::class_<IMenuProvider, PyIMenuProvider>(m, "IMenuProvider")
|
||||||
|
.def(py::init<>())
|
||||||
|
.def("id", &IMenuProvider::id)
|
||||||
|
.def("menu_path", &IMenuProvider::menu_path)
|
||||||
|
.def("items", &IMenuProvider::items)
|
||||||
|
.def("context_ids", &IMenuProvider::context_ids);
|
||||||
|
|
||||||
|
m.def("register_menu",
|
||||||
|
[](py::object provider) {
|
||||||
|
auto holder = std::make_unique<PyMenuHolder>(std::move(provider));
|
||||||
|
SDKRegistry::instance().registerMenu(std::move(holder));
|
||||||
|
},
|
||||||
|
py::arg("provider"),
|
||||||
|
"Register a menu provider for declarative menu placement.\n\n"
|
||||||
|
"Parameters\n"
|
||||||
|
"----------\n"
|
||||||
|
"provider : IMenuProvider\n"
|
||||||
|
" Menu provider implementing id(), menu_path(), items().\n"
|
||||||
|
" Optionally override context_ids() to limit to specific contexts.");
|
||||||
|
|
||||||
|
m.def("unregister_menu",
|
||||||
|
[](const std::string& id) {
|
||||||
|
SDKRegistry::instance().unregisterMenu(id);
|
||||||
|
},
|
||||||
|
py::arg("id"),
|
||||||
|
"Remove a registered menu provider.");
|
||||||
|
|
||||||
|
m.def("registered_menus",
|
||||||
|
[]() {
|
||||||
|
return SDKRegistry::instance().registeredMenus();
|
||||||
|
},
|
||||||
|
"Return IDs of all registered menu providers.");
|
||||||
|
|
||||||
// -- Theme engine API ---------------------------------------------------
|
// -- Theme engine API ---------------------------------------------------
|
||||||
|
|
||||||
m.def("theme_color",
|
m.def("theme_color",
|
||||||
|
|||||||
Reference in New Issue
Block a user