diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ed1baa88..b54e43cc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,6 +10,15 @@ mo2_configure_uibase(uibase target_compile_definitions(uibase PRIVATE -DUIBASE_EXPORT) mo2_install_target(uibase) +mo2_add_filter(NAME src/extensions GROUPS + extension + iextensionlist + theme + translation + requirements + ipluginloader +) + mo2_add_filter(NAME src/interfaces GROUPS ifiletree imoinfo @@ -24,7 +33,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 new file mode 100644 index 00000000..daa51ae3 --- /dev/null +++ b/src/extension.cpp @@ -0,0 +1,381 @@ +#include "extension.h" + +#include +#include +#include +#include + +#include "log.h" + +// name of the metadata file +// +static constexpr const char* METADATA_FILENAME = "metadata.json"; + +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 + 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)}, + m_Requirements{ExtensionRequirementFactory::parseRequirements(m_MetaData)} +{} + +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: + 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()); + 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) +{ + 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 std::unique_ptr{ + new ThemeExtension(path, metadata, std::move(themes))}; +} + +std::shared_ptr +ThemeExtension::parseTheme(std::filesystem::path const& extensionFolder, + const QString& identifier, const QJsonObject& jsonTheme) +{ + 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 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()); + } + } + + 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))}; +} + +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 = + globExtensionFiles(extensionFolder, jsonGlobFiles); + + if (name.isEmpty() || qm_files.empty()) { + return nullptr; + } + + return std::make_shared(identifier.toStdString(), name.toStdString(), + std::move(qm_files)); +} + +PluginExtension::PluginExtension( + std::filesystem::path path, ExtensionMetaData metadata, bool autodetect, + std::map 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) +{ + namespace fs = std::filesystem; + + // 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; + { + 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; + { + auto jsonTranslations = metadata.json()["translations"].toObject(); + + if (jsonTranslations.contains("*")) { + // * 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) { + const auto filePrefix = QFileInfo(prefix).fileName(); + 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).baseName().replace(filePrefix, "", Qt::CaseInsensitive); + filesPerLanguage[identifier].push_back(file); + } + } + + 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))); + } + + } 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), *autodetect, std::move(plugins), + std::move(themes), std::move(translations))); +} + +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 new file mode 100644 index 00000000..b94a8fe6 --- /dev/null +++ b/src/extension.h @@ -0,0 +1,238 @@ +#ifndef UIBASE_EXTENSION_H +#define UIBASE_EXTENSION_H + +#include +#include +#include + +#include +#include + +#include "dllimport.h" +#include "iplugingame.h" +#include "requirements.h" +#include "theme.h" +#include "translation.h" +#include "versioninfo.h" + +namespace MOBase +{ +class IExtension; + +enum class ExtensionType +{ + INVALID, + THEME, + TRANSLATION, + PLUGIN, + GAME +}; + +class QDLLEXPORT ExtensionMetaData +{ +public: + ExtensionMetaData(QJsonObject const& jsonData); + + // check if that metadata object is valid + // + bool isValid() const; + + // 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; } + + // retrieve the description of the extension. + // + auto description() const { return localized(m_Description); } + + // retrieve the version of the extension. + // + const auto& version() const { return m_Version; } + + // 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; + +private: + constexpr static const char* DEFAULT_TRANSLATIONS_FOLDER = "translations"; + constexpr static const char* DEFAULT_STYLESHEET_PATH = "stylesheets"; + + ExtensionType parseType(QString const& value) const; + +private: + QJsonObject m_JsonData; + QString m_TranslationContext; + + QString m_Identifier; + QString m_Name; + ExtensionType m_Type; + QString m_Description; + VersionInfo m_Version; + + std::filesystem::path m_TranslationFilesPrefix; + std::filesystem::path m_StyleSheetFilePath; +}; + +class QDLLEXPORT IExtension +{ +public: + // retrieve the folder containing the extension + // + std::filesystem::path directory() const { return m_Path; } + + // retrieve the metadata of this extension + // + const auto& metadata() const { return m_MetaData; } + + // retrieve the requirements of the extension + // + const auto& requirements() const { return m_Requirements; } + + virtual ~IExtension() {} + +protected: + IExtension(std::filesystem::path path, ExtensionMetaData metadata); + +private: + std::filesystem::path m_Path; + ExtensionMetaData m_MetaData; + std::vector m_Requirements; +}; + +// 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); + +private: + // 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; +}; + +// 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; } + +protected: + PluginExtension( + std::filesystem::path path, ExtensionMetaData metadata, bool autodetect, + std::map 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; + + // forced plugins + std::map m_Plugins; + + // theme and translations additions + std::vector> m_ThemeAdditions; + std::vector> m_TranslationAdditions; +}; + +// game extension that provides a game plugin, alongside other plugins, translation or +// theme (additions) +// +class QDLLEXPORT GameExtension : public PluginExtension +{ +private: + GameExtension(PluginExtension&& pluginExtension); + + friend class ExtensionFactory; + static std::unique_ptr loadExtension(std::filesystem::path path, + ExtensionMetaData metadata); +}; + +} // namespace MOBase + +#endif 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/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>> 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..f911a149 --- /dev/null +++ b/src/requirements.cpp @@ -0,0 +1,51 @@ +#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) +{ + if (!metadata.json().contains("requirements")) { + return {}; + } + + 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/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/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 7f94001f..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; @@ -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