feat(sdk): add panel provider and theme engine to kcsdk (#352, #353)

- IPanelProvider: abstract interface for dock panels with PySide widget bridging
- PyIPanelProvider/PyProviderHolder: pybind11 trampoline + GIL-safe holder
- WidgetBridge: PySide QWidget → C++ QWidget* conversion via Shiboken
- SDKRegistry: panel registration, creation, and lifecycle management
- ThemeEngine: C++ singleton with minimal YAML parser, palette cache,
  getColor/allTokens/formatQss matching Python Palette API
- kcsdk bindings: DockArea, PanelPersistence enums, panel functions,
  theme_color, theme_tokens, format_qss, load_palette
- dock.py: kcsdk delegation with FreeCADGui fallback
- theme.py: kcsdk delegation with Python YAML fallback
This commit is contained in:
forbes
2026-02-28 14:53:38 -06:00
parent 60c0489d73
commit 64644eb623
15 changed files with 1130 additions and 15 deletions

View File

@@ -3,6 +3,11 @@
set(KCSDK_SRCS
KCSDKGlobal.h
Types.h
IPanelProvider.h
WidgetBridge.h
WidgetBridge.cpp
ThemeEngine.h
ThemeEngine.cpp
SDKRegistry.h
SDKRegistry.cpp
)

View File

@@ -0,0 +1,80 @@
// 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_IPANELPROVIDER_H
#define KCSDK_IPANELPROVIDER_H
#include <string>
#include "KCSDKGlobal.h"
#include "Types.h"
class QWidget;
namespace KCSDK
{
/// Abstract interface for addon-provided dock panels.
///
/// Addons implement this interface to register dock panel factories with
/// the SDK registry. The registry calls create_widget() once to
/// instantiate the panel and embeds the result in a QDockWidget managed
/// by DockWindowManager.
class KCSDKExport IPanelProvider
{
public:
virtual ~IPanelProvider() = default;
/// Unique panel identifier (e.g. "silo.auth", "silo.activity").
virtual std::string id() const = 0;
/// Title displayed in the dock widget title bar.
virtual std::string title() const = 0;
/// Create the panel widget. Called exactly once by the registry.
/// Ownership of the returned widget transfers to the caller.
virtual QWidget* create_widget() = 0;
/// Preferred dock area. Default: Right.
virtual DockArea preferred_area() const
{
return DockArea::Right;
}
/// Whether visibility persists across sessions. Default: Session.
virtual PanelPersistence persistence() const
{
return PanelPersistence::Session;
}
/// Editing context affinity. If non-empty, the panel is only shown
/// when the named context is active. Default: "" (always visible).
virtual std::string context_affinity() const
{
return {};
}
};
} // namespace KCSDK
#endif // KCSDK_IPANELPROVIDER_H

View File

@@ -22,11 +22,13 @@
***************************************************************************/
#include "SDKRegistry.h"
#include "IPanelProvider.h"
#include <QString>
#include <QStringList>
#include <Base/Console.h>
#include <Gui/DockWindowManager.h>
#include <Gui/EditingContext.h>
namespace KCSDK
@@ -88,7 +90,12 @@ SDKRegistry::~SDKRegistry() = default;
std::vector<std::string> SDKRegistry::available() const
{
std::lock_guard<std::mutex> lock(mutex_);
return {};
std::vector<std::string> result;
result.reserve(panels_.size());
for (const auto& [id, _] : panels_) {
result.push_back(id);
}
return result;
}
// -- Editing context API ----------------------------------------------------
@@ -154,4 +161,95 @@ void SDKRegistry::refresh()
Gui::EditingContextResolver::instance()->refresh();
}
// -- Panel provider API -----------------------------------------------------
void SDKRegistry::registerPanel(std::unique_ptr<IPanelProvider> provider)
{
if (!provider) {
return;
}
std::string id = provider->id();
std::lock_guard<std::mutex> lock(mutex_);
panels_[id] = std::move(provider);
Base::Console().log("KCSDK: registered panel provider '%s'\n", id.c_str());
}
void SDKRegistry::unregisterPanel(const std::string& id)
{
std::lock_guard<std::mutex> lock(mutex_);
auto it = panels_.find(id);
if (it == panels_.end()) {
return;
}
// Remove the dock widget if it was created.
auto* dwm = Gui::DockWindowManager::instance();
if (dwm) {
dwm->removeDockWindow(id.c_str());
}
panels_.erase(it);
Base::Console().log("KCSDK: unregistered panel provider '%s'\n", id.c_str());
}
void SDKRegistry::createPanel(const std::string& id)
{
std::lock_guard<std::mutex> lock(mutex_);
auto it = panels_.find(id);
if (it == panels_.end()) {
Base::Console().warning("KCSDK: no panel provider '%s' registered\n", id.c_str());
return;
}
auto* dwm = Gui::DockWindowManager::instance();
if (!dwm) {
return;
}
// Skip if already created.
if (dwm->getDockWindow(id.c_str())) {
return;
}
IPanelProvider* provider = it->second.get();
QWidget* widget = provider->create_widget();
if (!widget) {
Base::Console().warning("KCSDK: panel '%s' create_widget() returned null\n",
id.c_str());
return;
}
auto qtArea = static_cast<Qt::DockWidgetArea>(provider->preferred_area());
dwm->addDockWindow(id.c_str(), widget, qtArea);
}
void SDKRegistry::createAllPanels()
{
// Collect IDs under lock, then create outside to avoid recursive locking.
std::vector<std::string> ids;
{
std::lock_guard<std::mutex> lock(mutex_);
ids.reserve(panels_.size());
for (const auto& [id, _] : panels_) {
ids.push_back(id);
}
}
for (const auto& id : ids) {
createPanel(id);
}
}
std::vector<std::string> SDKRegistry::registeredPanels() const
{
std::lock_guard<std::mutex> lock(mutex_);
std::vector<std::string> result;
result.reserve(panels_.size());
for (const auto& [id, _] : panels_) {
result.push_back(id);
}
return result;
}
} // namespace KCSDK

