Files
create/src/Gui/SDK/ThemeEngine.cpp
forbes 18532e3bd7 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
2026-02-28 14:53:38 -06:00

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