// 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 * * . * * * ***************************************************************************/ #include #include #include #include #include #include #include #include "PyIPanelProvider.h" #include "PyProviderHolder.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. /// The py::object is copied (reference counted) so it survives beyond the /// calling scope. The GIL is acquired before every invocation. std::function wrapMatchCallable(py::object pyCallable) { // Copy the py::object to prevent GC. auto held = std::make_shared(std::move(pyCallable)); return [held]() -> bool { py::gil_scoped_acquire gil; try { py::object result = (*held)(); return result.cast(); } 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& toolbars, py::object match, int priority) { if (!py::isinstance(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& toolbars, py::object match) { if (!py::isinstance(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& 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."); // -- Enums -------------------------------------------------------------- py::enum_(m, "DockArea") .value("Left", DockArea::Left) .value("Right", DockArea::Right) .value("Top", DockArea::Top) .value("Bottom", DockArea::Bottom); py::enum_(m, "PanelPersistence") .value("Session", PanelPersistence::Session) .value("Persistent", PanelPersistence::Persistent); // -- Panel provider API ------------------------------------------------- py::class_(m, "IPanelProvider") .def(py::init<>()) .def("id", &IPanelProvider::id) .def("title", &IPanelProvider::title) // create_widget() is not bound directly — Python subclasses override // it and return a PySide QWidget. The C++ side invokes it via // PyProviderHolder which handles the PySide→QWidget* conversion // through WidgetBridge. .def("preferred_area", &IPanelProvider::preferred_area) .def("persistence", &IPanelProvider::persistence) .def("context_affinity", &IPanelProvider::context_affinity); m.def("register_panel", [](py::object provider) { auto holder = std::make_unique(std::move(provider)); SDKRegistry::instance().registerPanel(std::move(holder)); }, py::arg("provider"), "Register a dock panel provider.\n\n" "Parameters\n" "----------\n" "provider : IPanelProvider\n" " Panel provider instance implementing id(), title(), create_widget()."); m.def("unregister_panel", [](const std::string& id) { SDKRegistry::instance().unregisterPanel(id); }, py::arg("id"), "Remove a registered panel provider and its dock widget."); m.def("create_panel", [](const std::string& id) { SDKRegistry::instance().createPanel(id); }, py::arg("id"), "Instantiate the dock widget for a registered panel."); m.def("create_all_panels", []() { SDKRegistry::instance().createAllPanels(); }, "Instantiate dock widgets for all registered panels."); m.def("registered_panels", []() { return SDKRegistry::instance().registeredPanels(); }, "Return IDs of all registered panel providers."); // -- Theme engine API --------------------------------------------------- m.def("theme_color", [](const std::string& token) { auto& engine = ThemeEngine::instance(); if (engine.activePaletteName().empty()) { engine.loadPalette(); } return engine.getColor(token); }, py::arg("token"), "Look up a color by role or name.\n\n" "Returns the hex string (e.g. \"#89b4fa\") or empty string if not found.\n" "Auto-loads the default palette on first call."); m.def("theme_tokens", []() { auto& engine = ThemeEngine::instance(); if (engine.activePaletteName().empty()) { engine.loadPalette(); } return engine.allTokens(); }, "Return all color tokens as {name: \"#hex\"}.\n\n" "Includes both raw colors and resolved semantic roles.\n" "Auto-loads the default palette on first call."); m.def("format_qss", [](const std::string& templateStr) { auto& engine = ThemeEngine::instance(); if (engine.activePaletteName().empty()) { engine.loadPalette(); } return engine.formatQss(templateStr); }, py::arg("template_str"), "Substitute {token} placeholders in a QSS template.\n\n" "Both raw color names ({blue}) and dotted role names\n" "({accent.primary}) are supported. Unknown tokens are left as-is.\n" "Auto-loads the default palette on first call."); m.def("load_palette", [](const std::string& name) { return ThemeEngine::instance().loadPalette(name); }, py::arg("name") = "catppuccin-mocha", "Load a named palette. Returns True on success."); }