View File

@@ -24,8 +24,10 @@
#ifndef KCSDK_SDKREGISTRY_H
#define KCSDK_SDKREGISTRY_H
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include "KCSDKGlobal.h"
@@ -34,6 +36,8 @@
namespace KCSDK
{
class IPanelProvider;
/// Current KCSDK API major version. Addons should check this at load time.
constexpr int API_VERSION_MAJOR = 1;
@@ -81,6 +85,23 @@ public:
/// Force re-resolution of the editing context.
void refresh();
// -- Panel provider API ------------------------------------------------
/// Register a dock panel provider. Ownership transfers to the registry.
void registerPanel(std::unique_ptr<IPanelProvider> provider);
/// Remove a registered panel provider and its dock widget (if created).
void unregisterPanel(const std::string& id);
/// Instantiate the dock widget for a registered panel.
void createPanel(const std::string& id);
/// Instantiate dock widgets for all registered panels.
void createAllPanels();
/// Return IDs of all registered panel providers.
std::vector<std::string> registeredPanels() const;
private:
SDKRegistry();
@@ -90,6 +111,7 @@ private:
SDKRegistry& operator=(SDKRegistry&&) = delete;
mutable std::mutex mutex_;
std::unordered_map<std::string, std::unique_ptr<IPanelProvider>> panels_;
};
} // namespace KCSDK

272
src/Gui/SDK/ThemeEngine.cpp Normal file
View File

