"Professional CMake" book suggest the following: "Targets should build successfully with or without compiler support for precompiled headers. It should be considered an optimization, not a requirement. In particular, do not explicitly include a precompile header (e.g. stdafx.h) in the source code, let CMake force-include an automatically generated precompile header on the compiler command line instead. This is more portable across the major compilers and is likely to be easier to maintain. It will also avoid warnings being generated from certain code checking tools like iwyu (include what you use)." Therefore, removed the "#include <PreCompiled.h>" from sources, also there is no need for the "#ifdef _PreComp_" anymore
449 lines
12 KiB
C++
449 lines
12 KiB
C++
// 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 "ParameterManager.h"
|
|
#include "Parser.h"
|
|
|
|
#include <QFile>
|
|
#include <fstream>
|
|
#include <yaml-cpp/yaml.h>
|
|
|
|
#include <QColor>
|
|
#include <QRegularExpression>
|
|
#include <QString>
|
|
#include <ranges>
|
|
#include <utility>
|
|
#include <variant>
|
|
|
|
#include <Base/Console.h>
|
|
|
|
FC_LOG_LEVEL_INIT("Gui", true, true)
|
|
|
|
namespace Gui::StyleParameters
|
|
{
|
|
|
|
Numeric Numeric::operator+(const Numeric& rhs) const
|
|
{
|
|
ensureEqualUnits(rhs);
|
|
return {value + rhs.value, unit};
|
|
}
|
|
|
|
Numeric Numeric::operator-(const Numeric& rhs) const
|
|
{
|
|
ensureEqualUnits(rhs);
|
|
return {value - rhs.value, unit};
|
|
}
|
|
|
|
Numeric Numeric::operator-() const
|
|
{
|
|
return {-value, unit};
|
|
}
|
|
|
|
Numeric Numeric::operator/(const Numeric& rhs) const
|
|
{
|
|
if (rhs.value == 0) {
|
|
THROWM(Base::RuntimeError, "Division by zero");
|
|
}
|
|
|
|
if (rhs.unit.empty() || unit.empty()) {
|
|
return {value / rhs.value, unit};
|
|
}
|
|
|
|
ensureEqualUnits(rhs);
|
|
return {value / rhs.value, unit};
|
|
}
|
|
|
|
Numeric Numeric::operator*(const Numeric& rhs) const
|
|
{
|
|
if (rhs.unit.empty() || unit.empty()) {
|
|
return {value * rhs.value, unit};
|
|
}
|
|
|
|
ensureEqualUnits(rhs);
|
|
return {value * rhs.value, unit};
|
|
}
|
|
|
|
void Numeric::ensureEqualUnits(const Numeric& rhs) const
|
|
{
|
|
if (unit != rhs.unit) {
|
|
THROWM(Base::RuntimeError,
|
|
fmt::format("Units mismatch left expression is '{}', right expression is '{}'",
|
|
unit,
|
|
rhs.unit));
|
|
}
|
|
}
|
|
|
|
std::string Value::toString() const
|
|
{
|
|
if (std::holds_alternative<Numeric>(*this)) {
|
|
auto [value, unit] = std::get<Numeric>(*this);
|
|
return fmt::format("{}{}", value, unit);
|
|
}
|
|
|
|
if (std::holds_alternative<Base::Color>(*this)) {
|
|
auto color = std::get<Base::Color>(*this);
|
|
return fmt::format("#{:0>6x}", color.getPackedRGB() >> 8); // NOLINT(*-magic-numbers)
|
|
}
|
|
|
|
return std::get<std::string>(*this);
|
|
}
|
|
|
|
ParameterSource::ParameterSource(const Metadata& metadata)
|
|
: metadata(metadata)
|
|
{}
|
|
|
|
InMemoryParameterSource::InMemoryParameterSource(const std::list<Parameter>& parameters,
|
|
const Metadata& metadata)
|
|
: ParameterSource(metadata)
|
|
{
|
|
for (const auto& parameter : parameters) {
|
|
InMemoryParameterSource::define(parameter);
|
|
}
|
|
}
|
|
|
|
std::list<Parameter> InMemoryParameterSource::all() const
|
|
{
|
|
auto values = parameters | std::ranges::views::values;
|
|
|
|
return std::list<Parameter>(values.begin(), values.end());
|
|
}
|
|
|
|
std::optional<Parameter> InMemoryParameterSource::get(const std::string& name) const
|
|
{
|
|
if (parameters.contains(name)) {
|
|
return parameters.at(name);
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
void InMemoryParameterSource::define(const Parameter& parameter)
|
|
{
|
|
parameters[parameter.name] = parameter;
|
|
}
|
|
|
|
void InMemoryParameterSource::remove(const std::string& name)
|
|
{
|
|
parameters.erase(name);
|
|
}
|
|
|
|
BuiltInParameterSource::BuiltInParameterSource(const Metadata& metadata)
|
|
: ParameterSource(metadata)
|
|
{
|
|
this->metadata.options |= ReadOnly;
|
|
}
|
|
|
|
std::list<Parameter> BuiltInParameterSource::all() const
|
|
{
|
|
std::list<Parameter> result;
|
|
|
|
for (const auto& name : params | std::views::keys) {
|
|
result.push_back(*get(name));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::optional<Parameter> BuiltInParameterSource::get(const std::string& name) const
|
|
{
|
|
if (params.contains(name)) {
|
|
unsigned long color = params.at(name)->GetUnsigned(name.c_str(), 0);
|
|
|
|
return Parameter {
|
|
.name = name,
|
|
.value = fmt::format("#{:0>6x}", 0x00FFFFFF & (color >> 8)), // NOLINT(*-magic-numbers)
|
|
};
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
UserParameterSource::UserParameterSource(ParameterGrp::handle hGrp, const Metadata& metadata)
|
|
: ParameterSource(metadata)
|
|
, hGrp(hGrp)
|
|
{}
|
|
|
|
std::list<Parameter> UserParameterSource::all() const
|
|
{
|
|
std::list<Parameter> result;
|
|
|
|
for (const auto& [token, value] : hGrp->GetASCIIMap()) {
|
|
result.push_back({
|
|
.name = token,
|
|
.value = value,
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::optional<Parameter> UserParameterSource::get(const std::string& name) const
|
|
{
|
|
if (auto value = hGrp->GetASCII(name.c_str(), ""); !value.empty()) {
|
|
return Parameter {
|
|
.name = name,
|
|
.value = value,
|
|
};
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
void UserParameterSource::define(const Parameter& parameter)
|
|
{
|
|
hGrp->SetASCII(parameter.name.c_str(), parameter.value);
|
|
}
|
|
|
|
void UserParameterSource::remove(const std::string& name)
|
|
{
|
|
hGrp->RemoveASCII(name.c_str());
|
|
}
|
|
|
|
YamlParameterSource::YamlParameterSource(const std::string& filePath, const Metadata& metadata)
|
|
: ParameterSource(metadata)
|
|
{
|
|
changeFilePath(filePath);
|
|
}
|
|
|
|
void YamlParameterSource::changeFilePath(const std::string& path)
|
|
{
|
|
this->filePath = path;
|
|
reload();
|
|
}
|
|
|
|
void YamlParameterSource::reload()
|
|
{
|
|
QFile file(QString::fromStdString(filePath));
|
|
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
FC_TRACE("StyleParameters: Unable to open file " << filePath);
|
|
return;
|
|
}
|
|
|
|
if (filePath.starts_with(":/")) {
|
|
this->metadata.options |= ReadOnly;
|
|
}
|
|
|
|
QTextStream in(&file);
|
|
std::string content = in.readAll().toStdString();
|
|
|
|
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>();
|
|
|
|
parameters[key] = Parameter {
|
|
.name = key,
|
|
.value = value,
|
|
};
|
|
}
|
|
}
|
|
|
|
std::list<Parameter> YamlParameterSource::all() const
|
|
{
|
|
std::list<Parameter> result;
|
|
for (const auto& param : parameters | std::views::values) {
|
|
result.push_back(param);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::optional<Parameter> YamlParameterSource::get(const std::string& name) const
|
|
{
|
|
if (auto it = parameters.find(name); it != parameters.end()) {
|
|
return it->second;
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
void YamlParameterSource::define(const Parameter& param)
|
|
{
|
|
parameters[param.name] = param;
|
|
}
|
|
|
|
void YamlParameterSource::remove(const std::string& name)
|
|
{
|
|
parameters.erase(name);
|
|
}
|
|
|
|
void YamlParameterSource::flush()
|
|
{
|
|
YAML::Node root;
|
|
for (const auto& [name, param] : parameters) {
|
|
root[name] = param.value;
|
|
}
|
|
|
|
QFile file(QString::fromStdString(filePath));
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
|
FC_WARN("StyleParameters: Unable to open file " << filePath);
|
|
return;
|
|
}
|
|
|
|
QTextStream out(&file);
|
|
out << QString::fromStdString(YAML::Dump(root));
|
|
}
|
|
|
|
ParameterManager::ParameterManager() = default;
|
|
|
|
void ParameterManager::reload()
|
|
{
|
|
_resolved.clear();
|
|
}
|
|
|
|
std::string ParameterManager::replacePlaceholders(const std::string& expression,
|
|
ResolveContext context) const
|
|
{
|
|
static const QRegularExpression regex(QStringLiteral("@(\\w+)"));
|
|
|
|
auto substituteWithCallback =
|
|
[](const QRegularExpression& regex,
|
|
const QString& input,
|
|
const std::function<QString(const QRegularExpressionMatch&)>& callback) {
|
|
QRegularExpressionMatchIterator it = regex.globalMatch(input);
|
|
|
|
QString result;
|
|
qsizetype lastIndex = 0;
|
|
|
|
while (it.hasNext()) {
|
|
QRegularExpressionMatch match = it.next();
|
|
|
|
qsizetype start = match.capturedStart();
|
|
qsizetype end = match.capturedEnd();
|
|
|
|
result += input.mid(lastIndex, start - lastIndex);
|
|
result += callback(match);
|
|
|
|
lastIndex = end;
|
|
}
|
|
|
|
// Append any remaining text after the last match
|
|
result += input.mid(lastIndex);
|
|
|
|
return result;
|
|
};
|
|
|
|
// clang-format off
|
|
return substituteWithCallback(
|
|
regex,
|
|
QString::fromStdString(expression),
|
|
[&](const QRegularExpressionMatch& match) {
|
|
auto tokenName = match.captured(1).toStdString();
|
|
auto tokenValue = resolve(tokenName, context);
|
|
|
|
if (!tokenValue) {
|
|
Base::Console().warning("Requested non-existent style parameter token '%s'.\n", tokenName);
|
|
return QStringLiteral("");
|
|
}
|
|
|
|
context.visited.erase(tokenName);
|
|
return QString::fromStdString(tokenValue->toString());
|
|
}
|
|
).toStdString();
|
|
// clang-format on
|
|
}
|
|
|
|
std::list<Parameter> ParameterManager::parameters() const
|
|
{
|
|
std::set<Parameter, Parameter::NameComparator> result;
|
|
|
|
// we need to traverse it in reverse order so more important tokens will take precedence
|
|
for (const ParameterSource* source : _sources | std::views::reverse) {
|
|
for (const Parameter& parameter : source->all()) {
|
|
result.insert(parameter);
|
|
}
|
|
}
|
|
|
|
return std::list(result.begin(), result.end());
|
|
}
|
|
|
|
std::optional<std::string> ParameterManager::expression(const std::string& name) const
|
|
{
|
|
if (auto param = parameter(name)) {
|
|
return param->value;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
std::optional<Value> ParameterManager::resolve(const std::string& name,
|
|
ResolveContext context) const
|
|
{
|
|
std::optional<Parameter> maybeParameter = this->parameter(name);
|
|
|
|
if (!maybeParameter) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (context.visited.contains(name)) {
|
|
Base::Console().warning("The style parameter '%s' contains circular-reference.\n", name);
|
|
return expression(name);
|
|
}
|
|
|
|
const Parameter& token = *maybeParameter;
|
|
|
|
if (!_resolved.contains(token.name)) {
|
|
context.visited.insert(token.name);
|
|
try {
|
|
_resolved[token.name] = evaluate(token.value, context);
|
|
}
|
|
catch (Base::Exception&) {
|
|
// in case of being unable to parse it, we need to treat it as a generic value
|
|
_resolved[token.name] = replacePlaceholders(token.value, context);
|
|
}
|
|
context.visited.erase(token.name);
|
|
}
|
|
|
|
return _resolved[token.name];
|
|
}
|
|
|
|
Value ParameterManager::evaluate(const std::string& expression, ResolveContext context) const
|
|
{
|
|
Parser parser(expression);
|
|
return parser.parse()->evaluate({.manager = this, .context = std::move(context)});
|
|
}
|
|
|
|
std::optional<Parameter> ParameterManager::parameter(const std::string& name) const
|
|
{
|
|
for (const ParameterSource* source : _sources) {
|
|
if (const auto& parameter = source->get(name)) {
|
|
return parameter;
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
void ParameterManager::addSource(ParameterSource* source)
|
|
{
|
|
_sources.push_front(source);
|
|
}
|
|
|
|
std::list<ParameterSource*> ParameterManager::sources() const
|
|
{
|
|
return _sources;
|
|
}
|
|
|
|
} // namespace Gui::StyleParameters
|