Gui: Support YAML arrays and objects as tuples in YamlParameterSource

YAML sequences load as unnamed tuple expressions, e.g. [10px, 20px]
becomes (10px, 20px). YAML maps load as named tuple expressions, e.g.
{top: 10px, right: 20px} becomes (top: 10px, right: 20px). Both are
handled recursively, so nested arrays of arrays also work. Scalar
values retain the existing behaviour.
This commit is contained in:
Kacper Donat
2026-02-26 22:36:08 +01:00
parent d863074714
commit 43fe9fb649
3 changed files with 241 additions and 2 deletions
+34 -2
View File
@@ -45,6 +45,38 @@ namespace Gui::StyleParameters
namespace
{
/// Converts a YAML node to a StyleParameters expression string.
/// Scalars are returned as-is; sequences become unnamed tuples "(a, b, ...)";
/// maps become named tuples "(key1: val1, key2: val2, ...)". Recursive.
std::string yamlNodeToExpression(const YAML::Node& node)
{
if (node.IsScalar()) {
return node.as<std::string>();
}
if (node.IsSequence()) {
std::vector<std::string> parts;
parts.reserve(node.size());
for (const auto& element : node) {
parts.push_back(yamlNodeToExpression(element));
}
return fmt::format("({})", fmt::join(parts, ", "));
}
if (node.IsMap()) {
std::vector<std::string> parts;
parts.reserve(node.size());
for (auto it = node.begin(); it != node.end(); ++it) {
parts.push_back(
fmt::format("{}: {}", it->first.as<std::string>(), yamlNodeToExpression(it->second))
);
}
return fmt::format("({})", fmt::join(parts, ", "));
}
return "";
}
/// Formats a Value for QSS output.
/// Tuples become space-separated values (e.g. "10px 5px 10px 5px"),
/// all other types delegate to toString().
@@ -212,8 +244,8 @@ void YamlParameterSource::reload()
YAML::Node root = YAML::Load(content);
parameters.clear();
for (auto it = root.begin(); it != root.end(); ++it) {
auto key = it->first.as<std::string>();
auto value = it->second.as<std::string>();
const auto key = it->first.as<std::string>();
const auto value = yamlNodeToExpression(it->second);
parameters[key] = Parameter {
.name = key,
+1
View File
@@ -10,6 +10,7 @@ add_executable(Gui_tests_run
StyleParameters/StyleParametersApplicationTest.cpp
StyleParameters/ParserTest.cpp
StyleParameters/ParameterManagerTest.cpp
StyleParameters/YamlParameterSourceTest.cpp
InputHintTest.cpp
)
@@ -0,0 +1,206 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 <filesystem>
#include <fstream>
#include <gtest/gtest.h>
#include <Gui/Application.h>
#include <Gui/StyleParameters/ParameterManager.h>
#include <Gui/StyleParameters/Value.h>
using namespace Gui::StyleParameters;
// Alias to avoid ambiguity with ParameterGrp::Manager() from Base/Parameter.h.
using StyleParameterManager = Gui::StyleParameters::ParameterManager;
class YamlParameterSourceTest: public ::testing::Test
{
protected:
std::filesystem::path tempPath = std::filesystem::temp_directory_path()
/ "test_yaml_parameter_source.yaml";
void TearDown() override
{
std::filesystem::remove(tempPath);
}
YamlParameterSource loadYaml(const std::string& content)
{
std::ofstream out(tempPath);
out << content;
out.close();
return YamlParameterSource(tempPath.string());
}
StyleParameterManager managerWith(YamlParameterSource& source)
{
StyleParameterManager manager;
manager.addSource(&source);
return manager;
}
};
TEST_F(YamlParameterSourceTest, ScalarStringIsLoadedAsExpression)
{
auto source = loadYaml("BasePadding: 10px\n");
const auto param = source.get("BasePadding");
ASSERT_TRUE(param.has_value());
EXPECT_EQ(param->value, "10px");
}
TEST_F(YamlParameterSourceTest, ScalarColorIsLoadedAsExpression)
{
auto source = loadYaml("AccentColor: \"#ff0000\"\n");
const auto param = source.get("AccentColor");
ASSERT_TRUE(param.has_value());
EXPECT_EQ(param->value, "#ff0000");
}
TEST_F(YamlParameterSourceTest, SequenceBecomesUnnamedTupleExpression)
{
auto source = loadYaml("Corners: [10px, 20px, 10px, 20px]\n");
const auto param = source.get("Corners");
ASSERT_TRUE(param.has_value());
EXPECT_EQ(param->value, "(10px, 20px, 10px, 20px)");
}
TEST_F(YamlParameterSourceTest, BlockSequenceBecomesUnnamedTupleExpression)
{
auto source = loadYaml(
"Padding:\n"
" - 10px\n"
" - 20px\n"
" - 5px\n"
" - 15px\n"
);
const auto param = source.get("Padding");
ASSERT_TRUE(param.has_value());
EXPECT_EQ(param->value, "(10px, 20px, 5px, 15px)");
}
TEST_F(YamlParameterSourceTest, MapBecomesNamedTupleExpression)
{
auto source = loadYaml(
"Insets:\n"
" top: 10px\n"
" right: 20px\n"
" bottom: 5px\n"
" left: 15px\n"
);
const auto param = source.get("Insets");
ASSERT_TRUE(param.has_value());
EXPECT_EQ(param->value, "(top: 10px, right: 20px, bottom: 5px, left: 15px)");
}
TEST_F(YamlParameterSourceTest, NestedSequenceBecomesNestedTuple)
{
auto source = loadYaml(
"Stops:\n"
" - [0, \"#ff0000\"]\n"
" - [1, \"#0000ff\"]\n"
);
const auto param = source.get("Stops");
ASSERT_TRUE(param.has_value());
EXPECT_EQ(param->value, "((0, #ff0000), (1, #0000ff))");
}
TEST_F(YamlParameterSourceTest, SequenceResolvesToTupleValue)
{
auto source = loadYaml("Padding: [10px, 20px]\n");
auto manager = managerWith(source);
const auto result = manager.resolve("Padding");
ASSERT_TRUE(result.has_value());
ASSERT_TRUE(result->holds<Tuple>());
const auto& tuple = result->get<Tuple>();
EXPECT_EQ(tuple.size(), 2);
EXPECT_DOUBLE_EQ(tuple.at(0).get<Numeric>().value, 10.0);
EXPECT_DOUBLE_EQ(tuple.at(1).get<Numeric>().value, 20.0);
}
TEST_F(YamlParameterSourceTest, MapResolvesToNamedTupleValue)
{
auto source = loadYaml(
"Insets:\n"
" top: 10px\n"
" right: 20px\n"
);
auto manager = managerWith(source);
const auto result = manager.resolve("Insets");
ASSERT_TRUE(result.has_value());
ASSERT_TRUE(result->holds<Tuple>());
const auto& tuple = result->get<Tuple>();
EXPECT_EQ(tuple.size(), 2);
ASSERT_NE(tuple.find("top"), nullptr);
EXPECT_DOUBLE_EQ(tuple.find("top")->get<Numeric>().value, 10.0);
ASSERT_NE(tuple.find("right"), nullptr);
EXPECT_DOUBLE_EQ(tuple.find("right")->get<Numeric>().value, 20.0);
}
TEST_F(YamlParameterSourceTest, MixedFileLoadsAllParameters)
{
auto source = loadYaml(
"BasePadding: 8px\n"
"AccentColor: \"#ff0000\"\n"
"Corners: [4px, 8px, 4px, 8px]\n"
"Insets:\n"
" top: 10px\n"
" right: 20px\n"
);
const auto all = source.all();
EXPECT_EQ(all.size(), 4);
EXPECT_TRUE(source.get("BasePadding").has_value());
EXPECT_TRUE(source.get("AccentColor").has_value());
EXPECT_TRUE(source.get("Corners").has_value());
EXPECT_TRUE(source.get("Insets").has_value());
}
TEST_F(YamlParameterSourceTest, FlushAndReloadPreservesSequenceValue)
{
auto source = loadYaml("Padding: [10px, 20px]\n");
source.flush();
YamlParameterSource reloaded(tempPath.string());
const auto param = reloaded.get("Padding");
ASSERT_TRUE(param.has_value());
// After flush the value is stored as a string expression — semantics are preserved
auto manager = managerWith(reloaded);
const auto result = manager.resolve("Padding");
ASSERT_TRUE(result.has_value());
ASSERT_TRUE(result->holds<Tuple>());
EXPECT_EQ(result->get<Tuple>().size(), 2);
}