@@ -0,0 +1,272 @@
// 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 "ThemeEngine.h"
#include <fstream>
#include <regex>
#include <App/Application.h>
#include <Base/Console.h>
namespace KCSDK
{
// -- Singleton --------------------------------------------------------------
ThemeEngine& ThemeEngine::instance()
{
static ThemeEngine engine;
return engine;
}
// -- Path resolution --------------------------------------------------------
std::string ThemeEngine::resolvePalettePath(const std::string& name)
{
std::string home = App::Application::getHomePath();
return home + "Mod/Create/kindred_sdk/palettes/" + name + ".yaml";
}
// -- Minimal YAML parser ----------------------------------------------------
bool ThemeEngine::parseYaml(const std::string& path, Palette& out)
{
std::ifstream file(path);
if (!file.is_open()) {
return false;
}
out.colors.clear();
out.roles.clear();
out.name.clear();
out.slug.clear();
std::string currentSection;
std::string line;
while (std::getline(file, line)) {
// Strip trailing whitespace (including \r on Windows)
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t')) {
line.pop_back();
}
// Skip blank lines and comments
if (line.empty() || line[0] == '#') {
continue;
}
// Detect indentation
std::size_t indent = 0;
while (indent < line.size() && (line[indent] == ' ' || line[indent] == '\t')) {
++indent;
}
// Find the colon separator
auto colonPos = line.find(':', indent);
if (colonPos == std::string::npos) {
continue;
}
// Extract key
std::string key = line.substr(indent, colonPos - indent);
// Trim trailing whitespace from key
while (!key.empty() && (key.back() == ' ' || key.back() == '\t')) {
key.pop_back();
}
// Extract value (everything after ": ")
std::string value;
std::size_t valueStart = colonPos + 1;
while (valueStart < line.size() && line[valueStart] == ' ') {
++valueStart;
}
if (valueStart < line.size()) {
value = line.substr(valueStart);
// Strip surrounding quotes
if (value.size() >= 2
&& ((value.front() == '"' && value.back() == '"')
|| (value.front() == '\'' && value.back() == '\''))) {
value = value.substr(1, value.size() - 2);
}
}
if (indent == 0) {
// Top-level key
if (value.empty()) {
// Start of a nested section
currentSection = key;
}
else {
if (key == "name") {
out.name = value;
}
else if (key == "slug") {
out.slug = value;
}
currentSection.clear();
}
}
else if (!currentSection.empty()) {
// Nested key within a section
if (currentSection == "colors") {
out.colors[key] = value;
}
else if (currentSection == "roles") {
// Roles map semantic names to color names — resolve to hex
auto it = out.colors.find(value);
if (it != out.colors.end()) {
out.roles[key] = it->second;
}
else {
// Store the raw name; will remain unresolved
out.roles[key] = value;
}
}
}
}
return true;
}
// -- Public API -------------------------------------------------------------
bool ThemeEngine::loadPalette(const std::string& name)
{
std::lock_guard<std::mutex> lock(mutex_);
// Return cached if already loaded
if (cache_.count(name)) {
activePalette_ = name;
return true;
}
std::string path = resolvePalettePath(name);
Palette palette;
if (!parseYaml(path, palette)) {
Base::Console().warning("KCSDK: palette file not found: %s\n", path.c_str());
return false;
}
if (palette.name.empty()) {
palette.name = name;
}
if (palette.slug.empty()) {
palette.slug = name;
}
Base::Console().log("KCSDK: loaded palette '%s' (%zu colors, %zu roles)\n",
palette.name.c_str(), palette.colors.size(), palette.roles.size());
cache_[name] = std::move(palette);
activePalette_ = name;
return true;
}
std::string ThemeEngine::getColor(const std::string& token) const
{
std::lock_guard<std::mutex> lock(mutex_);
auto cacheIt = cache_.find(activePalette_);
if (cacheIt == cache_.end()) {
return {};
}
const Palette& pal = cacheIt->second;
// Check roles first, then raw colors (matching Python Palette.get())
auto roleIt = pal.roles.find(token);
if (roleIt != pal.roles.end()) {
return roleIt->second;
}
auto colorIt = pal.colors.find(token);
if (colorIt != pal.colors.end()) {
return colorIt->second;
}
return {};
}
std::unordered_map<std::string, std::string> ThemeEngine::allTokens() const
{
std::lock_guard<std::mutex> lock(mutex_);
auto cacheIt = cache_.find(activePalette_);
if (cacheIt == cache_.end()) {
return {};
}
const Palette& pal = cacheIt->second;
// Start with colors, overlay roles (roles take precedence for same-named keys)
std::unordered_map<std::string, std::string> result = pal.colors;
for (const auto& [key, value] : pal.roles) {
result[key] = value;
}
return result;
}
std::string ThemeEngine::formatQss(const std::string& templateStr) const
{
auto tokens = allTokens();
if (tokens.empty()) {
return templateStr;
}
std::regex pattern(R"(\{([a-z][a-z0-9_.]*)\})");
std::string result;
auto begin = std::sregex_iterator(templateStr.begin(), templateStr.end(), pattern);
auto end = std::sregex_iterator();
std::size_t lastPos = 0;
for (auto it = begin; it != end; ++it) {
const auto& match = *it;
// Append text before this match
result.append(templateStr, lastPos, match.position() - lastPos);
std::string key = match[1].str();
auto tokenIt = tokens.find(key);
if (tokenIt != tokens.end()) {
result.append(tokenIt->second);
}
else {
// Leave unknown tokens as-is
result.append(match[0].str());
}
lastPos = match.position() + match.length();
}
// Append remaining text
result.append(templateStr, lastPos);
return result;
}
std::string ThemeEngine::activePaletteName() const
{
std::lock_guard<std::mutex> lock(mutex_);
return activePalette_;
}
} // namespace KCSDK

95
src/Gui/SDK/ThemeEngine.h Normal file
View File

