// 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 "ThemeEngine.h" #include #include #include #include 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 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 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 ThemeEngine::allTokens() const { std::lock_guard 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 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 lock(mutex_); return activePalette_; } } // namespace KCSDK