diff --git a/src/Gui/Application.cpp b/src/Gui/Application.cpp index 7acd57933f..8f46494588 100644 --- a/src/Gui/Application.cpp +++ b/src/Gui/Application.cpp @@ -141,6 +141,7 @@ #include "QtWidgets.h" #include +#include #include #ifdef BUILD_TRACY_FRAME_PROFILER @@ -381,20 +382,48 @@ struct PyMethodDef FreeCADGui_methods[] = { void Application::initStyleParameterManager() { + static ParamHandlers handlers; + + const auto deduceParametersFilePath = []() -> std::string { + const auto hMainWindowGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/MainWindow"); + + if (const std::string& path = hMainWindowGrp->GetASCII("ThemeStyleParametersFile"); + !path.empty()) { + return path; + } + + return fmt::format("qss:parameters/{}.yaml", hMainWindowGrp->GetASCII("Theme", "Classic")); + }; + + auto themeParametersSource = new StyleParameters::YamlParameterSource( + deduceParametersFilePath(), + {.name = QT_TR_NOOP("Theme Parameters"), + .options = StyleParameters::ParameterSourceOption::UserEditable}); + + handlers.addDelayedHandler( + "BaseApp/Preferences/MainWindow", + {"ThemeStyleParametersFiles", "Theme"}, + [themeParametersSource, deduceParametersFilePath](ParameterGrp::handle) { + themeParametersSource->changeFilePath(deduceParametersFilePath()); + }); + Base::registerServiceImplementation( new StyleParameters::BuiltInParameterSource({.name = QT_TR_NOOP("Built-in Parameters")})); - Base::registerServiceImplementation( - new StyleParameters::UserParameterSource( - App::GetApplication().GetParameterGroupByPath( - "User parameter:BaseApp/Preferences/Themes/Tokens"), - {.name = QT_TR_NOOP("Theme Parameters"), - .options = StyleParameters::ParameterSourceOption::UserEditable})); - + // todo: left for compatibility with older theme versions, to be removed before release Base::registerServiceImplementation( new StyleParameters::UserParameterSource( App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Themes/UserTokens"), + {.name = QT_TR_NOOP("Theme Parameters - Fallback"), + .options = StyleParameters::ParameterSourceOption::ReadOnly})); + + Base::registerServiceImplementation(themeParametersSource); + + Base::registerServiceImplementation( + new StyleParameters::UserParameterSource( + App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Themes/UserParameters"), {.name = QT_TR_NOOP("User Parameters"), .options = StyleParameters::ParameterSource::UserEditable})); diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index fb6969fe7c..719004ea20 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -146,7 +146,9 @@ target_include_directories( ${QtNetwork_INCLUDE_DIRS} ${QtUiTools_INCLUDE_DIRS} ${QtXml_INCLUDE_DIRS} + ${YAML_CPP_INCLUDE_DIRS} ) + list(APPEND FreeCADGui_LIBS ${QtCore_LIBRARIES} ${QtWidgets_LIBRARIES} @@ -159,6 +161,18 @@ list(APPEND FreeCADGui_LIBS ${QtUiTools_LIBRARIES} ) +link_directories(${YAML_CPP_LIBRARY_DIR}) + +if(yaml-cpp_VERSION VERSION_LESS 0.8.0) + list(APPEND FreeCADGui_LIBS + ${YAML_CPP_LIBRARIES} + ) +else() + list(APPEND FreeCADGui_LIBS + yaml-cpp::yaml-cpp + ) +endif() + if(${Qt5WinExtras_FOUND}) target_include_directories( FreeCADGui diff --git a/src/Gui/Dialogs/DlgThemeEditor.cpp b/src/Gui/Dialogs/DlgThemeEditor.cpp index efe66aa1c2..a590661027 100644 --- a/src/Gui/Dialogs/DlgThemeEditor.cpp +++ b/src/Gui/Dialogs/DlgThemeEditor.cpp @@ -465,6 +465,10 @@ void StyleParametersModel::flush() } }); + for (auto* source : sources) { + source->flush(); + } + reset(); } diff --git a/src/Gui/StyleParameters/ParameterManager.cpp b/src/Gui/StyleParameters/ParameterManager.cpp index 3e2e24ec8f..9aa9894eaf 100644 --- a/src/Gui/StyleParameters/ParameterManager.cpp +++ b/src/Gui/StyleParameters/ParameterManager.cpp @@ -26,6 +26,10 @@ #include "ParameterManager.h" #include "Parser.h" +#include +#include +#include + #ifndef _PreComp_ #include #include @@ -35,6 +39,8 @@ #include #endif +FC_LOG_LEVEL_INIT("Gui", true, true) + namespace Gui::StyleParameters { @@ -215,6 +221,92 @@ 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(); + auto value = it->second.as(); + + parameters[key] = Parameter { + .name = key, + .value = value, + }; + } +} + +std::list YamlParameterSource::all() const +{ + std::list result; + for (const auto& param : parameters | std::views::values) { + result.push_back(param); + } + return result; +} + +std::optional 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() diff --git a/src/Gui/StyleParameters/ParameterManager.h b/src/Gui/StyleParameters/ParameterManager.h index 53d3ed3703..fb13994494 100644 --- a/src/Gui/StyleParameters/ParameterManager.h +++ b/src/Gui/StyleParameters/ParameterManager.h @@ -306,6 +306,11 @@ public: * @param[in] name The name of the parameter to remove. */ virtual void remove([[maybe_unused]] const std::string& name) {} + + /** + * @brief Flushes buffered changes into more persistent storage. + */ + virtual void flush() {} }; /** @@ -376,6 +381,43 @@ public: void remove(const std::string& name) override; }; +/** + * @class YamlParameterSource + * @brief A ParameterSource implementation that loads and saves parameters + * from a YAML file using yaml-cpp. + * + * This class maintains an in-memory map of parameters loaded from a YAML file. + * Any changes through define() or remove() will also update the file. + */ +class GuiExport YamlParameterSource : public ParameterSource +{ +public: + /** + * @brief Constructs a YamlParameterSource that reads parameters from the given YAML file. + * + * If the file exists, all key-value pairs are loaded into memory. + * If the file does not exist, an empty parameter set is initialized. + * + * @param filePath Path to the YAML file used for persistence. + * @param metadata Optional metadata describing this source. + */ + explicit YamlParameterSource(const std::string& filePath, const Metadata& metadata = {}); + + void changeFilePath(const std::string& path); + void reload(); + + std::list all() const override; + std::optional get(const std::string& name) const override; + void define(const Parameter& param) override; + void remove(const std::string& name) override; + + void flush() override; + +private: + std::string filePath; + std::map parameters; +}; + /** * @brief Central manager for style parameters that aggregates multiple sources. *