All checks were successful
Build and Test / build (pull_request) Successful in 29m14s
Add context/overlay registration, injection, query, and refresh to the KCSDK C++ library and kcsdk pybind11 module. New files: - src/Gui/SDK/Types.h — ContextDef, OverlayDef, ContextSnapshot structs (plain C++, no Qt in public API) Modified: - src/Gui/SDK/SDKRegistry.h/.cpp — register_context/overlay, unregister, inject_commands, current_context, refresh (delegates to EditingContextResolver with std↔Qt conversion) - src/Gui/SDK/CMakeLists.txt — add Types.h, link FreeCADGui - src/Gui/SDK/bindings/kcsdk_py.cpp — bind all context functions with GIL-safe match callable wrapping and dict-based snapshot return - mods/sdk/kindred_sdk/context.py — try kcsdk first, fall back to FreeCADGui for backwards compatibility
181 lines
6.8 KiB
C++
181 lines
6.8 KiB
C++
// 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/>. *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
#include <pybind11/pybind11.h>
|
|
#include <pybind11/stl.h>
|
|
#include <pybind11/functional.h>
|
|
|
|
#include <Gui/SDK/SDKRegistry.h>
|
|
#include <Gui/SDK/Types.h>
|
|
|
|
namespace py = pybind11;
|
|
using namespace KCSDK;
|
|
|
|
// -- Dict conversion helpers ------------------------------------------------
|
|
|
|
namespace
|
|
{
|
|
|
|
/// Convert a ContextSnapshot to a Python dict (same keys as ApplicationPy).
|
|
py::dict contextSnapshotToDict(const ContextSnapshot& snap)
|
|
{
|
|
py::dict d;
|
|
d["id"] = snap.id;
|
|
d["label"] = snap.label;
|
|
d["color"] = snap.color;
|
|
d["toolbars"] = snap.toolbars;
|
|
d["breadcrumb"] = snap.breadcrumb;
|
|
d["breadcrumbColors"] = snap.breadcrumbColors;
|
|
return d;
|
|
}
|
|
|
|
/// Wrap a Python callable as a GIL-safe std::function<bool()>.
|
|
/// The py::object is copied (reference counted) so it survives beyond the
|
|
/// calling scope. The GIL is acquired before every invocation.
|
|
std::function<bool()> wrapMatchCallable(py::object pyCallable)
|
|
{
|
|
// Copy the py::object to prevent GC.
|
|
auto held = std::make_shared<py::object>(std::move(pyCallable));
|
|
return [held]() -> bool {
|
|
py::gil_scoped_acquire gil;
|
|
try {
|
|
py::object result = (*held)();
|
|
return result.cast<bool>();
|
|
}
|
|
catch (py::error_already_set& e) {
|
|
e.discard_as_unraisable(__func__);
|
|
return false;
|
|
}
|
|
};
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
// -- Module -----------------------------------------------------------------
|
|
|
|
PYBIND11_MODULE(kcsdk, m)
|
|
{
|
|
m.doc() = "KCSDK — Kindred Create addon SDK C++ API";
|
|
m.attr("API_VERSION_MAJOR") = API_VERSION_MAJOR;
|
|
|
|
m.def("available", []() {
|
|
return SDKRegistry::instance().available();
|
|
}, "Return names of all registered providers.");
|
|
|
|
// -- Editing context API ------------------------------------------------
|
|
|
|
m.def("register_context",
|
|
[](const std::string& id,
|
|
const std::string& label,
|
|
const std::string& color,
|
|
const std::vector<std::string>& toolbars,
|
|
py::object match,
|
|
int priority) {
|
|
if (!py::isinstance<py::function>(match) && !py::hasattr(match, "__call__")) {
|
|
throw py::type_error("match must be callable");
|
|
}
|
|
ContextDef def;
|
|
def.id = id;
|
|
def.labelTemplate = label;
|
|
def.color = color;
|
|
def.toolbars = toolbars;
|
|
def.priority = priority;
|
|
def.match = wrapMatchCallable(std::move(match));
|
|
SDKRegistry::instance().registerContext(def);
|
|
},
|
|
py::arg("id"),
|
|
py::arg("label"),
|
|
py::arg("color"),
|
|
py::arg("toolbars"),
|
|
py::arg("match"),
|
|
py::arg("priority") = 50,
|
|
"Register an editing context.\n\n"
|
|
"Parameters\n"
|
|
"----------\n"
|
|
"id : str\n Unique context identifier.\n"
|
|
"label : str\n Display label template ({name} placeholder).\n"
|
|
"color : str\n Hex color for breadcrumb.\n"
|
|
"toolbars : list[str]\n Toolbar names to show when active.\n"
|
|
"match : callable\n Zero-arg callable returning True when active.\n"
|
|
"priority : int\n Higher values checked first (default 50).");
|
|
|
|
m.def("unregister_context",
|
|
[](const std::string& id) {
|
|
SDKRegistry::instance().unregisterContext(id);
|
|
},
|
|
py::arg("id"),
|
|
"Unregister an editing context.");
|
|
|
|
m.def("register_overlay",
|
|
[](const std::string& id,
|
|
const std::vector<std::string>& toolbars,
|
|
py::object match) {
|
|
if (!py::isinstance<py::function>(match) && !py::hasattr(match, "__call__")) {
|
|
throw py::type_error("match must be callable");
|
|
}
|
|
OverlayDef def;
|
|
def.id = id;
|
|
def.toolbars = toolbars;
|
|
def.match = wrapMatchCallable(std::move(match));
|
|
SDKRegistry::instance().registerOverlay(def);
|
|
},
|
|
py::arg("id"),
|
|
py::arg("toolbars"),
|
|
py::arg("match"),
|
|
"Register an editing overlay (additive toolbars).");
|
|
|
|
m.def("unregister_overlay",
|
|
[](const std::string& id) {
|
|
SDKRegistry::instance().unregisterOverlay(id);
|
|
},
|
|
py::arg("id"),
|
|
"Unregister an editing overlay.");
|
|
|
|
m.def("inject_commands",
|
|
[](const std::string& contextId,
|
|
const std::string& toolbarName,
|
|
const std::vector<std::string>& commands) {
|
|
SDKRegistry::instance().injectCommands(contextId, toolbarName, commands);
|
|
},
|
|
py::arg("context_id"),
|
|
py::arg("toolbar_name"),
|
|
py::arg("commands"),
|
|
"Inject commands into a context's toolbar.");
|
|
|
|
m.def("current_context",
|
|
[]() -> py::object {
|
|
ContextSnapshot snap = SDKRegistry::instance().currentContext();
|
|
if (snap.id.empty()) {
|
|
return py::none();
|
|
}
|
|
return contextSnapshotToDict(snap);
|
|
},
|
|
"Return the current editing context as a dict, or None.");
|
|
|
|
m.def("refresh",
|
|
[]() {
|
|
SDKRegistry::instance().refresh();
|
|
},
|
|
"Force re-resolution of the editing context.");
|
|
}
|