@@ -0,0 +1,95 @@
// 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_THEMEENGINE_H
#define KCSDK_THEMEENGINE_H
#include <mutex>
#include <string>
#include <unordered_map>
#include "KCSDKGlobal.h"
namespace KCSDK
{
/// A loaded color palette with raw color tokens and semantic roles.
struct Palette
{
std::string name;
std::string slug;
std::unordered_map<std::string, std::string> colors;
std::unordered_map<std::string, std::string> roles;
};
/// Singleton theme engine that loads YAML palettes and provides color lookup.
///
/// Palette files use a minimal YAML subset (flat key-value pairs with one
/// level of nesting) matching the format in
/// ``mods/sdk/kindred_sdk/palettes/catppuccin-mocha.yaml``.
///
/// Thread safety: all public methods are internally synchronized.
class KCSDKExport ThemeEngine
{
public:
static ThemeEngine& instance();
/// Load a named palette from the palettes directory.
/// Returns false if the file was not found or could not be parsed.
bool loadPalette(const std::string& name = "catppuccin-mocha");
/// Look up a color by role first, then by raw color name.
/// Returns the hex string (e.g. "#89b4fa") or empty string if not found.
std::string getColor(const std::string& token) const;
/// Return all color tokens as {name: "#hex"} (colors + resolved roles).
std::unordered_map<std::string, std::string> allTokens() const;
/// Substitute {token} placeholders in a QSS template string.
/// Unknown tokens are left as-is.
std::string formatQss(const std::string& templateStr) const;
/// Return the name of the currently active palette, or empty if none.
std::string activePaletteName() const;
private:
ThemeEngine() = default;
~ThemeEngine() = default;
ThemeEngine(const ThemeEngine&) = delete;
ThemeEngine& operator=(const ThemeEngine&) = delete;
/// Parse a minimal YAML palette file into a Palette struct.
static bool parseYaml(const std::string& path, Palette& out);
/// Resolve the filesystem path to a named palette.
static std::string resolvePalettePath(const std::string& name);
mutable std::mutex mutex_;
std::unordered_map<std::string, Palette> cache_;
std::string activePalette_;
};
} // namespace KCSDK
#endif // KCSDK_THEMEENGINE_H

View File

@@ -63,6 +63,22 @@ struct KCSDKExport ContextSnapshot
std::vector<std::string> breadcrumbColors;
};
/// Dock widget area. Values match Qt::DockWidgetArea for direct casting.
enum class DockArea
{
Left = 1,
Right = 2,
Top = 4,
Bottom = 8,
};
/// Whether a dock panel's visibility persists across sessions.
enum class PanelPersistence
{
Session, ///< Visible until application close.
Persistent, ///< Saved to preferences and restored on next launch.
};
} // namespace KCSDK
#endif // KCSDK_TYPES_H

View File

@@ -0,0 +1,48 @@
// 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 "WidgetBridge.h"
#include <QWidget>
#include <CXX/Objects.hxx>
#include <Gui/PythonWrapper.h>
namespace KCSDK
{
QWidget* WidgetBridge::toQWidget(PyObject* pyWidget)
{
if (!pyWidget) {
return nullptr;
}
Gui::PythonWrapper wrap;
wrap.loadWidgetsModule();
QObject* obj = wrap.toQObject(Py::Object(pyWidget));
return qobject_cast<QWidget*>(obj);
}
} // namespace KCSDK

View File

@@ -0,0 +1,50 @@
// 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_WIDGETBRIDGE_H
#define KCSDK_WIDGETBRIDGE_H
#include "KCSDKGlobal.h"
struct _object;
using PyObject = _object;
class QWidget;
namespace KCSDK
{
/// Utility for converting between PySide QWidget objects and C++ QWidget*.
///
/// Uses Gui::PythonWrapper (Shiboken) internally.
class KCSDKExport WidgetBridge
{
public:
/// Extract a C++ QWidget* from a PySide QWidget PyObject.
/// Returns nullptr if the conversion fails.
static QWidget* toQWidget(PyObject* pyWidget);
};
} // namespace KCSDK
#endif // KCSDK_WIDGETBRIDGE_H

View File

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

View File

