- 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
273 lines
8.4 KiB
C++
273 lines
8.4 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 "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
|