- 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:
272
src/Gui/SDK/ThemeEngine.cpp
Normal file
272
src/Gui/SDK/ThemeEngine.cpp
Normal 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
|
||||
Reference in New Issue
Block a user