diff --git a/cmake/DtkDConfig/DtkDConfigConfig.cmake.in b/cmake/DtkDConfig/DtkDConfigConfig.cmake.in index 689c1b50..54f52c8e 100644 --- a/cmake/DtkDConfig/DtkDConfigConfig.cmake.in +++ b/cmake/DtkDConfig/DtkDConfigConfig.cmake.in @@ -22,6 +22,41 @@ if(NOT DEFINED DSG_DATA_DIR) endif() add_definitions(-DDSG_DATA_DIR=\"${DSG_DATA_DIR}\") + +# Define the helper function +function(dtk_config_to_cpp JSON_FILE OUTPUT_VAR) + if(NOT EXISTS ${JSON_FILE}) + message(FATAL_ERROR "JSON file ${JSON_FILE} does not exist.") + endif() + + # Generate the output header file name + get_filename_component(FILE_NAME_WE ${JSON_FILE} NAME_WE) + if(DEFINED OUTPUT_FILE_NAME) + set(OUTPUT_HEADER "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE_NAME}") + else() + set(OUTPUT_HEADER "${CMAKE_CURRENT_BINARY_DIR}/${FILE_NAME_WE}.hpp") + endif() + + # Check if CLASS_NAME is set + if(DEFINED CLASS_NAME) + set(CLASS_NAME_ARG -c ${CLASS_NAME}) + else() + set(CLASS_NAME_ARG "") + endif() + + # Add a custom command to run dconfig2cpp + add_custom_command( + OUTPUT ${OUTPUT_HEADER} + COMMAND ${TOOL_INSTALL_DIR}/dconfig2cpp -o ${OUTPUT_HEADER} ${CLASS_NAME_ARG} ${JSON_FILE} + DEPENDS ${JSON_FILE} + COMMENT "Generating ${OUTPUT_HEADER} from ${JSON_FILE}" + VERBATIM + ) + + # Add the generated header to the specified output variable + set(${OUTPUT_VAR} ${${OUTPUT_VAR}} ${OUTPUT_HEADER} PARENT_SCOPE) +endfunction() + # deploy some `meta` 's configure. # # FILES - deployed files. diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index f3ef2a58..63b8fc92 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -3,3 +3,4 @@ add_subdirectory(deepin-os-release) add_subdirectory(qdbusxml2cpp) add_subdirectory(settings) add_subdirectory(ch2py) +add_subdirectory(dconfig2cpp) diff --git a/tools/dconfig2cpp/CMakeLists.txt b/tools/dconfig2cpp/CMakeLists.txt new file mode 100644 index 00000000..bd2ce30c --- /dev/null +++ b/tools/dconfig2cpp/CMakeLists.txt @@ -0,0 +1,26 @@ +set(TARGET_NAME dconfig2cpp) +set(BIN_NAME ${TARGET_NAME}${DTK_VERSION_MAJOR}) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core) + +add_executable(${BIN_NAME} + main.cpp +) + +target_link_libraries( + ${BIN_NAME} PRIVATE + Qt${QT_VERSION_MAJOR}::Core +) + +set_target_properties( + ${BIN_NAME} PROPERTIES + OUTPUT_NAME ${TARGET_NAME} + EXPORT_NAME dconfig2cpp +) + +install( + TARGETS ${BIN_NAME} + EXPORT Dtk${DTK_VERSION_MAJOR}ToolsTargets + DESTINATION ${TOOL_INSTALL_DIR} +) diff --git a/tools/dconfig2cpp/main.cpp b/tools/dconfig2cpp/main.cpp new file mode 100644 index 00000000..4c7ec82e --- /dev/null +++ b/tools/dconfig2cpp/main.cpp @@ -0,0 +1,346 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static QString jsonValueToCppCode(const QJsonValue &value){ + if (value.isBool()) { + return value.toBool() ? "true" : "false"; + } else if (value.isDouble()) { + const auto variantValue = value.toVariant(); + if (variantValue.userType() == QVariant(static_cast(1)).userType()) { + return QString::number(value.toInt()); + } else if (variantValue.userType() == QVariant(static_cast(1)).userType()) { + return QString::number(variantValue.toLongLong()); + } + + return QString::number(value.toDouble()); + } else if (value.isString()) { + return QString("QStringLiteral(\"%1\")").arg(value.toString()); + } else if (value.isNull()) { + return "QVariant::fromValue(nullptr)"; + } else if (value.isArray()) { + QStringList elements; + const auto array = value.toArray(); + for (const QJsonValue &element : array) { + elements << "QVariant(" + jsonValueToCppCode(element) + ")"; + } + return "QList{" + elements.join(", ") + "}"; + } else if (value.isObject()) { + QStringList elements; + QJsonObject obj = value.toObject(); + for (auto it = obj.begin(); it != obj.end(); ++it) { + elements << QString("{QStringLiteral(\"%1\"), QVariant(%2)}").arg(it.key(), jsonValueToCppCode(it.value())); + } + return "QVariantMap{" + elements.join(", ") + "}"; + } else { + return "QVariant()"; + } +} + +namespace DTK_CORE_NAMESPACE { +class DConfig; +} + +int main(int argc, char *argv[]) { + QCoreApplication app(argc, argv); + QCommandLineParser parser; + parser.setApplicationDescription(QStringLiteral("DConfig to C++ class generator")); + parser.addHelpOption(); + + QCommandLineOption classNameOption(QStringList() << QStringLiteral("c") << QStringLiteral("class-name"), + QStringLiteral("Name of the generated class"), + QStringLiteral("className")); + parser.addOption(classNameOption); + + QCommandLineOption sourceFileOption(QStringList() << QStringLiteral("o") << QStringLiteral("output"), + QStringLiteral("Path to the output source(header only) file"), + QStringLiteral("sourceFile")); + parser.addOption(sourceFileOption); + + parser.addPositionalArgument(QStringLiteral("json-file"), QStringLiteral("Path to the input JSON file")); + parser.process(app); + + const QStringList args = parser.positionalArguments(); + if (args.size() != 1) { + parser.showHelp(-1); + } + + QString className = parser.value(classNameOption); + if (className.isEmpty()) { + QString jsonFileName = QFileInfo(args.first()).completeBaseName(); + className = jsonFileName.replace('.', '_'); + } + + QString sourceFilePath = parser.value(sourceFileOption); + if (sourceFilePath.isEmpty()) { + sourceFilePath = className.toLower() + QStringLiteral(".hpp"); + } + + QFile file(args.first()); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << QStringLiteral("Failed to open file:") << args.first(); + return -1; + } + + QByteArray data = file.readAll(); + QJsonDocument doc = QJsonDocument::fromJson(data); + QJsonObject root = doc.object(); + + // Check magic value + if (root[QStringLiteral("magic")].toString() != QStringLiteral("dsg.config.meta")) { + qWarning() << QStringLiteral("Invalid magic value in JSON file"); + return -1; + } + + // Generate header and source files + QFile headerFile(sourceFilePath); + if (!headerFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + qWarning() << QStringLiteral("Failed to open file for writing:") << sourceFilePath; + return -1; + } + + QTextStream headerStream(&headerFile); + + // Extract version and add it as a comment in the generated code + QString version = root[QStringLiteral("version")].toString(); + + // Generate header and source file comments + QString commandLineArgs = QCoreApplication::arguments().join(QStringLiteral(" ")); + QString generationTime = QDateTime::currentDateTime().toString(Qt::ISODate); + + QString headerComment = QString( + QStringLiteral("/**\n" + " * This file is generated by dconfig2cpp.\n" + " * Command line arguments: %1\n" + " * Generation time: %2\n" + " * JSON file version: %3\n" + " * \n" + " * WARNING: DO NOT MODIFY THIS FILE MANUALLY.\n" + " * If you need to change the content, please modify the dconfig2cpp tool.\n" + " */\n\n") + ).arg(commandLineArgs, generationTime, version); + + headerStream << headerComment; + QJsonObject contents = root[QStringLiteral("contents")].toObject(); + + // Write header file content + headerStream << "#ifndef " << className.toUpper() << "_H\n"; + headerStream << "#define " << className.toUpper() << "_H\n\n"; + headerStream << "#include \n"; + headerStream << "#include \n"; + headerStream << "#include \n\n"; + headerStream << "class " << className << " : public QObject {\n"; + headerStream << " Q_OBJECT\n\n"; + + struct Property { + QString typeName; + QString propertyName; + QString capitalizedPropertyName; + QJsonValue defaultValue; + }; + QList properties; + + for (auto it = contents.begin(); it != contents.end(); ++it) { + QJsonObject obj = it.value().toObject(); + QString propertyName = it.key(); + QString typeName; + const auto value = obj[QStringLiteral("value")]; + if (value.isBool()) { + typeName = "bool"; + } else if (value.isArray()) { + typeName = "QList"; + } else if (value.isObject()) { + typeName = "QVariantMap"; + } else if (value.isDouble()) { + const auto variantValue = value.toVariant(); + if (variantValue.userType() == QVariant(static_cast(1)).userType()) { + typeName = "int"; + } else if (variantValue.userType() == QVariant(static_cast(1)).userType()) { + typeName = "qint64"; + } else { + typeName = "double"; + } + } else if (value.isString()) { + typeName = "QString"; + } else { + typeName = "QVariant"; + } + + QString capitalizedPropertyName = propertyName; + if (!capitalizedPropertyName.isEmpty() && capitalizedPropertyName[0].isLower()) { + capitalizedPropertyName[0] = capitalizedPropertyName[0].toUpper(); + } + + properties.append(Property({ + typeName, + propertyName, + capitalizedPropertyName, + obj[QStringLiteral("value")] + })); + + headerStream << " Q_PROPERTY(" << typeName << " " << propertyName << " READ " << propertyName + << " WRITE set" << capitalizedPropertyName << " NOTIFY " << propertyName << "Changed)\n"; + } + headerStream << "public:\n"; + headerStream << " explicit " << className << R"((QThread *thread, const QString &appId, const QString &name, const QString &subpath, QObject *parent = nullptr) + : QObject(parent) { + if (!thread->isRunning()) { + qWarning() << QStringLiteral("Warning: The provided thread is not running."); + } + Q_ASSERT(QThread::currentThread() != thread); + auto worker = new QObject(); + worker->moveToThread(thread); + QMetaObject::invokeMethod(worker, [this, worker, appId, name, subpath]() { + auto config = DTK_CORE_NAMESPACE::DConfig::create(appId, name, subpath, nullptr); + if (!config) { + qWarning() << QStringLiteral("Failed to create DConfig instance."); + worker->deleteLater(); + return; + } + config->moveToThread(QThread::currentThread()); + initialize(config); + worker->deleteLater(); + }); + } + explicit )" << className << R"((QThread *thread, DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &appId, const QString &name, const QString &subpath, + QObject *parent = nullptr) + : QObject(parent) { + if (!thread->isRunning()) { + qWarning() << QStringLiteral("Warning: The provided thread is not running."); + } + Q_ASSERT(QThread::currentThread() != thread); + auto worker = new QObject(); + worker->moveToThread(thread); + QMetaObject::invokeMethod(worker, [this, worker, backend, appId, name, subpath]() { + auto config = DTK_CORE_NAMESPACE::DConfig::create(backend, appId, name, subpath, nullptr); + if (!config) { + qWarning() << QStringLiteral("Failed to create DConfig instance."); + worker->deleteLater(); + return; + } + config->moveToThread(QThread::currentThread()); + initialize(config); + worker->deleteLater(); + }); + } + explicit )" << className << R"((QThread *thread, const QString &name, const QString &subpath, + QObject *parent = nullptr) + : QObject(parent) { + if (!thread->isRunning()) { + qWarning() << QStringLiteral("Warning: The provided thread is not running."); + } + Q_ASSERT(QThread::currentThread() != thread); + auto worker = new QObject(); + worker->moveToThread(thread); + QMetaObject::invokeMethod(worker, [this, worker, name, subpath]() { + auto config = DTK_CORE_NAMESPACE::DConfig::createGeneric(name, subpath, nullptr); + if (!config) { + qWarning() << QStringLiteral("Failed to create DConfig instance."); + worker->deleteLater(); + return; + } + config->moveToThread(QThread::currentThread()); + initialize(config); + worker->deleteLater(); + }); + } + explicit )" << className << R"((QThread *thread, DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &name, const QString &subpath = QString(), + QObject *parent = nullptr) + : QObject(parent) { + if (!thread->isRunning()) { + qWarning() << QStringLiteral("Warning: The provided thread is not running."); + } + Q_ASSERT(QThread::currentThread() != thread); + auto worker = new QObject(); + worker->moveToThread(thread); + QMetaObject::invokeMethod(worker, [this, worker, backend, name, subpath]() { + auto config = DTK_CORE_NAMESPACE::DConfig::createGeneric(backend, name, subpath, nullptr); + if (!config) { + qWarning() << QStringLiteral("Failed to create DConfig instance."); + worker->deleteLater(); + return; + } + config->moveToThread(QThread::currentThread()); + initialize(config); + worker->deleteLater(); + }); + } + + ~)""" << className << R"(() { + if (m_config) { + m_config->deleteLater(); + } + } + + )"; + + for (const Property &property : std::as_const(properties)) { + headerStream << " " << property.typeName << " " << property.propertyName << "() const {\n" + << " return p_" << property.propertyName << ";\n }\n"; + headerStream << " void set" << property.capitalizedPropertyName << "(const " << property.typeName << " &value) {\n" + << " auto oldValue = p_" << property.propertyName << ";\n" + << " p_" << property.propertyName << " = value;\n" + << " QMetaObject::invokeMethod(m_config, [this, value]() {\n" + << " m_config->setValue(QStringLiteral(\"" << property.propertyName << "\"), value);\n" + << " });\n" + << " if (p_" << property.propertyName << " != oldValue) {\n" + << " Q_EMIT " << property.propertyName << "Changed();\n" + << " }\n" + << " }\n"; + } + headerStream << "Q_SIGNALS:\n"; + for (const Property &property : std::as_const(properties)) { + headerStream << " void " << property.propertyName << "Changed();\n"; + } + headerStream << "private:\n"; + headerStream << " void initialize(DTK_CORE_NAMESPACE::DConfig *config) {\n"; + headerStream << " Q_ASSERT(!m_config);\n m_config = config;\n"; + for (const Property &property : std::as_const(properties)) { + headerStream << " updateValue(QStringLiteral(\"" << property.propertyName << "\"), QVariant::fromValue(p_" << property.propertyName << "));\n"; + } + headerStream << R"( + connect(config, &DTK_CORE_NAMESPACE::DConfig::valueChanged, this, [this](const QString &key) { + updateValue(key); + }, Qt::DirectConnection); + } + void updateValue(const QString &key, const QVariant &fallback = QVariant()) { + Q_ASSERT(QThread::currentThread() == m_config->thread()); + const QVariant &value = m_config->value(key, fallback); +)"; + for (const Property &property : std::as_const(properties)) { + headerStream << " if (key == QStringLiteral(\"" << property.propertyName << "\")) {\n"; + headerStream << " auto newValue = qvariant_cast<" << property.typeName << ">(value);\n"; + headerStream << " QMetaObject::invokeMethod(this, [this, newValue]() {\n"; + headerStream << " if (p_" << property.propertyName << " != newValue) {\n"; + headerStream << " p_" << property.propertyName << " = newValue;\n"; + headerStream << " Q_EMIT " << property.propertyName << "Changed();\n"; + headerStream << " }\n"; + headerStream << " });\n"; + headerStream << " return;\n"; + headerStream << " }\n"; + } + headerStream << " }\n"; + headerStream << " DTK_CORE_NAMESPACE::DConfig *m_config = nullptr;\n\n"; + + for (const Property &property : std::as_const(properties)) { + if (property.typeName == "int" || property.typeName == "qint64") { + headerStream << " // Note: If you expect a double type, add 'e' to the number in the JSON value field, e.g., \"value\": 1.0e, not just 1.0\n"; + } + headerStream << " " << property.typeName << " p_" << property.propertyName << " { "; + headerStream << jsonValueToCppCode(property.defaultValue) << " };\n"; + } + headerStream << "};\n\n"; + headerStream << "#endif // " << className.toUpper() << "_H\n"; + + return 0; +}