From 99f7687f5fb1b6b25da7409ee0d4bee942dd0222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sat, 14 May 2022 17:41:41 +0200 Subject: [PATCH 1/9] Start working on extensions. --- src/CMakeLists.txt | 1 + src/extension.cpp | 73 +++++++++++++++++++ src/extension.h | 173 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 src/extension.cpp create mode 100644 src/extension.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ed1baa88..87700aea 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ target_compile_definitions(uibase PRIVATE -DUIBASE_EXPORT) mo2_install_target(uibase) mo2_add_filter(NAME src/interfaces GROUPS + extension ifiletree imoinfo installationtester diff --git a/src/extension.cpp b/src/extension.cpp new file mode 100644 index 00000000..1bac34e4 --- /dev/null +++ b/src/extension.cpp @@ -0,0 +1,73 @@ +#include "extension.h" + +#include +#include + +namespace MOBase +{ + +QString IExtension::createStyleSheet(std::filesystem::path const& path) +{ + if (!exists(path)) { + return ""; + } + + QString stylesheet; + + // read the whole file + { + QFile file(path); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + return ""; + } + + stylesheet = QTextStream(&file).readAll(); + } + + // replace url() in the file + // TODO + + return stylesheet; +} + +QTranslator IExtension::createTranslator(std::filesystem::path const& prefix, + QString const& language) +{ + const QFileInfo fileInfo(prefix); + + QTranslator translator; + + if (fileInfo.exists()) { + translator.load(fileInfo.fileName() + "_" + language, fileInfo.absolutePath()); + } + + return translator +} + +IExtension::IExtension(std::filesystem::path path, ExtensionMetaData metadata) + : m_Path{std::move(path)}, m_MetaData{std::move(metadata)} +{ + m_StyleSheet = createStyleSheet(m_MetaData.styleSheetFilePath()); +} + +QTranslator IExtension::translator(QString const& language) const +{ + return createTranslator(m_MetaData.translationsFilePrefix(), language); +} + +std::vector IExtension::plugins() const +{ + if (!m_Loaded) { + m_Plugins = fetchPlugins(); + m_Loaded = true; + } + return m_Plugins; +} + +std::vector IExtension::loadPlugins() +{ + // loadPlugins() is just exposed to pre-load plugins + return plugins(); +} + +} // namespace MOBase diff --git a/src/extension.h b/src/extension.h new file mode 100644 index 00000000..b70ea749 --- /dev/null +++ b/src/extension.h @@ -0,0 +1,173 @@ +#ifndef UIBASE_EXTENSION_H +#define UIBASE_EXTENSION_H + +#include +#include +#include + +#include +#include + +#include "versioninfo.h" + +namespace MOBase +{ + +class IExtension; + +class VersionRequirement +{}; +class GameRequirement +{}; +class ExtensionRequirement +{}; + +class ExtensionMetaData +{ +public: + ExtensionMetaData(const IExtension* extension, QJsonObject const& jsonData); + + /** + * @return the name of the extension. + */ + const auto& name() const { return localized(m_Name); } + + /** + * @return the description of the extension. + */ + const auto& description() const { return localized(m_Description); } + + /** + * @return the version of the extension. + */ + const auto& version() const { return m_Version; } + + /** + * @return the version requirement of the extension. + */ + const auto& versionRequirement() const { return m_VersionRequirement; } + + /** + * @return the game requirement of the extension. + */ + const auto& gameRequirement() const { return m_GameRequirement; } + + /** + * @return the extension requirement of the extension. + */ + const auto& extensionRequirement() const { return m_ExtensionRequirement; } + +private: + QString localized(QString const& value) const + { + const auto result = QCoreApplication::translate( + m_TranslationContext.toUtf8().data(), value.toUtf8().data()); + return result.isEmpty() ? value : result; + } + +private: + constexpr static const char* DEFAULT_TRANSLATIONS_FOLDER = "translations"; + constexpr static const char* DEFAULT_STYLESHEET_PATH = "stylesheets"; + +private: + // these functions are only for IExtension + // + friend class Extension; + + // retrieve the prefix translations, if there is one, e.g., translations/foo + // + const auto& translationsFilePrefix() const { return m_TranslationFilesPrefix; } + + // retrieve the path to the stylesheet, if there is one + // + const auto& styleSheetFilePath() const { return m_StyleSheetFilePath; } + +private: + const IExtension* m_Extension; + QString m_TranslationContext; + + QString m_Name; + QString m_Description; + VersionInfo m_Version; + + std::filesystem::path m_TranslationFilesPrefix; + std::filesystem::path m_StyleSheetFilePath; + + VersionRequirement m_VersionRequirement; + GameRequirement m_GameRequirement; + ExtensionRequirement m_ExtensionRequirement; +}; + +class IExtension +{ +public: + /** + * @brief Retrieve the plugins from this extension. If the plugins have not been + * loaded yet, the plugins are loaded. + * + * @return the list of plugins from this extension. + */ + std::vector plugins() const; + + /** + * @brief Load the plugins from this extension. + * + * @return the list of plugins from this extension. + */ + std::vector loadPlugins(); + + /** + * @brief Retrieve the path to this extension folder. + * + * @return the directory of this extension. + */ + std::filesystem::path directory() const { return m_Path; } + + /** + * @brief Retrieve the metadata of this extension. + * + * @return the metadata of this extension. + */ + const auto& metadata() const { return m_MetaData; } + + /** + * @brief Retrieve the translator of this extension for the given language. + * + * @param language Language to create a translator for, e.g., es, fr, fr_FR, etc. + * + * @return a translator for the given language, or an empty one if none exists. + */ + QTranslator translator(QString const& language) const; + + /** + * @brief Retrieve the stylesheet of this extension. + */ + const QString& stylesheet() const { return m_StyleSheet; } + + virtual ~IExtension() {} + +protected: + IExtension(std::filesystem::path path, ExtensionMetaData metadata); + + // retrieve the plugins for this extension + // + virtual std::vector fetchPlugins() const = 0; + +private: + static QString createStyleSheet(std::filesystem::path const& path); + static QString createTranslator(std::filesystem::path const& prefix, + QString const& language); + +private: + std::filesystem::path m_Path; + ExtensionMetaData m_MetaData; + + QString m_StyleSheet; + + mutable bool m_Loaded{false}; + mutable std::vector m_Plugins; +}; + +} // namespace MOBase + +#endif From 0ec75bc4006cad9d72484260b90dce7ca3ee10eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Mon, 23 May 2022 12:49:09 +0200 Subject: [PATCH 2/9] Add theme and translation for extensions. --- src/CMakeLists.txt | 7 ++++- src/extension.cpp | 47 +-------------------------------- src/extension.h | 55 +++++++++++++++++---------------------- src/theme.cpp | 21 +++++++++++++++ src/theme.h | 65 ++++++++++++++++++++++++++++++++++++++++++++++ src/translation.h | 62 +++++++++++++++++++++++++++++++++++++++++++ src/utility.cpp | 10 +++++++ src/utility.h | 2 ++ 8 files changed, 191 insertions(+), 78 deletions(-) create mode 100644 src/theme.cpp create mode 100644 src/theme.h create mode 100644 src/translation.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 87700aea..168d9cb4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,8 +10,13 @@ mo2_configure_uibase(uibase target_compile_definitions(uibase PRIVATE -DUIBASE_EXPORT) mo2_install_target(uibase) -mo2_add_filter(NAME src/interfaces GROUPS +mo2_add_filter(NAME src/extensions GROUPS extension + theme + translation +) + +mo2_add_filter(NAME src/interfaces GROUPS ifiletree imoinfo installationtester diff --git a/src/extension.cpp b/src/extension.cpp index 1bac34e4..1431b9a3 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -6,54 +6,9 @@ namespace MOBase { -QString IExtension::createStyleSheet(std::filesystem::path const& path) -{ - if (!exists(path)) { - return ""; - } - - QString stylesheet; - - // read the whole file - { - QFile file(path); - if (!file.open(QFile::ReadOnly | QFile::Text)) { - return ""; - } - - stylesheet = QTextStream(&file).readAll(); - } - - // replace url() in the file - // TODO - - return stylesheet; -} - -QTranslator IExtension::createTranslator(std::filesystem::path const& prefix, - QString const& language) -{ - const QFileInfo fileInfo(prefix); - - QTranslator translator; - - if (fileInfo.exists()) { - translator.load(fileInfo.fileName() + "_" + language, fileInfo.absolutePath()); - } - - return translator -} - IExtension::IExtension(std::filesystem::path path, ExtensionMetaData metadata) : m_Path{std::move(path)}, m_MetaData{std::move(metadata)} -{ - m_StyleSheet = createStyleSheet(m_MetaData.styleSheetFilePath()); -} - -QTranslator IExtension::translator(QString const& language) const -{ - return createTranslator(m_MetaData.translationsFilePrefix(), language); -} +{} std::vector IExtension::plugins() const { diff --git a/src/extension.h b/src/extension.h index b70ea749..bac8dadb 100644 --- a/src/extension.h +++ b/src/extension.h @@ -8,6 +8,9 @@ #include #include +#include "dllimport.h" +#include "theme.h" +#include "translation.h" #include "versioninfo.h" namespace MOBase @@ -22,7 +25,7 @@ class GameRequirement class ExtensionRequirement {}; -class ExtensionMetaData +class QDLLEXPORT ExtensionMetaData { public: ExtensionMetaData(const IExtension* extension, QJsonObject const& jsonData); @@ -72,7 +75,7 @@ class ExtensionMetaData private: // these functions are only for IExtension // - friend class Extension; + friend class IExtension; // retrieve the prefix translations, if there is one, e.g., translations/foo // @@ -98,24 +101,9 @@ class ExtensionMetaData ExtensionRequirement m_ExtensionRequirement; }; -class IExtension +class QDLLEXPORT IExtension { public: - /** - * @brief Retrieve the plugins from this extension. If the plugins have not been - * loaded yet, the plugins are loaded. - * - * @return the list of plugins from this extension. - */ - std::vector plugins() const; - - /** - * @brief Load the plugins from this extension. - * - * @return the list of plugins from this extension. - */ - std::vector loadPlugins(); - /** * @brief Retrieve the path to this extension folder. * @@ -131,18 +119,24 @@ class IExtension const auto& metadata() const { return m_MetaData; } /** - * @brief Retrieve the translator of this extension for the given language. - * - * @param language Language to create a translator for, e.g., es, fr, fr_FR, etc. + * @brief Retrieve the plugins from this extension. If the plugins have not been + * loaded yet, the plugins are loaded. * - * @return a translator for the given language, or an empty one if none exists. + * @return the list of plugins from this extension. */ - QTranslator translator(QString const& language) const; + std::vector plugins() const; /** - * @brief Retrieve the stylesheet of this extension. + * @brief Load the plugins from this extension. + * + * @return the list of plugins from this extension. */ - const QString& stylesheet() const { return m_StyleSheet; } + std::vector loadPlugins(); + + const auto& themes() const { return m_Themes; } + const auto& themeAdditions() const { return m_ThemeAdditions; } + const auto& translations() const { return m_Translations; } + const auto& translationAdditions() const { return m_TranslationAdditions; } virtual ~IExtension() {} @@ -153,16 +147,15 @@ class IExtension // virtual std::vector fetchPlugins() const = 0; -private: - static QString createStyleSheet(std::filesystem::path const& path); - static QString createTranslator(std::filesystem::path const& prefix, - QString const& language); - private: std::filesystem::path m_Path; ExtensionMetaData m_MetaData; - QString m_StyleSheet; + // theme and translations + std::vector> m_Themes; + std::vector> m_ThemeAdditions; + std::vector> m_Translations; + std::vector> m_TranslationAdditions; mutable bool m_Loaded{false}; mutable std::vector m_Plugins; diff --git a/src/theme.cpp b/src/theme.cpp new file mode 100644 index 00000000..4431ebce --- /dev/null +++ b/src/theme.cpp @@ -0,0 +1,21 @@ +#include "theme.h" + +#include "utility.h" + +namespace MOBase +{ + +ThemeAddition::ThemeAddition(std::string_view baseIdentifier, + std::filesystem::path stylesheet) + : baseThemeExpr_{QRegularExpression::fromWildcard( + ToQString(baseIdentifier), Qt::CaseInsensitive, + QRegularExpression::DefaultWildcardConversion)}, + stylesheet_{std::move(stylesheet)} +{} + +bool ThemeAddition::isAdditionFor(Theme const& theme) const +{ + return baseThemeExpr_.match(ToQString(theme.identifier())).hasMatch(); +} + +} // namespace MOBase diff --git a/src/theme.h b/src/theme.h new file mode 100644 index 00000000..624f1a01 --- /dev/null +++ b/src/theme.h @@ -0,0 +1,65 @@ +#ifndef UIBASE_THEME_H +#define UIBASE_THEME_H + +#include +#include + +#include + +#include "dllimport.h" + +namespace MOBase +{ + +// class representing a base theme for MO2, e.g., VS Dark or Skyrim +// +class QDLLEXPORT Theme +{ + std::string identifier_, name_; + std::filesystem::path stylesheet_; + +public: + Theme(std::string_view identifier, std::string_view name, + std::filesystem::path stylesheet) + : identifier_{identifier}, name_{name}, stylesheet_{std::move(stylesheet)} + {} + + // retrieve the identifier of the theme + // + const auto& identifier() const { return identifier_; } + + // retrieve the name of the theme + // + const auto& name() const { return name_; } + + // retrieve the path to the stylesheet of the theme + // + const auto& stylesheet() const { return stylesheet_; } +}; + +// class representing additions for a base theme +// +class QDLLEXPORT ThemeAddition +{ + QRegularExpression baseThemeExpr_; + std::filesystem::path stylesheet_; + +public: + ThemeAddition(std::filesystem::path stylesheet) + : ThemeAddition{"*", std::move(stylesheet)} + {} + + ThemeAddition(std::string_view baseIdentifier, std::filesystem::path stylesheet); + + // retrieve the identifier of the base theme, if there is one + // + bool isAdditionFor(Theme const& theme) const; + + // retrieve the path to the stylesheet for this extension + // + const auto& stylesheet() const { return stylesheet_; } +}; + +} // namespace MOBase + +#endif diff --git a/src/translation.h b/src/translation.h new file mode 100644 index 00000000..b1dd66f9 --- /dev/null +++ b/src/translation.h @@ -0,0 +1,62 @@ +#ifndef UIBASE_TRANSLATION_H +#define UIBASE_TRANSLATION_H + +#include +#include + +#include "dllimport.h" + +namespace MOBase +{ + +// class representing a base translation for MO2 +// +class QDLLEXPORT Translation +{ + std::string identifier_, language_; + std::vector qm_files_; + +public: + Translation(std::string_view identifier, std::string_view language, + std::vector qm_files) + : identifier_{identifier}, language_{language}, qm_files_{std::move(qm_files)} + {} + + // retrieve the identifier of the translation, e.g., en or fr_FR + // + const auto& identifier() const { return identifier_; } + + // retrieve the language of this translation + // + const auto& language() const { return language_; } + + // retrieve the path to the QM files including with this translation + // + const auto& files() const { return qm_files_; } +}; + +// class representing the extension of a base translation +// +class QDLLEXPORT TranslationAddition +{ + std::string baseIdentifier_; + std::vector qm_files_; + +public: + TranslationAddition(std::string_view baseIdentifier, + std::vector qm_files) + : baseIdentifier_{baseIdentifier}, qm_files_{std::move(qm_files)} + {} + + // retrieve the identifier of the base translation, if there is one + // + const auto& baseIdentifier() const { return baseIdentifier_; } + + // retrieve the path to the stylesheet for this extension + // + const auto& files() const { return qm_files_; } +}; + +} // namespace MOBase + +#endif diff --git a/src/utility.cpp b/src/utility.cpp index 7f94001f..f731a0d2 100644 --- a/src/utility.cpp +++ b/src/utility.cpp @@ -743,12 +743,22 @@ QString ToQString(const std::string& source) return QString::fromStdString(source); } +QString ToQString(std::string_view source) +{ + return QString::fromUtf8(source.data(), static_cast(source.size())); +} + QString ToQString(const std::wstring& source) { // return QString::fromWCharArray(source.c_str()); return QString::fromStdWString(source); } +QString ToQString(std::wstring_view source) +{ + return QString::fromWCharArray(source.data(), static_cast(source.size())); +} + QString ToString(const SYSTEMTIME& time) { char dateBuffer[100]; diff --git a/src/utility.h b/src/utility.h index 0c0ace25..196141f8 100644 --- a/src/utility.h +++ b/src/utility.h @@ -383,11 +383,13 @@ QDLLEXPORT std::string ToString(const QString& source, bool utf8 = true); * @brief convert std::string to QString (assuming the string to be utf-8 encoded) **/ QDLLEXPORT QString ToQString(const std::string& source); +QDLLEXPORT QString ToQString(std::string_view source); /** * @brief convert std::wstring to QString (assuming the wstring to be utf-16 encoded) **/ QDLLEXPORT QString ToQString(const std::wstring& source); +QDLLEXPORT QString ToQString(std::wstring_view source); /** * @brief convert a systemtime object to a string containing date and time in local From ae93baa81a2cbb5b0088f4d9e18ea659a18bbace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Mon, 23 May 2022 19:29:14 +0200 Subject: [PATCH 3/9] Implement loading theme and translation extensions. --- src/extension.cpp | 220 +++++++++++++++++++++++++++++++++++++++++-- src/extension.h | 235 ++++++++++++++++++++++++++++++---------------- 2 files changed, 368 insertions(+), 87 deletions(-) diff --git a/src/extension.cpp b/src/extension.cpp index 1431b9a3..4e8b0d39 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -1,28 +1,232 @@ #include "extension.h" +#include #include #include +#include + +#include "log.h" namespace MOBase { +ExtensionMetaData::ExtensionMetaData(QJsonObject const& jsonData) : m_JsonData{jsonData} +{ + // read basic fields + m_Identifier = jsonData["identifier"].toString(); + m_Type = parseType(jsonData["type"].toString()); + m_Name = jsonData["name"].toString(); + m_Description = jsonData["description"].toString(); + m_Version.parse(jsonData["version"].toString("0.0.0")); + + // TODO: name of the key + // translation context + m_TranslationContext = jsonData["translationContext"].toString(""); +} + +bool ExtensionMetaData::isValid() const +{ + return !m_Identifier.isEmpty() && !m_Name.isEmpty() && m_Version.isValid() && + m_Type != ExtensionType::INVALID; +} + +ExtensionType ExtensionMetaData::parseType(QString const& value) const +{ + std::map stringToTypes{ + {"theme", ExtensionType::THEME}, + {"translation", ExtensionType::TRANSLATION}, + {"plugin", ExtensionType::PLUGIN}, + {"game", ExtensionType::GAME}}; + + auto type = ExtensionType::INVALID; + for (auto& [k, v] : stringToTypes) { + if (k.compare(value, Qt::CaseInsensitive) == 0) { + type = v; + break; + } + } + + return type; +} + +QString ExtensionMetaData::localized(QString const& value) const +{ + // no translation context + if (m_TranslationContext.isEmpty()) { + return value; + } + + const auto result = QCoreApplication::translate(m_TranslationContext.toUtf8().data(), + value.toUtf8().data()); + return result.isEmpty() ? value : result; +} + IExtension::IExtension(std::filesystem::path path, ExtensionMetaData metadata) : m_Path{std::move(path)}, m_MetaData{std::move(metadata)} {} -std::vector IExtension::plugins() const +std::unique_ptr +ExtensionFactory::loadExtension(std::filesystem::path directory) +{ + const auto metadataPath = directory / METADATA_FILENAME; + + if (!exists(metadataPath)) { + log::warn("missing extension metadata in '{}'", directory.native()); + return nullptr; + } + + // load the meta data + QJsonParseError jsonError; + QJsonDocument jsonMetaData; + { + QFile file(metadataPath); + if (!file.open(QFile::ReadOnly)) { + return {}; + } + + const auto jsonContent = file.readAll(); + jsonMetaData = QJsonDocument::fromJson(jsonContent, &jsonError); + } + + if (jsonMetaData.isNull()) { + log::warn("failed to read metadata from '{}': {}", metadataPath.native(), + jsonError.errorString()); + return nullptr; + } + + return loadExtension(std::move(directory), ExtensionMetaData(jsonMetaData.object())); +} + +std::unique_ptr +ExtensionFactory::loadExtension(std::filesystem::path directory, + ExtensionMetaData metadata) +{ + if (!metadata.isValid()) { + log::warn("failed to load extension from '{}': invalid metadata", + directory.native()); + return nullptr; + } + + switch (metadata.type()) { + case ExtensionType::THEME: + return ThemeExtension::loadExtension(std::move(directory), std::move(metadata)); + case ExtensionType::TRANSLATION: + return TranslationExtension::loadExtension(std::move(directory), + std::move(metadata)); + case ExtensionType::PLUGIN: + case ExtensionType::GAME: + case ExtensionType::INVALID: + default: + log::warn("failed to load extension from '{}': invalid type", directory.native()); + return nullptr; + } +} + +ThemeExtension::ThemeExtension(std::filesystem::path path, ExtensionMetaData metadata, + std::vector> themes) + : IExtension{std::move(path), std::move(metadata)}, m_Themes{std::move(themes)} +{} + +std::unique_ptr +ThemeExtension::loadExtension(std::filesystem::path path, ExtensionMetaData metadata) { - if (!m_Loaded) { - m_Plugins = fetchPlugins(); - m_Loaded = true; + std::vector> themes; + const auto& jsonThemes = metadata.json()["themes"].toObject(); + for (auto it = jsonThemes.begin(); it != jsonThemes.end(); ++it) { + const auto theme = parseTheme(path, it.key(), it.value().toObject()); + if (theme) { + themes.push_back(theme); + } else { + log::warn("failed to parse theme '{}' from '{}'", it.key(), path.native()); + } + } + + if (themes.empty()) { + log::error("failed to parse themes from '{}'", path.native()); + return nullptr; } - return m_Plugins; + + return std::unique_ptr{ + new ThemeExtension(path, metadata, std::move(themes))}; } -std::vector IExtension::loadPlugins() +std::shared_ptr +ThemeExtension::parseTheme(std::filesystem::path const& extensionFolder, + const QString& identifier, const QJsonObject& jsonTheme) { - // loadPlugins() is just exposed to pre-load plugins - return plugins(); + const auto name = jsonTheme["name"].toString(); + const auto filepath = + extensionFolder / jsonTheme["path"].toString().toUtf8().toStdString(); + + if (name.isEmpty() || !is_regular_file(filepath)) { + return nullptr; + } + + return std::make_shared(identifier.toStdString(), name.toStdString(), + filepath); +} + +TranslationExtension::TranslationExtension( + std::filesystem::path path, ExtensionMetaData metadata, + std::vector> translations) + : IExtension{std::move(path), std::move(metadata)}, + m_Translations(std::move(translations)) +{} + +std::unique_ptr +TranslationExtension::loadExtension(std::filesystem::path path, + ExtensionMetaData metadata) +{ + std::vector> translations; + const auto& jsonTranslations = metadata.json()["translations"].toObject(); + for (auto it = jsonTranslations.begin(); it != jsonTranslations.end(); ++it) { + const auto theme = parseTranslation(path, it.key(), it.value().toObject()); + if (theme) { + translations.push_back(theme); + } else { + log::warn("failed to parse translation '{}' from '{}'", it.key(), path.native()); + } + } + + if (translations.empty()) { + log::error("failed to parse translations from '{}'", path.native()); + return nullptr; + } + + return std::unique_ptr{ + new TranslationExtension(path, metadata, std::move(translations))}; +} + +#pragma optimize("", off) +std::shared_ptr +TranslationExtension::parseTranslation(std::filesystem::path const& extensionFolder, + const QString& identifier, + const QJsonObject& jsonTranslation) +{ + const auto name = jsonTranslation["name"].toString(); + const auto jsonGlobFiles = jsonTranslation["files"].toVariant().toStringList(); + + std::vector qm_files; + for (const auto& globFile : jsonGlobFiles) { + // use a QFileInfo to extract the name (glob) and the directory - we currently do + // not handle recursive glob (**) + QFileInfo globFileInfo(extensionFolder, globFile); + + QDirIterator dirIterator(globFileInfo.absolutePath(), {globFileInfo.fileName()}, + QDir::Files); + while (dirIterator.hasNext()) { + dirIterator.next(); + qm_files.push_back(dirIterator.fileInfo().filesystemAbsoluteFilePath()); + } + } + + if (name.isEmpty() || qm_files.empty()) { + return nullptr; + } + + return std::make_shared(identifier.toStdString(), name.toStdString(), + std::move(qm_files)); } +#pragma optimize("", on) } // namespace MOBase diff --git a/src/extension.h b/src/extension.h index bac8dadb..8e91aa81 100644 --- a/src/extension.h +++ b/src/extension.h @@ -15,7 +15,6 @@ namespace MOBase { - class IExtension; class VersionRequirement @@ -25,71 +24,77 @@ class GameRequirement class ExtensionRequirement {}; +enum class ExtensionType +{ + INVALID, + THEME, + TRANSLATION, + PLUGIN, + GAME +}; + class QDLLEXPORT ExtensionMetaData { public: - ExtensionMetaData(const IExtension* extension, QJsonObject const& jsonData); + ExtensionMetaData(QJsonObject const& jsonData); + + // check if that metadata object is valid + // + bool isValid() const; - /** - * @return the name of the extension. - */ - const auto& name() const { return localized(m_Name); } + // retrieve the identifier of the extension + // + const auto& identifier() const { return m_Identifier; } + + // retrieve the name of the extension + // + auto name() const { return localized(m_Name); } + + // retrieve the type of the extension + // + auto type() const { return m_Type; } - /** - * @return the description of the extension. - */ - const auto& description() const { return localized(m_Description); } + // retrieve the description of the extension. + // + auto description() const { return localized(m_Description); } - /** - * @return the version of the extension. - */ + // retrieve the version of the extension. + // const auto& version() const { return m_Version; } - /** - * @return the version requirement of the extension. - */ + // retrieve the version requirement of the extension + // const auto& versionRequirement() const { return m_VersionRequirement; } - /** - * @return the game requirement of the extension. - */ + // retrieve the game requirement of the extension + // const auto& gameRequirement() const { return m_GameRequirement; } - /** - * @return the extension requirement of the extension. - */ + // retrieve the extension requirement of the extension + // const auto& extensionRequirement() const { return m_ExtensionRequirement; } + // retrieve the raw JSON metadata, this is mostly useful for specific extension type + // to extract custom parts + // + const auto& json() const { return m_JsonData; } + private: - QString localized(QString const& value) const - { - const auto result = QCoreApplication::translate( - m_TranslationContext.toUtf8().data(), value.toUtf8().data()); - return result.isEmpty() ? value : result; - } + QString localized(QString const& value) const; private: constexpr static const char* DEFAULT_TRANSLATIONS_FOLDER = "translations"; constexpr static const char* DEFAULT_STYLESHEET_PATH = "stylesheets"; -private: - // these functions are only for IExtension - // - friend class IExtension; - - // retrieve the prefix translations, if there is one, e.g., translations/foo - // - const auto& translationsFilePrefix() const { return m_TranslationFilesPrefix; } - - // retrieve the path to the stylesheet, if there is one - // - const auto& styleSheetFilePath() const { return m_StyleSheetFilePath; } + ExtensionType parseType(QString const& value) const; private: - const IExtension* m_Extension; + QJsonObject m_JsonData; QString m_TranslationContext; + QString m_Identifier; QString m_Name; + ExtensionType m_Type; QString m_Description; VersionInfo m_Version; @@ -104,61 +109,133 @@ class QDLLEXPORT ExtensionMetaData class QDLLEXPORT IExtension { public: - /** - * @brief Retrieve the path to this extension folder. - * - * @return the directory of this extension. - */ + // retrieve the folder containing the extension + // std::filesystem::path directory() const { return m_Path; } - /** - * @brief Retrieve the metadata of this extension. - * - * @return the metadata of this extension. - */ + // retrieve the metadata of this extension + // const auto& metadata() const { return m_MetaData; } - /** - * @brief Retrieve the plugins from this extension. If the plugins have not been - * loaded yet, the plugins are loaded. - * - * @return the list of plugins from this extension. - */ - std::vector plugins() const; - - /** - * @brief Load the plugins from this extension. - * - * @return the list of plugins from this extension. - */ - std::vector loadPlugins(); - - const auto& themes() const { return m_Themes; } - const auto& themeAdditions() const { return m_ThemeAdditions; } - const auto& translations() const { return m_Translations; } - const auto& translationAdditions() const { return m_TranslationAdditions; } - virtual ~IExtension() {} protected: IExtension(std::filesystem::path path, ExtensionMetaData metadata); - // retrieve the plugins for this extension - // - virtual std::vector fetchPlugins() const = 0; - private: std::filesystem::path m_Path; ExtensionMetaData m_MetaData; +}; + +// factory for extensions +// +class QDLLEXPORT ExtensionFactory +{ +public: + // load an extension from the given directory, return a null-pointer if the extension + // could not be load + // + static std::unique_ptr loadExtension(std::filesystem::path directory); - // theme and translations +private: + // name of the metadata file + // + static constexpr const char* METADATA_FILENAME = "mo2-metadata.json"; + + // load an extension from the given directory + // + static std::unique_ptr loadExtension(std::filesystem::path directory, + ExtensionMetaData metadata); +}; + +// theme extension that provides one or more base themes for MO2 +// +class QDLLEXPORT ThemeExtension : public IExtension +{ +public: + // retrieve the list of themes provided by this extension + // + const auto& themes() const { return m_Themes; } + +private: + ThemeExtension(std::filesystem::path path, ExtensionMetaData metadata, + std::vector> themes); + + friend class ExtensionFactory; + static std::unique_ptr loadExtension(std::filesystem::path path, + ExtensionMetaData metadata); + + static std::shared_ptr + parseTheme(std::filesystem::path const& extensionFolder, const QString& identifier, + const QJsonObject& jsonTheme); + +private: std::vector> m_Themes; - std::vector> m_ThemeAdditions; +}; + +// translation extension that provides one or more base translations for mo@ +// +class QDLLEXPORT TranslationExtension : public IExtension +{ +public: + // retrieve the list of translations provided by this extension + // + const auto& translations() const { return m_Translations; } + +private: + TranslationExtension(std::filesystem::path path, ExtensionMetaData metadata, + std::vector> translations); + + friend class ExtensionFactory; + static std::unique_ptr + loadExtension(std::filesystem::path path, ExtensionMetaData metadata); + + static std::shared_ptr + parseTranslation(std::filesystem::path const& extensionFolder, + const QString& identifier, const QJsonObject& jsonTranslation); + +private: std::vector> m_Translations; +}; + +// plugin extension that provides one or more plugins for MO2, alongside theme or +// translation additions +// +class QDLLEXPORT PluginExtension : public IExtension +{ +public: + using IExtension::IExtension; + + // auto-detect plugins + // + bool autodetect() const { return m_AutoDetect; } + + // list of specified plugins + // + const auto& plugins() const { return m_Plugins; } + + const auto& themeAdditions() const { return m_ThemeAdditions; } + const auto& translationAdditions() const { return m_TranslationAdditions; } + +private: + // auto-detect plugins + bool m_AutoDetect; + + // forced plugins + std::vector m_Plugins; + + // theme and translations additions + std::vector> m_ThemeAdditions; std::vector> m_TranslationAdditions; +}; - mutable bool m_Loaded{false}; - mutable std::vector m_Plugins; +// game extension that provides a game plugin, alongside other plugins, translation or +// theme (additions) +// +class QDLLEXPORT GameExtension : public PluginExtension +{ +public: + using PluginExtension::PluginExtension; }; } // namespace MOBase From 8b409194f167a82535100bcc0869a12c11b96767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Tue, 24 May 2022 17:07:56 +0200 Subject: [PATCH 4/9] Update for extensions. --- src/CMakeLists.txt | 2 +- src/extension.cpp | 35 +++++++++++++++++-- src/extension.h | 19 +++++++++-- src/ipluginloader.h | 65 +++++++++++++++++++++++++++++++++++ src/ipluginproxy.h | 83 --------------------------------------------- src/log.cpp | 5 +++ src/log.h | 6 ++++ 7 files changed, 127 insertions(+), 88 deletions(-) create mode 100644 src/ipluginloader.h delete mode 100644 src/ipluginproxy.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 168d9cb4..6b4c386a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,7 @@ mo2_add_filter(NAME src/extensions GROUPS extension theme translation + ipluginloader ) mo2_add_filter(NAME src/interfaces GROUPS @@ -30,7 +31,6 @@ mo2_add_filter(NAME src/interfaces GROUPS ipluginlist ipluginmodpage ipluginpreview - ipluginproxy iplugintool iprofile isavegame diff --git a/src/extension.cpp b/src/extension.cpp index 4e8b0d39..452d860d 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -114,7 +114,9 @@ ExtensionFactory::loadExtension(std::filesystem::path directory, return TranslationExtension::loadExtension(std::move(directory), std::move(metadata)); case ExtensionType::PLUGIN: + return PluginExtension::loadExtension(std::move(directory), std::move(metadata)); case ExtensionType::GAME: + return GameExtension::loadExtension(std::move(directory), std::move(metadata)); case ExtensionType::INVALID: default: log::warn("failed to load extension from '{}': invalid type", directory.native()); @@ -197,7 +199,6 @@ TranslationExtension::loadExtension(std::filesystem::path path, new TranslationExtension(path, metadata, std::move(translations))}; } -#pragma optimize("", off) std::shared_ptr TranslationExtension::parseTranslation(std::filesystem::path const& extensionFolder, const QString& identifier, @@ -227,6 +228,36 @@ TranslationExtension::parseTranslation(std::filesystem::path const& extensionFol return std::make_shared(identifier.toStdString(), name.toStdString(), std::move(qm_files)); } -#pragma optimize("", on) + +PluginExtension::PluginExtension( + std::filesystem::path path, ExtensionMetaData metadata, bool autodetect, + std::vector plugins, + std::vector> themeAdditions, + std::vector> translationAdditions) + : IExtension(std::move(path), std::move(metadata)), m_AutoDetect{autodetect}, + m_Plugins{std::move(plugins)}, m_ThemeAdditions{std::move(themeAdditions)}, + m_TranslationAdditions{std::move(translationAdditions)} +{} + +std::unique_ptr +PluginExtension::loadExtension(std::filesystem::path path, ExtensionMetaData metadata) +{ + // TODO + return std::unique_ptr( + new PluginExtension(std::move(path), std::move(metadata), true, {}, {}, {})); +} + +GameExtension::GameExtension(PluginExtension&& pluginExtension) + : PluginExtension(std::move(pluginExtension)) +{} + +std::unique_ptr GameExtension::loadExtension(std::filesystem::path path, + ExtensionMetaData metadata) +{ + auto extension = PluginExtension::loadExtension(std::move(path), std::move(metadata)); + return extension + ? std::unique_ptr(new GameExtension(std::move(*extension))) + : nullptr; +} } // namespace MOBase diff --git a/src/extension.h b/src/extension.h index 8e91aa81..acf10beb 100644 --- a/src/extension.h +++ b/src/extension.h @@ -217,6 +217,17 @@ class QDLLEXPORT PluginExtension : public IExtension const auto& themeAdditions() const { return m_ThemeAdditions; } const auto& translationAdditions() const { return m_TranslationAdditions; } +protected: + PluginExtension( + std::filesystem::path path, ExtensionMetaData metadata, bool autodetect, + std::vector plugins, + std::vector> themeAdditions, + std::vector> translationAdditions); + + friend class ExtensionFactory; + static std::unique_ptr loadExtension(std::filesystem::path path, + ExtensionMetaData metadata); + private: // auto-detect plugins bool m_AutoDetect; @@ -234,8 +245,12 @@ class QDLLEXPORT PluginExtension : public IExtension // class QDLLEXPORT GameExtension : public PluginExtension { -public: - using PluginExtension::PluginExtension; +private: + GameExtension(PluginExtension&& pluginExtension); + + friend class ExtensionFactory; + static std::unique_ptr loadExtension(std::filesystem::path path, + ExtensionMetaData metadata); }; } // namespace MOBase diff --git a/src/ipluginloader.h b/src/ipluginloader.h new file mode 100644 index 00000000..f18f886a --- /dev/null +++ b/src/ipluginloader.h @@ -0,0 +1,65 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This library 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 3 of the License, or (at your option) any later version. + +This library 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 this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef IPLUGINPROXY_H +#define IPLUGINPROXY_H + +#include +#include +#include + +#include "extension.h" + +namespace MOBase +{ + +class IPluginLoader : public QObject +{ +public: + // initialize the loader, set the error message on failure + // + virtual bool initialize(QString& errorMessage) = 0; + + // extract and load plugins from the given extension + // + // if multiple QObject* corresponds to the same Plugin, they should be returned + // together + // + virtual QList> load(const PluginExtension& extension) = 0; + + // unload plugins from the given extension + // + virtual void unload(const PluginExtension& identifier) = 0; + + // unload all plugins from this loader + // + virtual void unloadAll() = 0; + + virtual ~IPluginLoader() {} + +protected: + IPluginLoader() {} +}; + +} // namespace MOBase + +Q_DECLARE_INTERFACE(MOBase::IPluginLoader, "com.mo2.PluginLoader") + +#endif // IPLUGINPROXY_H diff --git a/src/ipluginproxy.h b/src/ipluginproxy.h deleted file mode 100644 index b0b8d843..00000000 --- a/src/ipluginproxy.h +++ /dev/null @@ -1,83 +0,0 @@ -/* -Mod Organizer shared UI functionality - -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This library 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 3 of the License, or (at your option) any later version. - -This library 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 this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -#ifndef IPLUGINPROXY_H -#define IPLUGINPROXY_H - -#include -#include -#include - -#include "iplugin.h" - -namespace MOBase -{ - -class IPluginProxy : public IPlugin -{ -public: - IPluginProxy() : m_ParentWidget(nullptr) {} - - /** - * @brief List the plugins managed by this proxy in the given - * folder. - * - * @param pluginPath Path containing the plugins. - * - * @return list of plugin identifiers that supported by this proxy. - */ - virtual QStringList pluginList(const QDir& pluginPath) const = 0; - - /** - * @brief Load the plugins corresponding to the given identifier. - * - * @param identifier Identifier of the proxied plugin to load. - * - * @return a list of QObject, one for each plugins in the given identifier. - */ - virtual QList load(const QString& identifier) = 0; - - /** - * @brief Unload the plugins corresponding to the given identifier. - * - * @param identifier Identifier of the proxied plugin to unload. - */ - virtual void unload(const QString& identifier) = 0; - - /** - * @brief Sets the widget that the tool should use as the parent whenever - * it creates a new modal dialog. - * - * @param widget The new parent widget. - */ - void setParentWidget(QWidget* widget) { m_ParentWidget = widget; } - -protected: - QWidget* parentWidget() const { return m_ParentWidget; } - -private: - QWidget* m_ParentWidget; -}; - -} // namespace MOBase - -Q_DECLARE_INTERFACE(MOBase::IPluginProxy, "com.tannin.ModOrganizer.PluginProxy/1.0") - -#endif // IPLUGINPROXY_H diff --git a/src/log.cpp b/src/log.cpp index 6f6f0fb9..2c6387e0 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -412,6 +412,11 @@ std::string converter::convert(const QVariant& v) : v.toString().toStdString())); } +std::string converter::convert(const std::filesystem::path& v) +{ + return v.string(); +} + void doLogImpl(spdlog::logger& lg, Levels lv, const std::string& s) noexcept { try { diff --git a/src/log.h b/src/log.h index a0b21f69..eb6658a4 100644 --- a/src/log.h +++ b/src/log.h @@ -108,6 +108,12 @@ struct QDLLEXPORT converter static std::string convert(const QVariant& v); }; +template <> +struct QDLLEXPORT converter +{ + static std::string convert(const std::filesystem::path& v); +}; + // custom converter for enum and enum class that seems to not work with latest fmt template struct QDLLEXPORT converter>> From 4c58206a1aa0bd301c03fd746b8d417195a0f9c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Tue, 24 May 2022 22:03:49 +0200 Subject: [PATCH 5/9] Implement theme and translation additions for plugin extension. --- src/extension.cpp | 100 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 18 deletions(-) diff --git a/src/extension.cpp b/src/extension.cpp index 452d860d..f1b400db 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -10,6 +10,34 @@ namespace MOBase { +namespace +{ + // retrieve all files matching one of the glob pattern in globPatterns, assuming paths + // (in patterns) are relative to basePath + // + auto globExtensionFiles(std::filesystem::path basePath, + QStringList const& globPatterns) + { + std::vector files; + + for (const auto& globFile : globPatterns) { + // use a QFileInfo to extract the name (glob) and the directory - we currently do + // not handle recursive glob (**) + QFileInfo globFileInfo(basePath, globFile); + + QDirIterator dirIterator(globFileInfo.absolutePath(), {globFileInfo.fileName()}, + QDir::Files); + while (dirIterator.hasNext()) { + dirIterator.next(); + files.push_back(dirIterator.fileInfo().filesystemAbsoluteFilePath()); + } + } + + return files; + } + +} // namespace + ExtensionMetaData::ExtensionMetaData(QJsonObject const& jsonData) : m_JsonData{jsonData} { // read basic fields @@ -182,9 +210,9 @@ TranslationExtension::loadExtension(std::filesystem::path path, std::vector> translations; const auto& jsonTranslations = metadata.json()["translations"].toObject(); for (auto it = jsonTranslations.begin(); it != jsonTranslations.end(); ++it) { - const auto theme = parseTranslation(path, it.key(), it.value().toObject()); - if (theme) { - translations.push_back(theme); + const auto translation = parseTranslation(path, it.key(), it.value().toObject()); + if (translation) { + translations.push_back(translation); } else { log::warn("failed to parse translation '{}' from '{}'", it.key(), path.native()); } @@ -207,19 +235,8 @@ TranslationExtension::parseTranslation(std::filesystem::path const& extensionFol const auto name = jsonTranslation["name"].toString(); const auto jsonGlobFiles = jsonTranslation["files"].toVariant().toStringList(); - std::vector qm_files; - for (const auto& globFile : jsonGlobFiles) { - // use a QFileInfo to extract the name (glob) and the directory - we currently do - // not handle recursive glob (**) - QFileInfo globFileInfo(extensionFolder, globFile); - - QDirIterator dirIterator(globFileInfo.absolutePath(), {globFileInfo.fileName()}, - QDir::Files); - while (dirIterator.hasNext()) { - dirIterator.next(); - qm_files.push_back(dirIterator.fileInfo().filesystemAbsoluteFilePath()); - } - } + std::vector qm_files = + globExtensionFiles(extensionFolder, jsonGlobFiles); if (name.isEmpty() || qm_files.empty()) { return nullptr; @@ -242,9 +259,56 @@ PluginExtension::PluginExtension( std::unique_ptr PluginExtension::loadExtension(std::filesystem::path path, ExtensionMetaData metadata) { - // TODO + // load themes + std::vector> themes; + { + const auto& jsonThemes = metadata.json()["themes"].toObject(); + for (auto it = jsonThemes.begin(); it != jsonThemes.end(); ++it) { + for (auto& file : globExtensionFiles(path, it.value().toVariant().toStringList())) + themes.push_back(std::make_shared(it.key().toStdString(), file)); + } + } + + // load translations + std::vector> translations; + { + const auto& jsonTranslations = metadata.json()["translations"].toObject(); + + // * is a custom entry + if (jsonTranslations.contains("*")) { + std::map> filesPerLanguage; + const auto prefixes = jsonTranslations["*"].toVariant().toStringList(); + + for (auto& prefix : prefixes) { + const auto filePrefix = QFileInfo(prefix).fileName(); + auto files = globExtensionFiles(path, {prefix + "*.qm"}); + for (auto& file : files) { + // extract the identifier from the match, e.g., if the prefix is + // translations/installer_manual_, the filePrefix is installer_manual_, + // and glob will be like translations/installer_manual_fr.qm + const auto identifier = + QFileInfo(file).fileName().replace(".qm", "").replace(filePrefix, ""); + filesPerLanguage[identifier].push_back(file); + } + } + + for (auto& [language, files] : filesPerLanguage) { + translations.push_back(std::make_shared( + language.toStdString(), std::move(files))); + } + + } else { + for (auto it = jsonTranslations.begin(); it != jsonTranslations.end(); ++it) { + translations.push_back(std::make_shared( + it.key().toStdString(), + globExtensionFiles(path, it.value().toVariant().toStringList()))); + } + } + } + return std::unique_ptr( - new PluginExtension(std::move(path), std::move(metadata), true, {}, {}, {})); + new PluginExtension(std::move(path), std::move(metadata), true, {}, + std::move(themes), std::move(translations))); } GameExtension::GameExtension(PluginExtension&& pluginExtension) From a0dfa5461185ddcb2dbb40078e75b87df72d5811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Thu, 26 May 2022 19:39:11 +0200 Subject: [PATCH 6/9] Allow specifying plugins in extensions. --- src/extension.cpp | 32 ++++++++++++++++++++++++++------ src/extension.h | 4 ++-- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/extension.cpp b/src/extension.cpp index f1b400db..ef99e592 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -248,7 +248,7 @@ TranslationExtension::parseTranslation(std::filesystem::path const& extensionFol PluginExtension::PluginExtension( std::filesystem::path path, ExtensionMetaData metadata, bool autodetect, - std::vector plugins, + std::map plugins, std::vector> themeAdditions, std::vector> translationAdditions) : IExtension(std::move(path), std::move(metadata)), m_AutoDetect{autodetect}, @@ -259,10 +259,30 @@ PluginExtension::PluginExtension( std::unique_ptr PluginExtension::loadExtension(std::filesystem::path path, ExtensionMetaData metadata) { + // load plugins + std::optional autodetect; + std::map plugins; + { + auto jsonPlugins = metadata.json()["plugins"].toObject(); + if (jsonPlugins.contains("autodetect")) { + autodetect = jsonPlugins["autodetect"].toBool(); + } + jsonPlugins.remove("autodetect"); + + for (auto it = jsonPlugins.begin(); it != jsonPlugins.end(); ++it) { + plugins[it.key().toStdString()] = + QFileInfo(path, it.value().toString()).filesystemAbsoluteFilePath(); + } + + if (!autodetect.has_value()) { + autodetect = plugins.empty(); + } + } + // load themes std::vector> themes; { - const auto& jsonThemes = metadata.json()["themes"].toObject(); + auto jsonThemes = metadata.json()["themes"].toObject(); for (auto it = jsonThemes.begin(); it != jsonThemes.end(); ++it) { for (auto& file : globExtensionFiles(path, it.value().toVariant().toStringList())) themes.push_back(std::make_shared(it.key().toStdString(), file)); @@ -272,7 +292,7 @@ PluginExtension::loadExtension(std::filesystem::path path, ExtensionMetaData met // load translations std::vector> translations; { - const auto& jsonTranslations = metadata.json()["translations"].toObject(); + auto jsonTranslations = metadata.json()["translations"].toObject(); // * is a custom entry if (jsonTranslations.contains("*")) { @@ -306,9 +326,9 @@ PluginExtension::loadExtension(std::filesystem::path path, ExtensionMetaData met } } - return std::unique_ptr( - new PluginExtension(std::move(path), std::move(metadata), true, {}, - std::move(themes), std::move(translations))); + return std::unique_ptr(new PluginExtension( + std::move(path), std::move(metadata), *autodetect, std::move(plugins), + std::move(themes), std::move(translations))); } GameExtension::GameExtension(PluginExtension&& pluginExtension) diff --git a/src/extension.h b/src/extension.h index acf10beb..65557155 100644 --- a/src/extension.h +++ b/src/extension.h @@ -220,7 +220,7 @@ class QDLLEXPORT PluginExtension : public IExtension protected: PluginExtension( std::filesystem::path path, ExtensionMetaData metadata, bool autodetect, - std::vector plugins, + std::map plugins, std::vector> themeAdditions, std::vector> translationAdditions); @@ -233,7 +233,7 @@ class QDLLEXPORT PluginExtension : public IExtension bool m_AutoDetect; // forced plugins - std::vector m_Plugins; + std::map m_Plugins; // theme and translations additions std::vector> m_ThemeAdditions; From 10c9b580f9453f2c9dfd40e57992ff7ead926540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sun, 16 Jul 2023 13:57:44 +0200 Subject: [PATCH 7/9] Add extension list interface. Start working on requirements. --- src/CMakeLists.txt | 2 ++ src/extension.cpp | 11 +++++-- src/extension.h | 34 ++++--------------- src/iextensionlist.h | 51 +++++++++++++++++++++++++++++ src/imoinfo.h | 6 ++++ src/report.cpp | 5 +-- src/requirements.cpp | 47 +++++++++++++++++++++++++++ src/requirements.h | 72 +++++++++++++++++++++++++++++++++++++++++ src/tutorialcontrol.cpp | 13 ++++---- src/utility.cpp | 4 +-- 10 files changed, 205 insertions(+), 40 deletions(-) create mode 100644 src/iextensionlist.h create mode 100644 src/requirements.cpp create mode 100644 src/requirements.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6b4c386a..b54e43cc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,8 +12,10 @@ mo2_install_target(uibase) mo2_add_filter(NAME src/extensions GROUPS extension + iextensionlist theme translation + requirements ipluginloader ) diff --git a/src/extension.cpp b/src/extension.cpp index ef99e592..d81db8e9 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -7,6 +7,10 @@ #include "log.h" +// name of the metadata file +// +static constexpr const char* METADATA_FILENAME = "metadata.json"; + namespace MOBase { @@ -90,7 +94,8 @@ QString ExtensionMetaData::localized(QString const& value) const } IExtension::IExtension(std::filesystem::path path, ExtensionMetaData metadata) - : m_Path{std::move(path)}, m_MetaData{std::move(metadata)} + : m_Path{std::move(path)}, m_MetaData{std::move(metadata)}, + m_Requirements{ExtensionRequirementFactory::parseRequirements(m_MetaData)} {} std::unique_ptr @@ -301,13 +306,13 @@ PluginExtension::loadExtension(std::filesystem::path path, ExtensionMetaData met for (auto& prefix : prefixes) { const auto filePrefix = QFileInfo(prefix).fileName(); - auto files = globExtensionFiles(path, {prefix + "*.qm"}); + const auto files = globExtensionFiles(path, {prefix + "*.qm"}); for (auto& file : files) { // extract the identifier from the match, e.g., if the prefix is // translations/installer_manual_, the filePrefix is installer_manual_, // and glob will be like translations/installer_manual_fr.qm const auto identifier = - QFileInfo(file).fileName().replace(".qm", "").replace(filePrefix, ""); + QFileInfo(file).baseName().replace(filePrefix, "", Qt::CaseInsensitive); filesPerLanguage[identifier].push_back(file); } } diff --git a/src/extension.h b/src/extension.h index 65557155..b94a8fe6 100644 --- a/src/extension.h +++ b/src/extension.h @@ -9,6 +9,8 @@ #include #include "dllimport.h" +#include "iplugingame.h" +#include "requirements.h" #include "theme.h" #include "translation.h" #include "versioninfo.h" @@ -17,13 +19,6 @@ namespace MOBase { class IExtension; -class VersionRequirement -{}; -class GameRequirement -{}; -class ExtensionRequirement -{}; - enum class ExtensionType { INVALID, @@ -62,18 +57,6 @@ class QDLLEXPORT ExtensionMetaData // const auto& version() const { return m_Version; } - // retrieve the version requirement of the extension - // - const auto& versionRequirement() const { return m_VersionRequirement; } - - // retrieve the game requirement of the extension - // - const auto& gameRequirement() const { return m_GameRequirement; } - - // retrieve the extension requirement of the extension - // - const auto& extensionRequirement() const { return m_ExtensionRequirement; } - // retrieve the raw JSON metadata, this is mostly useful for specific extension type // to extract custom parts // @@ -100,10 +83,6 @@ class QDLLEXPORT ExtensionMetaData std::filesystem::path m_TranslationFilesPrefix; std::filesystem::path m_StyleSheetFilePath; - - VersionRequirement m_VersionRequirement; - GameRequirement m_GameRequirement; - ExtensionRequirement m_ExtensionRequirement; }; class QDLLEXPORT IExtension @@ -117,6 +96,10 @@ class QDLLEXPORT IExtension // const auto& metadata() const { return m_MetaData; } + // retrieve the requirements of the extension + // + const auto& requirements() const { return m_Requirements; } + virtual ~IExtension() {} protected: @@ -125,6 +108,7 @@ class QDLLEXPORT IExtension private: std::filesystem::path m_Path; ExtensionMetaData m_MetaData; + std::vector m_Requirements; }; // factory for extensions @@ -138,10 +122,6 @@ class QDLLEXPORT ExtensionFactory static std::unique_ptr loadExtension(std::filesystem::path directory); private: - // name of the metadata file - // - static constexpr const char* METADATA_FILENAME = "mo2-metadata.json"; - // load an extension from the given directory // static std::unique_ptr loadExtension(std::filesystem::path directory, diff --git a/src/iextensionlist.h b/src/iextensionlist.h new file mode 100644 index 00000000..952ba16a --- /dev/null +++ b/src/iextensionlist.h @@ -0,0 +1,51 @@ +#ifndef UIBASE_IEXTENSIONLIST_H +#define UIBASE_IEXTENSIONLIST_H + +#include + +#include + +namespace MOBase +{ + +class IExtension; + +// interface to the list of extensions in MO2 +// +class IExtensionList +{ +public: + // check if the extension with the given identifier is installed or not + // + virtual bool installed(const QString& identifier) const = 0; + + // check if the extension with the given identifier is installed and enabled + // + virtual bool enabled(const QString& extension) const = 0; + + // check if the extension is enabled or not + // + virtual bool enabled(const IExtension& extension) const = 0; + + // retrieve the extension with the given identifier, throw std::out_of_range if no + // such extension is installed + // + virtual const IExtension& get(QString const& identifier) const = 0; + + // retrieve the installed extension at the given index, throw std::out_of_range if the + // index is out of range + // + virtual const IExtension& at(std::size_t const& index) const = 0; + virtual const IExtension& operator[](std::size_t const& index) const = 0; + + // retrieve the number of installed extensions + // + virtual std::size_t size() const = 0; + +public: + virtual ~IExtensionList() {} +}; + +} // namespace MOBase + +#endif diff --git a/src/imoinfo.h b/src/imoinfo.h index fb00047a..57fac3ac 100644 --- a/src/imoinfo.h +++ b/src/imoinfo.h @@ -37,6 +37,7 @@ namespace MOBase { class IFileTree; +class IExtensionList; class IModInterface; class IModRepositoryBridge; class IDownloadManager; @@ -303,6 +304,11 @@ class QDLLEXPORT IOrganizer : public QObject */ virtual MOBase::IDownloadManager* downloadManager() const = 0; + /** + * @return the interface to the extension list. + */ + virtual IExtensionList& extensionList() const = 0; + /** * @return interface to the list of plugins (esps, esms, and esls) */ diff --git a/src/report.cpp b/src/report.cpp index 205eab44..83d2f2ef 100644 --- a/src/report.cpp +++ b/src/report.cpp @@ -383,8 +383,9 @@ QPixmap TaskDialog::standardIcon(QMessageBox::Icon icon) const break; case QMessageBox::Question: i = s->standardIcon(QStyle::SP_MessageBoxQuestion, 0, m_dialog.get()); - - case QMessageBox::NoIcon: // fall-through + break; + case QMessageBox::NoIcon: + [[fallthrough]]; default: break; } diff --git a/src/requirements.cpp b/src/requirements.cpp new file mode 100644 index 00000000..08de8d8b --- /dev/null +++ b/src/requirements.cpp @@ -0,0 +1,47 @@ +#include "requirements.h" + +#include + +#include "extension.h" +#include "imoinfo.h" +#include "log.h" + +namespace MOBase +{ + +class ExtensionRequirementImpl +{}; + +} // namespace MOBase + +using namespace MOBase; + +ExtensionRequirement::ExtensionRequirement( + std::shared_ptr impl) + : m_Impl{std::move(impl)} +{} + +ExtensionRequirement::~ExtensionRequirement() = default; + +bool ExtensionRequirement::check(IOrganizer* organizer) const +{ + return true; +} + +std::vector +ExtensionRequirementFactory::parseRequirements(const ExtensionMetaData& metadata) +{ + const auto json_requirements = metadata.json()["requirements"]; + + if (!json_requirements.isArray()) { + log::warn("expected array of requirements for extension '{}', found '{}'", + metadata.identifier(), json_requirements.type()); + return {}; + } + + std::vector requirements; + for (const auto& json_requirement : json_requirements.toArray()) { + } + + return requirements; +} \ No newline at end of file diff --git a/src/requirements.h b/src/requirements.h new file mode 100644 index 00000000..1d71d52d --- /dev/null +++ b/src/requirements.h @@ -0,0 +1,72 @@ +#ifndef UIBASE_REQUIREMENTS_H +#define UIBASE_REQUIREMENTS_H + +#include + +#include +#include + +#include "dllimport.h" + +namespace MOBase +{ +class IOrganizer; +class ExtensionMetaData; + +class ExtensionRequirementImpl; + +// extension requirements +// +class QDLLEXPORT ExtensionRequirement +{ +public: + // type of requirement + // + enum class Type + { + // requirement on the version of MO2, might be ignored by user + // + VERSION, + + // require a specific game, cannot be ignore + // + GAME, + + // require another extension, cannot be ignore + // + DEPENDENCY + }; + + using enum Type; + +public: + // check if the requirement is met + // + bool check(IOrganizer* organizer) const; + + ~ExtensionRequirement(); + +private: + friend class ExtensionRequirementFactory; + + ExtensionRequirement(std::shared_ptr impl); + + std::shared_ptr m_Impl; +}; + +// factory for requirements +// +class QDLLEXPORT ExtensionRequirementFactory +{ +public: + // extract requirements from the given metadata + // + static std::vector + parseRequirements(const ExtensionMetaData& metadata); + +private: +}; + +} // namespace MOBase + +#endif \ No newline at end of file diff --git a/src/tutorialcontrol.cpp b/src/tutorialcontrol.cpp index d3b73025..1e1f7952 100644 --- a/src/tutorialcontrol.cpp +++ b/src/tutorialcontrol.cpp @@ -153,13 +153,14 @@ void TutorialControl::simulateClick(int x, int y) if (!wasTransparent) { m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, true); } - QWidget* hitControl = m_TargetControl->childAt(x, y); - QPoint globalPos = m_TargetControl->mapToGlobal(QPoint(x, y)); - QPoint hitPos = hitControl->mapFromGlobal(globalPos); - QMouseEvent* downEvent = new QMouseEvent( - QEvent::MouseButtonPress, hitPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + QWidget* hitControl = m_TargetControl->childAt(x, y); + QPoint globalPos = m_TargetControl->mapToGlobal(QPoint(x, y)); + QPoint hitPos = hitControl->mapFromGlobal(globalPos); + QMouseEvent* downEvent = + new QMouseEvent(QEvent::MouseButtonPress, hitPos, hitPos, Qt::LeftButton, + Qt::LeftButton, Qt::NoModifier); QMouseEvent* upEvent = - new QMouseEvent(QEvent::MouseButtonRelease, hitPos, Qt::LeftButton, + new QMouseEvent(QEvent::MouseButtonRelease, hitPos, hitPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); qApp->postEvent(hitControl, (QEvent*)downEvent); diff --git a/src/utility.cpp b/src/utility.cpp index f731a0d2..1479fdbf 100644 --- a/src/utility.cpp +++ b/src/utility.cpp @@ -717,9 +717,9 @@ std::wstring ToWString(const QString& source) { // FIXME // why not source.toStdWString() ? - wchar_t* buffer = new wchar_t[static_cast(source.count()) + 1]; + wchar_t* buffer = new wchar_t[static_cast(source.size()) + 1]; source.toWCharArray(buffer); - buffer[source.count()] = L'\0'; + buffer[source.size()] = L'\0'; std::wstring result(buffer); delete[] buffer; From 7477824e164cd2e85355cfb3fbcea8fbfb773e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sun, 16 Jul 2023 14:12:12 +0200 Subject: [PATCH 8/9] Add autodetect for translations to match mob/base-translations behavior. --- src/extension.cpp | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/extension.cpp b/src/extension.cpp index d81db8e9..daa51ae3 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -264,9 +264,11 @@ PluginExtension::PluginExtension( std::unique_ptr PluginExtension::loadExtension(std::filesystem::path path, ExtensionMetaData metadata) { + namespace fs = std::filesystem; + // load plugins std::optional autodetect; - std::map plugins; + std::map plugins; { auto jsonPlugins = metadata.json()["plugins"].toObject(); if (jsonPlugins.contains("autodetect")) { @@ -299,9 +301,15 @@ PluginExtension::loadExtension(std::filesystem::path path, ExtensionMetaData met { auto jsonTranslations = metadata.json()["translations"].toObject(); - // * is a custom entry if (jsonTranslations.contains("*")) { - std::map> filesPerLanguage; + // * is a custom entry - * should point to a list of file prefix, e.g., + // ["translations/foo_", "translations/bar_"] meaning that the translations files + // are prefixed by foo_ and bar_ inside the translations folder, language is + // extracted by removing the prefix + // + // TODO: remove this option + // + std::map> filesPerLanguage; const auto prefixes = jsonTranslations["*"].toVariant().toStringList(); for (auto& prefix : prefixes) { @@ -317,6 +325,27 @@ PluginExtension::loadExtension(std::filesystem::path path, ExtensionMetaData met } } + for (auto& [language, files] : filesPerLanguage) { + translations.push_back(std::make_shared( + language.toStdString(), std::move(files))); + } + } else if (jsonTranslations.contains("autodetect")) { + + // autodetect is a custom entry - "autodetect": "xxx" means that the extension + // contains a translations folder named "xxx" where each subfolder is a language + // (identifier) containing translation files for the language + + std::map> filesPerLanguage; + const auto folder = jsonTranslations["autodetect"].toString(); + for (const auto& lang : fs::directory_iterator(path / folder.toStdString())) { + if (!fs::is_directory(lang)) { + continue; + } + + filesPerLanguage[QString::fromStdWString(lang.path().filename().wstring())] = + globExtensionFiles(lang, {"*.qm"}); + } + for (auto& [language, files] : filesPerLanguage) { translations.push_back(std::make_shared( language.toStdString(), std::move(files))); From 0b438b8ed3b3e6ec3e7139c0d716eb7cf91a3b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sat, 22 Jul 2023 11:09:39 +0200 Subject: [PATCH 9/9] Allow missing requirements in extensions. --- src/requirements.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/requirements.cpp b/src/requirements.cpp index 08de8d8b..f911a149 100644 --- a/src/requirements.cpp +++ b/src/requirements.cpp @@ -31,6 +31,10 @@ bool ExtensionRequirement::check(IOrganizer* organizer) const std::vector ExtensionRequirementFactory::parseRequirements(const ExtensionMetaData& metadata) { + if (!metadata.json().contains("requirements")) { + return {}; + } + const auto json_requirements = metadata.json()["requirements"]; if (!json_requirements.isArray()) {