@@ -0,0 +1,83 @@
// 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_PYIPANELPROVIDER_H
#define KCSDK_PYIPANELPROVIDER_H
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <Gui/SDK/IPanelProvider.h>
namespace KCSDK
{
/// pybind11 trampoline class for IPanelProvider.
/// Enables Python subclasses that override virtual methods.
class PyIPanelProvider : public IPanelProvider
{
public:
using IPanelProvider::IPanelProvider;
// ── Pure virtuals ──────────────────────────────────────────────
std::string id() const override
{
PYBIND11_OVERRIDE_PURE(std::string, IPanelProvider, id);
}
std::string title() const override
{
PYBIND11_OVERRIDE_PURE(std::string, IPanelProvider, title);
}
// create_widget() is NOT overridden here — pybind11 cannot handle
// QWidget* (no type_caster for Qt types). Python dispatch for
// create_widget() goes through PyProviderHolder which calls
// obj_.attr("create_widget")() and converts via WidgetBridge.
QWidget* create_widget() override
{
return nullptr;
}
// ── Virtuals with defaults ─────────────────────────────────────
DockArea preferred_area() const override
{
PYBIND11_OVERRIDE(DockArea, IPanelProvider, preferred_area);
}
PanelPersistence persistence() const override
{
PYBIND11_OVERRIDE(PanelPersistence, IPanelProvider, persistence);
}
std::string context_affinity() const override
{
PYBIND11_OVERRIDE(std::string, IPanelProvider, context_affinity);
}
};
} // namespace KCSDK
#endif // KCSDK_PYIPANELPROVIDER_H

View File

@@ -0,0 +1,107 @@
// 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_PYPROVIDERHOLDER_H
#define KCSDK_PYPROVIDERHOLDER_H
#include <pybind11/pybind11.h>
#include <Gui/SDK/IPanelProvider.h>
#include <Gui/SDK/WidgetBridge.h>
namespace py = pybind11;
namespace KCSDK
{
/// GIL-safe forwarding wrapper that holds a Python IPanelProvider instance.
///
/// Stores the py::object to prevent garbage collection. Acquires the GIL
/// before every call into Python. For create_widget(), the Python return
/// (a PySide QWidget) is converted to a C++ QWidget* via WidgetBridge.
///
/// Follows the PySolverHolder pattern from kcsolve_py.cpp.
class PyProviderHolder : public IPanelProvider
{
public:
explicit PyProviderHolder(py::object obj)
: obj_(std::move(obj))
{
provider_ = obj_.cast<IPanelProvider*>();
}
std::string id() const override
{
py::gil_scoped_acquire gil;
return provider_->id();
}
std::string title() const override
{
py::gil_scoped_acquire gil;
return provider_->title();
}
QWidget* create_widget() override
{
py::gil_scoped_acquire gil;
try {
// Call the Python create_widget() which returns a PySide QWidget.
py::object pyWidget = obj_.attr("create_widget")();
if (pyWidget.is_none()) {
return nullptr;
}
return WidgetBridge::toQWidget(pyWidget.ptr());
}
catch (py::error_already_set& e) {
e.discard_as_unraisable(__func__);
return nullptr;
}
}
DockArea preferred_area() const override
{
py::gil_scoped_acquire gil;
return provider_->preferred_area();
}
PanelPersistence persistence() const override
{
py::gil_scoped_acquire gil;
return provider_->persistence();
}
std::string context_affinity() const override
{
py::gil_scoped_acquire gil;
return provider_->context_affinity();
}
private:
py::object obj_; ///< Prevents Python GC — keeps reference alive.
IPanelProvider* provider_; ///< Raw pointer into trampoline inside obj_.
};
} // namespace KCSDK
#endif // KCSDK_PYPROVIDERHOLDER_H

View File

@@ -25,9 +25,14 @@
#include <pybind11/stl.h>
#include <pybind11/functional.h>
#include <Gui/SDK/IPanelProvider.h>
#include <Gui/SDK/SDKRegistry.h>
#include <Gui/SDK/ThemeEngine.h>
#include <Gui/SDK/Types.h>
#include "PyIPanelProvider.h"
#include "PyProviderHolder.h"
namespace py = pybind11;
using namespace KCSDK;
@@ -177,4 +182,116 @@ PYBIND11_MODULE(kcsdk, m)
SDKRegistry::instance().refresh();
},
"Force re-resolution of the editing context.");
// -- Enums --------------------------------------------------------------
py::enum_<DockArea>(m, "DockArea")
.value("Left", DockArea::Left)
.value("Right", DockArea::Right)
.value("Top", DockArea::Top)
.value("Bottom", DockArea::Bottom);
py::enum_<PanelPersistence>(m, "PanelPersistence")
.value("Session", PanelPersistence::Session)
.value("Persistent", PanelPersistence::Persistent);
// -- Panel provider API -------------------------------------------------
py::class_<IPanelProvider, PyIPanelProvider>(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<PyProviderHolder>(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.");
}