| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | #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); |
| | } |
| |
|
| | 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)), |
| | }; |
| | } |
| |
|
| | 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; |
| | } |
| |
|
| | |
| | result += input.mid(lastIndex); |
| |
|
| | return result; |
| | }; |
| |
|
| | |
| | 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(); |
| | |
| | } |
| |
|
| | std::list<Parameter> ParameterManager::parameters() const |
| | { |
| | std::set<Parameter, Parameter::NameComparator> result; |
| |
|
| | |
| | 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&) { |
| | |
| | _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; |
| | } |
| |
|
| | } |
| |
|